From e375623abf3b54d95852bef467f30f615250f60b Mon Sep 17 00:00:00 2001 From: li Date: Fri, 17 Jul 2026 15:07:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(gateway):=20=E8=A2=AB=E5=8A=A8=20image=5Fge?= =?UTF-8?q?n=20namespace=20=E4=B8=8D=E5=86=8D=E5=BC=BA=E5=88=B6=E8=A6=81?= =?UTF-8?q?=E6=B1=82=20Responses=20capability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #4476 --- .../handler/openai_gateway_handler.go | 7 ++- .../service/image_generation_intent.go | 35 +++++++++++ .../image_generation_intent_explicit_test.go | 63 +++++++++++++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 backend/internal/service/image_generation_intent_explicit_test.go diff --git a/backend/internal/handler/openai_gateway_handler.go b/backend/internal/handler/openai_gateway_handler.go index ea23c0cf31..a31011b8ec 100644 --- a/backend/internal/handler/openai_gateway_handler.go +++ b/backend/internal/handler/openai_gateway_handler.go @@ -353,8 +353,10 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) { // 生图意图的 /v1/responses 请求必须调度到确实支持 Responses API 的账号,否则 // 会在 forward 阶段被静默降级为无法生图的 Chat Completions 直转(#4417)。 // 仅对 OpenAI 平台生效:Grok 生图走独立的 forwardGrokResponses 路径,不应被过滤。 + // 使用 IsExplicitImageGenerationIntent 排除被动 image_gen namespace 声明, + // 避免 Codex 的被动工具目录使 CC-only 账号被误过滤(#4476)。 requiredCapability := service.OpenAIEndpointCapabilityChatCompletions - if imageIntent && requestPlatform == service.PlatformOpenAI { + if service.IsExplicitImageGenerationIntent("/v1/responses", reqModel, body) && requestPlatform == service.PlatformOpenAI { requiredCapability = service.OpenAIEndpointCapabilityResponses } @@ -1609,8 +1611,9 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) { // 与 HTTP Responses 路径保持一致:生图意图请求要求账号支持 Responses API(#4417)。 // WSv2 传输本身已隐含 Responses 支持,此处为防御性对齐。 + // 使用 IsExplicitImageGenerationIntent 排除被动 namespace 声明(#4476)。 requiredCapability := service.OpenAIEndpointCapabilityChatCompletions - if imageIntent && requestPlatform == service.PlatformOpenAI { + if service.IsExplicitImageGenerationIntent("/v1/responses", reqModel, firstMessage) && requestPlatform == service.PlatformOpenAI { requiredCapability = service.OpenAIEndpointCapabilityResponses } diff --git a/backend/internal/service/image_generation_intent.go b/backend/internal/service/image_generation_intent.go index 9351fcd682..30da6ab6ba 100644 --- a/backend/internal/service/image_generation_intent.go +++ b/backend/internal/service/image_generation_intent.go @@ -79,6 +79,41 @@ func IsImageGenerationIntent(endpoint string, requestedModel string, body []byte return imageIntent } +// IsExplicitImageGenerationIntent 仅检测原生 image_generation 工具、图片模型和显式 tool_choice, +// 不检测被动的 image_gen namespace 声明。用于 capability 路由决策——被动 namespace 不应 +// 强制要求原生 Responses 能力,否则 Chat Completions-only 账号会被误过滤(#4476)。 +func IsExplicitImageGenerationIntent(endpoint string, requestedModel string, body []byte) bool { + if IsImageGenerationEndpoint(endpoint) || isOpenAIImageGenerationModel(requestedModel) { + return true + } + if len(body) == 0 || !gjson.ValidBytes(body) { + return false + } + var modelSeen, toolsSeen, toolChoiceSeen bool + imageIntent := false + parseRawJSONView(body).ForEach(func(key, value gjson.Result) bool { + switch key.Str { + case "model": + if !modelSeen { + modelSeen = true + imageIntent = isOpenAIImageGenerationModel(strings.TrimSpace(value.String())) + } + case "tools": + if !toolsSeen { + toolsSeen = true + imageIntent = openAIJSONToolsContainNativeImageGeneration(value) + } + case "tool_choice": + if !toolChoiceSeen { + toolChoiceSeen = true + imageIntent = openAIJSONToolChoiceSelectsExplicitImageGeneration(value) + } + } + return !imageIntent && (!modelSeen || !toolsSeen || !toolChoiceSeen) + }) + return imageIntent +} + // IsImageGenerationIntentForPlatform applies platform-specific intent rules. // // Codex advertises the image_gen namespace on ordinary Responses requests so diff --git a/backend/internal/service/image_generation_intent_explicit_test.go b/backend/internal/service/image_generation_intent_explicit_test.go new file mode 100644 index 0000000000..baff2d93f2 --- /dev/null +++ b/backend/internal/service/image_generation_intent_explicit_test.go @@ -0,0 +1,63 @@ +package service + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsExplicitImageGenerationIntent_IgnoresPassiveNamespace(t *testing.T) { + body := []byte(`{ + "model": "gpt-5.5", + "input": [{"type":"message","role":"user","content":"hello"}], + "tools": [ + {"type":"function","name":"Read"}, + {"type":"namespace","name":"image_gen","tools":[{"type":"function","name":"imagegen"}]} + ], + "tool_choice": "auto" + }`) + + assert.False(t, IsExplicitImageGenerationIntent("/v1/responses", "gpt-5.5", body), + "passive image_gen namespace should NOT be explicit image intent") + + assert.True(t, IsImageGenerationIntent("/v1/responses", "gpt-5.5", body), + "passive image_gen namespace SHOULD be general image intent (for permission check)") +} + +func TestIsExplicitImageGenerationIntent_DetectsNativeTool(t *testing.T) { + body := []byte(`{ + "model": "gpt-5.5", + "tools": [{"type":"image_generation","model":"gpt-image-2"}], + "tool_choice": "auto" + }`) + + assert.True(t, IsExplicitImageGenerationIntent("/v1/responses", "gpt-5.5", body), + "native image_generation tool IS explicit intent") +} + +func TestIsExplicitImageGenerationIntent_DetectsImageModel(t *testing.T) { + assert.True(t, IsExplicitImageGenerationIntent("/v1/responses", "gpt-image-2", nil), + "image model IS explicit intent") +} + +func TestIsExplicitImageGenerationIntent_DetectsImageEndpoint(t *testing.T) { + assert.True(t, IsExplicitImageGenerationIntent("/v1/images/generations", "gpt-5.5", nil), + "image endpoint IS explicit intent") +} + +func TestIsExplicitImageGenerationIntent_DetectsExplicitToolChoice(t *testing.T) { + body := []byte(`{"model":"gpt-5.5","tools":[{"type":"function","name":"Read"}],"tool_choice":"image_generation"}`) + assert.True(t, IsExplicitImageGenerationIntent("/v1/responses", "gpt-5.5", body), + "explicit tool_choice selecting image_generation IS explicit intent") +} + +func TestIsExplicitImageGenerationIntent_PlainTextRequest(t *testing.T) { + body := []byte(`{ + "model": "gpt-5.5", + "input": "hello", + "tools": [{"type":"function","name":"Read"}] + }`) + + assert.False(t, IsExplicitImageGenerationIntent("/v1/responses", "gpt-5.5", body), + "plain text request should NOT be explicit image intent") +}