diff --git a/internal/pluginhost/auth_provider.go b/internal/pluginhost/auth_provider.go --- a/internal/pluginhost/auth_provider.go +++ b/internal/pluginhost/auth_provider.go @@ -549,12 +549,13 @@ } path = strings.TrimSpace(path) if path != "" { - attributes["path"] = path - attributes["source"] = path + attributes[coreauth.AttributePath] = path + attributes[coreauth.AttributeSource] = path + attributes[coreauth.AttributeSourceBackend] = coreauth.AuthSourceFile } fileName = strings.TrimSpace(firstNonEmpty(data.FileName, fileName)) - if fileName != "" && attributes["source"] == "" { - attributes["source"] = fileName + if fileName != "" && attributes[coreauth.AttributeSource] == "" { + attributes[coreauth.AttributeSource] = fileName } id := strings.TrimSpace(data.ID) if id == "" { diff --git a/internal/store/gitstore.go b/internal/store/gitstore.go --- a/internal/store/gitstore.go +++ b/internal/store/gitstore.go @@ -324,7 +324,8 @@ if auth.Attributes == nil { auth.Attributes = make(map[string]string) } - auth.Attributes["path"] = path + auth.Attributes[cliproxyauth.AttributePath] = path + auth.Attributes[cliproxyauth.AttributeSourceBackend] = cliproxyauth.AuthSourceGit if strings.TrimSpace(auth.FileName) == "" { auth.FileName = auth.ID @@ -481,12 +482,15 @@ } id := s.idFor(path, baseDir) auth := &cliproxyauth.Auth{ - ID: id, - Provider: provider, - FileName: id, - Label: s.labelFor(metadata), - Status: cliproxyauth.StatusActive, - Attributes: map[string]string{"path": path}, + ID: id, + Provider: provider, + FileName: id, + Label: s.labelFor(metadata), + Status: cliproxyauth.StatusActive, + Attributes: map[string]string{ + cliproxyauth.AttributePath: path, + cliproxyauth.AttributeSourceBackend: cliproxyauth.AuthSourceGit, + }, Metadata: metadata, CreatedAt: info.ModTime(), UpdatedAt: info.ModTime(), diff --git a/internal/store/objectstore.go b/internal/store/objectstore.go --- a/internal/store/objectstore.go +++ b/internal/store/objectstore.go @@ -221,7 +221,8 @@ if auth.Attributes == nil { auth.Attributes = make(map[string]string) } - auth.Attributes["path"] = path + auth.Attributes[cliproxyauth.AttributePath] = path + auth.Attributes[cliproxyauth.AttributeSourceBackend] = cliproxyauth.AuthSourceObjectStore if strings.TrimSpace(auth.FileName) == "" { auth.FileName = auth.ID @@ -586,7 +587,10 @@ rel = filepath.Base(path) } rel = normalizeAuthID(rel) - attr := map[string]string{"path": path} + attr := map[string]string{ + cliproxyauth.AttributePath: path, + cliproxyauth.AttributeSourceBackend: cliproxyauth.AuthSourceObjectStore, + } if email := strings.TrimSpace(valueAsString(metadata["email"])); email != "" { attr["email"] = email } diff --git a/internal/store/postgresstore.go b/internal/store/postgresstore.go --- a/internal/store/postgresstore.go +++ b/internal/store/postgresstore.go @@ -251,7 +251,8 @@ if auth.Attributes == nil { auth.Attributes = make(map[string]string) } - auth.Attributes["path"] = path + auth.Attributes[cliproxyauth.AttributePath] = path + auth.Attributes[cliproxyauth.AttributeSourceBackend] = cliproxyauth.AuthSourcePostgres if strings.TrimSpace(auth.FileName) == "" { auth.FileName = auth.ID @@ -301,7 +302,10 @@ if provider == "" { provider = "unknown" } - attr := map[string]string{"path": path} + attr := map[string]string{ + cliproxyauth.AttributePath: path, + cliproxyauth.AttributeSourceBackend: cliproxyauth.AuthSourcePostgres, + } if email := strings.TrimSpace(valueAsString(metadata["email"])); email != "" { attr["email"] = email } diff --git a/sdk/auth/filestore.go b/sdk/auth/filestore.go --- a/sdk/auth/filestore.go +++ b/sdk/auth/filestore.go @@ -151,7 +151,9 @@ if auth.Attributes == nil { auth.Attributes = make(map[string]string) } - auth.Attributes["path"] = path + auth.Attributes[cliproxyauth.AttributePath] = path + auth.Attributes[cliproxyauth.AttributeSource] = path + auth.Attributes[cliproxyauth.AttributeSourceBackend] = cliproxyauth.AuthSourceFile if strings.TrimSpace(auth.FileName) == "" { auth.FileName = auth.ID @@ -264,8 +266,9 @@ if auth.Attributes == nil { auth.Attributes = make(map[string]string) } - auth.Attributes["path"] = path - auth.Attributes["source"] = path + auth.Attributes[cliproxyauth.AttributePath] = path + auth.Attributes[cliproxyauth.AttributeSource] = path + auth.Attributes[cliproxyauth.AttributeSourceBackend] = cliproxyauth.AuthSourceFile cliproxyauth.ApplyCustomHeadersFromMetadata(auth) } return auths, nil @@ -306,13 +309,17 @@ status = cliproxyauth.StatusDisabled } auth := &cliproxyauth.Auth{ - ID: id, - Provider: provider, - FileName: id, - Label: s.labelFor(metadata), - Status: status, - Disabled: disabled, - Attributes: map[string]string{"path": path}, + ID: id, + Provider: provider, + FileName: id, + Label: s.labelFor(metadata), + Status: status, + Disabled: disabled, + Attributes: map[string]string{ + cliproxyauth.AttributePath: path, + cliproxyauth.AttributeSource: path, + cliproxyauth.AttributeSourceBackend: cliproxyauth.AuthSourceFile, + }, Metadata: metadata, CreatedAt: info.ModTime(), UpdatedAt: info.ModTime(), diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -357,12 +357,7 @@ provider = "unknown" } - authKind := strings.ToLower(strings.TrimSpace(auth.Attributes["auth_kind"])) - if authKind == "" { - if kind, _ := auth.AccountInfo(); strings.EqualFold(kind, "api_key") { - authKind = "apikey" - } - } + authKind := auth.AuthKind() if authKind == "" { return provider } @@ -1221,12 +1216,7 @@ if providerKey == "" { providerKey = strings.ToLower(strings.TrimSpace(provider)) } - activeAuthKind := strings.ToLower(strings.TrimSpace(activeAuth.Attributes["auth_kind"])) - if activeAuthKind == "" { - if kind, _ := activeAuth.AccountInfo(); strings.EqualFold(kind, "api_key") { - activeAuthKind = "apikey" - } - } + activeAuthKind := activeAuth.AuthKind() activeExcluded := s.oauthExcludedModels(providerKey, activeAuthKind) if a == activeAuth && len(activeExcluded) == 0 { activeExcluded = excluded @@ -1923,12 +1913,7 @@ GlobalModelRegistry().UnregisterClient(a.ID) return } - authKind := strings.ToLower(strings.TrimSpace(a.Attributes["auth_kind"])) - if authKind == "" { - if kind, _ := a.AccountInfo(); strings.EqualFold(kind, "api_key") { - authKind = "apikey" - } - } + authKind := a.AuthKind() // Unregister legacy client ID (if present) to avoid double counting if a.Runtime != nil { if idGetter, ok := a.Runtime.(interface{ GetClientID() string }); ok { diff --git a/internal/registry/models/models.json b/internal/registry/models/models.json --- a/internal/registry/models/models.json +++ b/internal/registry/models/models.json @@ -871,6 +871,197 @@ } } ], + "gemini-cli": [ + { + "id": "gemini-2.5-pro", + "object": "model", + "created": 1750118400, + "owned_by": "google", + "type": "gemini", + "display_name": "Gemini 2.5 Pro", + "name": "models/gemini-2.5-pro", + "version": "2.5", + "description": "Stable release (June 17th, 2025) of Gemini 2.5 Pro", + "inputTokenLimit": 1048576, + "outputTokenLimit": 65536, + "supportedGenerationMethods": [ + "generateContent", + "countTokens", + "createCachedContent", + "batchGenerateContent" + ], + "thinking": { + "min": 128, + "max": 32768, + "dynamic_allowed": true + } + }, + { + "id": "gemini-2.5-flash", + "object": "model", + "created": 1750118400, + "owned_by": "google", + "type": "gemini", + "display_name": "Gemini 2.5 Flash", + "name": "models/gemini-2.5-flash", + "version": "001", + "description": "Stable version of Gemini 2.5 Flash, our mid-size multimodal model that supports up to 1 million tokens, released in June of 2025.", + "inputTokenLimit": 1048576, + "outputTokenLimit": 65536, + "supportedGenerationMethods": [ + "generateContent", + "countTokens", + "createCachedContent", + "batchGenerateContent" + ], + "thinking": { + "max": 24576, + "zero_allowed": true, + "dynamic_allowed": true + } + }, + { + "id": "gemini-2.5-flash-lite", + "object": "model", + "created": 1753142400, + "owned_by": "google", + "type": "gemini", + "display_name": "Gemini 2.5 Flash Lite", + "name": "models/gemini-2.5-flash-lite", + "version": "2.5", + "description": "Our smallest and most cost effective model, built for at scale usage.", + "inputTokenLimit": 1048576, + "outputTokenLimit": 65536, + "supportedGenerationMethods": [ + "generateContent", + "countTokens", + "createCachedContent", + "batchGenerateContent" + ], + "thinking": { + "max": 24576, + "zero_allowed": true, + "dynamic_allowed": true + } + }, + { + "id": "gemini-3-pro-preview", + "object": "model", + "created": 1737158400, + "owned_by": "google", + "type": "gemini", + "display_name": "Gemini 3 Pro Preview", + "name": "models/gemini-3-pro-preview", + "version": "3.0", + "description": "Our most intelligent model with SOTA reasoning and multimodal understanding, and powerful agentic and vibe coding capabilities", + "inputTokenLimit": 1048576, + "outputTokenLimit": 65536, + "supportedGenerationMethods": [ + "generateContent", + "countTokens", + "createCachedContent", + "batchGenerateContent" + ], + "thinking": { + "min": 128, + "max": 32768, + "dynamic_allowed": true, + "levels": [ + "low", + "high" + ] + } + }, + { + "id": "gemini-3.1-pro-preview", + "object": "model", + "created": 1771459200, + "owned_by": "google", + "type": "gemini", + "display_name": "Gemini 3.1 Pro Preview", + "name": "models/gemini-3.1-pro-preview", + "version": "3.1", + "description": "Gemini 3.1 Pro Preview", + "inputTokenLimit": 1048576, + "outputTokenLimit": 65536, + "supportedGenerationMethods": [ + "generateContent", + "countTokens", + "createCachedContent", + "batchGenerateContent" + ], + "thinking": { + "min": 128, + "max": 32768, + "dynamic_allowed": true, + "levels": [ + "low", + "medium", + "high" + ] + } + }, + { + "id": "gemini-3-flash-preview", + "object": "model", + "created": 1765929600, + "owned_by": "google", + "type": "gemini", + "display_name": "Gemini 3 Flash Preview", + "name": "models/gemini-3-flash-preview", + "version": "3.0", + "description": "Our most intelligent model built for speed, combining frontier intelligence with superior search and grounding.", + "inputTokenLimit": 1048576, + "outputTokenLimit": 65536, + "supportedGenerationMethods": [ + "generateContent", + "countTokens", + "createCachedContent", + "batchGenerateContent" + ], + "thinking": { + "min": 128, + "max": 32768, + "dynamic_allowed": true, + "levels": [ + "minimal", + "low", + "medium", + "high" + ] + } + }, + { + "id": "gemini-3.1-flash-lite-preview", + "object": "model", + "created": 1776288000, + "owned_by": "google", + "type": "gemini", + "display_name": "Gemini 3.1 Flash Lite Preview", + "name": "models/gemini-3.1-flash-lite-preview", + "version": "3.1", + "description": "Our smallest and most cost effective model, built for at scale usage.", + "inputTokenLimit": 1048576, + "outputTokenLimit": 65536, + "supportedGenerationMethods": [ + "generateContent", + "countTokens", + "createCachedContent", + "batchGenerateContent" + ], + "thinking": { + "min": 128, + "max": 32768, + "dynamic_allowed": true, + "levels": [ + "minimal", + "low", + "medium", + "high" + ] + } + } + ], "aistudio": [ { "id": "gemini-2.5-pro", @@ -1594,7 +1785,12 @@ "min": 1024, "max": 32000, "zero_allowed": true, - "dynamic_allowed": true + "dynamic_allowed": true, + "levels": [ + "low", + "medium", + "high" + ] } }, { @@ -1604,14 +1800,19 @@ "owned_by": "moonshot", "type": "kimi", "display_name": "Kimi K2.5", - "description": "Kimi K2.5 - Latest Moonshot AI coding model with improved capabilities", - "context_length": 131072, + "description": "Kimi K2.5 - Native multimodal agentic model with text, image, and video input; supports thinking and non-thinking modes", + "context_length": 262144, "max_completion_tokens": 32768, "thinking": { "min": 1024, "max": 32000, "zero_allowed": true, - "dynamic_allowed": true + "dynamic_allowed": true, + "levels": [ + "low", + "medium", + "high" + ] } }, { @@ -1621,14 +1822,19 @@ "owned_by": "moonshot", "type": "kimi", "display_name": "Kimi K2.6", - "description": "Kimi K2.6 - Latest Moonshot AI coding model with improved capabilities", + "description": "Kimi K2.6 - Native multimodal agentic model with stronger long-horizon agentic coding, long-context reasoning, and preserved thinking support", "context_length": 262144, "max_completion_tokens": 65536, "thinking": { "min": 1024, "max": 32000, "zero_allowed": true, - "dynamic_allowed": true + "dynamic_allowed": true, + "levels": [ + "low", + "medium", + "high" + ] } }, { @@ -1645,7 +1851,34 @@ "min": 1024, "max": 32000, "zero_allowed": false, - "dynamic_allowed": true + "dynamic_allowed": true, + "levels": [ + "low", + "medium", + "high" + ] + } + }, + { + "id": "kimi-k2.7-code-highspeed", + "object": "model", + "created": 1780396800, + "owned_by": "moonshot", + "type": "kimi", + "display_name": "Kimi K2.7 Code HighSpeed", + "description": "Kimi K2.7 Code HighSpeed - Same capabilities as Kimi K2.7 Code with higher output speed (~180 tokens/s)", + "context_length": 262144, + "max_completion_tokens": 65536, + "thinking": { + "min": 1024, + "max": 32000, + "zero_allowed": false, + "dynamic_allowed": true, + "levels": [ + "low", + "medium", + "high" + ] } } ], @@ -1972,4 +2205,4 @@ "max_completion_tokens": 32768 } ] -} \ No newline at end of file +} diff --git a/internal/watcher/synthesizer/file.go b/internal/watcher/synthesizer/file.go --- a/internal/watcher/synthesizer/file.go +++ b/internal/watcher/synthesizer/file.go @@ -106,8 +106,9 @@ if auth.Attributes == nil { auth.Attributes = make(map[string]string) } - auth.Attributes["path"] = fullPath - auth.Attributes["source"] = fullPath + auth.Attributes[coreauth.AttributePath] = fullPath + auth.Attributes[coreauth.AttributeSource] = fullPath + auth.Attributes[coreauth.AttributeSourceBackend] = coreauth.AuthSourceFile coreauth.SetOAuthModelAliasesAttribute(auth, perAccountModelAliases) ApplyAuthExcludedModelsMeta(auth, cfg, perAccountExcluded, "oauth") coreauth.ApplyCustomHeadersFromMetadata(auth) @@ -165,8 +166,9 @@ Status: status, Disabled: disabled, Attributes: map[string]string{ - "source": fullPath, - "path": fullPath, + coreauth.AttributeSource: fullPath, + coreauth.AttributePath: fullPath, + coreauth.AttributeSourceBackend: coreauth.AuthSourceFile, }, ProxyURL: proxyURL, Metadata: metadata, diff --git a/sdk/cliproxy/auth/auto_refresh_loop.go b/sdk/cliproxy/auth/auto_refresh_loop.go --- a/sdk/cliproxy/auth/auto_refresh_loop.go +++ b/sdk/cliproxy/auth/auto_refresh_loop.go @@ -343,8 +343,7 @@ return time.Time{}, false } - accountType, _ := auth.AccountInfo() - if accountType == "api_key" { + if auth.AuthKind() == AuthKindAPIKey { return time.Time{}, false } diff --git a/sdk/cliproxy/auth/classification.go b/sdk/cliproxy/auth/classification.go new file mode 100644 --- /dev/null +++ b/sdk/cliproxy/auth/classification.go @@ -0,0 +1,138 @@ +package auth + +import "strings" + +const ( + AuthKindAPIKey = "apikey" + AuthKindOAuth = "oauth" + + AuthSourceConfig = "config" + AuthSourceFile = "file" + AuthSourceGit = "git" + AuthSourceMemory = "memory" + AuthSourceObjectStore = "objectstore" + AuthSourcePostgres = "postgres" + + AttributeAPIKey = "api_key" + AttributeAuthKind = "auth_kind" + AttributePath = "path" + AttributeRuntimeOnly = "runtime_only" + AttributeSource = "source" + AttributeSourceBackend = "source_backend" +) + +// AuthKind returns the credential kind using explicit metadata first and legacy +// field-shape fallbacks second. +func (a *Auth) AuthKind() string { + if a == nil { + return "" + } + if kind := normalizeAuthKind(authAttribute(a, AttributeAuthKind)); kind != "" { + return kind + } + if kind := normalizeAuthKind(authMetadataString(a, AttributeAuthKind)); kind != "" { + return kind + } + if authAttribute(a, AttributeAPIKey) != "" { + return AuthKindAPIKey + } + if authHasOAuthMetadata(a) { + return AuthKindOAuth + } + return "" +} + +// AuthSourceKind returns where the Auth entry came from at runtime. +func (a *Auth) AuthSourceKind() string { + if a == nil { + return "" + } + if strings.EqualFold(authAttribute(a, AttributeRuntimeOnly), "true") { + return AuthSourceMemory + } + if source := normalizeAuthSourceKind(authAttribute(a, AttributeSourceBackend)); source != "" { + return source + } + source := authAttribute(a, AttributeSource) + if source != "" { + sourceLower := strings.ToLower(source) + if strings.HasPrefix(sourceLower, AuthSourceConfig+":") { + return AuthSourceConfig + } + if normalized := normalizeAuthSourceKind(source); normalized != "" { + return normalized + } + return AuthSourceFile + } + if authAttribute(a, AttributePath) != "" { + return AuthSourceFile + } + if strings.TrimSpace(a.FileName) != "" { + return AuthSourceFile + } + return "" +} + +func normalizeAuthKind(kind string) string { + switch strings.ToLower(strings.TrimSpace(kind)) { + case AuthKindAPIKey, "api_key", "api-key": + return AuthKindAPIKey + case AuthKindOAuth, "oauth2": + return AuthKindOAuth + default: + return "" + } +} + +func normalizeAuthSourceKind(source string) string { + switch strings.ToLower(strings.TrimSpace(source)) { + case AuthSourceConfig: + return AuthSourceConfig + case AuthSourceFile, "filesystem": + return AuthSourceFile + case AuthSourceGit: + return AuthSourceGit + case AuthSourceMemory, "runtime", "runtime_only": + return AuthSourceMemory + case AuthSourceObjectStore, "object-store": + return AuthSourceObjectStore + case AuthSourcePostgres, "postgresql", "database", "db": + return AuthSourcePostgres + default: + return "" + } +} + +func authHasOAuthMetadata(auth *Auth) bool { + if auth == nil || len(auth.Metadata) == 0 { + return false + } + for _, key := range []string{"access_token", "refresh_token", "id_token", "email", "token_type", "expires_at", "expired"} { + if authMetadataString(auth, key) != "" { + return true + } + } + if token, ok := auth.Metadata["token"].(map[string]any); ok && len(token) > 0 { + return true + } + return false +} + +func authAttribute(auth *Auth, key string) string { + if auth == nil || auth.Attributes == nil { + return "" + } + return strings.TrimSpace(auth.Attributes[key]) +} + +func authMetadataString(auth *Auth, key string) string { + if auth == nil || auth.Metadata == nil { + return "" + } + switch value := auth.Metadata[key].(type) { + case string: + return strings.TrimSpace(value) + default: + return "" + } +} diff --git a/sdk/cliproxy/auth/classification_test.go b/sdk/cliproxy/auth/classification_test.go new file mode 100644 --- /dev/null +++ b/sdk/cliproxy/auth/classification_test.go @@ -0,0 +1,125 @@ +package auth + +import "testing" + +func TestAuthKind(t *testing.T) { + tests := []struct { + name string + auth *Auth + want string + }{ + { + name: "explicit api key attribute", + auth: &Auth{Attributes: map[string]string{AttributeAuthKind: "api_key"}}, + want: AuthKindAPIKey, + }, + { + name: "explicit oauth attribute wins over api key fallback", + auth: &Auth{Attributes: map[string]string{AttributeAuthKind: "oauth", AttributeAPIKey: "k"}}, + want: AuthKindOAuth, + }, + { + name: "explicit oauth metadata", + auth: &Auth{Metadata: map[string]any{AttributeAuthKind: "oauth"}}, + want: AuthKindOAuth, + }, + { + name: "legacy api key attribute", + auth: &Auth{Attributes: map[string]string{AttributeAPIKey: "k"}}, + want: AuthKindAPIKey, + }, + { + name: "legacy oauth metadata", + auth: &Auth{Metadata: map[string]any{"access_token": "token"}}, + want: AuthKindOAuth, + }, + { + name: "unknown metadata shape", + auth: &Auth{Metadata: map[string]any{"type": "test"}}, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.auth.AuthKind(); got != tt.want { + t.Fatalf("AuthKind() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestAuthSourceKind(t *testing.T) { + tests := []struct { + name string + auth *Auth + want string + }{ + { + name: "runtime only memory", + auth: &Auth{Attributes: map[string]string{AttributeRuntimeOnly: "true", AttributeSourceBackend: AuthSourcePostgres}}, + want: AuthSourceMemory, + }, + { + name: "backend postgres", + auth: &Auth{Attributes: map[string]string{AttributeSourceBackend: "postgresql", AttributePath: "/tmp/auth.json"}}, + want: AuthSourcePostgres, + }, + { + name: "backend object store", + auth: &Auth{Attributes: map[string]string{AttributeSourceBackend: "object-store", AttributePath: "/tmp/auth.json"}}, + want: AuthSourceObjectStore, + }, + { + name: "config source", + auth: &Auth{Attributes: map[string]string{AttributeSource: "config:codex[abc]"}}, + want: AuthSourceConfig, + }, + { + name: "path source", + auth: &Auth{Attributes: map[string]string{AttributeSource: "/tmp/auth.json"}}, + want: AuthSourceFile, + }, + { + name: "path attribute", + auth: &Auth{Attributes: map[string]string{AttributePath: "/tmp/auth.json"}}, + want: AuthSourceFile, + }, + { + name: "filename fallback", + auth: &Auth{FileName: "codex.json"}, + want: AuthSourceFile, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.auth.AuthSourceKind(); got != tt.want { + t.Fatalf("AuthSourceKind() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestAccountInfoUsesAuthKind(t *testing.T) { + apiKeyAuth := &Auth{Attributes: map[string]string{AttributeAuthKind: "api-key", AttributeAPIKey: "k"}} + kind, value := apiKeyAuth.AccountInfo() + if kind != "api_key" || value != "k" { + t.Fatalf("api key AccountInfo() = %q, %q", kind, value) + } + + oauthAuth := &Auth{ + Attributes: map[string]string{AttributeAuthKind: AuthKindOAuth, AttributeAPIKey: "k"}, + Metadata: map[string]any{"email": "user@example.com"}, + } + kind, value = oauthAuth.AccountInfo() + if kind != "oauth" || value != "user@example.com" { + t.Fatalf("oauth AccountInfo() = %q, %q", kind, value) + } + + oauthWithoutEmail := &Auth{Metadata: map[string]any{"access_token": "token"}} + kind, value = oauthWithoutEmail.AccountInfo() + if kind != "oauth" || value != "" { + t.Fatalf("oauth without email AccountInfo() = %q, %q", kind, value) + } +} diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -1004,8 +1004,7 @@ if auth == nil { return false } - kind, _ := auth.AccountInfo() - return strings.EqualFold(strings.TrimSpace(kind), "api_key") + return auth.AuthKind() == AuthKindAPIKey } func isOpenAICompatAPIKeyAuth(auth *Auth) bool { @@ -1771,8 +1770,7 @@ if strings.TrimSpace(auth.ID) == "" { continue } - kind, _ := auth.AccountInfo() - if !strings.EqualFold(strings.TrimSpace(kind), "api_key") { + if auth.AuthKind() != AuthKindAPIKey { continue } @@ -2868,8 +2866,7 @@ return requestedModel } - kind, _ := auth.AccountInfo() - if !strings.EqualFold(strings.TrimSpace(kind), "api_key") { + if auth.AuthKind() != AuthKindAPIKey { return requestedModel } diff --git a/sdk/cliproxy/auth/config_apikey.go b/sdk/cliproxy/auth/config_apikey.go --- a/sdk/cliproxy/auth/config_apikey.go +++ b/sdk/cliproxy/auth/config_apikey.go @@ -1,14 +1,15 @@ package auth -import "strings" - // IsConfigAPIKeyAuth reports whether the auth entry is synthesized from config *-api-key lists. func IsConfigAPIKeyAuth(auth *Auth) bool { - if auth == nil || auth.Attributes == nil { + if auth == nil { return false } - if strings.TrimSpace(auth.Attributes["api_key"]) == "" { + if auth.AuthKind() != AuthKindAPIKey { return false } - return strings.HasPrefix(strings.ToLower(strings.TrimSpace(auth.Attributes["source"])), "config:") + if auth.AuthSourceKind() != AuthSourceConfig { + return false + } + return authAttribute(auth, AttributeAPIKey) != "" } diff --git a/sdk/cliproxy/auth/config_apikey_test.go b/sdk/cliproxy/auth/config_apikey_test.go --- a/sdk/cliproxy/auth/config_apikey_test.go +++ b/sdk/cliproxy/auth/config_apikey_test.go @@ -9,6 +9,17 @@ if IsConfigAPIKeyAuth(&Auth{Attributes: map[string]string{"source": "config:codex[x]"}}) { t.Fatal("expected missing api_key to be false") } + if IsConfigAPIKeyAuth(&Auth{ + ID: "codex:oauth:abc", + Provider: "codex", + Attributes: map[string]string{ + "auth_kind": "oauth", + "api_key": "k", + "source": "config:codex[abc]", + }, + }) { + t.Fatal("expected explicit oauth auth to be false") + } if !IsConfigAPIKeyAuth(&Auth{ ID: "codex:apikey:abc", Provider: "codex", diff --git a/sdk/cliproxy/auth/oauth_model_alias.go b/sdk/cliproxy/auth/oauth_model_alias.go --- a/sdk/cliproxy/auth/oauth_model_alias.go +++ b/sdk/cliproxy/auth/oauth_model_alias.go @@ -350,15 +350,7 @@ return "" } provider := strings.ToLower(strings.TrimSpace(auth.Provider)) - authKind := "" - if auth.Attributes != nil { - authKind = strings.ToLower(strings.TrimSpace(auth.Attributes["auth_kind"])) - } - if authKind == "" { - if kind, _ := auth.AccountInfo(); strings.EqualFold(kind, "api_key") { - authKind = "apikey" - } - } + authKind := auth.AuthKind() return OAuthModelAliasChannel(provider, authKind) } diff --git a/sdk/cliproxy/auth/types.go b/sdk/cliproxy/auth/types.go --- a/sdk/cliproxy/auth/types.go +++ b/sdk/cliproxy/auth/types.go @@ -559,22 +559,25 @@ if a == nil { return "", "" } - // Check metadata for email first (OAuth-style auth) - if a.Metadata != nil { - if v, ok := a.Metadata["email"].(string); ok { - email := strings.TrimSpace(v) - if email != "" { - return "oauth", email + switch a.AuthKind() { + case AuthKindOAuth: + if a.Metadata != nil { + if v, ok := a.Metadata["email"].(string); ok { + email := strings.TrimSpace(v) + if email != "" { + return "oauth", email + } } } - } - // Fall back to API key (API-key auth) - if a.Attributes != nil { - if v := a.Attributes["api_key"]; v != "" { - return "api_key", v + return "oauth", "" + case AuthKindAPIKey: + if apiKey := authAttribute(a, AttributeAPIKey); apiKey != "" { + return "api_key", apiKey } + return "api_key", "" + default: + return "", "" } - return "", "" } // ExpirationTime attempts to extract the credential expiration timestamp from metadata. diff --git a/internal/runtime/executor/helps/usage_helpers.go b/internal/runtime/executor/helps/usage_helpers.go --- a/internal/runtime/executor/helps/usage_helpers.go +++ b/internal/runtime/executor/helps/usage_helpers.go @@ -444,12 +444,7 @@ if auth == nil { return "" } - kind, _ := auth.AccountInfo() - kind = strings.TrimSpace(kind) - if kind == "api_key" { - return "apikey" - } - return kind + return auth.AuthKind() } func ParseCodexUsage(data []byte) (usage.Detail, bool) {