From 528b1a23076e9c074a35a2cb6ea05d36deda4ebe Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:48:18 +0800 Subject: [PATCH] feat(codex): pass through codex client identity headers --- internal/runtime/executor/codex_executor.go | 14 ++- .../executor/codex_websockets_executor.go | 9 +- .../codex_websockets_executor_test.go | 88 ++++++++++++++++++ .../claude/antigravity_claude_request.go | 92 +++++++++---------- 4 files changed, 151 insertions(+), 52 deletions(-) diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index e6f75b5d..7e4163b8 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -28,8 +28,8 @@ import ( ) const ( - codexClientVersion = "0.101.0" - codexUserAgent = "codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464" + codexUserAgent = "codex_cli_rs/0.116.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464" + codexOriginator = "codex_cli_rs" ) var dataTag = []byte("data:") @@ -645,8 +645,10 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s ginHeaders = ginCtx.Request.Header } - misc.EnsureHeader(r.Header, ginHeaders, "Version", codexClientVersion) + misc.EnsureHeader(r.Header, ginHeaders, "Version", "") misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString()) + misc.EnsureHeader(r.Header, ginHeaders, "X-Codex-Turn-Metadata", "") + misc.EnsureHeader(r.Header, ginHeaders, "X-Client-Request-Id", "") cfgUserAgent, _ := codexHeaderDefaults(cfg, auth) ensureHeaderWithConfigPrecedence(r.Header, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent) @@ -663,8 +665,12 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s isAPIKey = true } } + if originator := strings.TrimSpace(ginHeaders.Get("Originator")); originator != "" { + r.Header.Set("Originator", originator) + } else if !isAPIKey { + r.Header.Set("Originator", codexOriginator) + } if !isAPIKey { - r.Header.Set("Originator", "codex_cli_rs") if auth != nil && auth.Metadata != nil { if accountID, ok := auth.Metadata["account_id"].(string); ok { r.Header.Set("Chatgpt-Account-Id", accountID) diff --git a/internal/runtime/executor/codex_websockets_executor.go b/internal/runtime/executor/codex_websockets_executor.go index 3ea88e12..fca82fe7 100644 --- a/internal/runtime/executor/codex_websockets_executor.go +++ b/internal/runtime/executor/codex_websockets_executor.go @@ -814,9 +814,10 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth * ensureHeaderWithPriority(headers, ginHeaders, "x-codex-beta-features", cfgBetaFeatures, "") misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-state", "") misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-metadata", "") + misc.EnsureHeader(headers, ginHeaders, "x-client-request-id", "") misc.EnsureHeader(headers, ginHeaders, "x-responsesapi-include-timing-metrics", "") + misc.EnsureHeader(headers, ginHeaders, "Version", "") - misc.EnsureHeader(headers, ginHeaders, "Version", codexClientVersion) betaHeader := strings.TrimSpace(headers.Get("OpenAI-Beta")) if betaHeader == "" && ginHeaders != nil { betaHeader = strings.TrimSpace(ginHeaders.Get("OpenAI-Beta")) @@ -834,8 +835,12 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth * isAPIKey = true } } + if originator := strings.TrimSpace(ginHeaders.Get("Originator")); originator != "" { + headers.Set("Originator", originator) + } else if !isAPIKey { + headers.Set("Originator", codexOriginator) + } if !isAPIKey { - headers.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 != "" { diff --git a/internal/runtime/executor/codex_websockets_executor_test.go b/internal/runtime/executor/codex_websockets_executor_test.go index 755ac56a..d34e7c39 100644 --- a/internal/runtime/executor/codex_websockets_executor_test.go +++ b/internal/runtime/executor/codex_websockets_executor_test.go @@ -41,9 +41,46 @@ func TestApplyCodexWebsocketHeadersDefaultsToCurrentResponsesBeta(t *testing.T) if got := headers.Get("User-Agent"); got != codexUserAgent { t.Fatalf("User-Agent = %s, want %s", got, codexUserAgent) } + if got := headers.Get("Version"); got != "" { + t.Fatalf("Version = %q, want empty", got) + } if got := headers.Get("x-codex-beta-features"); got != "" { t.Fatalf("x-codex-beta-features = %q, want empty", got) } + if got := headers.Get("X-Codex-Turn-Metadata"); got != "" { + t.Fatalf("X-Codex-Turn-Metadata = %q, want empty", got) + } + if got := headers.Get("X-Client-Request-Id"); got != "" { + t.Fatalf("X-Client-Request-Id = %q, want empty", got) + } +} + +func TestApplyCodexWebsocketHeadersPassesThroughClientIdentityHeaders(t *testing.T) { + auth := &cliproxyauth.Auth{ + Provider: "codex", + Metadata: map[string]any{"email": "user@example.com"}, + } + ctx := contextWithGinHeaders(map[string]string{ + "Originator": "Codex Desktop", + "Version": "0.115.0-alpha.27", + "X-Codex-Turn-Metadata": `{"turn_id":"turn-1"}`, + "X-Client-Request-Id": "019d2233-e240-7162-992d-38df0a2a0e0d", + }) + + headers := applyCodexWebsocketHeaders(ctx, http.Header{}, auth, "", nil) + + if got := headers.Get("Originator"); got != "Codex Desktop" { + t.Fatalf("Originator = %s, want %s", got, "Codex Desktop") + } + if got := headers.Get("Version"); got != "0.115.0-alpha.27" { + t.Fatalf("Version = %s, want %s", got, "0.115.0-alpha.27") + } + if got := headers.Get("X-Codex-Turn-Metadata"); got != `{"turn_id":"turn-1"}` { + t.Fatalf("X-Codex-Turn-Metadata = %s, want %s", got, `{"turn_id":"turn-1"}`) + } + if got := headers.Get("X-Client-Request-Id"); got != "019d2233-e240-7162-992d-38df0a2a0e0d" { + t.Fatalf("X-Client-Request-Id = %s, want %s", got, "019d2233-e240-7162-992d-38df0a2a0e0d") + } } func TestApplyCodexWebsocketHeadersUsesConfigDefaultsForOAuth(t *testing.T) { @@ -177,6 +214,57 @@ func TestApplyCodexHeadersUsesConfigUserAgentForOAuth(t *testing.T) { } } +func TestApplyCodexHeadersPassesThroughClientIdentityHeaders(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, "https://example.com/responses", nil) + if err != nil { + t.Fatalf("NewRequest() error = %v", err) + } + auth := &cliproxyauth.Auth{ + Provider: "codex", + Metadata: map[string]any{"email": "user@example.com"}, + } + req = req.WithContext(contextWithGinHeaders(map[string]string{ + "Originator": "Codex Desktop", + "Version": "0.115.0-alpha.27", + "X-Codex-Turn-Metadata": `{"turn_id":"turn-1"}`, + "X-Client-Request-Id": "019d2233-e240-7162-992d-38df0a2a0e0d", + })) + + applyCodexHeaders(req, auth, "oauth-token", true, nil) + + if got := req.Header.Get("Originator"); got != "Codex Desktop" { + t.Fatalf("Originator = %s, want %s", got, "Codex Desktop") + } + if got := req.Header.Get("Version"); got != "0.115.0-alpha.27" { + t.Fatalf("Version = %s, want %s", got, "0.115.0-alpha.27") + } + if got := req.Header.Get("X-Codex-Turn-Metadata"); got != `{"turn_id":"turn-1"}` { + t.Fatalf("X-Codex-Turn-Metadata = %s, want %s", got, `{"turn_id":"turn-1"}`) + } + if got := req.Header.Get("X-Client-Request-Id"); got != "019d2233-e240-7162-992d-38df0a2a0e0d" { + t.Fatalf("X-Client-Request-Id = %s, want %s", got, "019d2233-e240-7162-992d-38df0a2a0e0d") + } +} + +func TestApplyCodexHeadersDoesNotInjectClientOnlyHeadersByDefault(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, "https://example.com/responses", nil) + if err != nil { + t.Fatalf("NewRequest() error = %v", err) + } + + applyCodexHeaders(req, nil, "oauth-token", true, nil) + + if got := req.Header.Get("Version"); got != "" { + t.Fatalf("Version = %q, want empty", got) + } + if got := req.Header.Get("X-Codex-Turn-Metadata"); got != "" { + t.Fatalf("X-Codex-Turn-Metadata = %q, want empty", got) + } + if got := req.Header.Get("X-Client-Request-Id"); got != "" { + t.Fatalf("X-Client-Request-Id = %q, want empty", got) + } +} + func contextWithGinHeaders(headers map[string]string) context.Context { gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() diff --git a/internal/translator/antigravity/claude/antigravity_claude_request.go b/internal/translator/antigravity/claude/antigravity_claude_request.go index 0a139e36..9e504d3f 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request.go @@ -104,59 +104,59 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ // Always try cached signature first (more reliable than client-provided) // Client may send stale or invalid signatures from different sessions - signature := "" - if thinkingText != "" { - if cachedSig := cache.GetCachedSignature(modelName, thinkingText); cachedSig != "" { - signature = cachedSig - // log.Debugf("Using cached signature for thinking block") - } - } - - // Fallback to client signature only if cache miss and client signature is valid - if signature == "" { - signatureResult := contentResult.Get("signature") - clientSignature := "" - if signatureResult.Exists() && signatureResult.String() != "" { - arrayClientSignatures := strings.SplitN(signatureResult.String(), "#", 2) - if len(arrayClientSignatures) == 2 { - if cache.GetModelGroup(modelName) == arrayClientSignatures[0] { - clientSignature = arrayClientSignatures[1] - } + signature := "" + if thinkingText != "" { + if cachedSig := cache.GetCachedSignature(modelName, thinkingText); cachedSig != "" { + signature = cachedSig + // log.Debugf("Using cached signature for thinking block") } } - if cache.HasValidSignature(modelName, clientSignature) { - signature = clientSignature + + // Fallback to client signature only if cache miss and client signature is valid + if signature == "" { + signatureResult := contentResult.Get("signature") + clientSignature := "" + if signatureResult.Exists() && signatureResult.String() != "" { + arrayClientSignatures := strings.SplitN(signatureResult.String(), "#", 2) + if len(arrayClientSignatures) == 2 { + if cache.GetModelGroup(modelName) == arrayClientSignatures[0] { + clientSignature = arrayClientSignatures[1] + } + } + } + if cache.HasValidSignature(modelName, clientSignature) { + signature = clientSignature + } + // log.Debugf("Using client-provided signature for thinking block") } - // log.Debugf("Using client-provided signature for thinking block") - } - // Store for subsequent tool_use in the same message - if cache.HasValidSignature(modelName, signature) { - currentMessageThinkingSignature = signature - } + // Store for subsequent tool_use in the same message + if cache.HasValidSignature(modelName, signature) { + currentMessageThinkingSignature = signature + } - // Skip trailing unsigned thinking blocks on last assistant message - isUnsigned := !cache.HasValidSignature(modelName, signature) + // Skip trailing unsigned thinking blocks on last assistant message + isUnsigned := !cache.HasValidSignature(modelName, signature) - // If unsigned, skip entirely (don't convert to text) - // Claude requires assistant messages to start with thinking blocks when thinking is enabled - // Converting to text would break this requirement - if isUnsigned { - // log.Debugf("Dropping unsigned thinking block (no valid signature)") - enableThoughtTranslate = false - continue - } + // If unsigned, skip entirely (don't convert to text) + // Claude requires assistant messages to start with thinking blocks when thinking is enabled + // Converting to text would break this requirement + if isUnsigned { + // log.Debugf("Dropping unsigned thinking block (no valid signature)") + enableThoughtTranslate = false + continue + } - // Valid signature, send as thought block - // Always include "text" field — Google Antigravity API requires it - // even for redacted thinking where the text is empty. - partJSON := []byte(`{}`) - partJSON, _ = sjson.SetBytes(partJSON, "thought", true) - partJSON, _ = sjson.SetBytes(partJSON, "text", thinkingText) - if signature != "" { - partJSON, _ = sjson.SetBytes(partJSON, "thoughtSignature", signature) - } - clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON) + // Valid signature, send as thought block + // Always include "text" field — Google Antigravity API requires it + // even for redacted thinking where the text is empty. + partJSON := []byte(`{}`) + partJSON, _ = sjson.SetBytes(partJSON, "thought", true) + partJSON, _ = sjson.SetBytes(partJSON, "text", thinkingText) + if signature != "" { + partJSON, _ = sjson.SetBytes(partJSON, "thoughtSignature", signature) + } + clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON) } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" { prompt := contentResult.Get("text").String() // Skip empty text parts to avoid Gemini API error: