Unified · Split
| @@ -28,8 +28,8 @@ import ( · 27 unmodified lines | ||
| 28 | 28 | ) |
| 29 | 29 | |
| 30 | 30 | const ( |
| 31 | − codexClientVersion = "0.101.0" | |
| 32 | − codexUserAgent = "codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464" | |
| 31 | + codexUserAgent = "codex_cli_rs/0.116.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464" | |
| 32 | + codexOriginator = "codex_cli_rs" | |
| 33 | 33 | ) |
| 34 | 34 | |
| 35 | 35 | var dataTag = []byte("data:") |
| @@ -645,8 +645,10 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s · 609 unmodified lines | ||
| 645 | 645 | ginHeaders = ginCtx.Request.Header |
| 646 | 646 | } |
| 647 | 647 | |
| 648 | − misc.EnsureHeader(r.Header, ginHeaders, "Version", codexClientVersion) | |
| 648 | + misc.EnsureHeader(r.Header, ginHeaders, "Version", "") | |
| 649 | 649 | misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString()) |
| 650 | + misc.EnsureHeader(r.Header, ginHeaders, "X-Codex-Turn-Metadata", "") | |
| 651 | + misc.EnsureHeader(r.Header, ginHeaders, "X-Client-Request-Id", "") | |
| 650 | 652 | cfgUserAgent, _ := codexHeaderDefaults(cfg, auth) |
| 651 | 653 | ensureHeaderWithConfigPrecedence(r.Header, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent) |
| 652 | 654 | |
| @@ -663,8 +665,12 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s · 10 unmodified lines | ||
| 663 | 665 | isAPIKey = true |
| 664 | 666 | } |
| 665 | 667 | } |
| 668 | + if originator := strings.TrimSpace(ginHeaders.Get("Originator")); originator != "" { | |
| 669 | + r.Header.Set("Originator", originator) | |
| 670 | + } else if !isAPIKey { | |
| 671 | + r.Header.Set("Originator", codexOriginator) | |
| 672 | + } | |
| 666 | 673 | if !isAPIKey { |
| 667 | − r.Header.Set("Originator", "codex_cli_rs") | |
| 668 | 674 | if auth != nil && auth.Metadata != nil { |
| 669 | 675 | if accountID, ok := auth.Metadata["account_id"].(string); ok { |
| 670 | 676 | r.Header.Set("Chatgpt-Account-Id", accountID) |
| @@ -814,9 +814,10 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth * · 813 unmodified lines | ||
| 814 | 814 | ensureHeaderWithPriority(headers, ginHeaders, "x-codex-beta-features", cfgBetaFeatures, "") |
| 815 | 815 | misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-state", "") |
| 816 | 816 | misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-metadata", "") |
| 817 | + misc.EnsureHeader(headers, ginHeaders, "x-client-request-id", "") | |
| 817 | 818 | misc.EnsureHeader(headers, ginHeaders, "x-responsesapi-include-timing-metrics", "") |
| 819 | + misc.EnsureHeader(headers, ginHeaders, "Version", "") | |
| 818 | 820 | |
| 819 | − misc.EnsureHeader(headers, ginHeaders, "Version", codexClientVersion) | |
| 820 | 821 | betaHeader := strings.TrimSpace(headers.Get("OpenAI-Beta")) |
| 821 | 822 | if betaHeader == "" && ginHeaders != nil { |
| 822 | 823 | betaHeader = strings.TrimSpace(ginHeaders.Get("OpenAI-Beta")) |
| @@ -834,8 +835,12 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth * · 11 unmodified lines | ||
| 834 | 835 | isAPIKey = true |
| 835 | 836 | } |
| 836 | 837 | } |
| 838 | + if originator := strings.TrimSpace(ginHeaders.Get("Originator")); originator != "" { | |
| 839 | + headers.Set("Originator", originator) | |
| 840 | + } else if !isAPIKey { | |
| 841 | + headers.Set("Originator", codexOriginator) | |
| 842 | + } | |
| 837 | 843 | if !isAPIKey { |
| 838 | − headers.Set("Originator", "codex_cli_rs") | |
| 839 | 844 | if auth != nil && auth.Metadata != nil { |
| 840 | 845 | if accountID, ok := auth.Metadata["account_id"].(string); ok { |
| 841 | 846 | if trimmed := strings.TrimSpace(accountID); trimmed != "" { |
| @@ -41,9 +41,46 @@ func TestApplyCodexWebsocketHeadersDefaultsToCurrentResponsesBeta(t *testing.T) · 40 unmodified lines | ||
| 41 | 41 | if got := headers.Get("User-Agent"); got != codexUserAgent { |
| 42 | 42 | t.Fatalf("User-Agent = %s, want %s", got, codexUserAgent) |
| 43 | 43 | } |
| 44 | + if got := headers.Get("Version"); got != "" { | |
| 45 | + t.Fatalf("Version = %q, want empty", got) | |
| 46 | + } | |
| 44 | 47 | if got := headers.Get("x-codex-beta-features"); got != "" { |
| 45 | 48 | t.Fatalf("x-codex-beta-features = %q, want empty", got) |
| 46 | 49 | } |
| 50 | + if got := headers.Get("X-Codex-Turn-Metadata"); got != "" { | |
| 51 | + t.Fatalf("X-Codex-Turn-Metadata = %q, want empty", got) | |
| 52 | + } | |
| 53 | + if got := headers.Get("X-Client-Request-Id"); got != "" { | |
| 54 | + t.Fatalf("X-Client-Request-Id = %q, want empty", got) | |
| 55 | + } | |
| 56 | +} | |
| 57 | + | |
| 58 | +func TestApplyCodexWebsocketHeadersPassesThroughClientIdentityHeaders(t *testing.T) { | |
| 59 | + auth := &cliproxyauth.Auth{ | |
| 60 | + Provider: "codex", | |
| 61 | + Metadata: map[string]any{"email": "user@example.com"}, | |
| 62 | + } | |
| 63 | + ctx := contextWithGinHeaders(map[string]string{ | |
| 64 | + "Originator": "Codex Desktop", | |
| 65 | + "Version": "0.115.0-alpha.27", | |
| 66 | + "X-Codex-Turn-Metadata": `{"turn_id":"turn-1"}`, | |
| 67 | + "X-Client-Request-Id": "019d2233-e240-7162-992d-38df0a2a0e0d", | |
| 68 | + }) | |
| 69 | + | |
| 70 | + headers := applyCodexWebsocketHeaders(ctx, http.Header{}, auth, "", nil) | |
| 71 | + | |
| 72 | + if got := headers.Get("Originator"); got != "Codex Desktop" { | |
| 73 | + t.Fatalf("Originator = %s, want %s", got, "Codex Desktop") | |
| 74 | + } | |
| 75 | + if got := headers.Get("Version"); got != "0.115.0-alpha.27" { | |
| 76 | + t.Fatalf("Version = %s, want %s", got, "0.115.0-alpha.27") | |
| 77 | + } | |
| 78 | + if got := headers.Get("X-Codex-Turn-Metadata"); got != `{"turn_id":"turn-1"}` { | |
| 79 | + t.Fatalf("X-Codex-Turn-Metadata = %s, want %s", got, `{"turn_id":"turn-1"}`) | |
| 80 | + } | |
| 81 | + if got := headers.Get("X-Client-Request-Id"); got != "019d2233-e240-7162-992d-38df0a2a0e0d" { | |
| 82 | + t.Fatalf("X-Client-Request-Id = %s, want %s", got, "019d2233-e240-7162-992d-38df0a2a0e0d") | |
| 83 | + } | |
| 47 | 84 | } |
| 48 | 85 | |
| 49 | 86 | func TestApplyCodexWebsocketHeadersUsesConfigDefaultsForOAuth(t *testing.T) { |
| @@ -177,6 +214,57 @@ func TestApplyCodexHeadersUsesConfigUserAgentForOAuth(t *testing.T) { · 127 unmodified lines | ||
| 177 | 214 | } |
| 178 | 215 | } |
| 179 | 216 | |
| 217 | +func TestApplyCodexHeadersPassesThroughClientIdentityHeaders(t *testing.T) { | |
| 218 | + req, err := http.NewRequest(http.MethodPost, "https://example.com/responses", nil) | |
| 219 | + if err != nil { | |
| 220 | + t.Fatalf("NewRequest() error = %v", err) | |
| 221 | + } | |
| 222 | + auth := &cliproxyauth.Auth{ | |
| 223 | + Provider: "codex", | |
| 224 | + Metadata: map[string]any{"email": "user@example.com"}, | |
| 225 | + } | |
| 226 | + req = req.WithContext(contextWithGinHeaders(map[string]string{ | |
| 227 | + "Originator": "Codex Desktop", | |
| 228 | + "Version": "0.115.0-alpha.27", | |
| 229 | + "X-Codex-Turn-Metadata": `{"turn_id":"turn-1"}`, | |
| 230 | + "X-Client-Request-Id": "019d2233-e240-7162-992d-38df0a2a0e0d", | |
| 231 | + })) | |
| 232 | + | |
| 233 | + applyCodexHeaders(req, auth, "oauth-token", true, nil) | |
| 234 | + | |
| 235 | + if got := req.Header.Get("Originator"); got != "Codex Desktop" { | |
| 236 | + t.Fatalf("Originator = %s, want %s", got, "Codex Desktop") | |
| 237 | + } | |
| 238 | + if got := req.Header.Get("Version"); got != "0.115.0-alpha.27" { | |
| 239 | + t.Fatalf("Version = %s, want %s", got, "0.115.0-alpha.27") | |
| 240 | + } | |
| 241 | + if got := req.Header.Get("X-Codex-Turn-Metadata"); got != `{"turn_id":"turn-1"}` { | |
| 242 | + t.Fatalf("X-Codex-Turn-Metadata = %s, want %s", got, `{"turn_id":"turn-1"}`) | |
| 243 | + } | |
| 244 | + if got := req.Header.Get("X-Client-Request-Id"); got != "019d2233-e240-7162-992d-38df0a2a0e0d" { | |
| 245 | + t.Fatalf("X-Client-Request-Id = %s, want %s", got, "019d2233-e240-7162-992d-38df0a2a0e0d") | |
| 246 | + } | |
| 247 | +} | |
| 248 | + | |
| 249 | +func TestApplyCodexHeadersDoesNotInjectClientOnlyHeadersByDefault(t *testing.T) { | |
| 250 | + req, err := http.NewRequest(http.MethodPost, "https://example.com/responses", nil) | |
| 251 | + if err != nil { | |
| 252 | + t.Fatalf("NewRequest() error = %v", err) | |
| 253 | + } | |
| 254 | + | |
| 255 | + applyCodexHeaders(req, nil, "oauth-token", true, nil) | |
| 256 | + | |
| 257 | + if got := req.Header.Get("Version"); got != "" { | |
| 258 | + t.Fatalf("Version = %q, want empty", got) | |
| 259 | + } | |
| 260 | + if got := req.Header.Get("X-Codex-Turn-Metadata"); got != "" { | |
| 261 | + t.Fatalf("X-Codex-Turn-Metadata = %q, want empty", got) | |
| 262 | + } | |
| 263 | + if got := req.Header.Get("X-Client-Request-Id"); got != "" { | |
| 264 | + t.Fatalf("X-Client-Request-Id = %q, want empty", got) | |
| 265 | + } | |
| 266 | +} | |
| 267 | + | |
| 180 | 268 | func contextWithGinHeaders(headers map[string]string) context.Context { |
| 181 | 269 | gin.SetMode(gin.TestMode) |
| 182 | 270 | recorder := httptest.NewRecorder() |
| @@ -104,59 +104,59 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 103 unmodified lines | ||
| 104 | 104 | |
| 105 | 105 | // Always try cached signature first (more reliable than client-provided) |
| 106 | 106 | // Client may send stale or invalid signatures from different sessions |
| 107 | − signature := "" | |
| 108 | − if thinkingText != "" { | |
| 109 | − if cachedSig := cache.GetCachedSignature(modelName, thinkingText); cachedSig != "" { | |
| 110 | − signature = cachedSig | |
| 111 | − // log.Debugf("Using cached signature for thinking block") | |
| 107 | + signature := "" | |
| 108 | + if thinkingText != "" { | |
| 109 | + if cachedSig := cache.GetCachedSignature(modelName, thinkingText); cachedSig != "" { | |
| 110 | + signature = cachedSig | |
| 111 | + // log.Debugf("Using cached signature for thinking block") | |
| 112 | + } | |
| 112 | 113 | } |
| 113 | − } | |
| 114 | 114 | |
| 115 | − // Fallback to client signature only if cache miss and client signature is valid | |
| 116 | − if signature == "" { | |
| 117 | − signatureResult := contentResult.Get("signature") | |
| 118 | − clientSignature := "" | |
| 119 | − if signatureResult.Exists() && signatureResult.String() != "" { | |
| 120 | − arrayClientSignatures := strings.SplitN(signatureResult.String(), "#", 2) | |
| 121 | − if len(arrayClientSignatures) == 2 { | |
| 122 | − if cache.GetModelGroup(modelName) == arrayClientSignatures[0] { | |
| 123 | − clientSignature = arrayClientSignatures[1] | |
| 115 | + // Fallback to client signature only if cache miss and client signature is valid | |
| 116 | + if signature == "" { | |
| 117 | + signatureResult := contentResult.Get("signature") | |
| 118 | + clientSignature := "" | |
| 119 | + if signatureResult.Exists() && signatureResult.String() != "" { | |
| 120 | + arrayClientSignatures := strings.SplitN(signatureResult.String(), "#", 2) | |
| 121 | + if len(arrayClientSignatures) == 2 { | |
| 122 | + if cache.GetModelGroup(modelName) == arrayClientSignatures[0] { | |
| 123 | + clientSignature = arrayClientSignatures[1] | |
| 124 | + } | |
| 124 | 125 | } |
| 125 | 126 | } |
| 127 | + if cache.HasValidSignature(modelName, clientSignature) { | |
| 128 | + signature = clientSignature | |
| 129 | + } | |
| 130 | + // log.Debugf("Using client-provided signature for thinking block") | |
| 126 | 131 | } |
| 127 | − if cache.HasValidSignature(modelName, clientSignature) { | |
| 128 | − signature = clientSignature | |
| 129 | − } | |
| 130 | − // log.Debugf("Using client-provided signature for thinking block") | |
| 131 | − } | |
| 132 | 132 | |
| 133 | − // Store for subsequent tool_use in the same message | |
| 134 | − if cache.HasValidSignature(modelName, signature) { | |
| 135 | − currentMessageThinkingSignature = signature | |
| 136 | − } | |
| 133 | + // Store for subsequent tool_use in the same message | |
| 134 | + if cache.HasValidSignature(modelName, signature) { | |
| 135 | + currentMessageThinkingSignature = signature | |
| 136 | + } | |
| 137 | 137 | |
| 138 | − // Skip trailing unsigned thinking blocks on last assistant message | |
| 139 | − isUnsigned := !cache.HasValidSignature(modelName, signature) | |
| 138 | + // Skip trailing unsigned thinking blocks on last assistant message | |
| 139 | + isUnsigned := !cache.HasValidSignature(modelName, signature) | |
| 140 | 140 | |
| 141 | − // If unsigned, skip entirely (don't convert to text) | |
| 142 | − // Claude requires assistant messages to start with thinking blocks when thinking is enabled | |
| 143 | − // Converting to text would break this requirement | |
| 144 | − if isUnsigned { | |
| 145 | − // log.Debugf("Dropping unsigned thinking block (no valid signature)") | |
| 146 | − enableThoughtTranslate = false | |
| 147 | − continue | |
| 148 | − } | |
| 141 | + // If unsigned, skip entirely (don't convert to text) | |
| 142 | + // Claude requires assistant messages to start with thinking blocks when thinking is enabled | |
| 143 | + // Converting to text would break this requirement | |
| 144 | + if isUnsigned { | |
| 145 | + // log.Debugf("Dropping unsigned thinking block (no valid signature)") | |
| 146 | + enableThoughtTranslate = false | |
| 147 | + continue | |
| 148 | + } | |
| 149 | 149 | |
| 150 | − // Valid signature, send as thought block | |
| 151 | − // Always include "text" field — Google Antigravity API requires it | |
| 152 | − // even for redacted thinking where the text is empty. | |
| 153 | − partJSON := []byte(`{}`) | |
| 154 | − partJSON, _ = sjson.SetBytes(partJSON, "thought", true) | |
| 155 | − partJSON, _ = sjson.SetBytes(partJSON, "text", thinkingText) | |
| 156 | − if signature != "" { | |
| 157 | − partJSON, _ = sjson.SetBytes(partJSON, "thoughtSignature", signature) | |
| 158 | − } | |
| 159 | − clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON) | |
| 150 | + // Valid signature, send as thought block | |
| 151 | + // Always include "text" field — Google Antigravity API requires it | |
| 152 | + // even for redacted thinking where the text is empty. | |
| 153 | + partJSON := []byte(`{}`) | |
| 154 | + partJSON, _ = sjson.SetBytes(partJSON, "thought", true) | |
| 155 | + partJSON, _ = sjson.SetBytes(partJSON, "text", thinkingText) | |
| 156 | + if signature != "" { | |
| 157 | + partJSON, _ = sjson.SetBytes(partJSON, "thoughtSignature", signature) | |
| 158 | + } | |
| 159 | + clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON) | |
| 160 | 160 | } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" { |
| 161 | 161 | prompt := contentResult.Get("text").String() |
| 162 | 162 | // Skip empty text parts to avoid Gemini API error: |