feat(auth): refactor credential kind detection and add dynamic source classification

- Introduced `AuthKind` and `AuthSourceKind` methods for improved credential type and source classification.
- Replaced legacy fallback logic with normalized attribute-based handling in `AuthKind`.
- Consolidated metadata inspection for both API key and OAuth attributes.
- Updated calls to replace direct `AccountInfo` usage with `AuthKind` and `AuthSourceKind`.
- Enhanced unit tests to validate explicit and fallback credential resolution scenarios.
This commit is contained in:
Luis Pater
2026-06-23 23:21:33 +08:00
parent b89c594aac
commit 70053beadb
17 changed files with 596 additions and 95 deletions
+5 -4
View File
@@ -549,12 +549,13 @@ func pluginAuthDataToCoreAuth(data pluginapi.AuthData, path, fileName string, au
}
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 == "" {
+241 -8
View File
@@ -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
}
]
}
}
@@ -444,12 +444,7 @@ func resolveUsageAuthType(auth *cliproxyauth.Auth) string {
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) {
+11 -7
View File
@@ -324,7 +324,8 @@ func (s *GitTokenStore) Save(_ context.Context, auth *cliproxyauth.Auth) (string
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 @@ func (s *GitTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth,
}
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(),
+6 -2
View File
@@ -221,7 +221,8 @@ func (s *ObjectTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (s
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 @@ func (s *ObjectTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Aut
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
}
+6 -2
View File
@@ -251,7 +251,8 @@ func (s *PostgresStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (stri
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 @@ func (s *PostgresStore) List(ctx context.Context) ([]*cliproxyauth.Auth, error)
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
}
+6 -4
View File
@@ -106,8 +106,9 @@ func synthesizeFileAuths(ctx *SynthesisContext, fullPath string, data []byte) []
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 @@ func synthesizeFileAuths(ctx *SynthesisContext, fullPath string, data []byte) []
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,
+17 -10
View File
@@ -151,7 +151,9 @@ func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (str
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 @@ func (s *FileTokenStore) readAuthFiles(path, baseDir string) ([]*cliproxyauth.Au
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 @@ func (s *FileTokenStore) readAuthFiles(path, baseDir string) ([]*cliproxyauth.Au
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(),
+1 -2
View File
@@ -343,8 +343,7 @@ func nextRefreshCheckAt(now time.Time, auth *Auth, interval time.Duration) (time
return time.Time{}, false
}
accountType, _ := auth.AccountInfo()
if accountType == "api_key" {
if auth.AuthKind() == AuthKindAPIKey {
return time.Time{}, false
}
+138
View File
@@ -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 ""
}
}
+125
View File
@@ -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": "[email protected]"},
}
kind, value = oauthAuth.AccountInfo()
if kind != "oauth" || value != "[email protected]" {
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)
}
}
+3 -6
View File
@@ -1004,8 +1004,7 @@ func isAPIKeyAuth(auth *Auth) bool {
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 @@ func (m *Manager) rebuildAPIKeyModelAliasLocked(cfg *internalconfig.Config) {
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 @@ func (m *Manager) applyAPIKeyModelAlias(auth *Auth, requestedModel string) strin
return requestedModel
}
kind, _ := auth.AccountInfo()
if !strings.EqualFold(strings.TrimSpace(kind), "api_key") {
if auth.AuthKind() != AuthKindAPIKey {
return requestedModel
}
+6 -5
View File
@@ -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) != ""
}
+11
View File
@@ -9,6 +9,17 @@ func TestIsConfigAPIKeyAuth(t *testing.T) {
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",
+1 -9
View File
@@ -350,15 +350,7 @@ func modelAliasChannel(auth *Auth) string {
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)
}
+15 -12
View File
@@ -559,22 +559,25 @@ func (a *Auth) AccountInfo() (string, string) {
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.
+3 -18
View File
@@ -357,12 +357,7 @@ func modelRegistrationCategory(auth *coreauth.Auth) string {
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 @@ func (s *Service) tryRegisterPluginModelsForAuth(ctx context.Context, a *coreaut
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 @@ func (s *Service) registerModelsForAuthWithCache(ctx context.Context, a *coreaut
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 {