diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index 10edfa29..bb5606db 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -13,7 +13,6 @@ import ( "net/http" "os" "path/filepath" - "runtime" "sort" "strconv" "strings" @@ -43,17 +42,13 @@ import ( var lastRefreshKeys = []string{"last_refresh", "lastRefresh", "last_refreshed_at", "lastRefreshedAt"} const ( - anthropicCallbackPort = 54545 - geminiCallbackPort = 8085 - codexCallbackPort = 1455 - geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" - geminiCLIVersion = "v1internal" + anthropicCallbackPort = 54545 + geminiCallbackPort = 8085 + codexCallbackPort = 1455 + geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" + geminiCLIVersion = "v1internal" ) -func getGeminiCLIUserAgent() string { - return fmt.Sprintf("GeminiCLI/1.0.0/unknown (%s; %s)", runtime.GOOS, runtime.GOARCH) -} - type callbackForwarder struct { provider string server *http.Server @@ -2287,7 +2282,7 @@ func callGeminiCLI(ctx context.Context, httpClient *http.Client, endpoint string return fmt.Errorf("create request: %w", errRequest) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", getGeminiCLIUserAgent()) + req.Header.Set("User-Agent", misc.GeminiCLIUserAgent("")) resp, errDo := httpClient.Do(req) if errDo != nil { @@ -2357,7 +2352,7 @@ func checkCloudAPIIsEnabled(ctx context.Context, httpClient *http.Client, projec return false, fmt.Errorf("failed to create request: %w", errRequest) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", getGeminiCLIUserAgent()) + req.Header.Set("User-Agent", misc.GeminiCLIUserAgent("")) resp, errDo := httpClient.Do(req) if errDo != nil { return false, fmt.Errorf("failed to execute request: %w", errDo) @@ -2378,7 +2373,7 @@ func checkCloudAPIIsEnabled(ctx context.Context, httpClient *http.Client, projec return false, fmt.Errorf("failed to create request: %w", errRequest) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", getGeminiCLIUserAgent()) + req.Header.Set("User-Agent", misc.GeminiCLIUserAgent("")) resp, errDo = httpClient.Do(req) if errDo != nil { return false, fmt.Errorf("failed to execute request: %w", errDo) diff --git a/internal/cmd/login.go b/internal/cmd/login.go index 1162dc68..16af718e 100644 --- a/internal/cmd/login.go +++ b/internal/cmd/login.go @@ -28,14 +28,10 @@ import ( ) const ( - geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" - geminiCLIVersion = "v1internal" + geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" + geminiCLIVersion = "v1internal" ) -func getGeminiCLIUserAgent() string { - return misc.GeminiCLIUserAgent("") -} - type projectSelectionRequiredError struct{} func (e *projectSelectionRequiredError) Error() string { @@ -411,7 +407,7 @@ func callGeminiCLI(ctx context.Context, httpClient *http.Client, endpoint string return fmt.Errorf("create request: %w", errRequest) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", getGeminiCLIUserAgent()) + req.Header.Set("User-Agent", misc.GeminiCLIUserAgent("")) resp, errDo := httpClient.Do(req) if errDo != nil { @@ -630,7 +626,7 @@ func checkCloudAPIIsEnabled(ctx context.Context, httpClient *http.Client, projec return false, fmt.Errorf("failed to create request: %w", errRequest) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", getGeminiCLIUserAgent()) + req.Header.Set("User-Agent", misc.GeminiCLIUserAgent("")) resp, errDo := httpClient.Do(req) if errDo != nil { return false, fmt.Errorf("failed to execute request: %w", errDo) @@ -651,7 +647,7 @@ func checkCloudAPIIsEnabled(ctx context.Context, httpClient *http.Client, projec return false, fmt.Errorf("failed to create request: %w", errRequest) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", getGeminiCLIUserAgent()) + req.Header.Set("User-Agent", misc.GeminiCLIUserAgent("")) resp, errDo = httpClient.Do(req) if errDo != nil { return false, fmt.Errorf("failed to execute request: %w", errDo) diff --git a/internal/misc/header_utils.go b/internal/misc/header_utils.go index e3711e43..5752a269 100644 --- a/internal/misc/header_utils.go +++ b/internal/misc/header_utils.go @@ -10,13 +10,43 @@ import ( "strings" ) +const ( + // GeminiCLIVersion is the version string reported in the User-Agent for upstream requests. + GeminiCLIVersion = "0.31.0" + + // GeminiCLIApiClientHeader is the value for the X-Goog-Api-Client header sent to the Gemini CLI upstream. + GeminiCLIApiClientHeader = "google-genai-sdk/1.41.0 gl-node/v22.19.0" +) + +// geminiCLIOS maps Go runtime OS names to the Node.js-style platform strings used by Gemini CLI. +func geminiCLIOS() string { + switch runtime.GOOS { + case "windows": + return "win32" + default: + return runtime.GOOS + } +} + +// geminiCLIArch maps Go runtime architecture names to the Node.js-style arch strings used by Gemini CLI. +func geminiCLIArch() string { + switch runtime.GOARCH { + case "amd64": + return "x64" + case "386": + return "x86" + default: + return runtime.GOARCH + } +} + // GeminiCLIUserAgent returns a User-Agent string that matches the Gemini CLI format. // The model parameter is included in the UA; pass "" or "unknown" when the model is not applicable. func GeminiCLIUserAgent(model string) string { if model == "" { model = "unknown" } - return fmt.Sprintf("GeminiCLI/1.0.0/%s (%s; %s)", model, runtime.GOOS, runtime.GOARCH) + return fmt.Sprintf("GeminiCLI/%s/%s (%s; %s)", GeminiCLIVersion, model, geminiCLIOS(), geminiCLIArch()) } // ScrubProxyAndFingerprintHeaders removes all headers that could reveal @@ -93,4 +123,3 @@ func EnsureHeader(target http.Header, source http.Header, key, defaultValue stri target.Set(key, val) } } - diff --git a/internal/runtime/executor/gemini_cli_executor.go b/internal/runtime/executor/gemini_cli_executor.go index 504f32c8..1be245b7 100644 --- a/internal/runtime/executor/gemini_cli_executor.go +++ b/internal/runtime/executor/gemini_cli_executor.go @@ -16,7 +16,6 @@ import ( "strings" "time" - "github.com/gin-gonic/gin" "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/runtime/geminicli" @@ -738,13 +737,11 @@ func stringValue(m map[string]any, key string) string { } // applyGeminiCLIHeaders sets required headers for the Gemini CLI upstream. +// User-Agent is always forced to the GeminiCLI format regardless of the client's value, +// so that upstream identifies the request as a native GeminiCLI client. func applyGeminiCLIHeaders(r *http.Request, model string) { - var ginHeaders http.Header - if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { - ginHeaders = ginCtx.Request.Header - } - - misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", misc.GeminiCLIUserAgent(model)) + r.Header.Set("User-Agent", misc.GeminiCLIUserAgent(model)) + r.Header.Set("X-Goog-Api-Client", misc.GeminiCLIApiClientHeader) } // cliPreviewFallbackOrder returns preview model candidates for a base model. diff --git a/internal/translator/codex/claude/codex_claude_response.go b/internal/translator/codex/claude/codex_claude_response.go index cdcf2e4f..7f597062 100644 --- a/internal/translator/codex/claude/codex_claude_response.go +++ b/internal/translator/codex/claude/codex_claude_response.go @@ -22,8 +22,8 @@ var ( // ConvertCodexResponseToClaudeParams holds parameters for response conversion. type ConvertCodexResponseToClaudeParams struct { - HasToolCall bool - BlockIndex int + HasToolCall bool + BlockIndex int HasReceivedArgumentsDelta bool } diff --git a/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go b/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go index 65732c3f..a2ede1b8 100644 --- a/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go +++ b/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go @@ -264,18 +264,18 @@ func TestConvertSystemRoleToDeveloper_AssistantRole(t *testing.T) { } } -func TestUserFieldDeletion(t *testing.T) { +func TestUserFieldDeletion(t *testing.T) { inputJSON := []byte(`{ "model": "gpt-5.2", "user": "test-user", "input": [{"role": "user", "content": "Hello"}] - }`) - - output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false) - outputStr := string(output) - - // Verify user field is deleted - userField := gjson.Get(outputStr, "user") + }`) + + output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false) + outputStr := string(output) + + // Verify user field is deleted + userField := gjson.Get(outputStr, "user") if userField.Exists() { t.Errorf("user field should be deleted, but it was found with value: %s", userField.Raw) }