History

feat(codex): pass through codex client identity headers

hkfires committed 7 months ago

Commit 528b1a21 parent d475aab4 files · +149 −50Browse files

Unified · Split

4 files
  1. internal/runtime/executor/codex_executor.go +10 −4
  2. internal/runtime/executor/codex_websockets_executor.go +7 −2
  3. internal/runtime/executor/codex_websockets_executor_test.go +88 −0
  4. internal/translator/antigravity/claude/antigravity_claude_request.go +44 −44
internal/runtime/executor/codex_executor.go+10−4
@@ -28,8 +28,8 @@ import ( · 27 unmodified lines
28)28)
29 29
30const (30const (
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
35var dataTag = []byte("data:")35var 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.Header645 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 = true665 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)
internal/runtime/executor/codex_websockets_executor.go+7−2
@@ -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 = true835 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 != "" {
internal/runtime/executor/codex_websockets_executor_test.go+88−0
@@ -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
58func 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
49func TestApplyCodexWebsocketHeadersUsesConfigDefaultsForOAuth(t *testing.T) {86func TestApplyCodexWebsocketHeadersUsesConfigDefaultsForOAuth(t *testing.T) {
@@ -177,6 +214,57 @@ func TestApplyCodexHeadersUsesConfigUserAgentForOAuth(t *testing.T) { · 127 unmodified lines
177 }214 }
178}215}
179 216
217func 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
249func 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
180func contextWithGinHeaders(headers map[string]string) context.Context {268func 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()
internal/translator/antigravity/claude/antigravity_claude_request.go+44−44
@@ -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 sessions106 // 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 = cachedSig110 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 valid115 // 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 message133 // Store for subsequent tool_use in the same message
134 if cache.HasValidSignature(modelName, signature) {134 if cache.HasValidSignature(modelName, signature) {
135 currentMessageThinkingSignature = signature135 currentMessageThinkingSignature = signature
136 }136 }
137 137
138 // Skip trailing unsigned thinking blocks on last assistant message138 // 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 enabled142 // Claude requires assistant messages to start with thinking blocks when thinking is enabled
143 // Converting to text would break this requirement143 // 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 = false146 enableThoughtTranslate = false
147 continue147 continue
148 }148 }
149 149
150 // Valid signature, send as thought block150 // Valid signature, send as thought block
151 // Always include "text" field — Google Antigravity API requires it151 // 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: