From 3c16e9824ac5735f34af428f90e268b67d3f3564 Mon Sep 17 00:00:00 2001 From: GreenTeodoro839 Date: Thu, 2 Jul 2026 10:53:57 +0800 Subject: [PATCH 1/2] fix(openai-compat): resolve alias for header-only auth OpenAI-compatibility providers that authenticate via custom headers (no api_key) had AuthKind() return empty, causing the entire model-alias resolution chain to skip. The client-visible alias was forwarded to the upstream verbatim, resulting in 'Not supported model' errors. Add IsOpenAICompatibleProvider() helper and teach AuthKind() to recognize openai-compat auths as API-key auths, enabling alias resolution for both api-key and header-authenticated entries. --- internal/util/provider.go | 15 +++++++++++++++ sdk/cliproxy/auth/classification.go | 29 ++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/internal/util/provider.go b/internal/util/provider.go index ae25a63148a..a58d2c8787a 100644 --- a/internal/util/provider.go +++ b/internal/util/provider.go @@ -26,6 +26,21 @@ func OpenAICompatibleProviderKey(name string) string { return openAICompatibleProviderPrefix + name } +// IsOpenAICompatibleProvider reports whether the given provider key identifies an +// OpenAI-compatibility provider (the generic "openai-compatibility" key or a named +// "openai-compatible-" key). Such providers always use static credentials +// (an API key and/or custom headers) and are therefore treated as API-key auths. +func IsOpenAICompatibleProvider(provider string) bool { + provider = strings.ToLower(strings.TrimSpace(provider)) + if provider == "" { + return false + } + if provider == "openai-compatibility" { + return true + } + return strings.HasPrefix(provider, openAICompatibleProviderPrefix) +} + // GetProviderName determines all AI service providers capable of serving a registered model. // It first queries the global model registry to retrieve the providers backing the supplied model name. // When the model has not been registered yet, it falls back to legacy string heuristics to infer diff --git a/sdk/cliproxy/auth/classification.go b/sdk/cliproxy/auth/classification.go index b8c7171844c..b002ca7f277 100644 --- a/sdk/cliproxy/auth/classification.go +++ b/sdk/cliproxy/auth/classification.go @@ -1,6 +1,10 @@ package auth -import "strings" +import ( + "strings" + + "github.com/router-for-me/CLIProxyAPI/v7/internal/util" +) const ( AuthKindAPIKey = "apikey" @@ -36,12 +40,35 @@ func (a *Auth) AuthKind() string { if authAttribute(a, AttributeAPIKey) != "" { return AuthKindAPIKey } + // OpenAI-compatibility providers always use static credentials: either an API + // key or custom headers (e.g. cookie/device headers) when no api-key is set. + // Treat them as API-key auths so model-alias resolution and per-auth model + // pools apply even for header-authenticated entries. + if isOpenAICompatibilityAuth(a) { + return AuthKindAPIKey + } if authHasOAuthMetadata(a) { return AuthKindOAuth } return "" } +// isOpenAICompatibilityAuth reports whether the auth belongs to an +// OpenAI-compatibility provider. Such auths carry the provider key produced by +// util.OpenAICompatibleProviderKey and/or a "compat_name" attribute. +func isOpenAICompatibilityAuth(a *Auth) bool { + if a == nil { + return false + } + if util.IsOpenAICompatibleProvider(a.Provider) { + return true + } + if a.Attributes != nil && strings.TrimSpace(a.Attributes["compat_name"]) != "" { + return true + } + return false +} + // AuthSourceKind returns where the Auth entry came from at runtime. func (a *Auth) AuthSourceKind() string { if a == nil { From 0afcca357e613fc111e1f86dccd8912fae0da496 Mon Sep 17 00:00:00 2001 From: GreenTeodoro839 Date: Thu, 2 Jul 2026 20:08:44 +0800 Subject: [PATCH 2/2] fix(openai-compat): handle normalized provider aliases --- internal/util/provider.go | 10 ++++++++ sdk/cliproxy/auth/classification.go | 8 +++---- sdk/cliproxy/auth/conductor.go | 36 ++++++++++++++++++----------- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/internal/util/provider.go b/internal/util/provider.go index a58d2c8787a..dc5d6f46b31 100644 --- a/internal/util/provider.go +++ b/internal/util/provider.go @@ -41,6 +41,16 @@ func IsOpenAICompatibleProvider(provider string) bool { return strings.HasPrefix(provider, openAICompatibleProviderPrefix) } +// OpenAICompatibleProviderName returns the config name encoded by a named +// OpenAI-compatible provider key such as "openai-compatible-vast". +func OpenAICompatibleProviderName(provider string) string { + provider = strings.ToLower(strings.TrimSpace(provider)) + if !strings.HasPrefix(provider, openAICompatibleProviderPrefix) { + return "" + } + return strings.TrimPrefix(provider, openAICompatibleProviderPrefix) +} + // GetProviderName determines all AI service providers capable of serving a registered model. // It first queries the global model registry to retrieve the providers backing the supplied model name. // When the model has not been registered yet, it falls back to legacy string heuristics to infer diff --git a/sdk/cliproxy/auth/classification.go b/sdk/cliproxy/auth/classification.go index b002ca7f277..b3e60241728 100644 --- a/sdk/cliproxy/auth/classification.go +++ b/sdk/cliproxy/auth/classification.go @@ -40,6 +40,9 @@ func (a *Auth) AuthKind() string { if authAttribute(a, AttributeAPIKey) != "" { return AuthKindAPIKey } + if authHasOAuthMetadata(a) { + return AuthKindOAuth + } // OpenAI-compatibility providers always use static credentials: either an API // key or custom headers (e.g. cookie/device headers) when no api-key is set. // Treat them as API-key auths so model-alias resolution and per-auth model @@ -47,9 +50,6 @@ func (a *Auth) AuthKind() string { if isOpenAICompatibilityAuth(a) { return AuthKindAPIKey } - if authHasOAuthMetadata(a) { - return AuthKindOAuth - } return "" } @@ -63,7 +63,7 @@ func isOpenAICompatibilityAuth(a *Auth) bool { if util.IsOpenAICompatibleProvider(a.Provider) { return true } - if a.Attributes != nil && strings.TrimSpace(a.Attributes["compat_name"]) != "" { + if authAttribute(a, "compat_name") != "" { return true } return false diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index 1597efff02c..bda40d8bf2c 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -135,7 +135,7 @@ func providerCoolingDisabledForAuth(auth *Auth, cfg *internalconfig.Config) bool providerKey = strings.TrimSpace(auth.Attributes["provider_key"]) compatName = strings.TrimSpace(auth.Attributes["compat_name"]) } - if providerKey == "" && compatName == "" && provider != "openai-compatibility" { + if providerKey == "" && compatName == "" && !util.IsOpenAICompatibleProvider(provider) { return false } if providerKey == "" { @@ -1011,13 +1011,13 @@ func isOpenAICompatAPIKeyAuth(auth *Auth) bool { if !isAPIKeyAuth(auth) { return false } - if strings.EqualFold(strings.TrimSpace(auth.Provider), "openai-compatibility") { + if util.IsOpenAICompatibleProvider(auth.Provider) { return true } if auth.Attributes == nil { return false } - return strings.TrimSpace(auth.Attributes["compat_name"]) != "" + return authAttribute(auth, "compat_name") != "" } func openAICompatProviderKey(auth *Auth) string { @@ -1300,7 +1300,7 @@ func (m *Manager) resolveAPIKeyModelAliasWithResult(auth *Auth, requestedModel s providerKey = strings.TrimSpace(auth.Attributes["provider_key"]) compatName = strings.TrimSpace(auth.Attributes["compat_name"]) } - if compatName != "" || strings.EqualFold(strings.TrimSpace(auth.Provider), "openai-compatibility") { + if compatName != "" || util.IsOpenAICompatibleProvider(auth.Provider) { if entry := resolveOpenAICompatConfig(cfg, providerKey, compatName, auth.Provider); entry != nil { models = asModelAliasEntries(entry.Models) } @@ -1979,7 +1979,7 @@ func (m *Manager) rebuildAPIKeyModelAliasLocked(cfg *internalconfig.Config) { providerKey = strings.TrimSpace(auth.Attributes["provider_key"]) compatName = strings.TrimSpace(auth.Attributes["compat_name"]) } - if compatName != "" || strings.EqualFold(strings.TrimSpace(auth.Provider), "openai-compatibility") { + if compatName != "" || util.IsOpenAICompatibleProvider(auth.Provider) { if entry := resolveOpenAICompatConfig(cfg, providerKey, compatName, auth.Provider); entry != nil { compileAPIKeyModelAliasForModels(byAlias, entry.Models) } @@ -3201,7 +3201,7 @@ func resolveUpstreamModelForOpenAICompatAPIKey(cfg *internalconfig.Config, auth providerKey = strings.TrimSpace(auth.Attributes["provider_key"]) compatName = strings.TrimSpace(auth.Attributes["compat_name"]) } - if compatName == "" && !strings.EqualFold(strings.TrimSpace(auth.Provider), "openai-compatibility") { + if compatName == "" && !util.IsOpenAICompatibleProvider(auth.Provider) { return "" } entry := resolveOpenAICompatConfig(cfg, providerKey, compatName, auth.Provider) @@ -3217,15 +3217,23 @@ func resolveOpenAICompatConfig(cfg *internalconfig.Config, providerKey, compatNa if cfg == nil { return nil } - candidates := make([]string, 0, 3) - if v := strings.TrimSpace(compatName); v != "" { - candidates = append(candidates, v) - } - if v := strings.TrimSpace(providerKey); v != "" { - candidates = append(candidates, v) + candidates := make([]string, 0, 6) + seen := make(map[string]struct{}, 6) + addCandidate := func(value string) { + value = strings.TrimSpace(value) + if value == "" { + return + } + key := strings.ToLower(value) + if _, ok := seen[key]; ok { + return + } + seen[key] = struct{}{} + candidates = append(candidates, value) } - if v := strings.TrimSpace(authProvider); v != "" { - candidates = append(candidates, v) + for _, value := range []string{compatName, providerKey, authProvider} { + addCandidate(value) + addCandidate(util.OpenAICompatibleProviderName(value)) } for i := range cfg.OpenAICompatibility { compat := &cfg.OpenAICompatibility[i]