mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:48:43 +08:00
fix(gateway): 被动 image_gen namespace 不再强制要求 Responses capability
Fixes #4476
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user