From 38ed7aefc62ccc0e2ab59366bcd588b20b7e0e14 Mon Sep 17 00:00:00 2001 From: sususu98 Date: Wed, 24 Jun 2026 14:07:00 +0800 Subject: [PATCH] fix(codex): sanitize downstream UA for direct image calls Direct OpenAI image calls can inherit downstream User-Agent values from the Gin request context, which may trigger upstream Cloudflare 1010 blocks. Clone inbound Codex headers for direct image requests and remove only User-Agent before applying defaults. This keeps Version, X-Codex-Turn-Metadata, X-Client-Request-Id, and Originator forwarding intact. Expand direct image regression coverage for downstream identity headers and align edit endpoint expectations with /images/edits. --- internal/runtime/executor/codex_executor.go | 22 ++++++++-- .../runtime/executor/codex_openai_images.go | 4 +- .../executor/codex_openai_images_test.go | 42 ++++++++++++++++--- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 16f3d2320..966a1320d 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -1578,15 +1578,29 @@ func codexIdentityConfuseUUID(authID string, kind string, value string) string { } func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config) { - r.Header.Set("Content-Type", "application/json") - r.Header.Set("Authorization", "Bearer "+token) - var ginHeaders http.Header if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { ginHeaders = ginCtx.Request.Header } + applyCodexHeadersFromSources(r, auth, token, stream, cfg, ginHeaders) +} - if ginHeaders.Get("X-Codex-Beta-Features") != "" { +// applyCodexDirectImageHeaders sets Codex upstream headers for direct /images/* calls. +// Downstream client User-Agent values are not forwarded to reduce Cloudflare 1010 blocks. +func applyCodexDirectImageHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config) { + var ginHeaders http.Header + if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { + ginHeaders = ginCtx.Request.Header.Clone() + ginHeaders.Del("User-Agent") + } + applyCodexHeadersFromSources(r, auth, token, stream, cfg, ginHeaders) +} + +func applyCodexHeadersFromSources(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config, ginHeaders http.Header) { + r.Header.Set("Content-Type", "application/json") + r.Header.Set("Authorization", "Bearer "+token) + + if ginHeaders != nil && ginHeaders.Get("X-Codex-Beta-Features") != "" { r.Header.Set("X-Codex-Beta-Features", ginHeaders.Get("X-Codex-Beta-Features")) } misc.EnsureHeader(r.Header, ginHeaders, "Version", "") diff --git a/internal/runtime/executor/codex_openai_images.go b/internal/runtime/executor/codex_openai_images.go index 114c5251a..539817996 100644 --- a/internal/runtime/executor/codex_openai_images.go +++ b/internal/runtime/executor/codex_openai_images.go @@ -334,7 +334,7 @@ func (e *CodexExecutor) executeDirectOpenAIImage(ctx context.Context, auth *clip if errCache != nil { return resp, errCache } - applyCodexHeaders(httpReq, auth, apiKey, false, e.cfg) + applyCodexDirectImageHeaders(httpReq, auth, apiKey, false, e.cfg) if contentType != "" { httpReq.Header.Set("Content-Type", contentType) } @@ -394,7 +394,7 @@ func (e *CodexExecutor) executeDirectOpenAIImageStream(ctx context.Context, auth if errCache != nil { return nil, errCache } - applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg) + applyCodexDirectImageHeaders(httpReq, auth, apiKey, true, e.cfg) if contentType != "" { httpReq.Header.Set("Content-Type", contentType) } diff --git a/internal/runtime/executor/codex_openai_images_test.go b/internal/runtime/executor/codex_openai_images_test.go index 0d27ec969..6bc5b6389 100644 --- a/internal/runtime/executor/codex_openai_images_test.go +++ b/internal/runtime/executor/codex_openai_images_test.go @@ -42,12 +42,22 @@ func TestCodexExecutorDirectOpenAIImageGenerationUsesImagesEndpoint(t *testing.T var gotPath string var gotAuth string var gotAccept string + var gotUA string + var gotVersion string + var gotTurnMetadata string + var gotClientRequestID string + var gotOriginator string var gotBody []byte upstreamBody := []byte(`{"created":1713833628,"data":[{"b64_json":"AA=="}],"usage":{"total_tokens":100,"input_tokens":50,"output_tokens":50}}`) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path gotAuth = r.Header.Get("Authorization") gotAccept = r.Header.Get("Accept") + gotUA = r.Header.Get("User-Agent") + gotVersion = r.Header.Get("Version") + gotTurnMetadata = r.Header.Get("X-Codex-Turn-Metadata") + gotClientRequestID = r.Header.Get("X-Client-Request-Id") + gotOriginator = r.Header.Get("Originator") var errRead error gotBody, errRead = io.ReadAll(r.Body) if errRead != nil { @@ -58,8 +68,15 @@ func TestCodexExecutorDirectOpenAIImageGenerationUsesImagesEndpoint(t *testing.T })) defer server.Close() + ctx := contextWithGinHeaders(map[string]string{ + "User-Agent": "downstream-client/9.9", + "Version": "0.135.0", + "X-Codex-Turn-Metadata": `{"turn_id":"turn-1"}`, + "X-Client-Request-Id": "client-request-1", + "Originator": "Codex Desktop", + }) executor := NewCodexExecutor(&config.Config{}) - resp, errExecute := executor.Execute(context.Background(), newCodexOpenAIImageTestAuth(server.URL), cliproxyexecutor.Request{ + resp, errExecute := executor.Execute(ctx, newCodexOpenAIImageTestAuth(server.URL), cliproxyexecutor.Request{ Model: "codex/gpt-image-1.5", Payload: []byte(`{"model":"codex/gpt-image-1.5","prompt":"A cute baby sea otter","n":1,"size":"1024x1024","quality":"high","background":"opaque","output_format":"jpeg","output_compression":70,"moderation":"low","extra":{"preserve":true},"stream":false}`), }, codexOpenAIImageTestOptions(codexImagesGenerationsPath, false)) @@ -76,6 +93,21 @@ func TestCodexExecutorDirectOpenAIImageGenerationUsesImagesEndpoint(t *testing.T if gotAccept != "application/json" { t.Fatalf("Accept = %q, want application/json", gotAccept) } + if gotUA != codexUserAgent { + t.Fatalf("User-Agent = %q, want codex default %q", gotUA, codexUserAgent) + } + if gotVersion != "0.135.0" { + t.Fatalf("Version = %q, want %q", gotVersion, "0.135.0") + } + if gotTurnMetadata != `{"turn_id":"turn-1"}` { + t.Fatalf("X-Codex-Turn-Metadata = %q, want %q", gotTurnMetadata, `{"turn_id":"turn-1"}`) + } + if gotClientRequestID != "client-request-1" { + t.Fatalf("X-Client-Request-Id = %q, want %q", gotClientRequestID, "client-request-1") + } + if gotOriginator != "Codex Desktop" { + t.Fatalf("Originator = %q, want %q", gotOriginator, "Codex Desktop") + } if got := gjson.GetBytes(gotBody, "model").String(); got != "gpt-image-1.5" { t.Fatalf("model = %q, want gpt-image-1.5; body=%s", got, string(gotBody)) } @@ -170,8 +202,8 @@ func TestCodexExecutorDirectOpenAIImageEditUsesImagesEditEndpointForJSON(t *test t.Fatalf("Execute() error = %v", errExecute) } - if gotPath != "/images/edit" { - t.Fatalf("path = %q, want /images/edit", gotPath) + if gotPath != "/images/edits" { + t.Fatalf("path = %q, want /images/edits", gotPath) } if got := gjson.GetBytes(gotBody, "model").String(); got != "gpt-image-2" { t.Fatalf("model = %q, want gpt-image-2; body=%s", got, string(gotBody)) @@ -250,8 +282,8 @@ func TestCodexExecutorDirectOpenAIImageEditUsesImagesEditEndpointForMultipart(t t.Fatalf("Execute() error = %v", errExecute) } - if gotPath != "/images/edit" { - t.Fatalf("path = %q, want /images/edit", gotPath) + if gotPath != "/images/edits" { + t.Fatalf("path = %q, want /images/edits", gotPath) } if !strings.HasPrefix(gotContentType, "application/json") { t.Fatalf("Content-Type = %q, want application/json", gotContentType)