mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-08 01:16:47 +08:00
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.
This commit is contained in:
@@ -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", "")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user