Something went wrong. Try again.
sloproxy
Something went wrong. Try again.
16 kB · 501 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502package pluginhost
import ( "context" "fmt" "strings"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" _ "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator/builtin" log "github.com/sirupsen/logrus")
type registryModelInfo = registry.ModelInfo
type modelRegistry interface { RegisterClient(clientID, clientProvider string, models []*registry.ModelInfo) UnregisterClient(clientID string)}
type modelProviderRegistry interface { modelRegistry GetModelProviders(modelID string) []string}
type pluginModelRegistration struct { pluginID string provider string priority int models []*registry.ModelInfo hasExecutor bool}
func normalizedExecutorModelScope(caps pluginapi.Capabilities) pluginapi.ExecutorModelScope { if caps.Executor == nil { return pluginapi.ExecutorModelScopeBoth } switch caps.ExecutorModelScope { case pluginapi.ExecutorModelScopeStatic, pluginapi.ExecutorModelScopeOAuth, pluginapi.ExecutorModelScopeBoth: return caps.ExecutorModelScope default: return pluginapi.ExecutorModelScopeBoth }}
func executorScopeAllowsStaticModels(caps pluginapi.Capabilities) bool { if caps.Executor == nil { return true } scope := normalizedExecutorModelScope(caps) return scope == pluginapi.ExecutorModelScopeStatic || scope == pluginapi.ExecutorModelScopeBoth}
func executorScopeAllowsOAuthModels(caps pluginapi.Capabilities) bool { if caps.Executor == nil { return true } scope := normalizedExecutorModelScope(caps) return scope == pluginapi.ExecutorModelScopeOAuth || scope == pluginapi.ExecutorModelScopeBoth}
func normalizeExecutorFormats(raw []string) []sdktranslator.Format { if len(raw) == 0 { return nil } out := make([]sdktranslator.Format, 0, len(raw)) seen := make(map[string]struct{}, len(raw)) for _, item := range raw { format := normalizeExecutorFormatName(item) if format == "" { continue } key := format.String() if _, exists := seen[key]; exists { continue } seen[key] = struct{}{} out = append(out, format) } return out}
func normalizeExecutorFormatName(raw string) sdktranslator.Format { switch strings.ToLower(strings.TrimSpace(raw)) { case "", "none": return "" case "chat-completions", "chat_completions", "openai-chat-completions", "openai_chat_completions": return sdktranslator.FormatOpenAI case "responses", "openai-responses", "openai_responses": return sdktranslator.FormatOpenAIResponse case "anthropic": return sdktranslator.FormatClaude default: return sdktranslator.FromString(strings.TrimSpace(raw)) }}
func executorFormatContains(formats []sdktranslator.Format, target sdktranslator.Format) bool { if target == "" { return false } for _, format := range formats { if format == target { return true } } return false}
type AuthModelResult struct { Provider string Models []*registry.ModelInfo Auth *coreauth.Auth Handled bool Err error}
func pluginModelInfoToRegistryModelInfo(model pluginapi.ModelInfo) *registry.ModelInfo { return ®istry.ModelInfo{ ID: model.ID, Object: model.Object, Created: model.Created, OwnedBy: model.OwnedBy, Type: model.Type, DisplayName: model.DisplayName, Name: model.Name, Version: model.Version, Description: model.Description, InputTokenLimit: int(model.InputTokenLimit), OutputTokenLimit: int(model.OutputTokenLimit), SupportedGenerationMethods: cloneStringSlice(model.SupportedGenerationMethods), ContextLength: int(model.ContextLength), MaxCompletionTokens: int(model.MaxCompletionTokens), SupportedParameters: cloneStringSlice(model.SupportedParameters), SupportedInputModalities: cloneStringSlice(model.SupportedInputModalities), SupportedOutputModalities: cloneStringSlice(model.SupportedOutputModalities), Thinking: pluginThinkingSupportToRegistryThinkingSupport(model.Thinking), UserDefined: model.UserDefined, }}
func pluginThinkingSupportToRegistryThinkingSupport(thinking *pluginapi.ThinkingSupport) *registry.ThinkingSupport { if thinking == nil { return nil } return ®istry.ThinkingSupport{ Min: thinking.Min, Max: thinking.Max, ZeroAllowed: thinking.ZeroAllowed, DynamicAllowed: thinking.DynamicAllowed, Levels: cloneStringSlice(thinking.Levels), }}
func registryModelInfoToPluginModelInfo(model *registry.ModelInfo) pluginapi.ModelInfo { if model == nil { return pluginapi.ModelInfo{} } return pluginapi.ModelInfo{ ID: model.ID, Object: model.Object, Created: model.Created, OwnedBy: model.OwnedBy, Type: model.Type, DisplayName: model.DisplayName, Name: model.Name, Version: model.Version, Description: model.Description, InputTokenLimit: int64(model.InputTokenLimit), OutputTokenLimit: int64(model.OutputTokenLimit), SupportedGenerationMethods: cloneStringSlice(model.SupportedGenerationMethods), ContextLength: int64(model.ContextLength), MaxCompletionTokens: int64(model.MaxCompletionTokens), SupportedParameters: cloneStringSlice(model.SupportedParameters), SupportedInputModalities: cloneStringSlice(model.SupportedInputModalities), SupportedOutputModalities: cloneStringSlice(model.SupportedOutputModalities), Thinking: registryThinkingSupportToPluginThinkingSupport(model.Thinking), UserDefined: model.UserDefined, }}
func registryThinkingSupportToPluginThinkingSupport(thinking *registry.ThinkingSupport) *pluginapi.ThinkingSupport { if thinking == nil { return nil } return &pluginapi.ThinkingSupport{ Min: thinking.Min, Max: thinking.Max, ZeroAllowed: thinking.ZeroAllowed, DynamicAllowed: thinking.DynamicAllowed, Levels: cloneStringSlice(thinking.Levels), }}
func cloneStringSlice(in []string) []string { if len(in) == 0 { return nil } return append([]string(nil), in...)}
func cloneRegistryModels(in []*registry.ModelInfo) []*registry.ModelInfo { if len(in) == 0 { return nil } out := make([]*registry.ModelInfo, 0, len(in)) for _, model := range in { if model == nil { continue } copyModel := *model copyModel.SupportedGenerationMethods = cloneStringSlice(model.SupportedGenerationMethods) copyModel.SupportedParameters = cloneStringSlice(model.SupportedParameters) copyModel.SupportedInputModalities = cloneStringSlice(model.SupportedInputModalities) copyModel.SupportedOutputModalities = cloneStringSlice(model.SupportedOutputModalities) if model.Thinking != nil { thinking := *model.Thinking thinking.Levels = cloneStringSlice(model.Thinking.Levels) copyModel.Thinking = &thinking } out = append(out, ©Model) } return out}
func (h *Host) RegisterModels(ctx context.Context, modelRegistry modelRegistry) { if h == nil || modelRegistry == nil { return }
snap := h.Snapshot() records := h.activeRecordsFromSnapshot(snap) registrations := make([]modelClientRegistration, 0) nextClients := make(map[string]struct{}) nextProviders := make(map[string]string) nextModelRegistrations := make(map[string]pluginModelRegistration) for _, record := range records { modelProvider := record.plugin.Capabilities.ModelProvider registrar := record.plugin.Capabilities.ModelRegistrar if modelProvider == nil && registrar == nil { continue } if !executorScopeAllowsStaticModels(record.plugin.Capabilities) { continue } var resp pluginapi.ModelRegistrationResponse var errRegisterModels error if modelProvider != nil { modelResp, errStaticModels := h.callModelProviderStaticModels(ctx, record, modelProvider) errRegisterModels = errStaticModels resp = pluginapi.ModelRegistrationResponse{ Provider: modelResp.Provider, Models: modelResp.Models, } } else { resp, errRegisterModels = h.callModelRegistrar(ctx, record, registrar) } if errRegisterModels != nil { log.Warnf("pluginhost: model registrar %s failed: %v", record.id, errRegisterModels) continue }
provider := strings.ToLower(strings.TrimSpace(resp.Provider)) if provider == "" || len(resp.Models) == 0 { continue }
models := make([]*registry.ModelInfo, 0, len(resp.Models)) for _, item := range resp.Models { model := pluginModelInfoToRegistryModelInfo(item) if model == nil || strings.TrimSpace(model.ID) == "" { continue } model.ID = strings.TrimSpace(model.ID) models = append(models, model) } if len(models) == 0 { continue }
nextModelRegistrations[record.id] = pluginModelRegistration{ pluginID: record.id, provider: provider, priority: record.priority, models: cloneRegistryModels(models), hasExecutor: record.plugin.Capabilities.Executor != nil, } nextProviders[record.id] = provider if record.plugin.Capabilities.Executor == nil { clientID := "plugin:" + record.id + ":" + provider registrations = append(registrations, modelClientRegistration{ clientID: clientID, provider: provider, models: models, }) nextClients[clientID] = struct{}{} } } h.commitModelClients(snap, modelRegistry, registrations, nextClients, nextProviders, nextModelRegistrations)}
func (h *Host) ModelsForAuth(ctx context.Context, auth *coreauth.Auth) AuthModelResult { if h == nil || auth == nil { return AuthModelResult{} } providerKey := normalizeProviderID(auth.Provider) if providerKey == "" { return AuthModelResult{} } for _, record := range h.activeRecords() { modelProvider := record.plugin.Capabilities.ModelProvider if modelProvider == nil || h.isPluginFused(record.id) { continue } if !executorScopeAllowsOAuthModels(record.plugin.Capabilities) { continue } authProvider := record.plugin.Capabilities.AuthProvider if authProvider != nil { identifier, okIdentifier := h.callAuthProviderIdentifier(record.id, authProvider) if !okIdentifier || normalizeProviderID(identifier) != providerKey { continue } } else { recordProvider := normalizeProviderID(h.modelProvider(record.id)) if recordProvider == "" { executor := record.plugin.Capabilities.Executor if executor != nil { candidate, okCandidate := h.executorProvider(record, executor) if okCandidate { recordProvider = candidate } } } if recordProvider != providerKey { continue } } resp, errModels := h.callModelsForAuth(ctx, record, modelProvider, auth) if errModels != nil { log.Warnf("pluginhost: models for auth %s failed: %v", auth.ID, errModels) return AuthModelResult{Handled: true, Err: errModels} } respProvider := normalizeProviderID(resp.Provider) if respProvider != "" && respProvider != providerKey { continue } if respProvider == "" { respProvider = providerKey } models := make([]*registry.ModelInfo, 0, len(resp.Models)) for _, item := range resp.Models { model := pluginModelInfoToRegistryModelInfo(item) if model != nil { model.ID = strings.TrimSpace(model.ID) } if model != nil && model.ID != "" { models = append(models, model) } } path := "" if auth.Attributes != nil { path = auth.Attributes["path"] } var updated *coreauth.Auth if authDataHasValue(resp.AuthUpdate) { updated = h.AuthDataToCoreAuth(authDataWithDefaults(resp.AuthUpdate, auth), path, auth.FileName) } return AuthModelResult{Provider: respProvider, Models: models, Auth: updated, Handled: true} } return AuthModelResult{}}
func authDataHasValue(data pluginapi.AuthData) bool { return strings.TrimSpace(data.Provider) != "" || strings.TrimSpace(data.ID) != "" || strings.TrimSpace(data.FileName) != "" || strings.TrimSpace(data.Label) != "" || strings.TrimSpace(data.Prefix) != "" || strings.TrimSpace(data.ProxyURL) != "" || data.Disabled || len(data.StorageJSON) > 0 || len(data.Metadata) > 0 || len(data.Attributes) > 0 || !data.NextRefreshAfter.IsZero()}
func authDataWithDefaults(data pluginapi.AuthData, auth *coreauth.Auth) pluginapi.AuthData { if auth == nil { return data } if strings.TrimSpace(data.Provider) == "" { data.Provider = auth.Provider } if strings.TrimSpace(data.ID) == "" { data.ID = auth.ID } if strings.TrimSpace(data.FileName) == "" { data.FileName = auth.FileName } if strings.TrimSpace(data.Label) == "" { data.Label = auth.Label } if strings.TrimSpace(data.Prefix) == "" { data.Prefix = auth.Prefix } if strings.TrimSpace(data.ProxyURL) == "" { data.ProxyURL = auth.ProxyURL } if len(data.Metadata) == 0 { data.Metadata = cloneAnyMap(auth.Metadata) } else { metadata := cloneAnyMap(data.Metadata) for key, value := range auth.Metadata { if _, exists := metadata[key]; !exists { metadata[key] = value } } data.Metadata = metadata } if len(data.Attributes) == 0 { data.Attributes = cloneStringMap(auth.Attributes) } else { attributes := cloneStringMap(data.Attributes) for key, value := range auth.Attributes { if _, exists := attributes[key]; !exists { attributes[key] = value } } data.Attributes = attributes } if len(data.StorageJSON) == 0 { data.StorageJSON = storageJSONFromAuth(auth) } if data.NextRefreshAfter.IsZero() { data.NextRefreshAfter = auth.NextRefreshAfter } return data}
type modelClientRegistration struct { clientID string provider string models []*registry.ModelInfo}
func (h *Host) callModelRegistrar(ctx context.Context, record capabilityRecord, registrar pluginapi.ModelRegistrar) (resp pluginapi.ModelRegistrationResponse, err error) { if h == nil || registrar == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) { return pluginapi.ModelRegistrationResponse{}, nil } defer func() { if recovered := recover(); recovered != nil { h.fusePlugin(record.id, "ModelRegistrar.RegisterModels", recovered) resp = pluginapi.ModelRegistrationResponse{} err = fmt.Errorf("model registrar panic: %v", recovered) } }() return registrar.RegisterModels(ctx, pluginapi.ModelRegistrationRequest{Plugin: record.meta})}
func (h *Host) callModelProviderStaticModels(ctx context.Context, record capabilityRecord, provider pluginapi.ModelProvider) (resp pluginapi.ModelResponse, err error) { if h == nil || provider == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) { return pluginapi.ModelResponse{}, nil } defer func() { if recovered := recover(); recovered != nil { h.fusePlugin(record.id, "ModelProvider.StaticModels", recovered) resp = pluginapi.ModelResponse{} err = fmt.Errorf("model provider panic: %v", recovered) } }() return provider.StaticModels(ctx, pluginapi.StaticModelRequest{ Plugin: record.meta, Host: h.hostConfigSummary(), })}
func (h *Host) callModelsForAuth(ctx context.Context, record capabilityRecord, provider pluginapi.ModelProvider, auth *coreauth.Auth) (resp pluginapi.ModelResponse, err error) { if h == nil || provider == nil || auth == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) { return pluginapi.ModelResponse{}, nil } defer func() { if recovered := recover(); recovered != nil { h.fusePlugin(record.id, "ModelProvider.ModelsForAuth", recovered) resp = pluginapi.ModelResponse{} err = fmt.Errorf("model provider per-auth models panic: %v", recovered) } }() return provider.ModelsForAuth(ctx, pluginapi.AuthModelRequest{ Plugin: record.meta, AuthID: auth.ID, AuthProvider: auth.Provider, StorageJSON: storageJSONFromAuth(auth), Metadata: cloneAnyMap(auth.Metadata), Attributes: cloneStringMap(auth.Attributes), Host: h.hostConfigSummary(), HTTPClient: h.newHTTPClient(auth), })}