优化 Responses 生图意图复用

This commit is contained in:
jjaw
2026-07-19 18:49:08 +08:00
parent d4b9797ff7
commit 8fd6f91b7e
3 changed files with 96 additions and 6 deletions
@@ -124,6 +124,13 @@ func openAICompatibleRequestPlatform(apiKey *service.APIKey) string {
return service.PlatformOpenAI
}
func openAIResponsesRequiredCapability(imageIntent bool, platform string) service.OpenAIEndpointCapability {
if imageIntent && platform == service.PlatformOpenAI {
return service.OpenAIEndpointCapabilityResponses
}
return service.OpenAIEndpointCapabilityChatCompletions
}
func allowOpenAICompatibleMessagesDispatch(apiKey *service.APIKey) bool {
if apiKey == nil || apiKey.Group == nil {
return true
@@ -363,12 +370,9 @@ 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 service.IsExplicitImageGenerationIntent("/v1/responses", reqModel, body) && requestPlatform == service.PlatformOpenAI {
requiredCapability = service.OpenAIEndpointCapabilityResponses
}
// 复用前置权限与并发阶段在未修改 body 上确认的显式生图意图,避免大 tools 请求重复扫描。
// 该判断已排除 Codex 被动 image_gen namespace,避免 CC-only 账号被误过滤(#4476)。
requiredCapability := openAIResponsesRequiredCapability(imageIntent, requestPlatform)
for {
// Streaming Forward intentionally detaches the upstream request so usage can
@@ -138,6 +138,39 @@ func TestOpenAIForwardSucceededForScheduling(t *testing.T) {
}))
}
func TestOpenAIResponsesRequiredCapability(t *testing.T) {
tests := []struct {
name string
imageIntent bool
platform string
want service.OpenAIEndpointCapability
}{
{
name: "OpenAI explicit image intent requires Responses",
imageIntent: true,
platform: service.PlatformOpenAI,
want: service.OpenAIEndpointCapabilityResponses,
},
{
name: "Grok explicit image intent keeps chat capability",
imageIntent: true,
platform: service.PlatformGrok,
want: service.OpenAIEndpointCapabilityChatCompletions,
},
{
name: "non-image intent keeps chat capability",
platform: service.PlatformOpenAI,
want: service.OpenAIEndpointCapabilityChatCompletions,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, openAIResponsesRequiredCapability(tt.imageIntent, tt.platform))
})
}
}
func TestResolveOpenAIMessagesMetadataSession_DoesNotDerivePromptCacheKey(t *testing.T) {
body := []byte(`{"model":"claude-sonnet-4-5","metadata":{"user_id":"claude-code-session"},"messages":[{"role":"user","content":"hello"}]}`)
@@ -0,0 +1,53 @@
package handler
import (
"strings"
"testing"
"github.com/Wei-Shaw/sub2api/internal/service"
)
var openAIResponsesImageIntentRoutingBenchmarkSink service.OpenAIEndpointCapability
func BenchmarkOpenAIResponsesImageIntentRouting_LargeToolsBody(b *testing.B) {
body := buildLargeOpenAIResponsesToolsBody(32 << 20)
if service.IsExplicitImageGenerationIntent("/v1/responses", "gpt-5.4", body) {
b.Fatal("large tools body must not have explicit image intent")
}
platform := service.PlatformOpenAI
b.Run("reuse_once", func(b *testing.B) {
b.SetBytes(int64(len(body)))
b.ReportAllocs()
for range b.N {
imageIntent := service.IsExplicitImageGenerationIntent("/v1/responses", "gpt-5.4", body)
openAIResponsesImageIntentRoutingBenchmarkSink = openAIResponsesRequiredCapability(imageIntent, platform)
}
})
b.Run("rescan_twice", func(b *testing.B) {
b.SetBytes(int64(len(body)))
b.ReportAllocs()
for range b.N {
imageIntent := service.IsExplicitImageGenerationIntent("/v1/responses", "gpt-5.4", body)
// 对照优化前路径:路由阶段会再次扫描同一份未修改的 body。
requiredCapability := service.OpenAIEndpointCapabilityChatCompletions
if service.IsExplicitImageGenerationIntent("/v1/responses", "gpt-5.4", body) && platform == service.PlatformOpenAI {
requiredCapability = service.OpenAIEndpointCapabilityResponses
}
if imageIntent && requiredCapability != service.OpenAIEndpointCapabilityResponses {
b.Fatal("explicit image intent must require Responses")
}
openAIResponsesImageIntentRoutingBenchmarkSink = requiredCapability
}
})
}
func buildLargeOpenAIResponsesToolsBody(targetBytes int) []byte {
var builder strings.Builder
builder.Grow(targetBytes + 256)
_, _ = builder.WriteString(`{"model":"gpt-5.4","tools":[{"type":"function","name":"search","description":"`)
_, _ = builder.WriteString(strings.Repeat("x", targetBytes))
_, _ = builder.WriteString(`"}],"tool_choice":"auto","input":"write code"}`)
return []byte(builder.String())
}