mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-10 18:36:47 +08:00
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:
@@ -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 == "" {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) != ""
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user