From 36d1b18e1060ce59b6ef0274989ca705316ba9df Mon Sep 17 00:00:00 2001 From: wangbo Date: Fri, 24 Jul 2026 09:28:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(images):=20=E8=A7=84=E8=8C=83=20OpenAI=20?= =?UTF-8?q?=E5=9B=BE=E5=83=8F=E5=B0=BA=E5=AF=B8=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/internal/clients/clients_test.go | 68 +++++++++++++++++++++-- apps/api/internal/clients/openai.go | 19 +++++++ 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/apps/api/internal/clients/clients_test.go b/apps/api/internal/clients/clients_test.go index 1e4c947..937e0f5 100644 --- a/apps/api/internal/clients/clients_test.go +++ b/apps/api/internal/clients/clients_test.go @@ -370,10 +370,14 @@ func TestOpenAIClientImageEditUsesMultipartWithMultipleImages(t *testing.T) { ModelType: "image_edit", Model: "gpt-image-2", Body: map[string]any{ - "model": "gpt-image-2", - "prompt": "combine the references", - "size": "1024x1536", - "quality": "medium", + "model": "gpt-image-2", + "prompt": "combine the references", + "size": "1024x1536", + "quality": "medium", + "aspect_ratio": "2:3", + "resolution": "2K", + "width": 1024, + "height": 1536, "images": []any{ "data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte("image one")), "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString([]byte("image two")), @@ -406,6 +410,62 @@ func TestOpenAIClientImageEditUsesMultipartWithMultipleImages(t *testing.T) { if _, ok := receivedFields["_metadata"]; ok { t.Fatalf("internal metadata must not be forwarded: %+v", receivedFields) } + for _, field := range []string{"aspect_ratio", "resolution", "width", "height"} { + if _, ok := receivedFields[field]; ok { + t.Fatalf("generic image geometry field %q must not be forwarded: %+v", field, receivedFields) + } + } +} + +func TestOpenAIClientImageGenerationUsesSizeWithoutGenericGeometryFields(t *testing.T) { + var received map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&received); err != nil { + t.Fatalf("decode image generation body: %v", err) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": []any{map[string]any{"b64_json": "aW1hZ2U="}}, + }) + })) + defer server.Close() + + _, err := (OpenAIClient{HTTPClient: server.Client()}).Run(context.Background(), Request{ + Kind: "images.generations", + ModelType: "image_generate", + Model: "gpt-image-2", + Body: map[string]any{ + "model": "gpt-image-2", + "prompt": "draw", + "size": "2048x1152", + "quality": "high", + "aspect_ratio": "16:9", + "aspectRatio": "16:9", + "resolution": "2K", + "width": 2048, + "height": 1152, + }, + Candidate: store.RuntimeModelCandidate{ + BaseURL: server.URL, + Provider: "openai", + ProviderModelName: "gpt-image-2-vip", + ModelType: "image_generate", + Credentials: map[string]any{"apiKey": "openai-key"}, + }, + }) + if err != nil { + t.Fatalf("run OpenAI image generation: %v", err) + } + if received["model"] != "gpt-image-2-vip" || + received["prompt"] != "draw" || + received["size"] != "2048x1152" || + received["quality"] != "high" { + t.Fatalf("unexpected image generation fields: %+v", received) + } + for _, field := range []string{"aspect_ratio", "aspectRatio", "resolution", "width", "height"} { + if _, ok := received[field]; ok { + t.Fatalf("generic image geometry field %q must not be forwarded: %+v", field, received) + } + } } func TestOpenAIClientChatReasoningParamsByProvider(t *testing.T) { diff --git a/apps/api/internal/clients/openai.go b/apps/api/internal/clients/openai.go index 3c97272..9c0ff52 100644 --- a/apps/api/internal/clients/openai.go +++ b/apps/api/internal/clients/openai.go @@ -65,6 +65,7 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error } } body["model"] = upstreamModelName(request.Candidate) + normalizeOpenAIImageRequestBody(endpointKind, body) stream := openAIEndpointSupportsStream(endpointKind) && (request.Stream || boolValue(body, "stream")) ensureOpenAIStreamUsage(body, endpointKind, stream) raw, contentType, err := openAIRequestPayload(endpointKind, body, request.Candidate) @@ -171,6 +172,24 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error }, nil } +func normalizeOpenAIImageRequestBody(endpointKind string, body map[string]any) { + if endpointKind != "images.generations" && endpointKind != "images.edits" { + return + } + // OpenAI image endpoints express output geometry through size. The Gateway + // keeps the generic fields for capability validation and billing, but they + // are not valid OpenAI wire parameters. + for _, key := range []string{ + "aspect_ratio", + "aspectRatio", + "resolution", + "width", + "height", + } { + delete(body, key) + } +} + func openAIRequestPayload(endpointKind string, body map[string]any, candidate store.RuntimeModelCandidate) ([]byte, string, error) { if endpointKind != "images.edits" { raw, err := json.Marshal(body)