diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index f1587e9a..0a079c08 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -54,9 +54,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r if err != nil { return cliproxyexecutor.Response{}, err } - httpReq.Header.Set("Authorization", "Bearer "+apiKey) - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Anthropic-Version", "2023-06-01") + applyClaudeHeaders(httpReq, apiKey, false) httpClient := &http.Client{} if rt, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && rt != nil { @@ -102,10 +100,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A if err != nil { return nil, err } - httpReq.Header.Set("Authorization", "Bearer "+apiKey) - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Anthropic-Version", "2023-06-01") - httpReq.Header.Set("Accept", "text/event-stream") + applyClaudeHeaders(httpReq, apiKey, true) httpClient := &http.Client{Timeout: 0} if rt, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && rt != nil { @@ -179,6 +174,32 @@ func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) ( return auth, nil } +func applyClaudeHeaders(r *http.Request, apiKey string, stream bool) { + r.Header.Set("Authorization", "Bearer "+apiKey) + r.Header.Set("Content-Type", "application/json") + r.Header.Set("Anthropic-Version", "2023-06-01") + r.Header.Set("Anthropic-Dangerous-Direct-Browser-Access", "true") + r.Header.Set("Anthropic-Beta", "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14") + r.Header.Set("Connection", "keep-alive") + r.Header.Set("User-Agent", "claude-cli/1.0.83 (external, cli)") + r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd") + r.Header.Set("X-App", "cli") + r.Header.Set("X-Stainless-Helper-Method", "stream") + r.Header.Set("X-Stainless-Retry-Count", "0") + r.Header.Set("X-Stainless-Runtime-Version", "v24.3.0") + r.Header.Set("X-Stainless-Package-Version", "0.55.1") + r.Header.Set("X-Stainless-Runtime", "node") + r.Header.Set("X-Stainless-Lang", "js") + r.Header.Set("X-Stainless-Arch", "arm64") + r.Header.Set("X-Stainless-Os", "MacOS") + r.Header.Set("X-Stainless-Timeout", "60") + if stream { + r.Header.Set("Accept", "text/event-stream") + return + } + r.Header.Set("Accept", "application/json") +} + func claudeCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) { if a == nil { return "", "" diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index e81108e4..f003b32b 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -17,6 +17,8 @@ import ( sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" log "github.com/sirupsen/logrus" "github.com/tidwall/sjson" + + "github.com/google/uuid" ) // CodexExecutor is a stateless executor for Codex (OpenAI Responses API entrypoint). @@ -76,8 +78,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re if err != nil { return cliproxyexecutor.Response{}, err } - httpReq.Header.Set("Authorization", "Bearer "+apiKey) - httpReq.Header.Set("Content-Type", "application/json") + applyCodexHeaders(httpReq, auth, apiKey) httpClient := &http.Client{} if rt, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && rt != nil { @@ -149,9 +150,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au if err != nil { return nil, err } - httpReq.Header.Set("Authorization", "Bearer "+apiKey) - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Accept", "text/event-stream") + applyCodexHeaders(httpReq, auth, apiKey) httpClient := &http.Client{Timeout: 0} if rt, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && rt != nil { @@ -230,6 +229,33 @@ func (e *CodexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (* return auth, nil } +func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string) { + r.Header.Set("Content-Type", "application/json") + r.Header.Set("Authorization", "Bearer "+token) + r.Header.Set("Version", "0.21.0") + r.Header.Set("Openai-Beta", "responses=experimental") + r.Header.Set("Session_id", uuid.NewString()) + r.Header.Set("Accept", "text/event-stream") + r.Header.Set("Connection", "Keep-Alive") + + isAPIKey := false + if auth != nil && auth.Attributes != nil { + if v := strings.TrimSpace(auth.Attributes["api_key"]); v != "" { + isAPIKey = true + } + } + if !isAPIKey { + r.Header.Set("Originator", "codex_cli_rs") + if auth != nil && auth.Metadata != nil { + if accountID, ok := auth.Metadata["account_id"].(string); ok { + if trimmed := strings.TrimSpace(accountID); trimmed != "" { + r.Header.Set("Chatgpt-Account-Id", trimmed) + } + } + } + } +} + func codexCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) { if a == nil { return "", "" diff --git a/internal/runtime/executor/gemini_cli_executor.go b/internal/runtime/executor/gemini_cli_executor.go index b167aeb9..8951b553 100644 --- a/internal/runtime/executor/gemini_cli_executor.go +++ b/internal/runtime/executor/gemini_cli_executor.go @@ -105,6 +105,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth reqHTTP.Header.Set("Content-Type", "application/json") reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) applyGeminiCLIHeaders(reqHTTP) + reqHTTP.Header.Set("Accept", "application/json") resp, errDo := httpClient.Do(reqHTTP) if errDo != nil { @@ -181,6 +182,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut reqHTTP.Header.Set("Content-Type", "application/json") reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) applyGeminiCLIHeaders(reqHTTP) + reqHTTP.Header.Set("Accept", "text/event-stream") resp, errDo := httpClient.Do(reqHTTP) if errDo != nil { diff --git a/internal/runtime/executor/qwen_executor.go b/internal/runtime/executor/qwen_executor.go index a4e1d658..1a3a6d2d 100644 --- a/internal/runtime/executor/qwen_executor.go +++ b/internal/runtime/executor/qwen_executor.go @@ -20,6 +20,12 @@ import ( "github.com/tidwall/sjson" ) +const ( + qwenUserAgent = "google-api-nodejs-client/9.15.1" + qwenXGoogAPIClient = "gl-node/22.17.0" + qwenClientMetadataValue = "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI" +) + // QwenExecutor is a stateless executor for Qwen Code using OpenAI-compatible chat completions. // If access token is unavailable, it falls back to legacy via ClientAdapter. type QwenExecutor struct { @@ -51,8 +57,7 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req if err != nil { return cliproxyexecutor.Response{}, err } - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Authorization", "Bearer "+token) + applyQwenHeaders(httpReq, token, false) httpClient := &http.Client{} if rt, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && rt != nil { @@ -105,9 +110,7 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut if err != nil { return nil, err } - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Authorization", "Bearer "+token) - httpReq.Header.Set("Accept", "text/event-stream") + applyQwenHeaders(httpReq, token, true) httpClient := &http.Client{Timeout: 0} if rt, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && rt != nil { @@ -187,6 +190,19 @@ func (e *QwenExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*c return auth, nil } +func applyQwenHeaders(r *http.Request, token string, stream bool) { + r.Header.Set("Content-Type", "application/json") + r.Header.Set("Authorization", "Bearer "+token) + r.Header.Set("User-Agent", qwenUserAgent) + r.Header.Set("X-Goog-Api-Client", qwenXGoogAPIClient) + r.Header.Set("Client-Metadata", qwenClientMetadataValue) + if stream { + r.Header.Set("Accept", "text/event-stream") + return + } + r.Header.Set("Accept", "application/json") +} + func qwenCreds(a *cliproxyauth.Auth) (token, baseURL string) { if a == nil { return "", ""