111 lines
3.3 KiB
Go
111 lines
3.3 KiB
Go
package openai_compat
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
const Name = "openai_compat"
|
|
|
|
// Adapter is the OpenAI-compatible adapter. It carries an explicit provider
|
|
// label, per-request auth/header injection and top-level option passthrough so
|
|
// a provider boundary and its config contract stay distinct.
|
|
type Adapter struct {
|
|
instanceName string
|
|
provider string
|
|
endpoint string
|
|
headers map[string]string
|
|
capacity int
|
|
maxQueue int
|
|
queueTimeoutMS int
|
|
requestTimeoutMS int
|
|
client *http.Client
|
|
logger *zap.Logger
|
|
// profile is the resolved concrete protocol profile snapshot. When non-nil,
|
|
// the adapter resolves operation URLs from the profile's operation paths
|
|
// instead of the legacy endpoint + /v1 suffix heuristic.
|
|
profile *config.ConcreteProtocolProfile
|
|
}
|
|
|
|
// New creates an OpenAI-compatible adapter. The optional instanceName is the
|
|
// registry instance key used to disambiguate multiple instances on a node.
|
|
func New(cfg config.OpenAICompatConf, logger *zap.Logger, instanceName ...string) *Adapter {
|
|
endpoint := strings.TrimRight(cfg.Endpoint, "/")
|
|
name := Name
|
|
if len(instanceName) > 0 && instanceName[0] != "" {
|
|
name = instanceName[0]
|
|
}
|
|
var headers map[string]string
|
|
if len(cfg.Headers) > 0 {
|
|
headers = make(map[string]string, len(cfg.Headers))
|
|
for k, v := range cfg.Headers {
|
|
headers[k] = v
|
|
}
|
|
}
|
|
var profile *config.ConcreteProtocolProfile
|
|
if cfg.RuntimeProfile != nil {
|
|
cloned := cfg.RuntimeProfile.Clone()
|
|
profile = &cloned
|
|
}
|
|
return &Adapter{
|
|
instanceName: name,
|
|
provider: cfg.Provider,
|
|
endpoint: endpoint,
|
|
headers: headers,
|
|
capacity: cfg.Capacity,
|
|
maxQueue: cfg.MaxQueue,
|
|
queueTimeoutMS: cfg.QueueTimeoutMS,
|
|
requestTimeoutMS: cfg.RequestTimeoutMS,
|
|
client: &http.Client{},
|
|
logger: logger,
|
|
profile: profile,
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) Name() string { return Name }
|
|
|
|
// operationURL resolves an operation id to its full URL. When the adapter has
|
|
// a concrete profile, the profile's operation path is used. When no profile
|
|
// is present (legacy payload), the endpoint is used with the legacy path
|
|
// appended.
|
|
func (a *Adapter) operationURL(op string, legacyPath string) (string, error) {
|
|
if a.profile != nil {
|
|
return a.profile.ResolveOperationURL(op)
|
|
}
|
|
if a.endpoint == "" {
|
|
return "", errEndpointRequired
|
|
}
|
|
return resolveLegacyURL(a.endpoint, legacyPath), nil
|
|
}
|
|
|
|
// resolveLegacyURL appends a legacy path to the endpoint, stripping a
|
|
// duplicate /v1 suffix to avoid /v1/v1.
|
|
func resolveLegacyURL(endpoint, path string) string {
|
|
u, err := url.Parse(endpoint)
|
|
if err != nil {
|
|
return strings.TrimRight(endpoint, "/") + path
|
|
}
|
|
basePath := strings.TrimRight(u.Path, "/")
|
|
if strings.HasSuffix(basePath, "/v1") && strings.HasPrefix(path, "/v1/") {
|
|
path = strings.TrimPrefix(path, "/v1")
|
|
}
|
|
u.Path = basePath + path
|
|
return u.String()
|
|
}
|
|
|
|
var errEndpointRequired = fmtError("openai_compat adapter: endpoint is required")
|
|
|
|
// fmtError is a small helper to avoid importing fmt just for error creation
|
|
// in the legacy fallback path.
|
|
func fmtError(msg string) error {
|
|
return &adapterError{msg: msg}
|
|
}
|
|
|
|
type adapterError struct{ msg string }
|
|
|
|
func (e *adapterError) Error() string { return e.msg }
|