hkfires commited on
Commit ·
e6bdaef
1
Parent(s): 4864a6f
feat(config): support HTTP headers across providers
Browse files- config.example.yaml +10 -4
- internal/config/config.go +26 -2
- internal/runtime/executor/claude_executor.go +12 -6
- internal/runtime/executor/codex_executor.go +5 -0
- internal/runtime/executor/gemini_executor.go +4 -37
- internal/runtime/executor/openai_compat_executor.go +11 -0
- internal/util/header_helpers.go +52 -0
- internal/watcher/watcher.go +30 -11
config.example.yaml
CHANGED
|
@@ -49,10 +49,10 @@ ws-auth: false
|
|
| 49 |
# Gemini API keys (preferred)
|
| 50 |
#gemini-api-key:
|
| 51 |
# - api-key: "AIzaSy...01"
|
| 52 |
-
#
|
| 53 |
-
#
|
| 54 |
-
#
|
| 55 |
-
#
|
| 56 |
# - api-key: "AIzaSy...02"
|
| 57 |
|
| 58 |
# API keys for official Generative Language API (legacy compatibility)
|
|
@@ -64,6 +64,8 @@ ws-auth: false
|
|
| 64 |
#codex-api-key:
|
| 65 |
# - api-key: "sk-atSM..."
|
| 66 |
# base-url: "https://www.example.com" # use the custom codex API endpoint
|
|
|
|
|
|
|
| 67 |
# proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
|
| 68 |
|
| 69 |
# Claude API keys
|
|
@@ -71,6 +73,8 @@ ws-auth: false
|
|
| 71 |
# - api-key: "sk-atSM..." # use the official claude API key, no need to set the base url
|
| 72 |
# - api-key: "sk-atSM..."
|
| 73 |
# base-url: "https://www.example.com" # use the custom claude API endpoint
|
|
|
|
|
|
|
| 74 |
# proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
|
| 75 |
# models:
|
| 76 |
# - name: "claude-3-5-sonnet-20241022" # upstream model name
|
|
@@ -80,6 +84,8 @@ ws-auth: false
|
|
| 80 |
#openai-compatibility:
|
| 81 |
# - name: "openrouter" # The name of the provider; it will be used in the user agent and other places.
|
| 82 |
# base-url: "https://openrouter.ai/api/v1" # The base URL of the provider.
|
|
|
|
|
|
|
| 83 |
# # New format with per-key proxy support (recommended):
|
| 84 |
# api-key-entries:
|
| 85 |
# - api-key: "sk-or-v1-...b780"
|
|
|
|
| 49 |
# Gemini API keys (preferred)
|
| 50 |
#gemini-api-key:
|
| 51 |
# - api-key: "AIzaSy...01"
|
| 52 |
+
# base-url: "https://generativelanguage.googleapis.com"
|
| 53 |
+
# headers:
|
| 54 |
+
# X-Custom-Header: "custom-value"
|
| 55 |
+
# proxy-url: "socks5://proxy.example.com:1080"
|
| 56 |
# - api-key: "AIzaSy...02"
|
| 57 |
|
| 58 |
# API keys for official Generative Language API (legacy compatibility)
|
|
|
|
| 64 |
#codex-api-key:
|
| 65 |
# - api-key: "sk-atSM..."
|
| 66 |
# base-url: "https://www.example.com" # use the custom codex API endpoint
|
| 67 |
+
# headers:
|
| 68 |
+
# X-Custom-Header: "custom-value"
|
| 69 |
# proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
|
| 70 |
|
| 71 |
# Claude API keys
|
|
|
|
| 73 |
# - api-key: "sk-atSM..." # use the official claude API key, no need to set the base url
|
| 74 |
# - api-key: "sk-atSM..."
|
| 75 |
# base-url: "https://www.example.com" # use the custom claude API endpoint
|
| 76 |
+
# headers:
|
| 77 |
+
# X-Custom-Header: "custom-value"
|
| 78 |
# proxy-url: "socks5://proxy.example.com:1080" # optional: per-key proxy override
|
| 79 |
# models:
|
| 80 |
# - name: "claude-3-5-sonnet-20241022" # upstream model name
|
|
|
|
| 84 |
#openai-compatibility:
|
| 85 |
# - name: "openrouter" # The name of the provider; it will be used in the user agent and other places.
|
| 86 |
# base-url: "https://openrouter.ai/api/v1" # The base URL of the provider.
|
| 87 |
+
# headers:
|
| 88 |
+
# X-Custom-Header: "custom-value"
|
| 89 |
# # New format with per-key proxy support (recommended):
|
| 90 |
# api-key-entries:
|
| 91 |
# - api-key: "sk-or-v1-...b780"
|
internal/config/config.go
CHANGED
|
@@ -100,6 +100,9 @@ type ClaudeKey struct {
|
|
| 100 |
|
| 101 |
// Models defines upstream model names and aliases for request routing.
|
| 102 |
Models []ClaudeModel `yaml:"models" json:"models"`
|
|
|
|
|
|
|
|
|
|
| 103 |
}
|
| 104 |
|
| 105 |
// ClaudeModel describes a mapping between an alias and the actual upstream model name.
|
|
@@ -123,6 +126,9 @@ type CodexKey struct {
|
|
| 123 |
|
| 124 |
// ProxyURL overrides the global proxy setting for this API key if provided.
|
| 125 |
ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
|
|
|
|
|
|
|
|
|
|
| 126 |
}
|
| 127 |
|
| 128 |
// GeminiKey represents the configuration for a Gemini API key,
|
|
@@ -159,6 +165,9 @@ type OpenAICompatibility struct {
|
|
| 159 |
|
| 160 |
// Models defines the model configurations including aliases for routing.
|
| 161 |
Models []OpenAICompatibilityModel `yaml:"models" json:"models"`
|
|
|
|
|
|
|
|
|
|
| 162 |
}
|
| 163 |
|
| 164 |
// OpenAICompatibilityAPIKey represents an API key configuration with optional proxy setting.
|
|
@@ -255,6 +264,9 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|
| 255 |
// Sanitize Codex keys: drop entries without base-url
|
| 256 |
sanitizeCodexKeys(&cfg)
|
| 257 |
|
|
|
|
|
|
|
|
|
|
| 258 |
// Return the populated configuration struct.
|
| 259 |
return &cfg, nil
|
| 260 |
}
|
|
@@ -271,6 +283,7 @@ func sanitizeOpenAICompatibility(cfg *Config) {
|
|
| 271 |
e := cfg.OpenAICompatibility[i]
|
| 272 |
e.Name = strings.TrimSpace(e.Name)
|
| 273 |
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
|
|
|
| 274 |
if e.BaseURL == "" {
|
| 275 |
// Skip providers with no base-url; treated as removed
|
| 276 |
continue
|
|
@@ -290,6 +303,7 @@ func sanitizeCodexKeys(cfg *Config) {
|
|
| 290 |
for i := range cfg.CodexKey {
|
| 291 |
e := cfg.CodexKey[i]
|
| 292 |
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
|
|
|
| 293 |
if e.BaseURL == "" {
|
| 294 |
continue
|
| 295 |
}
|
|
@@ -298,6 +312,16 @@ func sanitizeCodexKeys(cfg *Config) {
|
|
| 298 |
cfg.CodexKey = out
|
| 299 |
}
|
| 300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
func (cfg *Config) SyncGeminiKeys() {
|
| 302 |
if cfg == nil {
|
| 303 |
return
|
|
@@ -313,7 +337,7 @@ func (cfg *Config) SyncGeminiKeys() {
|
|
| 313 |
}
|
| 314 |
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
| 315 |
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
|
| 316 |
-
entry.Headers =
|
| 317 |
if _, exists := seen[entry.APIKey]; exists {
|
| 318 |
continue
|
| 319 |
}
|
|
@@ -356,7 +380,7 @@ func looksLikeBcrypt(s string) bool {
|
|
| 356 |
return len(s) > 4 && (s[:4] == "$2a$" || s[:4] == "$2b$" || s[:4] == "$2y$")
|
| 357 |
}
|
| 358 |
|
| 359 |
-
func
|
| 360 |
if len(headers) == 0 {
|
| 361 |
return nil
|
| 362 |
}
|
|
|
|
| 100 |
|
| 101 |
// Models defines upstream model names and aliases for request routing.
|
| 102 |
Models []ClaudeModel `yaml:"models" json:"models"`
|
| 103 |
+
|
| 104 |
+
// Headers optionally adds extra HTTP headers for requests sent with this key.
|
| 105 |
+
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
|
| 106 |
}
|
| 107 |
|
| 108 |
// ClaudeModel describes a mapping between an alias and the actual upstream model name.
|
|
|
|
| 126 |
|
| 127 |
// ProxyURL overrides the global proxy setting for this API key if provided.
|
| 128 |
ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
|
| 129 |
+
|
| 130 |
+
// Headers optionally adds extra HTTP headers for requests sent with this key.
|
| 131 |
+
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
|
| 132 |
}
|
| 133 |
|
| 134 |
// GeminiKey represents the configuration for a Gemini API key,
|
|
|
|
| 165 |
|
| 166 |
// Models defines the model configurations including aliases for routing.
|
| 167 |
Models []OpenAICompatibilityModel `yaml:"models" json:"models"`
|
| 168 |
+
|
| 169 |
+
// Headers optionally adds extra HTTP headers for requests sent to this provider.
|
| 170 |
+
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
|
| 171 |
}
|
| 172 |
|
| 173 |
// OpenAICompatibilityAPIKey represents an API key configuration with optional proxy setting.
|
|
|
|
| 264 |
// Sanitize Codex keys: drop entries without base-url
|
| 265 |
sanitizeCodexKeys(&cfg)
|
| 266 |
|
| 267 |
+
// Normalize Claude key headers
|
| 268 |
+
normalizeClaudeKeys(&cfg)
|
| 269 |
+
|
| 270 |
// Return the populated configuration struct.
|
| 271 |
return &cfg, nil
|
| 272 |
}
|
|
|
|
| 283 |
e := cfg.OpenAICompatibility[i]
|
| 284 |
e.Name = strings.TrimSpace(e.Name)
|
| 285 |
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
| 286 |
+
e.Headers = normalizeHeaders(e.Headers)
|
| 287 |
if e.BaseURL == "" {
|
| 288 |
// Skip providers with no base-url; treated as removed
|
| 289 |
continue
|
|
|
|
| 303 |
for i := range cfg.CodexKey {
|
| 304 |
e := cfg.CodexKey[i]
|
| 305 |
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
| 306 |
+
e.Headers = normalizeHeaders(e.Headers)
|
| 307 |
if e.BaseURL == "" {
|
| 308 |
continue
|
| 309 |
}
|
|
|
|
| 312 |
cfg.CodexKey = out
|
| 313 |
}
|
| 314 |
|
| 315 |
+
func normalizeClaudeKeys(cfg *Config) {
|
| 316 |
+
if cfg == nil || len(cfg.ClaudeKey) == 0 {
|
| 317 |
+
return
|
| 318 |
+
}
|
| 319 |
+
for i := range cfg.ClaudeKey {
|
| 320 |
+
entry := &cfg.ClaudeKey[i]
|
| 321 |
+
entry.Headers = normalizeHeaders(entry.Headers)
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
func (cfg *Config) SyncGeminiKeys() {
|
| 326 |
if cfg == nil {
|
| 327 |
return
|
|
|
|
| 337 |
}
|
| 338 |
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
| 339 |
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
|
| 340 |
+
entry.Headers = normalizeHeaders(entry.Headers)
|
| 341 |
if _, exists := seen[entry.APIKey]; exists {
|
| 342 |
continue
|
| 343 |
}
|
|
|
|
| 380 |
return len(s) > 4 && (s[:4] == "$2a$" || s[:4] == "$2b$" || s[:4] == "$2y$")
|
| 381 |
}
|
| 382 |
|
| 383 |
+
func normalizeHeaders(headers map[string]string) map[string]string {
|
| 384 |
if len(headers) == 0 {
|
| 385 |
return nil
|
| 386 |
}
|
internal/runtime/executor/claude_executor.go
CHANGED
|
@@ -17,6 +17,7 @@ import (
|
|
| 17 |
claudeauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude"
|
| 18 |
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
| 19 |
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
|
|
|
| 20 |
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
| 21 |
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
| 22 |
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
|
|
| 67 |
if err != nil {
|
| 68 |
return resp, err
|
| 69 |
}
|
| 70 |
-
applyClaudeHeaders(httpReq, apiKey, false)
|
| 71 |
var authID, authLabel, authType, authValue string
|
| 72 |
if auth != nil {
|
| 73 |
authID = auth.ID
|
|
@@ -159,7 +160,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|
| 159 |
if err != nil {
|
| 160 |
return nil, err
|
| 161 |
}
|
| 162 |
-
applyClaudeHeaders(httpReq, apiKey, true)
|
| 163 |
var authID, authLabel, authType, authValue string
|
| 164 |
if auth != nil {
|
| 165 |
authID = auth.ID
|
|
@@ -290,7 +291,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|
| 290 |
if err != nil {
|
| 291 |
return cliproxyexecutor.Response{}, err
|
| 292 |
}
|
| 293 |
-
applyClaudeHeaders(httpReq, apiKey, false)
|
| 294 |
var authID, authLabel, authType, authValue string
|
| 295 |
if auth != nil {
|
| 296 |
authID = auth.ID
|
|
@@ -529,7 +530,7 @@ func decodeResponseBody(body io.ReadCloser, contentEncoding string) (io.ReadClos
|
|
| 529 |
return body, nil
|
| 530 |
}
|
| 531 |
|
| 532 |
-
func applyClaudeHeaders(r *http.Request, apiKey string, stream bool) {
|
| 533 |
r.Header.Set("Authorization", "Bearer "+apiKey)
|
| 534 |
r.Header.Set("Content-Type", "application/json")
|
| 535 |
|
|
@@ -564,9 +565,14 @@ func applyClaudeHeaders(r *http.Request, apiKey string, stream bool) {
|
|
| 564 |
r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
| 565 |
if stream {
|
| 566 |
r.Header.Set("Accept", "text/event-stream")
|
| 567 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
}
|
| 569 |
-
|
| 570 |
}
|
| 571 |
|
| 572 |
func claudeCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {
|
|
|
|
| 17 |
claudeauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude"
|
| 18 |
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
| 19 |
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
| 20 |
+
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
| 21 |
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
| 22 |
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
| 23 |
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
|
|
|
| 68 |
if err != nil {
|
| 69 |
return resp, err
|
| 70 |
}
|
| 71 |
+
applyClaudeHeaders(httpReq, auth, apiKey, false)
|
| 72 |
var authID, authLabel, authType, authValue string
|
| 73 |
if auth != nil {
|
| 74 |
authID = auth.ID
|
|
|
|
| 160 |
if err != nil {
|
| 161 |
return nil, err
|
| 162 |
}
|
| 163 |
+
applyClaudeHeaders(httpReq, auth, apiKey, true)
|
| 164 |
var authID, authLabel, authType, authValue string
|
| 165 |
if auth != nil {
|
| 166 |
authID = auth.ID
|
|
|
|
| 291 |
if err != nil {
|
| 292 |
return cliproxyexecutor.Response{}, err
|
| 293 |
}
|
| 294 |
+
applyClaudeHeaders(httpReq, auth, apiKey, false)
|
| 295 |
var authID, authLabel, authType, authValue string
|
| 296 |
if auth != nil {
|
| 297 |
authID = auth.ID
|
|
|
|
| 530 |
return body, nil
|
| 531 |
}
|
| 532 |
|
| 533 |
+
func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string, stream bool) {
|
| 534 |
r.Header.Set("Authorization", "Bearer "+apiKey)
|
| 535 |
r.Header.Set("Content-Type", "application/json")
|
| 536 |
|
|
|
|
| 565 |
r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
| 566 |
if stream {
|
| 567 |
r.Header.Set("Accept", "text/event-stream")
|
| 568 |
+
} else {
|
| 569 |
+
r.Header.Set("Accept", "application/json")
|
| 570 |
+
}
|
| 571 |
+
var attrs map[string]string
|
| 572 |
+
if auth != nil {
|
| 573 |
+
attrs = auth.Attributes
|
| 574 |
}
|
| 575 |
+
util.ApplyCustomHeadersFromAttrs(r, attrs)
|
| 576 |
}
|
| 577 |
|
| 578 |
func claudeCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {
|
internal/runtime/executor/codex_executor.go
CHANGED
|
@@ -585,6 +585,11 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string) {
|
|
| 585 |
}
|
| 586 |
}
|
| 587 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
}
|
| 589 |
|
| 590 |
func codexCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {
|
|
|
|
| 585 |
}
|
| 586 |
}
|
| 587 |
}
|
| 588 |
+
var attrs map[string]string
|
| 589 |
+
if auth != nil {
|
| 590 |
+
attrs = auth.Attributes
|
| 591 |
+
}
|
| 592 |
+
util.ApplyCustomHeadersFromAttrs(r, attrs)
|
| 593 |
}
|
| 594 |
|
| 595 |
func codexCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {
|
internal/runtime/executor/gemini_executor.go
CHANGED
|
@@ -495,44 +495,11 @@ func resolveGeminiBaseURL(auth *cliproxyauth.Auth) string {
|
|
| 495 |
}
|
| 496 |
|
| 497 |
func applyGeminiHeaders(req *http.Request, auth *cliproxyauth.Auth) {
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
headers := geminiCustomHeaders(auth)
|
| 502 |
-
if len(headers) == 0 {
|
| 503 |
-
return
|
| 504 |
-
}
|
| 505 |
-
for k, v := range headers {
|
| 506 |
-
if k == "" || v == "" {
|
| 507 |
-
continue
|
| 508 |
-
}
|
| 509 |
-
req.Header.Set(k, v)
|
| 510 |
-
}
|
| 511 |
-
}
|
| 512 |
-
|
| 513 |
-
func geminiCustomHeaders(auth *cliproxyauth.Auth) map[string]string {
|
| 514 |
-
if auth == nil || auth.Attributes == nil {
|
| 515 |
-
return nil
|
| 516 |
-
}
|
| 517 |
-
headers := make(map[string]string, len(auth.Attributes))
|
| 518 |
-
for k, v := range auth.Attributes {
|
| 519 |
-
if !strings.HasPrefix(k, "header:") {
|
| 520 |
-
continue
|
| 521 |
-
}
|
| 522 |
-
name := strings.TrimSpace(strings.TrimPrefix(k, "header:"))
|
| 523 |
-
if name == "" {
|
| 524 |
-
continue
|
| 525 |
-
}
|
| 526 |
-
val := strings.TrimSpace(v)
|
| 527 |
-
if val == "" {
|
| 528 |
-
continue
|
| 529 |
-
}
|
| 530 |
-
headers[name] = val
|
| 531 |
-
}
|
| 532 |
-
if len(headers) == 0 {
|
| 533 |
-
return nil
|
| 534 |
}
|
| 535 |
-
|
| 536 |
}
|
| 537 |
|
| 538 |
func fixGeminiImageAspectRatio(modelName string, rawJSON []byte) []byte {
|
|
|
|
| 495 |
}
|
| 496 |
|
| 497 |
func applyGeminiHeaders(req *http.Request, auth *cliproxyauth.Auth) {
|
| 498 |
+
var attrs map[string]string
|
| 499 |
+
if auth != nil {
|
| 500 |
+
attrs = auth.Attributes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
}
|
| 502 |
+
util.ApplyCustomHeadersFromAttrs(req, attrs)
|
| 503 |
}
|
| 504 |
|
| 505 |
func fixGeminiImageAspectRatio(modelName string, rawJSON []byte) []byte {
|
internal/runtime/executor/openai_compat_executor.go
CHANGED
|
@@ -10,6 +10,7 @@ import (
|
|
| 10 |
"strings"
|
| 11 |
|
| 12 |
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
|
|
|
| 13 |
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
| 14 |
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
| 15 |
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
|
@@ -66,6 +67,11 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|
| 66 |
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
| 67 |
}
|
| 68 |
httpReq.Header.Set("User-Agent", "cli-proxy-openai-compat")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
var authID, authLabel, authType, authValue string
|
| 70 |
if auth != nil {
|
| 71 |
authID = auth.ID
|
|
@@ -143,6 +149,11 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|
| 143 |
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
| 144 |
}
|
| 145 |
httpReq.Header.Set("User-Agent", "cli-proxy-openai-compat")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
httpReq.Header.Set("Accept", "text/event-stream")
|
| 147 |
httpReq.Header.Set("Cache-Control", "no-cache")
|
| 148 |
var authID, authLabel, authType, authValue string
|
|
|
|
| 10 |
"strings"
|
| 11 |
|
| 12 |
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
| 13 |
+
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
| 14 |
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
| 15 |
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
| 16 |
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
|
|
|
| 67 |
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
| 68 |
}
|
| 69 |
httpReq.Header.Set("User-Agent", "cli-proxy-openai-compat")
|
| 70 |
+
var attrs map[string]string
|
| 71 |
+
if auth != nil {
|
| 72 |
+
attrs = auth.Attributes
|
| 73 |
+
}
|
| 74 |
+
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
| 75 |
var authID, authLabel, authType, authValue string
|
| 76 |
if auth != nil {
|
| 77 |
authID = auth.ID
|
|
|
|
| 149 |
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
| 150 |
}
|
| 151 |
httpReq.Header.Set("User-Agent", "cli-proxy-openai-compat")
|
| 152 |
+
var attrs map[string]string
|
| 153 |
+
if auth != nil {
|
| 154 |
+
attrs = auth.Attributes
|
| 155 |
+
}
|
| 156 |
+
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
| 157 |
httpReq.Header.Set("Accept", "text/event-stream")
|
| 158 |
httpReq.Header.Set("Cache-Control", "no-cache")
|
| 159 |
var authID, authLabel, authType, authValue string
|
internal/util/header_helpers.go
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package util
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
"strings"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
// ApplyCustomHeadersFromAttrs applies user-defined headers stored in the provided attributes map.
|
| 9 |
+
// Custom headers override built-in defaults when conflicts occur.
|
| 10 |
+
func ApplyCustomHeadersFromAttrs(r *http.Request, attrs map[string]string) {
|
| 11 |
+
if r == nil {
|
| 12 |
+
return
|
| 13 |
+
}
|
| 14 |
+
applyCustomHeaders(r, extractCustomHeaders(attrs))
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
func extractCustomHeaders(attrs map[string]string) map[string]string {
|
| 18 |
+
if len(attrs) == 0 {
|
| 19 |
+
return nil
|
| 20 |
+
}
|
| 21 |
+
headers := make(map[string]string)
|
| 22 |
+
for k, v := range attrs {
|
| 23 |
+
if !strings.HasPrefix(k, "header:") {
|
| 24 |
+
continue
|
| 25 |
+
}
|
| 26 |
+
name := strings.TrimSpace(strings.TrimPrefix(k, "header:"))
|
| 27 |
+
if name == "" {
|
| 28 |
+
continue
|
| 29 |
+
}
|
| 30 |
+
val := strings.TrimSpace(v)
|
| 31 |
+
if val == "" {
|
| 32 |
+
continue
|
| 33 |
+
}
|
| 34 |
+
headers[name] = val
|
| 35 |
+
}
|
| 36 |
+
if len(headers) == 0 {
|
| 37 |
+
return nil
|
| 38 |
+
}
|
| 39 |
+
return headers
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func applyCustomHeaders(r *http.Request, headers map[string]string) {
|
| 43 |
+
if r == nil || len(headers) == 0 {
|
| 44 |
+
return
|
| 45 |
+
}
|
| 46 |
+
for k, v := range headers {
|
| 47 |
+
if k == "" || v == "" {
|
| 48 |
+
continue
|
| 49 |
+
}
|
| 50 |
+
r.Header.Set(k, v)
|
| 51 |
+
}
|
| 52 |
+
}
|
internal/watcher/watcher.go
CHANGED
|
@@ -762,16 +762,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|
| 762 |
if base != "" {
|
| 763 |
attrs["base_url"] = base
|
| 764 |
}
|
| 765 |
-
|
| 766 |
-
for hk, hv := range entry.Headers {
|
| 767 |
-
key := strings.TrimSpace(hk)
|
| 768 |
-
val := strings.TrimSpace(hv)
|
| 769 |
-
if key == "" || val == "" {
|
| 770 |
-
continue
|
| 771 |
-
}
|
| 772 |
-
attrs["header:"+key] = val
|
| 773 |
-
}
|
| 774 |
-
}
|
| 775 |
a := &coreauth.Auth{
|
| 776 |
ID: id,
|
| 777 |
Provider: "gemini",
|
|
@@ -803,6 +794,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|
| 803 |
if hash := computeClaudeModelsHash(ck.Models); hash != "" {
|
| 804 |
attrs["models_hash"] = hash
|
| 805 |
}
|
|
|
|
| 806 |
proxyURL := strings.TrimSpace(ck.ProxyURL)
|
| 807 |
a := &coreauth.Auth{
|
| 808 |
ID: id,
|
|
@@ -831,6 +823,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|
| 831 |
if ck.BaseURL != "" {
|
| 832 |
attrs["base_url"] = ck.BaseURL
|
| 833 |
}
|
|
|
|
| 834 |
proxyURL := strings.TrimSpace(ck.ProxyURL)
|
| 835 |
a := &coreauth.Auth{
|
| 836 |
ID: id,
|
|
@@ -873,6 +866,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|
| 873 |
if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" {
|
| 874 |
attrs["models_hash"] = hash
|
| 875 |
}
|
|
|
|
| 876 |
a := &coreauth.Auth{
|
| 877 |
ID: id,
|
| 878 |
Provider: providerName,
|
|
@@ -905,6 +899,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|
| 905 |
if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" {
|
| 906 |
attrs["models_hash"] = hash
|
| 907 |
}
|
|
|
|
| 908 |
a := &coreauth.Auth{
|
| 909 |
ID: id,
|
| 910 |
Provider: providerName,
|
|
@@ -930,6 +925,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|
| 930 |
if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" {
|
| 931 |
attrs["models_hash"] = hash
|
| 932 |
}
|
|
|
|
| 933 |
a := &coreauth.Auth{
|
| 934 |
ID: id,
|
| 935 |
Provider: providerName,
|
|
@@ -1131,13 +1127,16 @@ func describeOpenAICompatibilityUpdate(oldEntry, newEntry config.OpenAICompatibi
|
|
| 1131 |
newKeyCount := countAPIKeys(newEntry)
|
| 1132 |
oldModelCount := countOpenAIModels(oldEntry.Models)
|
| 1133 |
newModelCount := countOpenAIModels(newEntry.Models)
|
| 1134 |
-
details := make([]string, 0,
|
| 1135 |
if oldKeyCount != newKeyCount {
|
| 1136 |
details = append(details, fmt.Sprintf("api-keys %d -> %d", oldKeyCount, newKeyCount))
|
| 1137 |
}
|
| 1138 |
if oldModelCount != newModelCount {
|
| 1139 |
details = append(details, fmt.Sprintf("models %d -> %d", oldModelCount, newModelCount))
|
| 1140 |
}
|
|
|
|
|
|
|
|
|
|
| 1141 |
if len(details) == 0 {
|
| 1142 |
return ""
|
| 1143 |
}
|
|
@@ -1303,6 +1302,9 @@ func buildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
|
| 1303 |
if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) {
|
| 1304 |
changes = append(changes, fmt.Sprintf("claude[%d].api-key: updated", i))
|
| 1305 |
}
|
|
|
|
|
|
|
|
|
|
| 1306 |
}
|
| 1307 |
}
|
| 1308 |
|
|
@@ -1325,6 +1327,9 @@ func buildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
|
| 1325 |
if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) {
|
| 1326 |
changes = append(changes, fmt.Sprintf("codex[%d].api-key: updated", i))
|
| 1327 |
}
|
|
|
|
|
|
|
|
|
|
| 1328 |
}
|
| 1329 |
}
|
| 1330 |
|
|
@@ -1357,6 +1362,20 @@ func buildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
|
| 1357 |
return changes
|
| 1358 |
}
|
| 1359 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1360 |
func trimStrings(in []string) []string {
|
| 1361 |
out := make([]string, len(in))
|
| 1362 |
for i := range in {
|
|
|
|
| 762 |
if base != "" {
|
| 763 |
attrs["base_url"] = base
|
| 764 |
}
|
| 765 |
+
addConfigHeadersToAttrs(entry.Headers, attrs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
a := &coreauth.Auth{
|
| 767 |
ID: id,
|
| 768 |
Provider: "gemini",
|
|
|
|
| 794 |
if hash := computeClaudeModelsHash(ck.Models); hash != "" {
|
| 795 |
attrs["models_hash"] = hash
|
| 796 |
}
|
| 797 |
+
addConfigHeadersToAttrs(ck.Headers, attrs)
|
| 798 |
proxyURL := strings.TrimSpace(ck.ProxyURL)
|
| 799 |
a := &coreauth.Auth{
|
| 800 |
ID: id,
|
|
|
|
| 823 |
if ck.BaseURL != "" {
|
| 824 |
attrs["base_url"] = ck.BaseURL
|
| 825 |
}
|
| 826 |
+
addConfigHeadersToAttrs(ck.Headers, attrs)
|
| 827 |
proxyURL := strings.TrimSpace(ck.ProxyURL)
|
| 828 |
a := &coreauth.Auth{
|
| 829 |
ID: id,
|
|
|
|
| 866 |
if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" {
|
| 867 |
attrs["models_hash"] = hash
|
| 868 |
}
|
| 869 |
+
addConfigHeadersToAttrs(compat.Headers, attrs)
|
| 870 |
a := &coreauth.Auth{
|
| 871 |
ID: id,
|
| 872 |
Provider: providerName,
|
|
|
|
| 899 |
if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" {
|
| 900 |
attrs["models_hash"] = hash
|
| 901 |
}
|
| 902 |
+
addConfigHeadersToAttrs(compat.Headers, attrs)
|
| 903 |
a := &coreauth.Auth{
|
| 904 |
ID: id,
|
| 905 |
Provider: providerName,
|
|
|
|
| 925 |
if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" {
|
| 926 |
attrs["models_hash"] = hash
|
| 927 |
}
|
| 928 |
+
addConfigHeadersToAttrs(compat.Headers, attrs)
|
| 929 |
a := &coreauth.Auth{
|
| 930 |
ID: id,
|
| 931 |
Provider: providerName,
|
|
|
|
| 1127 |
newKeyCount := countAPIKeys(newEntry)
|
| 1128 |
oldModelCount := countOpenAIModels(oldEntry.Models)
|
| 1129 |
newModelCount := countOpenAIModels(newEntry.Models)
|
| 1130 |
+
details := make([]string, 0, 3)
|
| 1131 |
if oldKeyCount != newKeyCount {
|
| 1132 |
details = append(details, fmt.Sprintf("api-keys %d -> %d", oldKeyCount, newKeyCount))
|
| 1133 |
}
|
| 1134 |
if oldModelCount != newModelCount {
|
| 1135 |
details = append(details, fmt.Sprintf("models %d -> %d", oldModelCount, newModelCount))
|
| 1136 |
}
|
| 1137 |
+
if !equalStringMap(oldEntry.Headers, newEntry.Headers) {
|
| 1138 |
+
details = append(details, "headers updated")
|
| 1139 |
+
}
|
| 1140 |
if len(details) == 0 {
|
| 1141 |
return ""
|
| 1142 |
}
|
|
|
|
| 1302 |
if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) {
|
| 1303 |
changes = append(changes, fmt.Sprintf("claude[%d].api-key: updated", i))
|
| 1304 |
}
|
| 1305 |
+
if !equalStringMap(o.Headers, n.Headers) {
|
| 1306 |
+
changes = append(changes, fmt.Sprintf("claude[%d].headers: updated", i))
|
| 1307 |
+
}
|
| 1308 |
}
|
| 1309 |
}
|
| 1310 |
|
|
|
|
| 1327 |
if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) {
|
| 1328 |
changes = append(changes, fmt.Sprintf("codex[%d].api-key: updated", i))
|
| 1329 |
}
|
| 1330 |
+
if !equalStringMap(o.Headers, n.Headers) {
|
| 1331 |
+
changes = append(changes, fmt.Sprintf("codex[%d].headers: updated", i))
|
| 1332 |
+
}
|
| 1333 |
}
|
| 1334 |
}
|
| 1335 |
|
|
|
|
| 1362 |
return changes
|
| 1363 |
}
|
| 1364 |
|
| 1365 |
+
func addConfigHeadersToAttrs(headers map[string]string, attrs map[string]string) {
|
| 1366 |
+
if len(headers) == 0 || attrs == nil {
|
| 1367 |
+
return
|
| 1368 |
+
}
|
| 1369 |
+
for hk, hv := range headers {
|
| 1370 |
+
key := strings.TrimSpace(hk)
|
| 1371 |
+
val := strings.TrimSpace(hv)
|
| 1372 |
+
if key == "" || val == "" {
|
| 1373 |
+
continue
|
| 1374 |
+
}
|
| 1375 |
+
attrs["header:"+key] = val
|
| 1376 |
+
}
|
| 1377 |
+
}
|
| 1378 |
+
|
| 1379 |
func trimStrings(in []string) []string {
|
| 1380 |
out := make([]string, len(in))
|
| 1381 |
for i := range in {
|