fix(management): stabilize auth-index mapping
This commit is contained in:
@@ -1,22 +1,13 @@
|
|||||||
package management
|
package management
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer"
|
"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 {
|
type geminiKeyWithAuthIndex struct {
|
||||||
config.GeminiKey
|
config.GeminiKey
|
||||||
AuthIndex string `json:"auth-index,omitempty"`
|
AuthIndex string `json:"auth-index,omitempty"`
|
||||||
@@ -53,170 +44,174 @@ type openAICompatibilityWithAuthIndex struct {
|
|||||||
AuthIndex string `json:"auth-index,omitempty"`
|
AuthIndex string `json:"auth-index,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) buildConfigAuthIndexViews() configAuthIndexViews {
|
func (h *Handler) liveAuthIndexByID() map[string]string {
|
||||||
cfg := h.cfg
|
out := map[string]string{}
|
||||||
if cfg == nil {
|
if h == nil {
|
||||||
return configAuthIndexViews{}
|
return out
|
||||||
}
|
}
|
||||||
|
h.mu.Lock()
|
||||||
liveIndexByID := map[string]string{}
|
manager := h.authManager
|
||||||
if h != nil && h.authManager != nil {
|
h.mu.Unlock()
|
||||||
for _, auth := range h.authManager.List() {
|
if manager == nil {
|
||||||
if auth == nil || strings.TrimSpace(auth.ID) == "" {
|
return out
|
||||||
continue
|
|
||||||
}
|
|
||||||
auth.EnsureIndex()
|
|
||||||
if auth.Index == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
liveIndexByID[auth.ID] = auth.Index
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// authManager.List() returns clones, so EnsureIndex only affects these copies.
|
||||||
views := configAuthIndexViews{
|
for _, auth := range manager.List() {
|
||||||
gemini: make([]string, len(cfg.GeminiKey)),
|
if auth == nil {
|
||||||
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
|
continue
|
||||||
}
|
}
|
||||||
views.gemini[i] = nextAuthIndex()
|
id := strings.TrimSpace(auth.ID)
|
||||||
}
|
if id == "" {
|
||||||
for i := range cfg.ClaudeKey {
|
|
||||||
if strings.TrimSpace(cfg.ClaudeKey[i].APIKey) == "" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
views.claude[i] = nextAuthIndex()
|
idx := strings.TrimSpace(auth.Index)
|
||||||
}
|
if idx == "" {
|
||||||
for i := range cfg.CodexKey {
|
idx = auth.EnsureIndex()
|
||||||
if strings.TrimSpace(cfg.CodexKey[i].APIKey) == "" {
|
}
|
||||||
|
if idx == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
views.codex[i] = nextAuthIndex()
|
out[id] = idx
|
||||||
}
|
}
|
||||||
for i := range cfg.OpenAICompatibility {
|
return out
|
||||||
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 {
|
func (h *Handler) geminiKeysWithAuthIndex() []geminiKeyWithAuthIndex {
|
||||||
if h == nil || h.cfg == nil {
|
if h == nil {
|
||||||
return 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))
|
out := make([]geminiKeyWithAuthIndex, len(h.cfg.GeminiKey))
|
||||||
for i := range 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{
|
out[i] = geminiKeyWithAuthIndex{
|
||||||
GeminiKey: h.cfg.GeminiKey[i],
|
GeminiKey: entry,
|
||||||
AuthIndex: views.gemini[i],
|
AuthIndex: authIndex,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) claudeKeysWithAuthIndex() []claudeKeyWithAuthIndex {
|
func (h *Handler) claudeKeysWithAuthIndex() []claudeKeyWithAuthIndex {
|
||||||
if h == nil || h.cfg == nil {
|
if h == nil {
|
||||||
return 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))
|
out := make([]claudeKeyWithAuthIndex, len(h.cfg.ClaudeKey))
|
||||||
for i := range 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{
|
out[i] = claudeKeyWithAuthIndex{
|
||||||
ClaudeKey: h.cfg.ClaudeKey[i],
|
ClaudeKey: entry,
|
||||||
AuthIndex: views.claude[i],
|
AuthIndex: authIndex,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) codexKeysWithAuthIndex() []codexKeyWithAuthIndex {
|
func (h *Handler) codexKeysWithAuthIndex() []codexKeyWithAuthIndex {
|
||||||
if h == nil || h.cfg == nil {
|
if h == nil {
|
||||||
return 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))
|
out := make([]codexKeyWithAuthIndex, len(h.cfg.CodexKey))
|
||||||
for i := range 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{
|
out[i] = codexKeyWithAuthIndex{
|
||||||
CodexKey: h.cfg.CodexKey[i],
|
CodexKey: entry,
|
||||||
AuthIndex: views.codex[i],
|
AuthIndex: authIndex,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) vertexCompatKeysWithAuthIndex() []vertexCompatKeyWithAuthIndex {
|
func (h *Handler) vertexCompatKeysWithAuthIndex() []vertexCompatKeyWithAuthIndex {
|
||||||
if h == nil || h.cfg == nil {
|
if h == nil {
|
||||||
return 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))
|
out := make([]vertexCompatKeyWithAuthIndex, len(h.cfg.VertexCompatAPIKey))
|
||||||
for i := range 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{
|
out[i] = vertexCompatKeyWithAuthIndex{
|
||||||
VertexCompatKey: h.cfg.VertexCompatAPIKey[i],
|
VertexCompatKey: entry,
|
||||||
AuthIndex: views.vertex[i],
|
AuthIndex: authIndex,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) openAICompatibilityWithAuthIndex() []openAICompatibilityWithAuthIndex {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
views := h.buildConfigAuthIndexViews()
|
|
||||||
normalized := normalizedOpenAICompatibilityEntries(h.cfg.OpenAICompatibility)
|
normalized := normalizedOpenAICompatibilityEntries(h.cfg.OpenAICompatibility)
|
||||||
out := make([]openAICompatibilityWithAuthIndex, len(normalized))
|
out := make([]openAICompatibilityWithAuthIndex, len(normalized))
|
||||||
|
idGen := synthesizer.NewStableIDGenerator()
|
||||||
for i := range normalized {
|
for i := range normalized {
|
||||||
entry := normalized[i]
|
entry := normalized[i]
|
||||||
|
providerName := strings.ToLower(strings.TrimSpace(entry.Name))
|
||||||
|
if providerName == "" {
|
||||||
|
providerName = "openai-compatibility"
|
||||||
|
}
|
||||||
|
idKind := fmt.Sprintf("openai-compatibility:%s", providerName)
|
||||||
|
|
||||||
response := openAICompatibilityWithAuthIndex{
|
response := openAICompatibilityWithAuthIndex{
|
||||||
Name: entry.Name,
|
Name: entry.Name,
|
||||||
Priority: entry.Priority,
|
Priority: entry.Priority,
|
||||||
@@ -224,18 +219,19 @@ func (h *Handler) openAICompatibilityWithAuthIndex() []openAICompatibilityWithAu
|
|||||||
BaseURL: entry.BaseURL,
|
BaseURL: entry.BaseURL,
|
||||||
Models: entry.Models,
|
Models: entry.Models,
|
||||||
Headers: entry.Headers,
|
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))
|
response.APIKeyEntries = make([]openAICompatibilityAPIKeyWithAuthIndex, len(entry.APIKeyEntries))
|
||||||
for j := range entry.APIKeyEntries {
|
for j := range entry.APIKeyEntries {
|
||||||
authIndex := ""
|
apiKeyEntry := entry.APIKeyEntries[j]
|
||||||
if i < len(views.openAIEntries) && j < len(views.openAIEntries[i]) {
|
id, _ := idGen.Next(idKind, apiKeyEntry.APIKey, entry.BaseURL, apiKeyEntry.ProxyURL)
|
||||||
authIndex = views.openAIEntries[i][j]
|
|
||||||
}
|
|
||||||
response.APIKeyEntries[j] = openAICompatibilityAPIKeyWithAuthIndex{
|
response.APIKeyEntries[j] = openAICompatibilityAPIKeyWithAuthIndex{
|
||||||
OpenAICompatibilityAPIKey: entry.APIKeyEntries[j],
|
OpenAICompatibilityAPIKey: apiKeyEntry,
|
||||||
AuthIndex: authIndex,
|
AuthIndex: liveIndexByID[id],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
arr = obj.Items
|
||||||
}
|
}
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
h.cfg.GeminiKey = append([]config.GeminiKey(nil), arr...)
|
h.cfg.GeminiKey = append([]config.GeminiKey(nil), arr...)
|
||||||
h.cfg.SanitizeGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
||||||
type geminiKeyPatch struct {
|
type geminiKeyPatch struct {
|
||||||
@@ -161,6 +163,9 @@ func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": "invalid body"})
|
c.JSON(400, gin.H{"error": "invalid body"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
targetIndex := -1
|
targetIndex := -1
|
||||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.GeminiKey) {
|
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.GeminiKey) {
|
||||||
targetIndex = *body.Index
|
targetIndex = *body.Index
|
||||||
@@ -187,7 +192,7 @@ func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
|||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
h.cfg.GeminiKey = append(h.cfg.GeminiKey[:targetIndex], h.cfg.GeminiKey[targetIndex+1:]...)
|
h.cfg.GeminiKey = append(h.cfg.GeminiKey[:targetIndex], h.cfg.GeminiKey[targetIndex+1:]...)
|
||||||
h.cfg.SanitizeGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
entry.APIKey = trimmed
|
entry.APIKey = trimmed
|
||||||
@@ -209,10 +214,12 @@ func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
h.cfg.GeminiKey[targetIndex] = entry
|
h.cfg.GeminiKey[targetIndex] = entry
|
||||||
h.cfg.SanitizeGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) DeleteGeminiKey(c *gin.Context) {
|
func (h *Handler) DeleteGeminiKey(c *gin.Context) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
||||||
if baseRaw, okBase := c.GetQuery("base-url"); okBase {
|
if baseRaw, okBase := c.GetQuery("base-url"); okBase {
|
||||||
base := strings.TrimSpace(baseRaw)
|
base := strings.TrimSpace(baseRaw)
|
||||||
@@ -226,7 +233,7 @@ func (h *Handler) DeleteGeminiKey(c *gin.Context) {
|
|||||||
if len(out) != len(h.cfg.GeminiKey) {
|
if len(out) != len(h.cfg.GeminiKey) {
|
||||||
h.cfg.GeminiKey = out
|
h.cfg.GeminiKey = out
|
||||||
h.cfg.SanitizeGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
} else {
|
} else {
|
||||||
c.JSON(404, gin.H{"error": "item not found"})
|
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.GeminiKey = append(h.cfg.GeminiKey[:matchIndex], h.cfg.GeminiKey[matchIndex+1:]...)
|
||||||
h.cfg.SanitizeGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if idxStr := c.Query("index"); idxStr != "" {
|
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) {
|
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.GeminiKey = append(h.cfg.GeminiKey[:idx], h.cfg.GeminiKey[idx+1:]...)
|
||||||
h.cfg.SanitizeGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,9 +299,11 @@ func (h *Handler) PutClaudeKeys(c *gin.Context) {
|
|||||||
for i := range arr {
|
for i := range arr {
|
||||||
normalizeClaudeKey(&arr[i])
|
normalizeClaudeKey(&arr[i])
|
||||||
}
|
}
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
h.cfg.ClaudeKey = arr
|
h.cfg.ClaudeKey = arr
|
||||||
h.cfg.SanitizeClaudeKeys()
|
h.cfg.SanitizeClaudeKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchClaudeKey(c *gin.Context) {
|
func (h *Handler) PatchClaudeKey(c *gin.Context) {
|
||||||
type claudeKeyPatch struct {
|
type claudeKeyPatch struct {
|
||||||
@@ -315,6 +324,9 @@ func (h *Handler) PatchClaudeKey(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": "invalid body"})
|
c.JSON(400, gin.H{"error": "invalid body"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
targetIndex := -1
|
targetIndex := -1
|
||||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.ClaudeKey) {
|
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.ClaudeKey) {
|
||||||
targetIndex = *body.Index
|
targetIndex = *body.Index
|
||||||
@@ -358,10 +370,12 @@ func (h *Handler) PatchClaudeKey(c *gin.Context) {
|
|||||||
normalizeClaudeKey(&entry)
|
normalizeClaudeKey(&entry)
|
||||||
h.cfg.ClaudeKey[targetIndex] = entry
|
h.cfg.ClaudeKey[targetIndex] = entry
|
||||||
h.cfg.SanitizeClaudeKeys()
|
h.cfg.SanitizeClaudeKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) DeleteClaudeKey(c *gin.Context) {
|
func (h *Handler) DeleteClaudeKey(c *gin.Context) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
||||||
if baseRaw, okBase := c.GetQuery("base-url"); okBase {
|
if baseRaw, okBase := c.GetQuery("base-url"); okBase {
|
||||||
base := strings.TrimSpace(baseRaw)
|
base := strings.TrimSpace(baseRaw)
|
||||||
@@ -374,7 +388,7 @@ func (h *Handler) DeleteClaudeKey(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
h.cfg.ClaudeKey = out
|
h.cfg.ClaudeKey = out
|
||||||
h.cfg.SanitizeClaudeKeys()
|
h.cfg.SanitizeClaudeKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
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.ClaudeKey = append(h.cfg.ClaudeKey[:matchIndex], h.cfg.ClaudeKey[matchIndex+1:]...)
|
||||||
}
|
}
|
||||||
h.cfg.SanitizeClaudeKeys()
|
h.cfg.SanitizeClaudeKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if idxStr := c.Query("index"); idxStr != "" {
|
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) {
|
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.ClaudeKey = append(h.cfg.ClaudeKey[:idx], h.cfg.ClaudeKey[idx+1:]...)
|
||||||
h.cfg.SanitizeClaudeKeys()
|
h.cfg.SanitizeClaudeKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,9 +454,11 @@ func (h *Handler) PutOpenAICompat(c *gin.Context) {
|
|||||||
filtered = append(filtered, arr[i])
|
filtered = append(filtered, arr[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
h.cfg.OpenAICompatibility = filtered
|
h.cfg.OpenAICompatibility = filtered
|
||||||
h.cfg.SanitizeOpenAICompatibility()
|
h.cfg.SanitizeOpenAICompatibility()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
||||||
type openAICompatPatch struct {
|
type openAICompatPatch struct {
|
||||||
@@ -462,6 +478,9 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": "invalid body"})
|
c.JSON(400, gin.H{"error": "invalid body"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
targetIndex := -1
|
targetIndex := -1
|
||||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.OpenAICompatibility) {
|
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.OpenAICompatibility) {
|
||||||
targetIndex = *body.Index
|
targetIndex = *body.Index
|
||||||
@@ -492,7 +511,7 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
|||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
h.cfg.OpenAICompatibility = append(h.cfg.OpenAICompatibility[:targetIndex], h.cfg.OpenAICompatibility[targetIndex+1:]...)
|
h.cfg.OpenAICompatibility = append(h.cfg.OpenAICompatibility[:targetIndex], h.cfg.OpenAICompatibility[targetIndex+1:]...)
|
||||||
h.cfg.SanitizeOpenAICompatibility()
|
h.cfg.SanitizeOpenAICompatibility()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
entry.BaseURL = trimmed
|
entry.BaseURL = trimmed
|
||||||
@@ -509,10 +528,12 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
|||||||
normalizeOpenAICompatibilityEntry(&entry)
|
normalizeOpenAICompatibilityEntry(&entry)
|
||||||
h.cfg.OpenAICompatibility[targetIndex] = entry
|
h.cfg.OpenAICompatibility[targetIndex] = entry
|
||||||
h.cfg.SanitizeOpenAICompatibility()
|
h.cfg.SanitizeOpenAICompatibility()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) DeleteOpenAICompat(c *gin.Context) {
|
func (h *Handler) DeleteOpenAICompat(c *gin.Context) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
if name := c.Query("name"); name != "" {
|
if name := c.Query("name"); name != "" {
|
||||||
out := make([]config.OpenAICompatibility, 0, len(h.cfg.OpenAICompatibility))
|
out := make([]config.OpenAICompatibility, 0, len(h.cfg.OpenAICompatibility))
|
||||||
for _, v := range 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.OpenAICompatibility = out
|
||||||
h.cfg.SanitizeOpenAICompatibility()
|
h.cfg.SanitizeOpenAICompatibility()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if idxStr := c.Query("index"); idxStr != "" {
|
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) {
|
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.OpenAICompatibility = append(h.cfg.OpenAICompatibility[:idx], h.cfg.OpenAICompatibility[idx+1:]...)
|
||||||
h.cfg.SanitizeOpenAICompatibility()
|
h.cfg.SanitizeOpenAICompatibility()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -566,9 +587,11 @@ func (h *Handler) PutVertexCompatKeys(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
h.cfg.VertexCompatAPIKey = append([]config.VertexCompatKey(nil), arr...)
|
h.cfg.VertexCompatAPIKey = append([]config.VertexCompatKey(nil), arr...)
|
||||||
h.cfg.SanitizeVertexCompatKeys()
|
h.cfg.SanitizeVertexCompatKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchVertexCompatKey(c *gin.Context) {
|
func (h *Handler) PatchVertexCompatKey(c *gin.Context) {
|
||||||
type vertexCompatPatch struct {
|
type vertexCompatPatch struct {
|
||||||
@@ -589,6 +612,9 @@ func (h *Handler) PatchVertexCompatKey(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": "invalid body"})
|
c.JSON(400, gin.H{"error": "invalid body"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
targetIndex := -1
|
targetIndex := -1
|
||||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.VertexCompatAPIKey) {
|
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.VertexCompatAPIKey) {
|
||||||
targetIndex = *body.Index
|
targetIndex = *body.Index
|
||||||
@@ -615,7 +641,7 @@ func (h *Handler) PatchVertexCompatKey(c *gin.Context) {
|
|||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
h.cfg.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:targetIndex], h.cfg.VertexCompatAPIKey[targetIndex+1:]...)
|
h.cfg.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:targetIndex], h.cfg.VertexCompatAPIKey[targetIndex+1:]...)
|
||||||
h.cfg.SanitizeVertexCompatKeys()
|
h.cfg.SanitizeVertexCompatKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
entry.APIKey = trimmed
|
entry.APIKey = trimmed
|
||||||
@@ -628,7 +654,7 @@ func (h *Handler) PatchVertexCompatKey(c *gin.Context) {
|
|||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
h.cfg.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:targetIndex], h.cfg.VertexCompatAPIKey[targetIndex+1:]...)
|
h.cfg.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:targetIndex], h.cfg.VertexCompatAPIKey[targetIndex+1:]...)
|
||||||
h.cfg.SanitizeVertexCompatKeys()
|
h.cfg.SanitizeVertexCompatKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
entry.BaseURL = trimmed
|
entry.BaseURL = trimmed
|
||||||
@@ -648,10 +674,12 @@ func (h *Handler) PatchVertexCompatKey(c *gin.Context) {
|
|||||||
normalizeVertexCompatKey(&entry)
|
normalizeVertexCompatKey(&entry)
|
||||||
h.cfg.VertexCompatAPIKey[targetIndex] = entry
|
h.cfg.VertexCompatAPIKey[targetIndex] = entry
|
||||||
h.cfg.SanitizeVertexCompatKeys()
|
h.cfg.SanitizeVertexCompatKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) DeleteVertexCompatKey(c *gin.Context) {
|
func (h *Handler) DeleteVertexCompatKey(c *gin.Context) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
||||||
if baseRaw, okBase := c.GetQuery("base-url"); okBase {
|
if baseRaw, okBase := c.GetQuery("base-url"); okBase {
|
||||||
base := strings.TrimSpace(baseRaw)
|
base := strings.TrimSpace(baseRaw)
|
||||||
@@ -664,7 +692,7 @@ func (h *Handler) DeleteVertexCompatKey(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
h.cfg.VertexCompatAPIKey = out
|
h.cfg.VertexCompatAPIKey = out
|
||||||
h.cfg.SanitizeVertexCompatKeys()
|
h.cfg.SanitizeVertexCompatKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
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.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:matchIndex], h.cfg.VertexCompatAPIKey[matchIndex+1:]...)
|
||||||
}
|
}
|
||||||
h.cfg.SanitizeVertexCompatKeys()
|
h.cfg.SanitizeVertexCompatKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if idxStr := c.Query("index"); idxStr != "" {
|
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) {
|
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.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:idx], h.cfg.VertexCompatAPIKey[idx+1:]...)
|
||||||
h.cfg.SanitizeVertexCompatKeys()
|
h.cfg.SanitizeVertexCompatKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -915,9 +943,11 @@ func (h *Handler) PutCodexKeys(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
filtered = append(filtered, entry)
|
filtered = append(filtered, entry)
|
||||||
}
|
}
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
h.cfg.CodexKey = filtered
|
h.cfg.CodexKey = filtered
|
||||||
h.cfg.SanitizeCodexKeys()
|
h.cfg.SanitizeCodexKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchCodexKey(c *gin.Context) {
|
func (h *Handler) PatchCodexKey(c *gin.Context) {
|
||||||
type codexKeyPatch struct {
|
type codexKeyPatch struct {
|
||||||
@@ -938,6 +968,9 @@ func (h *Handler) PatchCodexKey(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": "invalid body"})
|
c.JSON(400, gin.H{"error": "invalid body"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
targetIndex := -1
|
targetIndex := -1
|
||||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.CodexKey) {
|
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.CodexKey) {
|
||||||
targetIndex = *body.Index
|
targetIndex = *body.Index
|
||||||
@@ -968,7 +1001,7 @@ func (h *Handler) PatchCodexKey(c *gin.Context) {
|
|||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
h.cfg.CodexKey = append(h.cfg.CodexKey[:targetIndex], h.cfg.CodexKey[targetIndex+1:]...)
|
h.cfg.CodexKey = append(h.cfg.CodexKey[:targetIndex], h.cfg.CodexKey[targetIndex+1:]...)
|
||||||
h.cfg.SanitizeCodexKeys()
|
h.cfg.SanitizeCodexKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
entry.BaseURL = trimmed
|
entry.BaseURL = trimmed
|
||||||
@@ -988,10 +1021,12 @@ func (h *Handler) PatchCodexKey(c *gin.Context) {
|
|||||||
normalizeCodexKey(&entry)
|
normalizeCodexKey(&entry)
|
||||||
h.cfg.CodexKey[targetIndex] = entry
|
h.cfg.CodexKey[targetIndex] = entry
|
||||||
h.cfg.SanitizeCodexKeys()
|
h.cfg.SanitizeCodexKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) DeleteCodexKey(c *gin.Context) {
|
func (h *Handler) DeleteCodexKey(c *gin.Context) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
||||||
if baseRaw, okBase := c.GetQuery("base-url"); okBase {
|
if baseRaw, okBase := c.GetQuery("base-url"); okBase {
|
||||||
base := strings.TrimSpace(baseRaw)
|
base := strings.TrimSpace(baseRaw)
|
||||||
@@ -1004,7 +1039,7 @@ func (h *Handler) DeleteCodexKey(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
h.cfg.CodexKey = out
|
h.cfg.CodexKey = out
|
||||||
h.cfg.SanitizeCodexKeys()
|
h.cfg.SanitizeCodexKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
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.CodexKey = append(h.cfg.CodexKey[:matchIndex], h.cfg.CodexKey[matchIndex+1:]...)
|
||||||
}
|
}
|
||||||
h.cfg.SanitizeCodexKeys()
|
h.cfg.SanitizeCodexKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if idxStr := c.Query("index"); idxStr != "" {
|
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) {
|
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.CodexKey = append(h.cfg.CodexKey[:idx], h.cfg.CodexKey[idx+1:]...)
|
||||||
h.cfg.SanitizeCodexKeys()
|
h.cfg.SanitizeCodexKeys()
|
||||||
h.persist(c)
|
h.persistLocked(c)
|
||||||
return
|
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.
|
// 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.
|
// 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.
|
// SetUsageStatistics allows replacing the usage statistics reference.
|
||||||
func (h *Handler) SetUsageStatistics(stats *usage.RequestStatistics) { h.usageStats = stats }
|
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 {
|
func (h *Handler) persist(c *gin.Context) bool {
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
defer h.mu.Unlock()
|
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
|
// Preserve comments when writing
|
||||||
if err := config.SaveConfigPreserveComments(h.configFilePath, h.cfg); err != nil {
|
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)})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to save config: %v", err)})
|
||||||
|
|||||||
Reference in New Issue
Block a user