Unified · Split
| @@ -28,8 +28,8 @@ import ( · 27 unmodified lines | |||
| 28 | ) | 28 | ) |
| 29 | 29 | ||
| 30 | const ( | 30 | const ( |
| 31 | codexClientVersion = "0.101.0" | 31 | codexUserAgent = "codex_cli_rs/0.116.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464" |
| 32 | codexUserAgent = "codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464" | 32 | codexOriginator = "codex_cli_rs" |
| 33 | ) | 33 | ) |
| 34 | 34 | ||
| 35 | var dataTag = []byte("data:") | 35 | var dataTag = []byte("data:") |
| @@ -645,8 +645,10 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s · 609 unmodified lines | |||
| 645 | ginHeaders = ginCtx.Request.Header | 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 | misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString()) | 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 | cfgUserAgent, _ := codexHeaderDefaults(cfg, auth) | 652 | cfgUserAgent, _ := codexHeaderDefaults(cfg, auth) |
| 651 | ensureHeaderWithConfigPrecedence(r.Header, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent) | 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 | isAPIKey = true | 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 | if !isAPIKey { | 673 | if !isAPIKey { |
| 667 | r.Header.Set("Originator", "codex_cli_rs") | ||
| 668 | if auth != nil && auth.Metadata != nil { | 674 | if auth != nil && auth.Metadata != nil { |
| 669 | if accountID, ok := auth.Metadata["account_id"].(string); ok { | 675 | if accountID, ok := auth.Metadata["account_id"].(string); ok { |
| 670 | r.Header.Set("Chatgpt-Account-Id", accountID) | 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 | ensureHeaderWithPriority(headers, ginHeaders, "x-codex-beta-features", cfgBetaFeatures, "") | 814 | ensureHeaderWithPriority(headers, ginHeaders, "x-codex-beta-features", cfgBetaFeatures, "") |
| 815 | misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-state", "") | 815 | misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-state", "") |
| 816 | misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-metadata", "") | 816 | misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-metadata", "") |
| 817 | misc.EnsureHeader(headers, ginHeaders, "x-client-request-id", "") | ||
| 817 | misc.EnsureHeader(headers, ginHeaders, "x-responsesapi-include-timing-metrics", "") | 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 | betaHeader := strings.TrimSpace(headers.Get("OpenAI-Beta")) | 821 | betaHeader := strings.TrimSpace(headers.Get("OpenAI-Beta")) |
| 821 | if betaHeader == "" && ginHeaders != nil { | 822 | if betaHeader == "" && ginHeaders != nil { |
| 822 | betaHeader = strings.TrimSpace(ginHeaders.Get("OpenAI-Beta")) | 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 | isAPIKey = true | 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 | if !isAPIKey { | 843 | if !isAPIKey { |
| 838 | headers.Set("Originator", "codex_cli_rs") | ||
| 839 | if auth != nil && auth.Metadata != nil { | 844 | if auth != nil && auth.Metadata != nil { |
| 840 | if accountID, ok := auth.Metadata["account_id"].(string); ok { | 845 | if accountID, ok := auth.Metadata["account_id"].(string); ok { |
| 841 | if trimmed := strings.TrimSpace(accountID); trimmed != "" { | 846 | if trimmed := strings.TrimSpace(accountID); trimmed != "" { |
| @@ -41,9 +41,46 @@ func TestApplyCodexWebsocketHeadersDefaultsToCurrentResponsesBeta(t *testing.T) · 40 unmodified lines | |||
| 41 | if got := headers.Get("User-Agent"); got != codexUserAgent { | 41 | if got := headers.Get("User-Agent"); got != codexUserAgent { |
| 42 | t.Fatalf("User-Agent = %s, want %s", got, codexUserAgent) | 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 | if got := headers.Get("x-codex-beta-features"); got != "" { | 47 | if got := headers.Get("x-codex-beta-features"); got != "" { |
| 45 | t.Fatalf("x-codex-beta-features = %q, want empty", got) | 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 | func TestApplyCodexWebsocketHeadersUsesConfigDefaultsForOAuth(t *testing.T) { | 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 | func contextWithGinHeaders(headers map[string]string) context.Context { | 268 | func contextWithGinHeaders(headers map[string]string) context.Context { |
| 181 | gin.SetMode(gin.TestMode) | 269 | gin.SetMode(gin.TestMode) |
| 182 | recorder := httptest.NewRecorder() | 270 | recorder := httptest.NewRecorder() |
| @@ -104,59 +104,59 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 103 unmodified lines | |||
| 104 | 104 | ||
| 105 | // Always try cached signature first (more reliable than client-provided) | 105 | // Always try cached signature first (more reliable than client-provided) |
| 106 | // Client may send stale or invalid signatures from different sessions | 106 | // Client may send stale or invalid signatures from different sessions |
| 107 | signature := "" | 107 | signature := "" |
| 108 | if thinkingText != "" { | 108 | if thinkingText != "" { |
| 109 | if cachedSig := cache.GetCachedSignature(modelName, thinkingText); cachedSig != "" { | 109 | if cachedSig := cache.GetCachedSignature(modelName, thinkingText); cachedSig != "" { |
| 110 | signature = cachedSig | 110 | signature = cachedSig |
| 111 | // log.Debugf("Using cached signature for thinking block") | 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 | 115 | // Fallback to client signature only if cache miss and client signature is valid |
| 116 | if signature == "" { | 116 | if signature == "" { |
| 117 | signatureResult := contentResult.Get("signature") | 117 | signatureResult := contentResult.Get("signature") |
| 118 | clientSignature := "" | 118 | clientSignature := "" |
| 119 | if signatureResult.Exists() && signatureResult.String() != "" { | 119 | if signatureResult.Exists() && signatureResult.String() != "" { |
| 120 | arrayClientSignatures := strings.SplitN(signatureResult.String(), "#", 2) | 120 | arrayClientSignatures := strings.SplitN(signatureResult.String(), "#", 2) |
| 121 | if len(arrayClientSignatures) == 2 { | 121 | if len(arrayClientSignatures) == 2 { |
| 122 | if cache.GetModelGroup(modelName) == arrayClientSignatures[0] { | 122 | if cache.GetModelGroup(modelName) == arrayClientSignatures[0] { |
| 123 | clientSignature = arrayClientSignatures[1] | 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 | 133 | // Store for subsequent tool_use in the same message |
| 134 | if cache.HasValidSignature(modelName, signature) { | 134 | if cache.HasValidSignature(modelName, signature) { |
| 135 | currentMessageThinkingSignature = signature | 135 | currentMessageThinkingSignature = signature |
| 136 | } | 136 | } |
| 137 | 137 | ||
| 138 | // Skip trailing unsigned thinking blocks on last assistant message | 138 | // Skip trailing unsigned thinking blocks on last assistant message |
| 139 | isUnsigned := !cache.HasValidSignature(modelName, signature) | 139 | isUnsigned := !cache.HasValidSignature(modelName, signature) |
| 140 | 140 | ||
| 141 | // If unsigned, skip entirely (don't convert to text) | 141 | // If unsigned, skip entirely (don't convert to text) |
| 142 | // Claude requires assistant messages to start with thinking blocks when thinking is enabled | 142 | // Claude requires assistant messages to start with thinking blocks when thinking is enabled |
| 143 | // Converting to text would break this requirement | 143 | // Converting to text would break this requirement |
| 144 | if isUnsigned { | 144 | if isUnsigned { |
| 145 | // log.Debugf("Dropping unsigned thinking block (no valid signature)") | 145 | // log.Debugf("Dropping unsigned thinking block (no valid signature)") |
| 146 | enableThoughtTranslate = false | 146 | enableThoughtTranslate = false |
| 147 | continue | 147 | continue |
| 148 | } | 148 | } |
| 149 | 149 | ||
| 150 | // Valid signature, send as thought block | 150 | // Valid signature, send as thought block |
| 151 | // Always include "text" field — Google Antigravity API requires it | 151 | // Always include "text" field — Google Antigravity API requires it |
| 152 | // even for redacted thinking where the text is empty. | 152 | // even for redacted thinking where the text is empty. |
| 153 | partJSON := []byte(`{}`) | 153 | partJSON := []byte(`{}`) |
| 154 | partJSON, _ = sjson.SetBytes(partJSON, "thought", true) | 154 | partJSON, _ = sjson.SetBytes(partJSON, "thought", true) |
| 155 | partJSON, _ = sjson.SetBytes(partJSON, "text", thinkingText) | 155 | partJSON, _ = sjson.SetBytes(partJSON, "text", thinkingText) |
| 156 | if signature != "" { | 156 | if signature != "" { |
| 157 | partJSON, _ = sjson.SetBytes(partJSON, "thoughtSignature", signature) | 157 | partJSON, _ = sjson.SetBytes(partJSON, "thoughtSignature", signature) |
| 158 | } | 158 | } |
| 159 | clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON) | 159 | clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON) |
| 160 | } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" { | 160 | } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" { |
| 161 | prompt := contentResult.Get("text").String() | 161 | prompt := contentResult.Get("text").String() |
| 162 | // Skip empty text parts to avoid Gemini API error: | 162 | // Skip empty text parts to avoid Gemini API error: |