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
2828 )
2929
3030 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"
3333 )
3434
3535 var dataTag = []byte("data:")
@@ -645,8 +645,10 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s · 609 unmodified lines
645645 ginHeaders = ginCtx.Request.Header
646646 }
647647
648− misc.EnsureHeader(r.Header, ginHeaders, "Version", codexClientVersion)
648+ misc.EnsureHeader(r.Header, ginHeaders, "Version", "")
649649 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", "")
650652 cfgUserAgent, _ := codexHeaderDefaults(cfg, auth)
651653 ensureHeaderWithConfigPrecedence(r.Header, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent)
652654
@@ -663,8 +665,12 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s · 10 unmodified lines
663665 isAPIKey = true
664666 }
665667 }
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+ }
666673 if !isAPIKey {
667− r.Header.Set("Originator", "codex_cli_rs")
668674 if auth != nil && auth.Metadata != nil {
669675 if accountID, ok := auth.Metadata["account_id"].(string); ok {
670676 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
814814 ensureHeaderWithPriority(headers, ginHeaders, "x-codex-beta-features", cfgBetaFeatures, "")
815815 misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-state", "")
816816 misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-metadata", "")
817+ misc.EnsureHeader(headers, ginHeaders, "x-client-request-id", "")
817818 misc.EnsureHeader(headers, ginHeaders, "x-responsesapi-include-timing-metrics", "")
819+ misc.EnsureHeader(headers, ginHeaders, "Version", "")
818820
819− misc.EnsureHeader(headers, ginHeaders, "Version", codexClientVersion)
820821 betaHeader := strings.TrimSpace(headers.Get("OpenAI-Beta"))
821822 if betaHeader == "" && ginHeaders != nil {
822823 betaHeader = strings.TrimSpace(ginHeaders.Get("OpenAI-Beta"))
@@ -834,8 +835,12 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth * · 11 unmodified lines
834835 isAPIKey = true
835836 }
836837 }
838+ if originator := strings.TrimSpace(ginHeaders.Get("Originator")); originator != "" {
839+ headers.Set("Originator", originator)
840+ } else if !isAPIKey {
841+ headers.Set("Originator", codexOriginator)
842+ }
837843 if !isAPIKey {
838− headers.Set("Originator", "codex_cli_rs")
839844 if auth != nil && auth.Metadata != nil {
840845 if accountID, ok := auth.Metadata["account_id"].(string); ok {
841846 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
4141 if got := headers.Get("User-Agent"); got != codexUserAgent {
4242 t.Fatalf("User-Agent = %s, want %s", got, codexUserAgent)
4343 }
44+ if got := headers.Get("Version"); got != "" {
45+ t.Fatalf("Version = %q, want empty", got)
46+ }
4447 if got := headers.Get("x-codex-beta-features"); got != "" {
4548 t.Fatalf("x-codex-beta-features = %q, want empty", got)
4649 }
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+ }
4784 }
4885
4986 func TestApplyCodexWebsocketHeadersUsesConfigDefaultsForOAuth(t *testing.T) {
@@ -177,6 +214,57 @@ func TestApplyCodexHeadersUsesConfigUserAgentForOAuth(t *testing.T) { · 127 unmodified lines
177214 }
178215 }
179216
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+
180268 func contextWithGinHeaders(headers map[string]string) context.Context {
181269 gin.SetMode(gin.TestMode)
182270 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
104104
105105 // Always try cached signature first (more reliable than client-provided)
106106 // 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+ }
112113 }
113− }
114114
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+ }
124125 }
125126 }
127+ if cache.HasValidSignature(modelName, clientSignature) {
128+ signature = clientSignature
129+ }
130+ // log.Debugf("Using client-provided signature for thinking block")
126131 }
127− if cache.HasValidSignature(modelName, clientSignature) {
128− signature = clientSignature
129− }
130− // log.Debugf("Using client-provided signature for thinking block")
131− }
132132
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+ }
137137
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)
140140
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+ }
149149
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)
160160 } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
161161 prompt := contentResult.Get("text").String()
162162 // Skip empty text parts to avoid Gemini API error: