mirror of
https://mirror.skon.top/github.com/router-for-me/CLIProxyAPI
synced 2026-04-22 01:30:37 +08:00
fix(management): stabilize auth-index mapping
This commit is contained in:
@@ -1,22 +1,13 @@
|
||||
package management
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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"`
|
||||
@@ -53,170 +44,174 @@ type openAICompatibilityWithAuthIndex struct {
|
||||
AuthIndex string `json:"auth-index,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Handler) buildConfigAuthIndexViews() configAuthIndexViews {
|
||||
cfg := h.cfg
|
||||
if cfg == nil {
|
||||
return configAuthIndexViews{}
|
||||
func (h *Handler) liveAuthIndexByID() map[string]string {
|
||||
out := map[string]string{}
|
||||
if h == nil {
|
||||
return out
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
h.mu.Lock()
|
||||
manager := h.authManager
|
||||
h.mu.Unlock()
|
||||
if manager == nil {
|
||||
return out
|
||||
}
|
||||
|
||||
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) == "" {
|
||||
// authManager.List() returns clones, so EnsureIndex only affects these copies.
|
||||
for _, auth := range manager.List() {
|
||||
if auth == nil {
|
||||
continue
|
||||
}
|
||||
views.gemini[i] = nextAuthIndex()
|
||||
}
|
||||
for i := range cfg.ClaudeKey {
|
||||
if strings.TrimSpace(cfg.ClaudeKey[i].APIKey) == "" {
|
||||
id := strings.TrimSpace(auth.ID)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
views.claude[i] = nextAuthIndex()
|
||||
}
|
||||
for i := range cfg.CodexKey {
|
||||
if strings.TrimSpace(cfg.CodexKey[i].APIKey) == "" {
|
||||
idx := strings.TrimSpace(auth.Index)
|
||||
if idx == "" {
|
||||
idx = auth.EnsureIndex()
|
||||
}
|
||||
if idx == "" {
|
||||
continue
|
||||
}
|
||||
views.codex[i] = nextAuthIndex()
|
||||
out[id] = idx
|
||||
}
|
||||
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
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *Handler) geminiKeysWithAuthIndex() []geminiKeyWithAuthIndex {
|
||||
if h == nil || h.cfg == nil {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
views := h.buildConfigAuthIndexViews()
|
||||
liveIndexByID := h.liveAuthIndexByID()
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
idGen := synthesizer.NewStableIDGenerator()
|
||||
out := make([]geminiKeyWithAuthIndex, len(h.cfg.GeminiKey))
|
||||
for i := range h.cfg.GeminiKey {
|
||||
entry := h.cfg.GeminiKey[i]
|
||||
authIndex := ""
|
||||
if key := strings.TrimSpace(entry.APIKey); key != "" {
|
||||
id, _ := idGen.Next("gemini:apikey", key, entry.BaseURL)
|
||||
authIndex = liveIndexByID[id]
|
||||
}
|
||||
out[i] = geminiKeyWithAuthIndex{
|
||||
GeminiKey: h.cfg.GeminiKey[i],
|
||||
AuthIndex: views.gemini[i],
|
||||
GeminiKey: entry,
|
||||
AuthIndex: authIndex,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *Handler) claudeKeysWithAuthIndex() []claudeKeyWithAuthIndex {
|
||||
if h == nil || h.cfg == nil {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
views := h.buildConfigAuthIndexViews()
|
||||
liveIndexByID := h.liveAuthIndexByID()
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
idGen := synthesizer.NewStableIDGenerator()
|
||||
out := make([]claudeKeyWithAuthIndex, len(h.cfg.ClaudeKey))
|
||||
for i := range h.cfg.ClaudeKey {
|
||||
entry := h.cfg.ClaudeKey[i]
|
||||
authIndex := ""
|
||||
if key := strings.TrimSpace(entry.APIKey); key != "" {
|
||||
id, _ := idGen.Next("claude:apikey", key, entry.BaseURL)
|
||||
authIndex = liveIndexByID[id]
|
||||
}
|
||||
out[i] = claudeKeyWithAuthIndex{
|
||||
ClaudeKey: h.cfg.ClaudeKey[i],
|
||||
AuthIndex: views.claude[i],
|
||||
ClaudeKey: entry,
|
||||
AuthIndex: authIndex,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *Handler) codexKeysWithAuthIndex() []codexKeyWithAuthIndex {
|
||||
if h == nil || h.cfg == nil {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
views := h.buildConfigAuthIndexViews()
|
||||
liveIndexByID := h.liveAuthIndexByID()
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
idGen := synthesizer.NewStableIDGenerator()
|
||||
out := make([]codexKeyWithAuthIndex, len(h.cfg.CodexKey))
|
||||
for i := range h.cfg.CodexKey {
|
||||
entry := h.cfg.CodexKey[i]
|
||||
authIndex := ""
|
||||
if key := strings.TrimSpace(entry.APIKey); key != "" {
|
||||
id, _ := idGen.Next("codex:apikey", key, entry.BaseURL)
|
||||
authIndex = liveIndexByID[id]
|
||||
}
|
||||
out[i] = codexKeyWithAuthIndex{
|
||||
CodexKey: h.cfg.CodexKey[i],
|
||||
AuthIndex: views.codex[i],
|
||||
CodexKey: entry,
|
||||
AuthIndex: authIndex,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *Handler) vertexCompatKeysWithAuthIndex() []vertexCompatKeyWithAuthIndex {
|
||||
if h == nil || h.cfg == nil {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
views := h.buildConfigAuthIndexViews()
|
||||
liveIndexByID := h.liveAuthIndexByID()
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
idGen := synthesizer.NewStableIDGenerator()
|
||||
out := make([]vertexCompatKeyWithAuthIndex, len(h.cfg.VertexCompatAPIKey))
|
||||
for i := range h.cfg.VertexCompatAPIKey {
|
||||
entry := h.cfg.VertexCompatAPIKey[i]
|
||||
id, _ := idGen.Next("vertex:apikey", entry.APIKey, entry.BaseURL, entry.ProxyURL)
|
||||
authIndex := liveIndexByID[id]
|
||||
out[i] = vertexCompatKeyWithAuthIndex{
|
||||
VertexCompatKey: h.cfg.VertexCompatAPIKey[i],
|
||||
AuthIndex: views.vertex[i],
|
||||
VertexCompatKey: entry,
|
||||
AuthIndex: authIndex,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *Handler) openAICompatibilityWithAuthIndex() []openAICompatibilityWithAuthIndex {
|
||||
if h == nil || h.cfg == nil {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
liveIndexByID := h.liveAuthIndexByID()
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
views := h.buildConfigAuthIndexViews()
|
||||
normalized := normalizedOpenAICompatibilityEntries(h.cfg.OpenAICompatibility)
|
||||
out := make([]openAICompatibilityWithAuthIndex, len(normalized))
|
||||
idGen := synthesizer.NewStableIDGenerator()
|
||||
for i := range normalized {
|
||||
entry := normalized[i]
|
||||
providerName := strings.ToLower(strings.TrimSpace(entry.Name))
|
||||
if providerName == "" {
|
||||
providerName = "openai-compatibility"
|
||||
}
|
||||
idKind := fmt.Sprintf("openai-compatibility:%s", providerName)
|
||||
|
||||
response := openAICompatibilityWithAuthIndex{
|
||||
Name: entry.Name,
|
||||
Priority: entry.Priority,
|
||||
@@ -224,18 +219,19 @@ func (h *Handler) openAICompatibilityWithAuthIndex() []openAICompatibilityWithAu
|
||||
BaseURL: entry.BaseURL,
|
||||
Models: entry.Models,
|
||||
Headers: entry.Headers,
|
||||
AuthIndex: views.openAIFallback[i],
|
||||
AuthIndex: "",
|
||||
}
|
||||
if len(entry.APIKeyEntries) > 0 {
|
||||
if len(entry.APIKeyEntries) == 0 {
|
||||
id, _ := idGen.Next(idKind, entry.BaseURL)
|
||||
response.AuthIndex = liveIndexByID[id]
|
||||
} else {
|
||||
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]
|
||||
}
|
||||
apiKeyEntry := entry.APIKeyEntries[j]
|
||||
id, _ := idGen.Next(idKind, apiKeyEntry.APIKey, entry.BaseURL, apiKeyEntry.ProxyURL)
|
||||
response.APIKeyEntries[j] = openAICompatibilityAPIKeyWithAuthIndex{
|
||||
OpenAICompatibilityAPIKey: entry.APIKeyEntries[j],
|
||||
AuthIndex: authIndex,
|
||||
OpenAICompatibilityAPIKey: apiKeyEntry,
|
||||
AuthIndex: liveIndexByID[id],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
250
internal/api/handlers/management/config_auth_index_test.go
Normal file
250
internal/api/handlers/management/config_auth_index_test.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package management
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer"
|
||||
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||
)
|
||||
|
||||
func synthesizeConfigAuths(t *testing.T, cfg *config.Config) []*coreauth.Auth {
|
||||
t.Helper()
|
||||
|
||||
auths, errSynthesize := synthesizer.NewConfigSynthesizer().Synthesize(&synthesizer.SynthesisContext{
|
||||
Config: cfg,
|
||||
Now: time.Unix(0, 0),
|
||||
IDGenerator: synthesizer.NewStableIDGenerator(),
|
||||
})
|
||||
if errSynthesize != nil {
|
||||
t.Fatalf("synthesize config auths: %v", errSynthesize)
|
||||
}
|
||||
return auths
|
||||
}
|
||||
|
||||
func findAuth(t *testing.T, auths []*coreauth.Auth, predicate func(*coreauth.Auth) bool) *coreauth.Auth {
|
||||
t.Helper()
|
||||
for _, auth := range auths {
|
||||
if predicate(auth) {
|
||||
return auth
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestConfigAuthIndexResolvesLiveIndexes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{
|
||||
GeminiKey: []config.GeminiKey{
|
||||
{APIKey: "shared-key", BaseURL: "https://a.example.com"},
|
||||
{APIKey: "shared-key", BaseURL: "https://b.example.com"},
|
||||
},
|
||||
ClaudeKey: []config.ClaudeKey{
|
||||
{APIKey: "claude-key", BaseURL: "https://claude.example.com"},
|
||||
},
|
||||
CodexKey: []config.CodexKey{
|
||||
{APIKey: "codex-key", BaseURL: "https://codex.example.com/v1"},
|
||||
},
|
||||
VertexCompatAPIKey: []config.VertexCompatKey{
|
||||
{APIKey: "vertex-key", BaseURL: "https://vertex.example.com", ProxyURL: "http://proxy.example.com:8080"},
|
||||
},
|
||||
OpenAICompatibility: []config.OpenAICompatibility{
|
||||
{
|
||||
Name: "bohe",
|
||||
BaseURL: "https://bohe.example.com/v1",
|
||||
APIKeyEntries: []config.OpenAICompatibilityAPIKey{
|
||||
{APIKey: "compat-key"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
auths := synthesizeConfigAuths(t, cfg)
|
||||
manager := coreauth.NewManager(nil, nil, nil)
|
||||
for _, auth := range auths {
|
||||
if auth == nil {
|
||||
continue
|
||||
}
|
||||
if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
|
||||
t.Fatalf("register auth %q: %v", auth.ID, errRegister)
|
||||
}
|
||||
}
|
||||
|
||||
h := &Handler{cfg: cfg, authManager: manager}
|
||||
|
||||
geminiAuthA := findAuth(t, auths, func(auth *coreauth.Auth) bool {
|
||||
if auth == nil {
|
||||
return false
|
||||
}
|
||||
return auth.Provider == "gemini" && auth.Attributes["api_key"] == "shared-key" && auth.Attributes["base_url"] == "https://a.example.com"
|
||||
})
|
||||
if geminiAuthA == nil {
|
||||
t.Fatal("expected synthesized gemini auth (base a)")
|
||||
}
|
||||
geminiAuthB := findAuth(t, auths, func(auth *coreauth.Auth) bool {
|
||||
if auth == nil {
|
||||
return false
|
||||
}
|
||||
return auth.Provider == "gemini" && auth.Attributes["api_key"] == "shared-key" && auth.Attributes["base_url"] == "https://b.example.com"
|
||||
})
|
||||
if geminiAuthB == nil {
|
||||
t.Fatal("expected synthesized gemini auth (base b)")
|
||||
}
|
||||
|
||||
gemini := h.geminiKeysWithAuthIndex()
|
||||
if len(gemini) != 2 {
|
||||
t.Fatalf("gemini keys = %d, want 2", len(gemini))
|
||||
}
|
||||
if got, want := gemini[0].AuthIndex, geminiAuthA.EnsureIndex(); got != want {
|
||||
t.Fatalf("gemini[0] auth-index = %q, want %q", got, want)
|
||||
}
|
||||
if got, want := gemini[1].AuthIndex, geminiAuthB.EnsureIndex(); got != want {
|
||||
t.Fatalf("gemini[1] auth-index = %q, want %q", got, want)
|
||||
}
|
||||
if gemini[0].AuthIndex == gemini[1].AuthIndex {
|
||||
t.Fatalf("duplicate gemini entries returned the same auth-index %q", gemini[0].AuthIndex)
|
||||
}
|
||||
|
||||
claudeAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool {
|
||||
if auth == nil {
|
||||
return false
|
||||
}
|
||||
return auth.Provider == "claude" && auth.Attributes["api_key"] == "claude-key"
|
||||
})
|
||||
if claudeAuth == nil {
|
||||
t.Fatal("expected synthesized claude auth")
|
||||
}
|
||||
|
||||
claude := h.claudeKeysWithAuthIndex()
|
||||
if len(claude) != 1 {
|
||||
t.Fatalf("claude keys = %d, want 1", len(claude))
|
||||
}
|
||||
if got, want := claude[0].AuthIndex, claudeAuth.EnsureIndex(); got != want {
|
||||
t.Fatalf("claude auth-index = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
codexAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool {
|
||||
if auth == nil {
|
||||
return false
|
||||
}
|
||||
return auth.Provider == "codex" && auth.Attributes["api_key"] == "codex-key"
|
||||
})
|
||||
if codexAuth == nil {
|
||||
t.Fatal("expected synthesized codex auth")
|
||||
}
|
||||
|
||||
codex := h.codexKeysWithAuthIndex()
|
||||
if len(codex) != 1 {
|
||||
t.Fatalf("codex keys = %d, want 1", len(codex))
|
||||
}
|
||||
if got, want := codex[0].AuthIndex, codexAuth.EnsureIndex(); got != want {
|
||||
t.Fatalf("codex auth-index = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
vertexAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool {
|
||||
if auth == nil {
|
||||
return false
|
||||
}
|
||||
return auth.Provider == "vertex" && auth.Attributes["api_key"] == "vertex-key"
|
||||
})
|
||||
if vertexAuth == nil {
|
||||
t.Fatal("expected synthesized vertex auth")
|
||||
}
|
||||
|
||||
vertex := h.vertexCompatKeysWithAuthIndex()
|
||||
if len(vertex) != 1 {
|
||||
t.Fatalf("vertex keys = %d, want 1", len(vertex))
|
||||
}
|
||||
if got, want := vertex[0].AuthIndex, vertexAuth.EnsureIndex(); got != want {
|
||||
t.Fatalf("vertex auth-index = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
compatAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool {
|
||||
if auth == nil {
|
||||
return false
|
||||
}
|
||||
if auth.Provider != "bohe" {
|
||||
return false
|
||||
}
|
||||
if auth.Attributes["provider_key"] != "bohe" || auth.Attributes["compat_name"] != "bohe" {
|
||||
return false
|
||||
}
|
||||
return auth.Attributes["api_key"] == "compat-key"
|
||||
})
|
||||
if compatAuth == nil {
|
||||
t.Fatal("expected synthesized openai-compat auth")
|
||||
}
|
||||
|
||||
compat := h.openAICompatibilityWithAuthIndex()
|
||||
if len(compat) != 1 {
|
||||
t.Fatalf("openai-compat providers = %d, want 1", len(compat))
|
||||
}
|
||||
if len(compat[0].APIKeyEntries) != 1 {
|
||||
t.Fatalf("openai-compat api-key-entries = %d, want 1", len(compat[0].APIKeyEntries))
|
||||
}
|
||||
if compat[0].AuthIndex != "" {
|
||||
t.Fatalf("provider-level auth-index should be empty when api-key-entries exist, got %q", compat[0].AuthIndex)
|
||||
}
|
||||
if got, want := compat[0].APIKeyEntries[0].AuthIndex, compatAuth.EnsureIndex(); got != want {
|
||||
t.Fatalf("openai-compat auth-index = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigAuthIndexOmitsIndexesNotInManager(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{
|
||||
GeminiKey: []config.GeminiKey{
|
||||
{APIKey: "gemini-key", BaseURL: "https://a.example.com"},
|
||||
},
|
||||
OpenAICompatibility: []config.OpenAICompatibility{
|
||||
{
|
||||
Name: "bohe",
|
||||
BaseURL: "https://bohe.example.com/v1",
|
||||
APIKeyEntries: []config.OpenAICompatibilityAPIKey{
|
||||
{APIKey: "compat-key"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
auths := synthesizeConfigAuths(t, cfg)
|
||||
geminiAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool {
|
||||
if auth == nil {
|
||||
return false
|
||||
}
|
||||
return auth.Provider == "gemini" && auth.Attributes["api_key"] == "gemini-key"
|
||||
})
|
||||
if geminiAuth == nil {
|
||||
t.Fatal("expected synthesized gemini auth")
|
||||
}
|
||||
|
||||
manager := coreauth.NewManager(nil, nil, nil)
|
||||
if _, errRegister := manager.Register(context.Background(), geminiAuth); errRegister != nil {
|
||||
t.Fatalf("register gemini auth: %v", errRegister)
|
||||
}
|
||||
|
||||
h := &Handler{cfg: cfg, authManager: manager}
|
||||
|
||||
gemini := h.geminiKeysWithAuthIndex()
|
||||
if len(gemini) != 1 {
|
||||
t.Fatalf("gemini keys = %d, want 1", len(gemini))
|
||||
}
|
||||
if gemini[0].AuthIndex == "" {
|
||||
t.Fatal("expected gemini auth-index to be set")
|
||||
}
|
||||
|
||||
compat := h.openAICompatibilityWithAuthIndex()
|
||||
if len(compat) != 1 {
|
||||
t.Fatalf("openai-compat providers = %d, want 1", len(compat))
|
||||
}
|
||||
if len(compat[0].APIKeyEntries) != 1 {
|
||||
t.Fatalf("openai-compat api-key-entries = %d, want 1", len(compat[0].APIKeyEntries))
|
||||
}
|
||||
if compat[0].APIKeyEntries[0].AuthIndex != "" {
|
||||
t.Fatalf("openai-compat auth-index = %q, want empty", compat[0].APIKeyEntries[0].AuthIndex)
|
||||
}
|
||||
}
|
||||
@@ -139,9 +139,11 @@ func (h *Handler) PutGeminiKeys(c *gin.Context) {
|
||||
}
|
||||
arr = obj.Items
|
||||
}
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.cfg.GeminiKey = append([]config.GeminiKey(nil), arr...)
|
||||
h.cfg.SanitizeGeminiKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
}
|
||||
func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
||||
type geminiKeyPatch struct {
|
||||
@@ -161,6 +163,9 @@ func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
||||
c.JSON(400, gin.H{"error": "invalid body"})
|
||||
return
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
targetIndex := -1
|
||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.GeminiKey) {
|
||||
targetIndex = *body.Index
|
||||
@@ -187,7 +192,7 @@ func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
||||
if trimmed == "" {
|
||||
h.cfg.GeminiKey = append(h.cfg.GeminiKey[:targetIndex], h.cfg.GeminiKey[targetIndex+1:]...)
|
||||
h.cfg.SanitizeGeminiKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
entry.APIKey = trimmed
|
||||
@@ -209,10 +214,12 @@ func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
||||
}
|
||||
h.cfg.GeminiKey[targetIndex] = entry
|
||||
h.cfg.SanitizeGeminiKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteGeminiKey(c *gin.Context) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
||||
if baseRaw, okBase := c.GetQuery("base-url"); okBase {
|
||||
base := strings.TrimSpace(baseRaw)
|
||||
@@ -226,7 +233,7 @@ func (h *Handler) DeleteGeminiKey(c *gin.Context) {
|
||||
if len(out) != len(h.cfg.GeminiKey) {
|
||||
h.cfg.GeminiKey = out
|
||||
h.cfg.SanitizeGeminiKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
} else {
|
||||
c.JSON(404, gin.H{"error": "item not found"})
|
||||
}
|
||||
@@ -253,7 +260,7 @@ func (h *Handler) DeleteGeminiKey(c *gin.Context) {
|
||||
}
|
||||
h.cfg.GeminiKey = append(h.cfg.GeminiKey[:matchIndex], h.cfg.GeminiKey[matchIndex+1:]...)
|
||||
h.cfg.SanitizeGeminiKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
if idxStr := c.Query("index"); idxStr != "" {
|
||||
@@ -261,7 +268,7 @@ func (h *Handler) DeleteGeminiKey(c *gin.Context) {
|
||||
if _, err := fmt.Sscanf(idxStr, "%d", &idx); err == nil && idx >= 0 && idx < len(h.cfg.GeminiKey) {
|
||||
h.cfg.GeminiKey = append(h.cfg.GeminiKey[:idx], h.cfg.GeminiKey[idx+1:]...)
|
||||
h.cfg.SanitizeGeminiKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -292,9 +299,11 @@ func (h *Handler) PutClaudeKeys(c *gin.Context) {
|
||||
for i := range arr {
|
||||
normalizeClaudeKey(&arr[i])
|
||||
}
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.cfg.ClaudeKey = arr
|
||||
h.cfg.SanitizeClaudeKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
}
|
||||
func (h *Handler) PatchClaudeKey(c *gin.Context) {
|
||||
type claudeKeyPatch struct {
|
||||
@@ -315,6 +324,9 @@ func (h *Handler) PatchClaudeKey(c *gin.Context) {
|
||||
c.JSON(400, gin.H{"error": "invalid body"})
|
||||
return
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
targetIndex := -1
|
||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.ClaudeKey) {
|
||||
targetIndex = *body.Index
|
||||
@@ -358,10 +370,12 @@ func (h *Handler) PatchClaudeKey(c *gin.Context) {
|
||||
normalizeClaudeKey(&entry)
|
||||
h.cfg.ClaudeKey[targetIndex] = entry
|
||||
h.cfg.SanitizeClaudeKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteClaudeKey(c *gin.Context) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
||||
if baseRaw, okBase := c.GetQuery("base-url"); okBase {
|
||||
base := strings.TrimSpace(baseRaw)
|
||||
@@ -374,7 +388,7 @@ func (h *Handler) DeleteClaudeKey(c *gin.Context) {
|
||||
}
|
||||
h.cfg.ClaudeKey = out
|
||||
h.cfg.SanitizeClaudeKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -396,7 +410,7 @@ func (h *Handler) DeleteClaudeKey(c *gin.Context) {
|
||||
h.cfg.ClaudeKey = append(h.cfg.ClaudeKey[:matchIndex], h.cfg.ClaudeKey[matchIndex+1:]...)
|
||||
}
|
||||
h.cfg.SanitizeClaudeKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
if idxStr := c.Query("index"); idxStr != "" {
|
||||
@@ -405,7 +419,7 @@ func (h *Handler) DeleteClaudeKey(c *gin.Context) {
|
||||
if err == nil && idx >= 0 && idx < len(h.cfg.ClaudeKey) {
|
||||
h.cfg.ClaudeKey = append(h.cfg.ClaudeKey[:idx], h.cfg.ClaudeKey[idx+1:]...)
|
||||
h.cfg.SanitizeClaudeKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -440,9 +454,11 @@ func (h *Handler) PutOpenAICompat(c *gin.Context) {
|
||||
filtered = append(filtered, arr[i])
|
||||
}
|
||||
}
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.cfg.OpenAICompatibility = filtered
|
||||
h.cfg.SanitizeOpenAICompatibility()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
}
|
||||
func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
||||
type openAICompatPatch struct {
|
||||
@@ -462,6 +478,9 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
||||
c.JSON(400, gin.H{"error": "invalid body"})
|
||||
return
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
targetIndex := -1
|
||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.OpenAICompatibility) {
|
||||
targetIndex = *body.Index
|
||||
@@ -492,7 +511,7 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
||||
if trimmed == "" {
|
||||
h.cfg.OpenAICompatibility = append(h.cfg.OpenAICompatibility[:targetIndex], h.cfg.OpenAICompatibility[targetIndex+1:]...)
|
||||
h.cfg.SanitizeOpenAICompatibility()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
entry.BaseURL = trimmed
|
||||
@@ -509,10 +528,12 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
||||
normalizeOpenAICompatibilityEntry(&entry)
|
||||
h.cfg.OpenAICompatibility[targetIndex] = entry
|
||||
h.cfg.SanitizeOpenAICompatibility()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteOpenAICompat(c *gin.Context) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if name := c.Query("name"); name != "" {
|
||||
out := make([]config.OpenAICompatibility, 0, len(h.cfg.OpenAICompatibility))
|
||||
for _, v := range h.cfg.OpenAICompatibility {
|
||||
@@ -522,7 +543,7 @@ func (h *Handler) DeleteOpenAICompat(c *gin.Context) {
|
||||
}
|
||||
h.cfg.OpenAICompatibility = out
|
||||
h.cfg.SanitizeOpenAICompatibility()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
if idxStr := c.Query("index"); idxStr != "" {
|
||||
@@ -531,7 +552,7 @@ func (h *Handler) DeleteOpenAICompat(c *gin.Context) {
|
||||
if err == nil && idx >= 0 && idx < len(h.cfg.OpenAICompatibility) {
|
||||
h.cfg.OpenAICompatibility = append(h.cfg.OpenAICompatibility[:idx], h.cfg.OpenAICompatibility[idx+1:]...)
|
||||
h.cfg.SanitizeOpenAICompatibility()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -566,9 +587,11 @@ func (h *Handler) PutVertexCompatKeys(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.cfg.VertexCompatAPIKey = append([]config.VertexCompatKey(nil), arr...)
|
||||
h.cfg.SanitizeVertexCompatKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
}
|
||||
func (h *Handler) PatchVertexCompatKey(c *gin.Context) {
|
||||
type vertexCompatPatch struct {
|
||||
@@ -589,6 +612,9 @@ func (h *Handler) PatchVertexCompatKey(c *gin.Context) {
|
||||
c.JSON(400, gin.H{"error": "invalid body"})
|
||||
return
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
targetIndex := -1
|
||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.VertexCompatAPIKey) {
|
||||
targetIndex = *body.Index
|
||||
@@ -615,7 +641,7 @@ func (h *Handler) PatchVertexCompatKey(c *gin.Context) {
|
||||
if trimmed == "" {
|
||||
h.cfg.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:targetIndex], h.cfg.VertexCompatAPIKey[targetIndex+1:]...)
|
||||
h.cfg.SanitizeVertexCompatKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
entry.APIKey = trimmed
|
||||
@@ -628,7 +654,7 @@ func (h *Handler) PatchVertexCompatKey(c *gin.Context) {
|
||||
if trimmed == "" {
|
||||
h.cfg.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:targetIndex], h.cfg.VertexCompatAPIKey[targetIndex+1:]...)
|
||||
h.cfg.SanitizeVertexCompatKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
entry.BaseURL = trimmed
|
||||
@@ -648,10 +674,12 @@ func (h *Handler) PatchVertexCompatKey(c *gin.Context) {
|
||||
normalizeVertexCompatKey(&entry)
|
||||
h.cfg.VertexCompatAPIKey[targetIndex] = entry
|
||||
h.cfg.SanitizeVertexCompatKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteVertexCompatKey(c *gin.Context) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
||||
if baseRaw, okBase := c.GetQuery("base-url"); okBase {
|
||||
base := strings.TrimSpace(baseRaw)
|
||||
@@ -664,7 +692,7 @@ func (h *Handler) DeleteVertexCompatKey(c *gin.Context) {
|
||||
}
|
||||
h.cfg.VertexCompatAPIKey = out
|
||||
h.cfg.SanitizeVertexCompatKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -686,7 +714,7 @@ func (h *Handler) DeleteVertexCompatKey(c *gin.Context) {
|
||||
h.cfg.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:matchIndex], h.cfg.VertexCompatAPIKey[matchIndex+1:]...)
|
||||
}
|
||||
h.cfg.SanitizeVertexCompatKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
if idxStr := c.Query("index"); idxStr != "" {
|
||||
@@ -695,7 +723,7 @@ func (h *Handler) DeleteVertexCompatKey(c *gin.Context) {
|
||||
if errScan == nil && idx >= 0 && idx < len(h.cfg.VertexCompatAPIKey) {
|
||||
h.cfg.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:idx], h.cfg.VertexCompatAPIKey[idx+1:]...)
|
||||
h.cfg.SanitizeVertexCompatKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -915,9 +943,11 @@ func (h *Handler) PutCodexKeys(c *gin.Context) {
|
||||
}
|
||||
filtered = append(filtered, entry)
|
||||
}
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.cfg.CodexKey = filtered
|
||||
h.cfg.SanitizeCodexKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
}
|
||||
func (h *Handler) PatchCodexKey(c *gin.Context) {
|
||||
type codexKeyPatch struct {
|
||||
@@ -938,6 +968,9 @@ func (h *Handler) PatchCodexKey(c *gin.Context) {
|
||||
c.JSON(400, gin.H{"error": "invalid body"})
|
||||
return
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
targetIndex := -1
|
||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.CodexKey) {
|
||||
targetIndex = *body.Index
|
||||
@@ -968,7 +1001,7 @@ func (h *Handler) PatchCodexKey(c *gin.Context) {
|
||||
if trimmed == "" {
|
||||
h.cfg.CodexKey = append(h.cfg.CodexKey[:targetIndex], h.cfg.CodexKey[targetIndex+1:]...)
|
||||
h.cfg.SanitizeCodexKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
entry.BaseURL = trimmed
|
||||
@@ -988,10 +1021,12 @@ func (h *Handler) PatchCodexKey(c *gin.Context) {
|
||||
normalizeCodexKey(&entry)
|
||||
h.cfg.CodexKey[targetIndex] = entry
|
||||
h.cfg.SanitizeCodexKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteCodexKey(c *gin.Context) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
||||
if baseRaw, okBase := c.GetQuery("base-url"); okBase {
|
||||
base := strings.TrimSpace(baseRaw)
|
||||
@@ -1004,7 +1039,7 @@ func (h *Handler) DeleteCodexKey(c *gin.Context) {
|
||||
}
|
||||
h.cfg.CodexKey = out
|
||||
h.cfg.SanitizeCodexKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1026,7 +1061,7 @@ func (h *Handler) DeleteCodexKey(c *gin.Context) {
|
||||
h.cfg.CodexKey = append(h.cfg.CodexKey[:matchIndex], h.cfg.CodexKey[matchIndex+1:]...)
|
||||
}
|
||||
h.cfg.SanitizeCodexKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
if idxStr := c.Query("index"); idxStr != "" {
|
||||
@@ -1035,7 +1070,7 @@ func (h *Handler) DeleteCodexKey(c *gin.Context) {
|
||||
if err == nil && idx >= 0 && idx < len(h.cfg.CodexKey) {
|
||||
h.cfg.CodexKey = append(h.cfg.CodexKey[:idx], h.cfg.CodexKey[idx+1:]...)
|
||||
h.cfg.SanitizeCodexKeys()
|
||||
h.persist(c)
|
||||
h.persistLocked(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,10 +105,24 @@ func NewHandlerWithoutConfigFilePath(cfg *config.Config, manager *coreauth.Manag
|
||||
}
|
||||
|
||||
// SetConfig updates the in-memory config reference when the server hot-reloads.
|
||||
func (h *Handler) SetConfig(cfg *config.Config) { h.cfg = cfg }
|
||||
func (h *Handler) SetConfig(cfg *config.Config) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
h.cfg = cfg
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetAuthManager updates the auth manager reference used by management endpoints.
|
||||
func (h *Handler) SetAuthManager(manager *coreauth.Manager) { h.authManager = manager }
|
||||
func (h *Handler) SetAuthManager(manager *coreauth.Manager) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
h.authManager = manager
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetUsageStatistics allows replacing the usage statistics reference.
|
||||
func (h *Handler) SetUsageStatistics(stats *usage.RequestStatistics) { h.usageStats = stats }
|
||||
@@ -276,6 +290,12 @@ func (h *Handler) Middleware() gin.HandlerFunc {
|
||||
func (h *Handler) persist(c *gin.Context) bool {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
return h.persistLocked(c)
|
||||
}
|
||||
|
||||
// persistLocked saves the current in-memory config to disk.
|
||||
// It expects the caller to hold h.mu.
|
||||
func (h *Handler) persistLocked(c *gin.Context) bool {
|
||||
// Preserve comments when writing
|
||||
if err := config.SaveConfigPreserveComments(h.configFilePath, h.cfg); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to save config: %v", err)})
|
||||
|
||||
Reference in New Issue
Block a user