From 894baad829f5f5a53411edc0d1af4f1af9f9d60d Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sat, 18 Apr 2026 16:44:33 +0800 Subject: [PATCH] feat(api): integrate auth index into key retrieval endpoints for Gemini, Claude, Codex, OpenAI, and Vertex --- .../handlers/management/config_auth_index.go | 245 ++++++++++++++++++ .../api/handlers/management/config_lists.go | 10 +- 2 files changed, 250 insertions(+), 5 deletions(-) create mode 100644 internal/api/handlers/management/config_auth_index.go diff --git a/internal/api/handlers/management/config_auth_index.go b/internal/api/handlers/management/config_auth_index.go new file mode 100644 index 00000000..51f71aac --- /dev/null +++ b/internal/api/handlers/management/config_auth_index.go @@ -0,0 +1,245 @@ +package management + +import ( + "strings" + "time" + + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer" +) + +type configAuthIndexViews struct { + gemini []string + claude []string + codex []string + vertex []string + openAIEntries [][]string + openAIFallback []string +} + +type geminiKeyWithAuthIndex struct { + config.GeminiKey + AuthIndex string `json:"auth-index,omitempty"` +} + +type claudeKeyWithAuthIndex struct { + config.ClaudeKey + AuthIndex string `json:"auth-index,omitempty"` +} + +type codexKeyWithAuthIndex struct { + config.CodexKey + AuthIndex string `json:"auth-index,omitempty"` +} + +type vertexCompatKeyWithAuthIndex struct { + config.VertexCompatKey + AuthIndex string `json:"auth-index,omitempty"` +} + +type openAICompatibilityAPIKeyWithAuthIndex struct { + config.OpenAICompatibilityAPIKey + AuthIndex string `json:"auth-index,omitempty"` +} + +type openAICompatibilityWithAuthIndex struct { + Name string `json:"name"` + Priority int `json:"priority,omitempty"` + Prefix string `json:"prefix,omitempty"` + BaseURL string `json:"base-url"` + APIKeyEntries []openAICompatibilityAPIKeyWithAuthIndex `json:"api-key-entries,omitempty"` + Models []config.OpenAICompatibilityModel `json:"models,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + AuthIndex string `json:"auth-index,omitempty"` +} + +func (h *Handler) buildConfigAuthIndexViews() configAuthIndexViews { + cfg := h.cfg + if cfg == nil { + return configAuthIndexViews{} + } + + liveIndexByID := map[string]string{} + if h != nil && h.authManager != nil { + for _, auth := range h.authManager.List() { + if auth == nil || strings.TrimSpace(auth.ID) == "" { + continue + } + auth.EnsureIndex() + if auth.Index == "" { + continue + } + liveIndexByID[auth.ID] = auth.Index + } + } + + views := configAuthIndexViews{ + gemini: make([]string, len(cfg.GeminiKey)), + claude: make([]string, len(cfg.ClaudeKey)), + codex: make([]string, len(cfg.CodexKey)), + vertex: make([]string, len(cfg.VertexCompatAPIKey)), + openAIEntries: make([][]string, len(cfg.OpenAICompatibility)), + openAIFallback: make([]string, len(cfg.OpenAICompatibility)), + } + + auths, errSynthesize := synthesizer.NewConfigSynthesizer().Synthesize(&synthesizer.SynthesisContext{ + Config: cfg, + Now: time.Now(), + IDGenerator: synthesizer.NewStableIDGenerator(), + }) + if errSynthesize != nil { + return views + } + + cursor := 0 + nextAuthIndex := func() string { + if cursor >= len(auths) { + return "" + } + auth := auths[cursor] + cursor++ + if auth == nil || strings.TrimSpace(auth.ID) == "" { + return "" + } + // Do not expose an auth-index until it is present in the live auth manager. + // API tools resolve auth_index against h.authManager.List(), so returning + // config-only indexes can temporarily break tool calls around config edits. + return liveIndexByID[auth.ID] + } + + for i := range cfg.GeminiKey { + if strings.TrimSpace(cfg.GeminiKey[i].APIKey) == "" { + continue + } + views.gemini[i] = nextAuthIndex() + } + for i := range cfg.ClaudeKey { + if strings.TrimSpace(cfg.ClaudeKey[i].APIKey) == "" { + continue + } + views.claude[i] = nextAuthIndex() + } + for i := range cfg.CodexKey { + if strings.TrimSpace(cfg.CodexKey[i].APIKey) == "" { + continue + } + views.codex[i] = nextAuthIndex() + } + for i := range cfg.OpenAICompatibility { + entries := cfg.OpenAICompatibility[i].APIKeyEntries + if len(entries) == 0 { + views.openAIFallback[i] = nextAuthIndex() + continue + } + + views.openAIEntries[i] = make([]string, len(entries)) + for j := range entries { + views.openAIEntries[i][j] = nextAuthIndex() + } + } + for i := range cfg.VertexCompatAPIKey { + if strings.TrimSpace(cfg.VertexCompatAPIKey[i].APIKey) == "" { + continue + } + views.vertex[i] = nextAuthIndex() + } + + return views +} + +func (h *Handler) geminiKeysWithAuthIndex() []geminiKeyWithAuthIndex { + if h == nil || h.cfg == nil { + return nil + } + views := h.buildConfigAuthIndexViews() + out := make([]geminiKeyWithAuthIndex, len(h.cfg.GeminiKey)) + for i := range h.cfg.GeminiKey { + out[i] = geminiKeyWithAuthIndex{ + GeminiKey: h.cfg.GeminiKey[i], + AuthIndex: views.gemini[i], + } + } + return out +} + +func (h *Handler) claudeKeysWithAuthIndex() []claudeKeyWithAuthIndex { + if h == nil || h.cfg == nil { + return nil + } + views := h.buildConfigAuthIndexViews() + out := make([]claudeKeyWithAuthIndex, len(h.cfg.ClaudeKey)) + for i := range h.cfg.ClaudeKey { + out[i] = claudeKeyWithAuthIndex{ + ClaudeKey: h.cfg.ClaudeKey[i], + AuthIndex: views.claude[i], + } + } + return out +} + +func (h *Handler) codexKeysWithAuthIndex() []codexKeyWithAuthIndex { + if h == nil || h.cfg == nil { + return nil + } + views := h.buildConfigAuthIndexViews() + out := make([]codexKeyWithAuthIndex, len(h.cfg.CodexKey)) + for i := range h.cfg.CodexKey { + out[i] = codexKeyWithAuthIndex{ + CodexKey: h.cfg.CodexKey[i], + AuthIndex: views.codex[i], + } + } + return out +} + +func (h *Handler) vertexCompatKeysWithAuthIndex() []vertexCompatKeyWithAuthIndex { + if h == nil || h.cfg == nil { + return nil + } + views := h.buildConfigAuthIndexViews() + out := make([]vertexCompatKeyWithAuthIndex, len(h.cfg.VertexCompatAPIKey)) + for i := range h.cfg.VertexCompatAPIKey { + out[i] = vertexCompatKeyWithAuthIndex{ + VertexCompatKey: h.cfg.VertexCompatAPIKey[i], + AuthIndex: views.vertex[i], + } + } + return out +} + +func (h *Handler) openAICompatibilityWithAuthIndex() []openAICompatibilityWithAuthIndex { + if h == nil || h.cfg == nil { + return nil + } + + views := h.buildConfigAuthIndexViews() + normalized := normalizedOpenAICompatibilityEntries(h.cfg.OpenAICompatibility) + out := make([]openAICompatibilityWithAuthIndex, len(normalized)) + for i := range normalized { + entry := normalized[i] + response := openAICompatibilityWithAuthIndex{ + Name: entry.Name, + Priority: entry.Priority, + Prefix: entry.Prefix, + BaseURL: entry.BaseURL, + Models: entry.Models, + Headers: entry.Headers, + AuthIndex: views.openAIFallback[i], + } + if len(entry.APIKeyEntries) > 0 { + response.APIKeyEntries = make([]openAICompatibilityAPIKeyWithAuthIndex, len(entry.APIKeyEntries)) + for j := range entry.APIKeyEntries { + authIndex := "" + if i < len(views.openAIEntries) && j < len(views.openAIEntries[i]) { + authIndex = views.openAIEntries[i][j] + } + response.APIKeyEntries[j] = openAICompatibilityAPIKeyWithAuthIndex{ + OpenAICompatibilityAPIKey: entry.APIKeyEntries[j], + AuthIndex: authIndex, + } + } + } + out[i] = response + } + return out +} diff --git a/internal/api/handlers/management/config_lists.go b/internal/api/handlers/management/config_lists.go index fbaad956..8d384133 100644 --- a/internal/api/handlers/management/config_lists.go +++ b/internal/api/handlers/management/config_lists.go @@ -120,7 +120,7 @@ func (h *Handler) DeleteAPIKeys(c *gin.Context) { // gemini-api-key: []GeminiKey func (h *Handler) GetGeminiKeys(c *gin.Context) { - c.JSON(200, gin.H{"gemini-api-key": h.cfg.GeminiKey}) + c.JSON(200, gin.H{"gemini-api-key": h.geminiKeysWithAuthIndex()}) } func (h *Handler) PutGeminiKeys(c *gin.Context) { data, err := c.GetRawData() @@ -270,7 +270,7 @@ func (h *Handler) DeleteGeminiKey(c *gin.Context) { // claude-api-key: []ClaudeKey func (h *Handler) GetClaudeKeys(c *gin.Context) { - c.JSON(200, gin.H{"claude-api-key": h.cfg.ClaudeKey}) + c.JSON(200, gin.H{"claude-api-key": h.claudeKeysWithAuthIndex()}) } func (h *Handler) PutClaudeKeys(c *gin.Context) { data, err := c.GetRawData() @@ -414,7 +414,7 @@ func (h *Handler) DeleteClaudeKey(c *gin.Context) { // openai-compatibility: []OpenAICompatibility func (h *Handler) GetOpenAICompat(c *gin.Context) { - c.JSON(200, gin.H{"openai-compatibility": normalizedOpenAICompatibilityEntries(h.cfg.OpenAICompatibility)}) + c.JSON(200, gin.H{"openai-compatibility": h.openAICompatibilityWithAuthIndex()}) } func (h *Handler) PutOpenAICompat(c *gin.Context) { data, err := c.GetRawData() @@ -540,7 +540,7 @@ func (h *Handler) DeleteOpenAICompat(c *gin.Context) { // vertex-api-key: []VertexCompatKey func (h *Handler) GetVertexCompatKeys(c *gin.Context) { - c.JSON(200, gin.H{"vertex-api-key": h.cfg.VertexCompatAPIKey}) + c.JSON(200, gin.H{"vertex-api-key": h.vertexCompatKeysWithAuthIndex()}) } func (h *Handler) PutVertexCompatKeys(c *gin.Context) { data, err := c.GetRawData() @@ -886,7 +886,7 @@ func (h *Handler) DeleteOAuthModelAlias(c *gin.Context) { // codex-api-key: []CodexKey func (h *Handler) GetCodexKeys(c *gin.Context) { - c.JSON(200, gin.H{"codex-api-key": h.cfg.CodexKey}) + c.JSON(200, gin.H{"codex-api-key": h.codexKeysWithAuthIndex()}) } func (h *Handler) PutCodexKeys(c *gin.Context) { data, err := c.GetRawData()