From 7c642bee0964b3ec75acfb676d1cfbdd51fe24e9 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Tue, 30 Sep 2025 23:36:22 +0800 Subject: [PATCH] feat(auth): normalize OpenAI compatibility entries and enhance proxy configuration - Added automatic trimming of API keys and migration of legacy `api-keys` to `api-key-entries`. - Introduced per-key `proxy-url` handling across OpenAI, Codex, and Claude API configurations. - Updated documentation to clarify usage of `proxy-url` with examples, ensuring backward compatibility. - Added normalization logic to reduce duplication and improve configuration consistency. --- MANAGEMENT_API.md | 63 ++++++++++--------- MANAGEMENT_API_CN.md | 63 ++++++++++--------- .../api/handlers/management/config_lists.go | 34 ++++++++++ internal/api/server.go | 7 ++- 4 files changed, 106 insertions(+), 61 deletions(-) diff --git a/MANAGEMENT_API.md b/MANAGEMENT_API.md index 3688d794..5a062049 100644 --- a/MANAGEMENT_API.md +++ b/MANAGEMENT_API.md @@ -95,7 +95,7 @@ If a plaintext key is detected in the config at startup, it will be bcrypt‑has ``` - Response: ```json - {"debug":true,"proxy-url":"","api-keys":["1...5","JS...W"],"quota-exceeded":{"switch-project":true,"switch-preview-model":true},"generative-language-api-key":["AI...01", "AI...02", "AI...03"],"request-log":true,"request-retry":3,"claude-api-key":[{"api-key":"cr...56","base-url":"https://example.com/api"},{"api-key":"cr...e3","base-url":"http://example.com:3000/api"},{"api-key":"sk-...q2","base-url":"https://example.com"}],"codex-api-key":[{"api-key":"sk...01","base-url":"https://example/v1"}],"openai-compatibility":[{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-keys":["sk...01"],"models":[{"name":"moonshotai/kimi-k2:free","alias":"kimi-k2"}]},{"name":"iflow","base-url":"https://apis.iflow.cn/v1","api-keys":["sk...7e"],"models":[{"name":"deepseek-v3.1","alias":"deepseek-v3.1"},{"name":"glm-4.5","alias":"glm-4.5"},{"name":"kimi-k2","alias":"kimi-k2"}]}]} + {"debug":true,"proxy-url":"","api-keys":["1...5","JS...W"],"quota-exceeded":{"switch-project":true,"switch-preview-model":true},"generative-language-api-key":["AI...01","AI...02","AI...03"],"request-log":true,"request-retry":3,"claude-api-key":[{"api-key":"cr...56","base-url":"https://example.com/api","proxy-url":"socks5://proxy.example.com:1080"},{"api-key":"cr...e3","base-url":"http://example.com:3000/api","proxy-url":""},{"api-key":"sk-...q2","base-url":"https://example.com","proxy-url":""}],"codex-api-key":[{"api-key":"sk...01","base-url":"https://example/v1","proxy-url":""}],"openai-compatibility":[{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-key-entries":[{"api-key":"sk...01","proxy-url":""}],"models":[{"name":"moonshotai/kimi-k2:free","alias":"kimi-k2"}]},{"name":"iflow","base-url":"https://apis.iflow.cn/v1","api-key-entries":[{"api-key":"sk...7e","proxy-url":"socks5://proxy.example.com:1080"}],"models":[{"name":"deepseek-v3.1","alias":"deepseek-v3.1"},{"name":"glm-4.5","alias":"glm-4.5"},{"name":"kimi-k2","alias":"kimi-k2"}]}]} ``` ### Debug @@ -335,14 +335,14 @@ These endpoints update the inline `config-api-key` provider inside the `auth.pro ``` - Response: ```json - { "codex-api-key": [ { "api-key": "sk-a", "base-url": "" } ] } + { "codex-api-key": [ { "api-key": "sk-a", "base-url": "", "proxy-url": "" } ] } ``` - PUT `/codex-api-key` — Replace the list - Request: ```bash curl -X PUT -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '[{"api-key":"sk-a"},{"api-key":"sk-b","base-url":"https://c.example.com"}]' \ + -d '[{"api-key":"sk-a","proxy-url":"socks5://proxy.example.com:1080"},{"api-key":"sk-b","base-url":"https://c.example.com","proxy-url":""}]' \ http://localhost:8317/v0/management/codex-api-key ``` - Response: @@ -354,14 +354,14 @@ These endpoints update the inline `config-api-key` provider inside the `auth.pro ```bash curl -X PATCH -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '{"index":1,"value":{"api-key":"sk-b2","base-url":"https://c.example.com"}}' \ + -d '{"index":1,"value":{"api-key":"sk-b2","base-url":"https://c.example.com","proxy-url":""}}' \ http://localhost:8317/v0/management/codex-api-key ``` - Request (by match): ```bash curl -X PATCH -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '{"match":"sk-a","value":{"api-key":"sk-a","base-url":""}}' \ + -d '{"match":"sk-a","value":{"api-key":"sk-a","base-url":"","proxy-url":"socks5://proxy.example.com:1080"}}' \ http://localhost:8317/v0/management/codex-api-key ``` - Response: @@ -430,22 +430,22 @@ These endpoints update the inline `config-api-key` provider inside the `auth.pro ### Claude API KEY (object array) - GET `/claude-api-key` — List all - - Request: - ```bash - curl -H 'Authorization: Bearer ' http://localhost:8317/v0/management/claude-api-key - ``` - - Response: - ```json - { "claude-api-key": [ { "api-key": "sk-a", "base-url": "" } ] } - ``` + - Request: + ```bash + curl -H 'Authorization: Bearer ' http://localhost:8317/v0/management/claude-api-key + ``` + - Response: + ```json + { "claude-api-key": [ { "api-key": "sk-a", "base-url": "", "proxy-url": "" } ] } + ``` - PUT `/claude-api-key` — Replace the list - - Request: - ```bash - curl -X PUT -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer ' \ - -d '[{"api-key":"sk-a"},{"api-key":"sk-b","base-url":"https://c.example.com"}]' \ - http://localhost:8317/v0/management/claude-api-key - ``` + - Request: + ```bash + curl -X PUT -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer ' \ + -d '[{"api-key":"sk-a","proxy-url":"socks5://proxy.example.com:1080"},{"api-key":"sk-b","base-url":"https://c.example.com","proxy-url":""}]' \ + http://localhost:8317/v0/management/claude-api-key + ``` - Response: ```json { "status": "ok" } @@ -455,16 +455,16 @@ These endpoints update the inline `config-api-key` provider inside the `auth.pro ```bash curl -X PATCH -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '{"index":1,"value":{"api-key":"sk-b2","base-url":"https://c.example.com"}}' \ - http://localhost:8317/v0/management/claude-api-key - ``` + -d '{"index":1,"value":{"api-key":"sk-b2","base-url":"https://c.example.com","proxy-url":""}}' \ + http://localhost:8317/v0/management/claude-api-key + ``` - Request (by match): ```bash curl -X PATCH -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '{"match":"sk-a","value":{"api-key":"sk-a","base-url":""}}' \ - http://localhost:8317/v0/management/claude-api-key - ``` + -d '{"match":"sk-a","value":{"api-key":"sk-a","base-url":"","proxy-url":"socks5://proxy.example.com:1080"}}' \ + http://localhost:8317/v0/management/claude-api-key + ``` - Response: ```json { "status": "ok" } @@ -491,14 +491,14 @@ These endpoints update the inline `config-api-key` provider inside the `auth.pro ``` - Response: ```json - { "openai-compatibility": [ { "name": "openrouter", "base-url": "https://openrouter.ai/api/v1", "api-keys": [], "models": [] } ] } + { "openai-compatibility": [ { "name": "openrouter", "base-url": "https://openrouter.ai/api/v1", "api-key-entries": [ { "api-key": "sk", "proxy-url": "" } ], "models": [] } ] } ``` - PUT `/openai-compatibility` — Replace the list - Request: ```bash curl -X PUT -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '[{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-keys":["sk"],"models":[{"name":"m","alias":"a"}]}]' \ + -d '[{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-key-entries":[{"api-key":"sk","proxy-url":""}],"models":[{"name":"m","alias":"a"}]}]' \ http://localhost:8317/v0/management/openai-compatibility ``` - Response: @@ -510,20 +510,23 @@ These endpoints update the inline `config-api-key` provider inside the `auth.pro ```bash curl -X PATCH -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '{"name":"openrouter","value":{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-keys":[],"models":[]}}' \ + -d '{"name":"openrouter","value":{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-key-entries":[{"api-key":"sk","proxy-url":""}],"models":[]}}' \ http://localhost:8317/v0/management/openai-compatibility ``` - Request (by index): ```bash curl -X PATCH -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '{"index":0,"value":{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-keys":[],"models":[]}}' \ + -d '{"index":0,"value":{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-key-entries":[{"api-key":"sk","proxy-url":""}],"models":[]}}' \ http://localhost:8317/v0/management/openai-compatibility ``` - Response: ```json { "status": "ok" } ``` + + - Notes: + - Legacy `api-keys` input remains accepted; keys are migrated into `api-key-entries` automatically so the legacy field will eventually remain empty in responses. - DELETE `/openai-compatibility` — Delete (`?name=` or `?index=`) - Request (by name): ```bash diff --git a/MANAGEMENT_API_CN.md b/MANAGEMENT_API_CN.md index 19c2cb88..bb0d8ee0 100644 --- a/MANAGEMENT_API_CN.md +++ b/MANAGEMENT_API_CN.md @@ -95,7 +95,7 @@ ``` - 响应: ```json - {"debug":true,"proxy-url":"","api-keys":["1...5","JS...W"],"quota-exceeded":{"switch-project":true,"switch-preview-model":true},"generative-language-api-key":["AI...01", "AI...02", "AI...03"],"request-log":true,"request-retry":3,"claude-api-key":[{"api-key":"cr...56","base-url":"https://example.com/api"},{"api-key":"cr...e3","base-url":"http://example.com:3000/api"},{"api-key":"sk-...q2","base-url":"https://example.com"}],"codex-api-key":[{"api-key":"sk...01","base-url":"https://example/v1"}],"openai-compatibility":[{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-keys":["sk...01"],"models":[{"name":"moonshotai/kimi-k2:free","alias":"kimi-k2"}]},{"name":"iflow","base-url":"https://apis.iflow.cn/v1","api-keys":["sk...7e"],"models":[{"name":"deepseek-v3.1","alias":"deepseek-v3.1"},{"name":"glm-4.5","alias":"glm-4.5"},{"name":"kimi-k2","alias":"kimi-k2"}]}]} + {"debug":true,"proxy-url":"","api-keys":["1...5","JS...W"],"quota-exceeded":{"switch-project":true,"switch-preview-model":true},"generative-language-api-key":["AI...01","AI...02","AI...03"],"request-log":true,"request-retry":3,"claude-api-key":[{"api-key":"cr...56","base-url":"https://example.com/api","proxy-url":"socks5://proxy.example.com:1080"},{"api-key":"cr...e3","base-url":"http://example.com:3000/api","proxy-url":""},{"api-key":"sk-...q2","base-url":"https://example.com","proxy-url":""}],"codex-api-key":[{"api-key":"sk...01","base-url":"https://example/v1","proxy-url":""}],"openai-compatibility":[{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-key-entries":[{"api-key":"sk...01","proxy-url":""}],"models":[{"name":"moonshotai/kimi-k2:free","alias":"kimi-k2"}]},{"name":"iflow","base-url":"https://apis.iflow.cn/v1","api-key-entries":[{"api-key":"sk...7e","proxy-url":"socks5://proxy.example.com:1080"}],"models":[{"name":"deepseek-v3.1","alias":"deepseek-v3.1"},{"name":"glm-4.5","alias":"glm-4.5"},{"name":"kimi-k2","alias":"kimi-k2"}]}]} ``` ### Debug @@ -335,14 +335,14 @@ ``` - 响应: ```json - { "codex-api-key": [ { "api-key": "sk-a", "base-url": "" } ] } + { "codex-api-key": [ { "api-key": "sk-a", "base-url": "", "proxy-url": "" } ] } ``` - PUT `/codex-api-key` — 完整改写列表 - 请求: ```bash curl -X PUT -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '[{"api-key":"sk-a"},{"api-key":"sk-b","base-url":"https://c.example.com"}]' \ + -d '[{"api-key":"sk-a","proxy-url":"socks5://proxy.example.com:1080"},{"api-key":"sk-b","base-url":"https://c.example.com","proxy-url":""}]' \ http://localhost:8317/v0/management/codex-api-key ``` - 响应: @@ -354,14 +354,14 @@ ```bash curl -X PATCH -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '{"index":1,"value":{"api-key":"sk-b2","base-url":"https://c.example.com"}}' \ + -d '{"index":1,"value":{"api-key":"sk-b2","base-url":"https://c.example.com","proxy-url":""}}' \ http://localhost:8317/v0/management/codex-api-key ``` - 请求(按匹配): ```bash curl -X PATCH -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '{"match":"sk-a","value":{"api-key":"sk-a","base-url":""}}' \ + -d '{"match":"sk-a","value":{"api-key":"sk-a","base-url":"","proxy-url":"socks5://proxy.example.com:1080"}}' \ http://localhost:8317/v0/management/codex-api-key ``` - 响应: @@ -430,22 +430,22 @@ ### Claude API KEY(对象数组) - GET `/claude-api-key` — 列出全部 - - 请求: - ```bash - curl -H 'Authorization: Bearer ' http://localhost:8317/v0/management/claude-api-key - ``` - - 响应: - ```json - { "claude-api-key": [ { "api-key": "sk-a", "base-url": "" } ] } - ``` + - 请求: + ```bash + curl -H 'Authorization: Bearer ' http://localhost:8317/v0/management/claude-api-key + ``` + - 响应: + ```json + { "claude-api-key": [ { "api-key": "sk-a", "base-url": "", "proxy-url": "" } ] } + ``` - PUT `/claude-api-key` — 完整改写列表 - - 请求: - ```bash - curl -X PUT -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer ' \ - -d '[{"api-key":"sk-a"},{"api-key":"sk-b","base-url":"https://c.example.com"}]' \ - http://localhost:8317/v0/management/claude-api-key - ``` + - 请求: + ```bash + curl -X PUT -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer ' \ + -d '[{"api-key":"sk-a","proxy-url":"socks5://proxy.example.com:1080"},{"api-key":"sk-b","base-url":"https://c.example.com","proxy-url":""}]' \ + http://localhost:8317/v0/management/claude-api-key + ``` - 响应: ```json { "status": "ok" } @@ -455,16 +455,16 @@ ```bash curl -X PATCH -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '{"index":1,"value":{"api-key":"sk-b2","base-url":"https://c.example.com"}}' \ - http://localhost:8317/v0/management/claude-api-key - ``` + -d '{"index":1,"value":{"api-key":"sk-b2","base-url":"https://c.example.com","proxy-url":""}}' \ + http://localhost:8317/v0/management/claude-api-key + ``` - 请求(按匹配): ```bash curl -X PATCH -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '{"match":"sk-a","value":{"api-key":"sk-a","base-url":""}}' \ - http://localhost:8317/v0/management/claude-api-key - ``` + -d '{"match":"sk-a","value":{"api-key":"sk-a","base-url":"","proxy-url":"socks5://proxy.example.com:1080"}}' \ + http://localhost:8317/v0/management/claude-api-key + ``` - 响应: ```json { "status": "ok" } @@ -491,14 +491,14 @@ ``` - 响应: ```json - { "openai-compatibility": [ { "name": "openrouter", "base-url": "https://openrouter.ai/api/v1", "api-keys": [], "models": [] } ] } + { "openai-compatibility": [ { "name": "openrouter", "base-url": "https://openrouter.ai/api/v1", "api-key-entries": [ { "api-key": "sk", "proxy-url": "" } ], "models": [] } ] } ``` - PUT `/openai-compatibility` — 完整改写列表 - 请求: ```bash curl -X PUT -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '[{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-keys":["sk"],"models":[{"name":"m","alias":"a"}]}]' \ + -d '[{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-key-entries":[{"api-key":"sk","proxy-url":""}],"models":[{"name":"m","alias":"a"}]}]' \ http://localhost:8317/v0/management/openai-compatibility ``` - 响应: @@ -510,20 +510,23 @@ ```bash curl -X PATCH -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '{"name":"openrouter","value":{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-keys":[],"models":[]}}' \ + -d '{"name":"openrouter","value":{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-key-entries":[{"api-key":"sk","proxy-url":""}],"models":[]}}' \ http://localhost:8317/v0/management/openai-compatibility ``` - 请求(按索引): ```bash curl -X PATCH -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '{"index":0,"value":{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-keys":[],"models":[]}}' \ + -d '{"index":0,"value":{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-key-entries":[{"api-key":"sk","proxy-url":""}],"models":[]}}' \ http://localhost:8317/v0/management/openai-compatibility ``` - 响应: ```json { "status": "ok" } ``` + + - 说明: + - 仍可提交遗留的 `api-keys` 字段,但所有密钥会自动迁移到 `api-key-entries` 中,返回结果中的 `api-keys` 会逐步留空。 - DELETE `/openai-compatibility` — 删除(`?name=` 或 `?index=`) - 请求(按名称): ```bash diff --git a/internal/api/handlers/management/config_lists.go b/internal/api/handlers/management/config_lists.go index 668ae24b..12fdf349 100644 --- a/internal/api/handlers/management/config_lists.go +++ b/internal/api/handlers/management/config_lists.go @@ -3,6 +3,7 @@ package management import ( "encoding/json" "fmt" + "strings" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" @@ -221,6 +222,9 @@ func (h *Handler) PutOpenAICompat(c *gin.Context) { } arr = obj.Items } + for i := range arr { + normalizeOpenAICompatibilityEntry(&arr[i]) + } h.cfg.OpenAICompatibility = arr h.persist(c) } @@ -234,6 +238,7 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) { c.JSON(400, gin.H{"error": "invalid body"}) return } + normalizeOpenAICompatibilityEntry(body.Value) if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.OpenAICompatibility) { h.cfg.OpenAICompatibility[*body.Index] = *body.Value h.persist(c) @@ -347,3 +352,32 @@ func (h *Handler) DeleteCodexKey(c *gin.Context) { } c.JSON(400, gin.H{"error": "missing api-key or index"}) } + +func normalizeOpenAICompatibilityEntry(entry *config.OpenAICompatibility) { + if entry == nil { + return + } + existing := make(map[string]struct{}, len(entry.APIKeyEntries)) + for i := range entry.APIKeyEntries { + trimmed := strings.TrimSpace(entry.APIKeyEntries[i].APIKey) + entry.APIKeyEntries[i].APIKey = trimmed + if trimmed != "" { + existing[trimmed] = struct{}{} + } + } + if len(entry.APIKeys) == 0 { + return + } + for _, legacyKey := range entry.APIKeys { + trimmed := strings.TrimSpace(legacyKey) + if trimmed == "" { + continue + } + if _, ok := existing[trimmed]; ok { + continue + } + entry.APIKeyEntries = append(entry.APIKeyEntries, config.OpenAICompatibilityAPIKey{APIKey: trimmed}) + existing[trimmed] = struct{}{} + } + entry.APIKeys = nil +} diff --git a/internal/api/server.go b/internal/api/server.go index 5212be56..09ca7c7c 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -626,7 +626,12 @@ func (s *Server) UpdateClients(cfg *config.Config) { codexAPIKeyCount := len(cfg.CodexKey) openAICompatCount := 0 for i := range cfg.OpenAICompatibility { - openAICompatCount += len(cfg.OpenAICompatibility[i].APIKeys) + entry := cfg.OpenAICompatibility[i] + if len(entry.APIKeyEntries) > 0 { + openAICompatCount += len(entry.APIKeyEntries) + continue + } + openAICompatCount += len(entry.APIKeys) } total := authFiles + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount