Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef7e8206d3 | |||
| 87291c0d75 | |||
| 51d2766d5c | |||
| a00ba77604 | |||
| 3264605c2d | |||
| cfb9cb8951 | |||
| bb00436509 | |||
| 1afbc4dd96 | |||
| d745f07044 |
+10
-4
@@ -49,10 +49,10 @@ ws-auth: false
|
|||||||
# Gemini API keys (preferred)
|
# Gemini API keys (preferred)
|
||||||
#gemini-api-key:
|
#gemini-api-key:
|
||||||
# - api-key: "AIzaSy...01"
|
# - api-key: "AIzaSy...01"
|
||||||
# # base-url: "https://generativelanguage.googleapis.com"
|
# base-url: "https://generativelanguage.googleapis.com"
|
||||||
# # headers:
|
# headers:
|
||||||
# # X-Custom-Header: "custom-value"
|
# X-Custom-Header: "custom-value"
|
||||||
# # proxy-url: "socks5://proxy.example.com:1080"
|
# proxy-url: "socks5://proxy.example.com:1080"
|
||||||
# - api-key: "AIzaSy...02"
|
# - api-key: "AIzaSy...02"
|
||||||
|
|
||||||
# API keys for official Generative Language API (legacy compatibility)
|
# API keys for official Generative Language API (legacy compatibility)
|
||||||
@@ -64,6 +64,8 @@ ws-auth: false
|
|||||||
#codex-api-key:
|
#codex-api-key:
|
||||||
# - api-key: "sk-atSM..."
|
# - api-key: "sk-atSM..."
|
||||||
# base-url: "https://www.example.com" # use the custom codex API endpoint
|
# base-url: "https://www.example.com" # use the custom codex API endpoint
|
||||||
|
# headers:
|
||||||
|
# X-Custom-Header: "custom-value"
|
||||||
# proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
|
# proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
|
||||||
|
|
||||||
# Claude API keys
|
# Claude API keys
|
||||||
@@ -71,6 +73,8 @@ ws-auth: false
|
|||||||
# - api-key: "sk-atSM..." # use the official claude API key, no need to set the base url
|
# - api-key: "sk-atSM..." # use the official claude API key, no need to set the base url
|
||||||
# - api-key: "sk-atSM..."
|
# - api-key: "sk-atSM..."
|
||||||
# base-url: "https://www.example.com" # use the custom claude API endpoint
|
# base-url: "https://www.example.com" # use the custom claude API endpoint
|
||||||
|
# headers:
|
||||||
|
# X-Custom-Header: "custom-value"
|
||||||
# proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
|
# proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
|
||||||
# models:
|
# models:
|
||||||
# - name: "claude-3-5-sonnet-20241022" # upstream model name
|
# - name: "claude-3-5-sonnet-20241022" # upstream model name
|
||||||
@@ -80,6 +84,8 @@ ws-auth: false
|
|||||||
#openai-compatibility:
|
#openai-compatibility:
|
||||||
# - name: "openrouter" # The name of the provider; it will be used in the user agent and other places.
|
# - name: "openrouter" # The name of the provider; it will be used in the user agent and other places.
|
||||||
# base-url: "https://openrouter.ai/api/v1" # The base URL of the provider.
|
# base-url: "https://openrouter.ai/api/v1" # The base URL of the provider.
|
||||||
|
# headers:
|
||||||
|
# X-Custom-Header: "custom-value"
|
||||||
# # New format with per-key proxy support (recommended):
|
# # New format with per-key proxy support (recommended):
|
||||||
# api-key-entries:
|
# api-key-entries:
|
||||||
# - api-key: "sk-or-v1-...b780"
|
# - api-key: "sk-or-v1-...b780"
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ func (h *Handler) applyLegacyKeys(keys []string) {
|
|||||||
}
|
}
|
||||||
h.cfg.GeminiKey = newList
|
h.cfg.GeminiKey = newList
|
||||||
h.cfg.GlAPIKey = sanitized
|
h.cfg.GlAPIKey = sanitized
|
||||||
h.cfg.SyncGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
// api-keys
|
// api-keys
|
||||||
@@ -206,7 +206,7 @@ func (h *Handler) PutGeminiKeys(c *gin.Context) {
|
|||||||
arr = obj.Items
|
arr = obj.Items
|
||||||
}
|
}
|
||||||
h.cfg.GeminiKey = append([]config.GeminiKey(nil), arr...)
|
h.cfg.GeminiKey = append([]config.GeminiKey(nil), arr...)
|
||||||
h.cfg.SyncGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
||||||
@@ -227,7 +227,7 @@ func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
|||||||
// Treat empty API key as delete.
|
// Treat empty API key as delete.
|
||||||
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) {
|
||||||
h.cfg.GeminiKey = append(h.cfg.GeminiKey[:*body.Index], h.cfg.GeminiKey[*body.Index+1:]...)
|
h.cfg.GeminiKey = append(h.cfg.GeminiKey[:*body.Index], h.cfg.GeminiKey[*body.Index+1:]...)
|
||||||
h.cfg.SyncGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -245,7 +245,7 @@ func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if removed {
|
if removed {
|
||||||
h.cfg.GeminiKey = out
|
h.cfg.GeminiKey = out
|
||||||
h.cfg.SyncGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -257,7 +257,7 @@ func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
|||||||
|
|
||||||
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) {
|
||||||
h.cfg.GeminiKey[*body.Index] = value
|
h.cfg.GeminiKey[*body.Index] = value
|
||||||
h.cfg.SyncGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -266,7 +266,7 @@ func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
|||||||
for i := range h.cfg.GeminiKey {
|
for i := range h.cfg.GeminiKey {
|
||||||
if h.cfg.GeminiKey[i].APIKey == match {
|
if h.cfg.GeminiKey[i].APIKey == match {
|
||||||
h.cfg.GeminiKey[i] = value
|
h.cfg.GeminiKey[i] = value
|
||||||
h.cfg.SyncGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -284,7 +284,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.SyncGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
} else {
|
} else {
|
||||||
c.JSON(404, gin.H{"error": "item not found"})
|
c.JSON(404, gin.H{"error": "item not found"})
|
||||||
@@ -295,7 +295,7 @@ func (h *Handler) DeleteGeminiKey(c *gin.Context) {
|
|||||||
var idx int
|
var idx int
|
||||||
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.SyncGeminiKeys()
|
h.cfg.SanitizeGeminiKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -328,6 +328,7 @@ func (h *Handler) PutClaudeKeys(c *gin.Context) {
|
|||||||
normalizeClaudeKey(&arr[i])
|
normalizeClaudeKey(&arr[i])
|
||||||
}
|
}
|
||||||
h.cfg.ClaudeKey = arr
|
h.cfg.ClaudeKey = arr
|
||||||
|
h.cfg.SanitizeClaudeKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchClaudeKey(c *gin.Context) {
|
func (h *Handler) PatchClaudeKey(c *gin.Context) {
|
||||||
@@ -340,16 +341,19 @@ 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
|
||||||
}
|
}
|
||||||
normalizeClaudeKey(body.Value)
|
value := *body.Value
|
||||||
|
normalizeClaudeKey(&value)
|
||||||
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) {
|
||||||
h.cfg.ClaudeKey[*body.Index] = *body.Value
|
h.cfg.ClaudeKey[*body.Index] = value
|
||||||
|
h.cfg.SanitizeClaudeKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if body.Match != nil {
|
if body.Match != nil {
|
||||||
for i := range h.cfg.ClaudeKey {
|
for i := range h.cfg.ClaudeKey {
|
||||||
if h.cfg.ClaudeKey[i].APIKey == *body.Match {
|
if h.cfg.ClaudeKey[i].APIKey == *body.Match {
|
||||||
h.cfg.ClaudeKey[i] = *body.Value
|
h.cfg.ClaudeKey[i] = value
|
||||||
|
h.cfg.SanitizeClaudeKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -366,6 +370,7 @@ func (h *Handler) DeleteClaudeKey(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
h.cfg.ClaudeKey = out
|
h.cfg.ClaudeKey = out
|
||||||
|
h.cfg.SanitizeClaudeKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -374,6 +379,7 @@ func (h *Handler) DeleteClaudeKey(c *gin.Context) {
|
|||||||
_, err := fmt.Sscanf(idxStr, "%d", &idx)
|
_, err := fmt.Sscanf(idxStr, "%d", &idx)
|
||||||
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.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -413,6 +419,7 @@ func (h *Handler) PutOpenAICompat(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
h.cfg.OpenAICompatibility = filtered
|
h.cfg.OpenAICompatibility = filtered
|
||||||
|
h.cfg.SanitizeOpenAICompatibility()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
||||||
@@ -430,6 +437,7 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
|||||||
if strings.TrimSpace(body.Value.BaseURL) == "" {
|
if strings.TrimSpace(body.Value.BaseURL) == "" {
|
||||||
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) {
|
||||||
h.cfg.OpenAICompatibility = append(h.cfg.OpenAICompatibility[:*body.Index], h.cfg.OpenAICompatibility[*body.Index+1:]...)
|
h.cfg.OpenAICompatibility = append(h.cfg.OpenAICompatibility[:*body.Index], h.cfg.OpenAICompatibility[*body.Index+1:]...)
|
||||||
|
h.cfg.SanitizeOpenAICompatibility()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -445,6 +453,7 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if removed {
|
if removed {
|
||||||
h.cfg.OpenAICompatibility = out
|
h.cfg.OpenAICompatibility = out
|
||||||
|
h.cfg.SanitizeOpenAICompatibility()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -454,6 +463,7 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
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) {
|
||||||
h.cfg.OpenAICompatibility[*body.Index] = *body.Value
|
h.cfg.OpenAICompatibility[*body.Index] = *body.Value
|
||||||
|
h.cfg.SanitizeOpenAICompatibility()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -461,6 +471,7 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
|||||||
for i := range h.cfg.OpenAICompatibility {
|
for i := range h.cfg.OpenAICompatibility {
|
||||||
if h.cfg.OpenAICompatibility[i].Name == *body.Name {
|
if h.cfg.OpenAICompatibility[i].Name == *body.Name {
|
||||||
h.cfg.OpenAICompatibility[i] = *body.Value
|
h.cfg.OpenAICompatibility[i] = *body.Value
|
||||||
|
h.cfg.SanitizeOpenAICompatibility()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -477,6 +488,7 @@ func (h *Handler) DeleteOpenAICompat(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
h.cfg.OpenAICompatibility = out
|
h.cfg.OpenAICompatibility = out
|
||||||
|
h.cfg.SanitizeOpenAICompatibility()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -485,6 +497,7 @@ func (h *Handler) DeleteOpenAICompat(c *gin.Context) {
|
|||||||
_, err := fmt.Sscanf(idxStr, "%d", &idx)
|
_, err := fmt.Sscanf(idxStr, "%d", &idx)
|
||||||
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.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -517,13 +530,17 @@ func (h *Handler) PutCodexKeys(c *gin.Context) {
|
|||||||
filtered := make([]config.CodexKey, 0, len(arr))
|
filtered := make([]config.CodexKey, 0, len(arr))
|
||||||
for i := range arr {
|
for i := range arr {
|
||||||
entry := arr[i]
|
entry := arr[i]
|
||||||
|
entry.APIKey = strings.TrimSpace(entry.APIKey)
|
||||||
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
||||||
|
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
|
||||||
|
entry.Headers = config.NormalizeHeaders(entry.Headers)
|
||||||
if entry.BaseURL == "" {
|
if entry.BaseURL == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
filtered = append(filtered, entry)
|
filtered = append(filtered, entry)
|
||||||
}
|
}
|
||||||
h.cfg.CodexKey = filtered
|
h.cfg.CodexKey = filtered
|
||||||
|
h.cfg.SanitizeCodexKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchCodexKey(c *gin.Context) {
|
func (h *Handler) PatchCodexKey(c *gin.Context) {
|
||||||
@@ -536,10 +553,16 @@ 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
|
||||||
}
|
}
|
||||||
|
value := *body.Value
|
||||||
|
value.APIKey = strings.TrimSpace(value.APIKey)
|
||||||
|
value.BaseURL = strings.TrimSpace(value.BaseURL)
|
||||||
|
value.ProxyURL = strings.TrimSpace(value.ProxyURL)
|
||||||
|
value.Headers = config.NormalizeHeaders(value.Headers)
|
||||||
// If base-url becomes empty, delete instead of update
|
// If base-url becomes empty, delete instead of update
|
||||||
if strings.TrimSpace(body.Value.BaseURL) == "" {
|
if value.BaseURL == "" {
|
||||||
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) {
|
||||||
h.cfg.CodexKey = append(h.cfg.CodexKey[:*body.Index], h.cfg.CodexKey[*body.Index+1:]...)
|
h.cfg.CodexKey = append(h.cfg.CodexKey[:*body.Index], h.cfg.CodexKey[*body.Index+1:]...)
|
||||||
|
h.cfg.SanitizeCodexKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -555,20 +578,23 @@ func (h *Handler) PatchCodexKey(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if removed {
|
if removed {
|
||||||
h.cfg.CodexKey = out
|
h.cfg.CodexKey = out
|
||||||
|
h.cfg.SanitizeCodexKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
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) {
|
||||||
h.cfg.CodexKey[*body.Index] = *body.Value
|
h.cfg.CodexKey[*body.Index] = value
|
||||||
|
h.cfg.SanitizeCodexKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if body.Match != nil {
|
if body.Match != nil {
|
||||||
for i := range h.cfg.CodexKey {
|
for i := range h.cfg.CodexKey {
|
||||||
if h.cfg.CodexKey[i].APIKey == *body.Match {
|
if h.cfg.CodexKey[i].APIKey == *body.Match {
|
||||||
h.cfg.CodexKey[i] = *body.Value
|
h.cfg.CodexKey[i] = value
|
||||||
|
h.cfg.SanitizeCodexKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -586,6 +612,7 @@ func (h *Handler) DeleteCodexKey(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
h.cfg.CodexKey = out
|
h.cfg.CodexKey = out
|
||||||
|
h.cfg.SanitizeCodexKeys()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -594,6 +621,7 @@ func (h *Handler) DeleteCodexKey(c *gin.Context) {
|
|||||||
_, err := fmt.Sscanf(idxStr, "%d", &idx)
|
_, err := fmt.Sscanf(idxStr, "%d", &idx)
|
||||||
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.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -607,6 +635,7 @@ func normalizeOpenAICompatibilityEntry(entry *config.OpenAICompatibility) {
|
|||||||
}
|
}
|
||||||
// Trim base-url; empty base-url indicates provider should be removed by sanitization
|
// Trim base-url; empty base-url indicates provider should be removed by sanitization
|
||||||
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
||||||
|
entry.Headers = config.NormalizeHeaders(entry.Headers)
|
||||||
existing := make(map[string]struct{}, len(entry.APIKeyEntries))
|
existing := make(map[string]struct{}, len(entry.APIKeyEntries))
|
||||||
for i := range entry.APIKeyEntries {
|
for i := range entry.APIKeyEntries {
|
||||||
trimmed := strings.TrimSpace(entry.APIKeyEntries[i].APIKey)
|
trimmed := strings.TrimSpace(entry.APIKeyEntries[i].APIKey)
|
||||||
@@ -658,6 +687,7 @@ func normalizeClaudeKey(entry *config.ClaudeKey) {
|
|||||||
entry.APIKey = strings.TrimSpace(entry.APIKey)
|
entry.APIKey = strings.TrimSpace(entry.APIKey)
|
||||||
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
||||||
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
|
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
|
||||||
|
entry.Headers = config.NormalizeHeaders(entry.Headers)
|
||||||
if len(entry.Models) == 0 {
|
if len(entry.Models) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-13
@@ -100,6 +100,9 @@ type ClaudeKey struct {
|
|||||||
|
|
||||||
// Models defines upstream model names and aliases for request routing.
|
// Models defines upstream model names and aliases for request routing.
|
||||||
Models []ClaudeModel `yaml:"models" json:"models"`
|
Models []ClaudeModel `yaml:"models" json:"models"`
|
||||||
|
|
||||||
|
// Headers optionally adds extra HTTP headers for requests sent with this key.
|
||||||
|
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClaudeModel describes a mapping between an alias and the actual upstream model name.
|
// ClaudeModel describes a mapping between an alias and the actual upstream model name.
|
||||||
@@ -123,6 +126,9 @@ type CodexKey struct {
|
|||||||
|
|
||||||
// ProxyURL overrides the global proxy setting for this API key if provided.
|
// ProxyURL overrides the global proxy setting for this API key if provided.
|
||||||
ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
|
ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
|
||||||
|
|
||||||
|
// Headers optionally adds extra HTTP headers for requests sent with this key.
|
||||||
|
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeminiKey represents the configuration for a Gemini API key,
|
// GeminiKey represents the configuration for a Gemini API key,
|
||||||
@@ -159,6 +165,9 @@ type OpenAICompatibility struct {
|
|||||||
|
|
||||||
// Models defines the model configurations including aliases for routing.
|
// Models defines the model configurations including aliases for routing.
|
||||||
Models []OpenAICompatibilityModel `yaml:"models" json:"models"`
|
Models []OpenAICompatibilityModel `yaml:"models" json:"models"`
|
||||||
|
|
||||||
|
// Headers optionally adds extra HTTP headers for requests sent to this provider.
|
||||||
|
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenAICompatibilityAPIKey represents an API key configuration with optional proxy setting.
|
// OpenAICompatibilityAPIKey represents an API key configuration with optional proxy setting.
|
||||||
@@ -246,23 +255,26 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|||||||
// Sync request authentication providers with inline API keys for backwards compatibility.
|
// Sync request authentication providers with inline API keys for backwards compatibility.
|
||||||
syncInlineAccessProvider(&cfg)
|
syncInlineAccessProvider(&cfg)
|
||||||
|
|
||||||
// Normalize Gemini API key configuration and migrate legacy entries.
|
// Sanitize Gemini API key configuration and migrate legacy entries.
|
||||||
cfg.SyncGeminiKeys()
|
cfg.SanitizeGeminiKeys()
|
||||||
|
|
||||||
// Sanitize OpenAI compatibility providers: drop entries without base-url
|
|
||||||
sanitizeOpenAICompatibility(&cfg)
|
|
||||||
|
|
||||||
// Sanitize Codex keys: drop entries without base-url
|
// Sanitize Codex keys: drop entries without base-url
|
||||||
sanitizeCodexKeys(&cfg)
|
cfg.SanitizeCodexKeys()
|
||||||
|
|
||||||
|
// Sanitize Claude key headers
|
||||||
|
cfg.SanitizeClaudeKeys()
|
||||||
|
|
||||||
|
// Sanitize OpenAI compatibility providers: drop entries without base-url
|
||||||
|
cfg.SanitizeOpenAICompatibility()
|
||||||
|
|
||||||
// Return the populated configuration struct.
|
// Return the populated configuration struct.
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitizeOpenAICompatibility removes OpenAI-compatibility provider entries that are
|
// SanitizeOpenAICompatibility removes OpenAI-compatibility provider entries that are
|
||||||
// not actionable, specifically those missing a BaseURL. It trims whitespace before
|
// not actionable, specifically those missing a BaseURL. It trims whitespace before
|
||||||
// evaluation and preserves the relative order of remaining entries.
|
// evaluation and preserves the relative order of remaining entries.
|
||||||
func sanitizeOpenAICompatibility(cfg *Config) {
|
func (cfg *Config) SanitizeOpenAICompatibility() {
|
||||||
if cfg == nil || len(cfg.OpenAICompatibility) == 0 {
|
if cfg == nil || len(cfg.OpenAICompatibility) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -271,6 +283,7 @@ func sanitizeOpenAICompatibility(cfg *Config) {
|
|||||||
e := cfg.OpenAICompatibility[i]
|
e := cfg.OpenAICompatibility[i]
|
||||||
e.Name = strings.TrimSpace(e.Name)
|
e.Name = strings.TrimSpace(e.Name)
|
||||||
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
||||||
|
e.Headers = NormalizeHeaders(e.Headers)
|
||||||
if e.BaseURL == "" {
|
if e.BaseURL == "" {
|
||||||
// Skip providers with no base-url; treated as removed
|
// Skip providers with no base-url; treated as removed
|
||||||
continue
|
continue
|
||||||
@@ -280,9 +293,9 @@ func sanitizeOpenAICompatibility(cfg *Config) {
|
|||||||
cfg.OpenAICompatibility = out
|
cfg.OpenAICompatibility = out
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitizeCodexKeys removes Codex API key entries missing a BaseURL.
|
// SanitizeCodexKeys removes Codex API key entries missing a BaseURL.
|
||||||
// It trims whitespace and preserves order for remaining entries.
|
// It trims whitespace and preserves order for remaining entries.
|
||||||
func sanitizeCodexKeys(cfg *Config) {
|
func (cfg *Config) SanitizeCodexKeys() {
|
||||||
if cfg == nil || len(cfg.CodexKey) == 0 {
|
if cfg == nil || len(cfg.CodexKey) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -290,6 +303,7 @@ func sanitizeCodexKeys(cfg *Config) {
|
|||||||
for i := range cfg.CodexKey {
|
for i := range cfg.CodexKey {
|
||||||
e := cfg.CodexKey[i]
|
e := cfg.CodexKey[i]
|
||||||
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
||||||
|
e.Headers = NormalizeHeaders(e.Headers)
|
||||||
if e.BaseURL == "" {
|
if e.BaseURL == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -298,7 +312,19 @@ func sanitizeCodexKeys(cfg *Config) {
|
|||||||
cfg.CodexKey = out
|
cfg.CodexKey = out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) SyncGeminiKeys() {
|
// SanitizeClaudeKeys normalizes headers for Claude credentials.
|
||||||
|
func (cfg *Config) SanitizeClaudeKeys() {
|
||||||
|
if cfg == nil || len(cfg.ClaudeKey) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range cfg.ClaudeKey {
|
||||||
|
entry := &cfg.ClaudeKey[i]
|
||||||
|
entry.Headers = NormalizeHeaders(entry.Headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanitizeGeminiKeys deduplicates and normalizes Gemini credentials.
|
||||||
|
func (cfg *Config) SanitizeGeminiKeys() {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -313,7 +339,7 @@ func (cfg *Config) SyncGeminiKeys() {
|
|||||||
}
|
}
|
||||||
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
||||||
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
|
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
|
||||||
entry.Headers = normalizeGeminiHeaders(entry.Headers)
|
entry.Headers = NormalizeHeaders(entry.Headers)
|
||||||
if _, exists := seen[entry.APIKey]; exists {
|
if _, exists := seen[entry.APIKey]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -356,7 +382,8 @@ func looksLikeBcrypt(s string) bool {
|
|||||||
return len(s) > 4 && (s[:4] == "$2a$" || s[:4] == "$2b$" || s[:4] == "$2y$")
|
return len(s) > 4 && (s[:4] == "$2a$" || s[:4] == "$2b$" || s[:4] == "$2y$")
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeGeminiHeaders(headers map[string]string) map[string]string {
|
// NormalizeHeaders trims header keys and values and removes empty pairs.
|
||||||
|
func NormalizeHeaders(headers map[string]string) map[string]string {
|
||||||
if len(headers) == 0 {
|
if len(headers) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,26 +124,53 @@ func GetGeminiModels() []*ModelInfo { return GeminiModels() }
|
|||||||
|
|
||||||
// GetGeminiCLIModels returns the standard Gemini model definitions
|
// GetGeminiCLIModels returns the standard Gemini model definitions
|
||||||
func GetGeminiCLIModels() []*ModelInfo {
|
func GetGeminiCLIModels() []*ModelInfo {
|
||||||
base := GeminiModels()
|
return []*ModelInfo{
|
||||||
return append(base,
|
{
|
||||||
[]*ModelInfo{
|
ID: "gemini-2.5-flash",
|
||||||
{
|
Object: "model",
|
||||||
ID: "gemini-3-pro-preview-11-2025",
|
Created: time.Now().Unix(),
|
||||||
Object: "model",
|
OwnedBy: "google",
|
||||||
Created: time.Now().Unix(),
|
Type: "gemini",
|
||||||
OwnedBy: "google",
|
Name: "models/gemini-2.5-flash",
|
||||||
Type: "gemini",
|
Version: "001",
|
||||||
Name: "models/gemini-3-pro-preview-11-2025",
|
DisplayName: "Gemini 2.5 Flash",
|
||||||
Version: "3",
|
Description: "Stable version of Gemini 2.5 Flash, our mid-size multimodal model that supports up to 1 million tokens, released in June of 2025.",
|
||||||
DisplayName: "Gemini 3 Pro Preview 11-2025",
|
InputTokenLimit: 1048576,
|
||||||
Description: "Latest preview of Gemini Pro",
|
OutputTokenLimit: 65536,
|
||||||
InputTokenLimit: 1048576,
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
OutputTokenLimit: 65536,
|
Thinking: &ThinkingSupport{Min: 0, Max: 24576, ZeroAllowed: true, DynamicAllowed: true},
|
||||||
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
},
|
||||||
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
|
{
|
||||||
},
|
ID: "gemini-2.5-pro",
|
||||||
}...,
|
Object: "model",
|
||||||
)
|
Created: time.Now().Unix(),
|
||||||
|
OwnedBy: "google",
|
||||||
|
Type: "gemini",
|
||||||
|
Name: "models/gemini-2.5-pro",
|
||||||
|
Version: "2.5",
|
||||||
|
DisplayName: "Gemini 2.5 Pro",
|
||||||
|
Description: "Stable release (June 17th, 2025) of Gemini 2.5 Pro",
|
||||||
|
InputTokenLimit: 1048576,
|
||||||
|
OutputTokenLimit: 65536,
|
||||||
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
|
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "gemini-3-pro-preview-11-2025",
|
||||||
|
Object: "model",
|
||||||
|
Created: time.Now().Unix(),
|
||||||
|
OwnedBy: "google",
|
||||||
|
Type: "gemini",
|
||||||
|
Name: "models/gemini-3-pro-preview-11-2025",
|
||||||
|
Version: "3",
|
||||||
|
DisplayName: "Gemini 3 Pro Preview 11-2025",
|
||||||
|
Description: "Latest preview of Gemini Pro",
|
||||||
|
InputTokenLimit: 1048576,
|
||||||
|
OutputTokenLimit: 65536,
|
||||||
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
|
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAIStudioModels returns the Gemini model definitions for AI Studio integrations
|
// GetAIStudioModels returns the Gemini model definitions for AI Studio integrations
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
claudeauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude"
|
claudeauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude"
|
||||||
"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/misc"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||||
@@ -67,7 +68,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
applyClaudeHeaders(httpReq, apiKey, false)
|
applyClaudeHeaders(httpReq, auth, apiKey, false)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -159,7 +160,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
applyClaudeHeaders(httpReq, apiKey, true)
|
applyClaudeHeaders(httpReq, auth, apiKey, true)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -290,7 +291,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
applyClaudeHeaders(httpReq, apiKey, false)
|
applyClaudeHeaders(httpReq, auth, apiKey, false)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -529,7 +530,7 @@ func decodeResponseBody(body io.ReadCloser, contentEncoding string) (io.ReadClos
|
|||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyClaudeHeaders(r *http.Request, apiKey string, stream bool) {
|
func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string, stream bool) {
|
||||||
r.Header.Set("Authorization", "Bearer "+apiKey)
|
r.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
r.Header.Set("Content-Type", "application/json")
|
r.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
@@ -564,9 +565,14 @@ func applyClaudeHeaders(r *http.Request, apiKey string, stream bool) {
|
|||||||
r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||||
if stream {
|
if stream {
|
||||||
r.Header.Set("Accept", "text/event-stream")
|
r.Header.Set("Accept", "text/event-stream")
|
||||||
return
|
} else {
|
||||||
|
r.Header.Set("Accept", "application/json")
|
||||||
}
|
}
|
||||||
r.Header.Set("Accept", "application/json")
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(r, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func claudeCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {
|
func claudeCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {
|
||||||
|
|||||||
@@ -585,6 +585,11 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(r, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func codexCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {
|
func codexCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {
|
||||||
|
|||||||
@@ -495,44 +495,11 @@ func resolveGeminiBaseURL(auth *cliproxyauth.Auth) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applyGeminiHeaders(req *http.Request, auth *cliproxyauth.Auth) {
|
func applyGeminiHeaders(req *http.Request, auth *cliproxyauth.Auth) {
|
||||||
if req == nil {
|
var attrs map[string]string
|
||||||
return
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
}
|
}
|
||||||
headers := geminiCustomHeaders(auth)
|
util.ApplyCustomHeadersFromAttrs(req, attrs)
|
||||||
if len(headers) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for k, v := range headers {
|
|
||||||
if k == "" || v == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
req.Header.Set(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func geminiCustomHeaders(auth *cliproxyauth.Auth) map[string]string {
|
|
||||||
if auth == nil || auth.Attributes == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
headers := make(map[string]string, len(auth.Attributes))
|
|
||||||
for k, v := range auth.Attributes {
|
|
||||||
if !strings.HasPrefix(k, "header:") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := strings.TrimSpace(strings.TrimPrefix(k, "header:"))
|
|
||||||
if name == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val := strings.TrimSpace(v)
|
|
||||||
if val == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
headers[name] = val
|
|
||||||
}
|
|
||||||
if len(headers) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return headers
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fixGeminiImageAspectRatio(modelName string, rawJSON []byte) []byte {
|
func fixGeminiImageAspectRatio(modelName string, rawJSON []byte) []byte {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"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/util"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||||
@@ -66,6 +67,11 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
}
|
}
|
||||||
httpReq.Header.Set("User-Agent", "cli-proxy-openai-compat")
|
httpReq.Header.Set("User-Agent", "cli-proxy-openai-compat")
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -110,6 +116,8 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, body)
|
appendAPIResponseChunk(ctx, e.cfg, body)
|
||||||
reporter.publish(ctx, parseOpenAIUsage(body))
|
reporter.publish(ctx, parseOpenAIUsage(body))
|
||||||
|
// Ensure we at least record the request even if upstream doesn't return usage
|
||||||
|
reporter.ensurePublished(ctx)
|
||||||
// Translate response back to source format when needed
|
// Translate response back to source format when needed
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, body, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, body, ¶m)
|
||||||
@@ -143,6 +151,11 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|||||||
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
}
|
}
|
||||||
httpReq.Header.Set("User-Agent", "cli-proxy-openai-compat")
|
httpReq.Header.Set("User-Agent", "cli-proxy-openai-compat")
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
httpReq.Header.Set("Accept", "text/event-stream")
|
httpReq.Header.Set("Accept", "text/event-stream")
|
||||||
httpReq.Header.Set("Cache-Control", "no-cache")
|
httpReq.Header.Set("Cache-Control", "no-cache")
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
@@ -214,6 +227,8 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|||||||
reporter.publishFailure(ctx)
|
reporter.publishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
}
|
}
|
||||||
|
// Ensure we record the request if no usage chunk was ever seen
|
||||||
|
reporter.ensurePublished(ctx)
|
||||||
}()
|
}()
|
||||||
return stream, nil
|
return stream, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,28 @@ func (r *usageReporter) publishWithOutcome(ctx context.Context, detail usage.Det
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensurePublished guarantees that a usage record is emitted exactly once.
|
||||||
|
// It is safe to call multiple times; only the first call wins due to once.Do.
|
||||||
|
// This is used to ensure request counting even when upstream responses do not
|
||||||
|
// include any usage fields (tokens), especially for streaming paths.
|
||||||
|
func (r *usageReporter) ensurePublished(ctx context.Context) {
|
||||||
|
if r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.once.Do(func() {
|
||||||
|
usage.PublishRecord(ctx, usage.Record{
|
||||||
|
Provider: r.provider,
|
||||||
|
Model: r.model,
|
||||||
|
Source: r.source,
|
||||||
|
APIKey: r.apiKey,
|
||||||
|
AuthID: r.authID,
|
||||||
|
RequestedAt: r.requestedAt,
|
||||||
|
Failed: false,
|
||||||
|
Detail: usage.Detail{},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func apiKeyFromContext(ctx context.Context) string {
|
func apiKeyFromContext(ctx context.Context) string {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -133,27 +133,16 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create main message if there's text content or tool calls
|
// Emit text/image content as one message
|
||||||
if len(contentItems) > 0 || len(toolCalls) > 0 {
|
if len(contentItems) > 0 {
|
||||||
msgJSON := `{"role":"","content":""}`
|
msgJSON := `{"role":"","content":""}`
|
||||||
msgJSON, _ = sjson.Set(msgJSON, "role", role)
|
msgJSON, _ = sjson.Set(msgJSON, "role", role)
|
||||||
|
|
||||||
// Set content
|
contentArrayJSON := "[]"
|
||||||
if len(contentItems) > 0 {
|
for _, contentItem := range contentItems {
|
||||||
contentArrayJSON := "[]"
|
contentArrayJSON, _ = sjson.SetRaw(contentArrayJSON, "-1", contentItem)
|
||||||
for _, contentItem := range contentItems {
|
|
||||||
contentArrayJSON, _ = sjson.SetRaw(contentArrayJSON, "-1", contentItem)
|
|
||||||
}
|
|
||||||
msgJSON, _ = sjson.SetRaw(msgJSON, "content", contentArrayJSON)
|
|
||||||
} else {
|
|
||||||
msgJSON, _ = sjson.Set(msgJSON, "content", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set tool calls for assistant messages
|
|
||||||
if role == "assistant" && len(toolCalls) > 0 {
|
|
||||||
toolCallsJSON, _ := json.Marshal(toolCalls)
|
|
||||||
msgJSON, _ = sjson.SetRaw(msgJSON, "tool_calls", string(toolCallsJSON))
|
|
||||||
}
|
}
|
||||||
|
msgJSON, _ = sjson.SetRaw(msgJSON, "content", contentArrayJSON)
|
||||||
|
|
||||||
contentValue := gjson.Get(msgJSON, "content")
|
contentValue := gjson.Get(msgJSON, "content")
|
||||||
hasContent := false
|
hasContent := false
|
||||||
@@ -168,11 +157,19 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
hasContent = contentValue.Raw != "" && contentValue.Raw != "null"
|
hasContent = contentValue.Raw != "" && contentValue.Raw != "null"
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasContent || len(toolCalls) != 0 {
|
if hasContent {
|
||||||
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(msgJSON).Value())
|
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(msgJSON).Value())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit tool calls in a separate assistant message
|
||||||
|
if role == "assistant" && len(toolCalls) > 0 {
|
||||||
|
toolCallMsgJSON := `{"role":"assistant","tool_calls":[]}`
|
||||||
|
toolCallsJSON, _ := json.Marshal(toolCalls)
|
||||||
|
toolCallMsgJSON, _ = sjson.SetRaw(toolCallMsgJSON, "tool_calls", string(toolCallsJSON))
|
||||||
|
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(toolCallMsgJSON).Value())
|
||||||
|
}
|
||||||
|
|
||||||
} else if contentResult.Exists() && contentResult.Type == gjson.String {
|
} else if contentResult.Exists() && contentResult.Type == gjson.String {
|
||||||
// Simple string content
|
// Simple string content
|
||||||
msgJSON := `{"role":"","content":""}`
|
msgJSON := `{"role":"","content":""}`
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApplyCustomHeadersFromAttrs applies user-defined headers stored in the provided attributes map.
|
||||||
|
// Custom headers override built-in defaults when conflicts occur.
|
||||||
|
func ApplyCustomHeadersFromAttrs(r *http.Request, attrs map[string]string) {
|
||||||
|
if r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
applyCustomHeaders(r, extractCustomHeaders(attrs))
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractCustomHeaders(attrs map[string]string) map[string]string {
|
||||||
|
if len(attrs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
headers := make(map[string]string)
|
||||||
|
for k, v := range attrs {
|
||||||
|
if !strings.HasPrefix(k, "header:") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(strings.TrimPrefix(k, "header:"))
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val := strings.TrimSpace(v)
|
||||||
|
if val == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
headers[name] = val
|
||||||
|
}
|
||||||
|
if len(headers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyCustomHeaders(r *http.Request, headers map[string]string) {
|
||||||
|
if r == nil || len(headers) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for k, v := range headers {
|
||||||
|
if k == "" || v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-11
@@ -762,16 +762,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
if base != "" {
|
if base != "" {
|
||||||
attrs["base_url"] = base
|
attrs["base_url"] = base
|
||||||
}
|
}
|
||||||
if len(entry.Headers) > 0 {
|
addConfigHeadersToAttrs(entry.Headers, attrs)
|
||||||
for hk, hv := range entry.Headers {
|
|
||||||
key := strings.TrimSpace(hk)
|
|
||||||
val := strings.TrimSpace(hv)
|
|
||||||
if key == "" || val == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
attrs["header:"+key] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
a := &coreauth.Auth{
|
a := &coreauth.Auth{
|
||||||
ID: id,
|
ID: id,
|
||||||
Provider: "gemini",
|
Provider: "gemini",
|
||||||
@@ -803,6 +794,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
if hash := computeClaudeModelsHash(ck.Models); hash != "" {
|
if hash := computeClaudeModelsHash(ck.Models); hash != "" {
|
||||||
attrs["models_hash"] = hash
|
attrs["models_hash"] = hash
|
||||||
}
|
}
|
||||||
|
addConfigHeadersToAttrs(ck.Headers, attrs)
|
||||||
proxyURL := strings.TrimSpace(ck.ProxyURL)
|
proxyURL := strings.TrimSpace(ck.ProxyURL)
|
||||||
a := &coreauth.Auth{
|
a := &coreauth.Auth{
|
||||||
ID: id,
|
ID: id,
|
||||||
@@ -831,6 +823,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
if ck.BaseURL != "" {
|
if ck.BaseURL != "" {
|
||||||
attrs["base_url"] = ck.BaseURL
|
attrs["base_url"] = ck.BaseURL
|
||||||
}
|
}
|
||||||
|
addConfigHeadersToAttrs(ck.Headers, attrs)
|
||||||
proxyURL := strings.TrimSpace(ck.ProxyURL)
|
proxyURL := strings.TrimSpace(ck.ProxyURL)
|
||||||
a := &coreauth.Auth{
|
a := &coreauth.Auth{
|
||||||
ID: id,
|
ID: id,
|
||||||
@@ -873,6 +866,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" {
|
if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" {
|
||||||
attrs["models_hash"] = hash
|
attrs["models_hash"] = hash
|
||||||
}
|
}
|
||||||
|
addConfigHeadersToAttrs(compat.Headers, attrs)
|
||||||
a := &coreauth.Auth{
|
a := &coreauth.Auth{
|
||||||
ID: id,
|
ID: id,
|
||||||
Provider: providerName,
|
Provider: providerName,
|
||||||
@@ -905,6 +899,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" {
|
if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" {
|
||||||
attrs["models_hash"] = hash
|
attrs["models_hash"] = hash
|
||||||
}
|
}
|
||||||
|
addConfigHeadersToAttrs(compat.Headers, attrs)
|
||||||
a := &coreauth.Auth{
|
a := &coreauth.Auth{
|
||||||
ID: id,
|
ID: id,
|
||||||
Provider: providerName,
|
Provider: providerName,
|
||||||
@@ -930,6 +925,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" {
|
if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" {
|
||||||
attrs["models_hash"] = hash
|
attrs["models_hash"] = hash
|
||||||
}
|
}
|
||||||
|
addConfigHeadersToAttrs(compat.Headers, attrs)
|
||||||
a := &coreauth.Auth{
|
a := &coreauth.Auth{
|
||||||
ID: id,
|
ID: id,
|
||||||
Provider: providerName,
|
Provider: providerName,
|
||||||
@@ -1131,13 +1127,16 @@ func describeOpenAICompatibilityUpdate(oldEntry, newEntry config.OpenAICompatibi
|
|||||||
newKeyCount := countAPIKeys(newEntry)
|
newKeyCount := countAPIKeys(newEntry)
|
||||||
oldModelCount := countOpenAIModels(oldEntry.Models)
|
oldModelCount := countOpenAIModels(oldEntry.Models)
|
||||||
newModelCount := countOpenAIModels(newEntry.Models)
|
newModelCount := countOpenAIModels(newEntry.Models)
|
||||||
details := make([]string, 0, 2)
|
details := make([]string, 0, 3)
|
||||||
if oldKeyCount != newKeyCount {
|
if oldKeyCount != newKeyCount {
|
||||||
details = append(details, fmt.Sprintf("api-keys %d -> %d", oldKeyCount, newKeyCount))
|
details = append(details, fmt.Sprintf("api-keys %d -> %d", oldKeyCount, newKeyCount))
|
||||||
}
|
}
|
||||||
if oldModelCount != newModelCount {
|
if oldModelCount != newModelCount {
|
||||||
details = append(details, fmt.Sprintf("models %d -> %d", oldModelCount, newModelCount))
|
details = append(details, fmt.Sprintf("models %d -> %d", oldModelCount, newModelCount))
|
||||||
}
|
}
|
||||||
|
if !equalStringMap(oldEntry.Headers, newEntry.Headers) {
|
||||||
|
details = append(details, "headers updated")
|
||||||
|
}
|
||||||
if len(details) == 0 {
|
if len(details) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -1303,6 +1302,9 @@ func buildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
|||||||
if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) {
|
if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) {
|
||||||
changes = append(changes, fmt.Sprintf("claude[%d].api-key: updated", i))
|
changes = append(changes, fmt.Sprintf("claude[%d].api-key: updated", i))
|
||||||
}
|
}
|
||||||
|
if !equalStringMap(o.Headers, n.Headers) {
|
||||||
|
changes = append(changes, fmt.Sprintf("claude[%d].headers: updated", i))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1325,6 +1327,9 @@ func buildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
|||||||
if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) {
|
if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) {
|
||||||
changes = append(changes, fmt.Sprintf("codex[%d].api-key: updated", i))
|
changes = append(changes, fmt.Sprintf("codex[%d].api-key: updated", i))
|
||||||
}
|
}
|
||||||
|
if !equalStringMap(o.Headers, n.Headers) {
|
||||||
|
changes = append(changes, fmt.Sprintf("codex[%d].headers: updated", i))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1357,6 +1362,20 @@ func buildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
|||||||
return changes
|
return changes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addConfigHeadersToAttrs(headers map[string]string, attrs map[string]string) {
|
||||||
|
if len(headers) == 0 || attrs == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for hk, hv := range headers {
|
||||||
|
key := strings.TrimSpace(hk)
|
||||||
|
val := strings.TrimSpace(hv)
|
||||||
|
if key == "" || val == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
attrs["header:"+key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func trimStrings(in []string) []string {
|
func trimStrings(in []string) []string {
|
||||||
out := make([]string, len(in))
|
out := make([]string, len(in))
|
||||||
for i := range in {
|
for i := range in {
|
||||||
|
|||||||
@@ -305,6 +305,12 @@ func (s *Service) ensureExecutorsForAuth(a *coreauth.Auth) {
|
|||||||
if s == nil || a == nil {
|
if s == nil || a == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Skip disabled auth entries when (re)binding executors.
|
||||||
|
// Disabled auths can linger during config reloads (e.g., removed OpenAI-compat entries)
|
||||||
|
// and must not override active provider executors (such as iFlow OAuth accounts).
|
||||||
|
if a.Disabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
if compatProviderKey, _, isCompat := openAICompatInfoFromAuth(a); isCompat {
|
if compatProviderKey, _, isCompat := openAICompatInfoFromAuth(a); isCompat {
|
||||||
if compatProviderKey == "" {
|
if compatProviderKey == "" {
|
||||||
compatProviderKey = strings.ToLower(strings.TrimSpace(a.Provider))
|
compatProviderKey = strings.ToLower(strings.TrimSpace(a.Provider))
|
||||||
@@ -738,7 +744,7 @@ func (s *Service) resolveConfigClaudeKey(auth *coreauth.Auth) *config.ClaudeKey
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if attrKey != "" && strings.EqualFold(cfgKey, attrKey) {
|
if attrKey != "" && strings.EqualFold(cfgKey, attrKey) {
|
||||||
if attrBase == "" || cfgBase == "" || strings.EqualFold(cfgBase, attrBase) {
|
if cfgBase == "" || strings.EqualFold(cfgBase, attrBase) {
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user