Merge pull request #4311 from jianjianai/codex/perf-openai-forwarding-final

perf(openai): 优化 Responses 图片意图判定与透传流式刷新
This commit is contained in:
Wesley Liddick
2026-07-15 10:12:05 +08:00
committed by GitHub
14 changed files with 1052 additions and 18 deletions
@@ -56,6 +56,14 @@ func openAIModelMappedBody(body []byte, mapped bool, mappedModel string, replace
return replace(body, mappedModel)
}
func seedOpenAIForwardImageIntentHint(c *gin.Context, channelMapped bool, imageIntent bool) {
if channelMapped {
// 渠道映射改变了规范请求,保持 unknown,由 Forward 按映射后的 model/body 初始化。
return
}
service.SetOpenAIImageIntentHint(c, imageIntent)
}
func newOpenAIModelMappedBodyCache(body []byte, replace openAIModelBodyReplaceFunc) func(bool, string) []byte {
replacedBodies := make(map[string][]byte)
return func(mapped bool, mappedModel string) []byte {
@@ -282,6 +290,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
// 解析渠道级模型映射
channelMapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, reqModel)
forwardBody := openAIModelMappedBody(body, channelMapping.Mapped, channelMapping.MappedModel, h.gatewayService.ReplaceModelInBody)
seedOpenAIForwardImageIntentHint(c, channelMapping.Mapped, imageIntent)
// 提前校验 function_call_output 是否具备可关联上下文,避免上游 400。
if !h.validateFunctionCallOutputRequest(c, body, reqLog) {
@@ -0,0 +1,44 @@
package handler
import (
"testing"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func TestSeedOpenAIForwardImageIntentHint(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
channelMapped bool
imageIntent bool
wantHint bool
}{
{name: "seed true", imageIntent: true, wantHint: true},
{name: "seed false", imageIntent: false, wantHint: true},
{name: "mapped body stays unknown", channelMapped: true, imageIntent: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &gin.Context{}
service.SetOpenAIClientTransport(c, service.OpenAIClientTransportHTTP)
seedOpenAIForwardImageIntentHint(c, tt.channelMapped, tt.imageIntent)
var hintValues []bool
for _, value := range c.Keys {
if hint, ok := value.(bool); ok {
hintValues = append(hintValues, hint)
}
}
if !tt.wantHint {
require.Empty(t, hintValues)
return
}
require.Equal(t, []bool{tt.imageIntent}, hintValues)
})
}
}
@@ -47,16 +47,36 @@ func IsImageGenerationIntent(endpoint string, requestedModel string, body []byte
if len(body) == 0 || !gjson.ValidBytes(body) {
return false
}
if model := strings.TrimSpace(gjson.GetBytes(body, "model").String()); isOpenAIImageGenerationModel(model) {
return true
}
if openAIJSONToolsContainImageGeneration(gjson.GetBytes(body, "tools")) {
return true
}
if openAIJSONInputContainsImageGenTool(gjson.GetBytes(body, "input")) {
return true
}
return openAIJSONToolChoiceSelectsImageGeneration(gjson.GetBytes(body, "tool_choice"))
var modelSeen, toolsSeen, inputSeen, toolChoiceSeen bool
imageIntent := false
parseRawJSONView(body).ForEach(func(key, value gjson.Result) bool {
// GetBytes returns the first duplicate key; retain that behavior while walking the root once.
switch key.Str {
case "model":
if !modelSeen {
modelSeen = true
imageIntent = isOpenAIImageGenerationModel(strings.TrimSpace(value.String()))
}
case "tools":
if !toolsSeen {
toolsSeen = true
imageIntent = openAIJSONToolsContainImageGeneration(value)
}
case "input":
if !inputSeen {
inputSeen = true
imageIntent = openAIJSONInputContainsImageGenTool(value)
}
case "tool_choice":
if !toolChoiceSeen {
toolChoiceSeen = true
imageIntent = openAIJSONToolChoiceSelectsImageGeneration(value)
}
}
return !imageIntent && (!modelSeen || !toolsSeen || !inputSeen || !toolChoiceSeen)
})
return imageIntent
}
// IsImageGenerationIntentMap is the map-backed variant used after service-side request mutation.
@@ -0,0 +1,59 @@
package service
import (
"strings"
"testing"
)
var imageGenerationIntentBenchmarkResult bool
func BenchmarkIsImageGenerationIntent(b *testing.B) {
largeInput := strings.Repeat("x", 1<<20)
benchmarks := []struct {
name string
body []byte
want bool
}{
{
name: "1MiBInputNoImage",
body: []byte(`{"model":"gpt-5.5","tools":[],"input":"` + largeInput + `","tool_choice":"auto"}`),
want: false,
},
{
name: "1MiBInputLeadingImageTool",
body: []byte(`{"model":"gpt-5.5","tools":[{"type":"image_generation"}],"input":"` + largeInput + `"}`),
want: true,
},
{
name: "1MiBInputTrailingToolChoice",
body: []byte(`{"model":"gpt-5.5","tools":[],"input":"` + largeInput + `","tool_choice":{"type":"image_generation"}}`),
want: true,
},
{
name: "Invalid1MiBJSON",
body: []byte(`{"model":"gpt-5.5","input":"` + largeInput),
want: false,
},
{
name: "DuplicateKeysFirstWins",
body: []byte(`{"model":"gpt-5.5","model":"gpt-image-2","tools":[],"tools":[{"type":"image_generation"}],"input":"` + largeInput + `","tool_choice":"auto","tool_choice":{"type":"image_generation"}}`),
want: false,
},
}
for _, benchmark := range benchmarks {
b.Run(benchmark.name, func(b *testing.B) {
if got := IsImageGenerationIntent("/v1/responses", "gpt-5.5", benchmark.body); got != benchmark.want {
b.Fatalf("IsImageGenerationIntent() = %v, want %v", got, benchmark.want)
}
b.ReportAllocs()
b.SetBytes(int64(len(benchmark.body)))
b.ResetTimer()
var result bool
for i := 0; i < b.N; i++ {
result = IsImageGenerationIntent("/v1/responses", "gpt-5.5", benchmark.body)
}
imageGenerationIntentBenchmarkResult = result
})
}
}
@@ -1,6 +1,7 @@
package service
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
@@ -106,6 +107,77 @@ func TestIsImageGenerationIntent(t *testing.T) {
}
}
func TestIsImageGenerationIntentJSONSemantics(t *testing.T) {
largeInput := strings.Repeat("x", 1<<20)
tests := []struct {
name string
endpoint string
body []byte
want bool
}{
{
name: "chat body image model",
endpoint: "/v1/chat/completions",
body: []byte(`{"model":"gpt-image-2"}`),
want: true,
},
{
name: "large responses input with trailing namespace tool choice",
endpoint: "/v1/responses",
body: []byte(`{"model":"gpt-5.5","input":"` + largeInput + `","tool_choice":{"type":"namespace","name":"image_gen"}}`),
want: true,
},
{
name: "invalid json with image tool",
endpoint: "/v1/responses",
body: []byte(`{"tools":[{"type":"image_generation"}]`),
want: false,
},
{
name: "duplicate model uses first value",
endpoint: "/v1/responses",
body: []byte(`{"model":"gpt-5.5","model":"gpt-image-2"}`),
want: false,
},
{
name: "duplicate null model still uses first value",
endpoint: "/v1/responses",
body: []byte(`{"model":null,"model":"gpt-image-2"}`),
want: false,
},
{
name: "duplicate tools uses first value",
endpoint: "/v1/responses",
body: []byte(`{"tools":[],"tools":[{"type":"image_generation"}]}`),
want: false,
},
{
name: "duplicate input uses first value",
endpoint: "/v1/responses",
body: []byte(`{"input":[],"input":[{"type":"additional_tools","tools":[{"type":"namespace","name":"image_gen"}]}]}`),
want: false,
},
{
name: "duplicate tool choice uses first value",
endpoint: "/v1/responses",
body: []byte(`{"tool_choice":"required","tool_choice":{"type":"image_generation"}}`),
want: false,
},
{
name: "escaped top level key",
endpoint: "/v1/responses",
body: []byte(`{"tool_\u0063hoice":{"type":"image_generation"}}`),
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, IsImageGenerationIntent(tt.endpoint, "gpt-5.5", tt.body))
})
}
}
func TestIsImageGenerationIntentMap_NamespaceImageGen(t *testing.T) {
tests := []struct {
name string
@@ -20,6 +20,8 @@ import (
// Forward forwards request to OpenAI API
func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, account *Account, body []byte) (*OpenAIForwardResult, error) {
startTime := time.Now()
// 固定渠道映射后的请求级 canonical body;账号 normalize/strip 不得改写跨 failover hint。
canonicalImageIntentBody := body
restrictionResult := s.detectCodexClientRestriction(c, account, body)
apiKeyID := getAPIKeyIDFromContext(c)
@@ -107,6 +109,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
return nil, errors.New("openai ws v1 is temporarily unsupported; use ws v2")
}
if passthroughEnabled {
attemptImageIntentInvalidated := false
if isCodexCLI && codexImageGenerationExplicitToolPolicy == codexImageGenerationExplicitToolPolicyStrip {
strippedBody, changed, stripErr := stripOpenAIImageGenerationToolsFromRawPayload(body)
if stripErr != nil {
@@ -115,6 +118,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
if changed {
body = strippedBody
originalBody = strippedBody
attemptImageIntentInvalidated = true
logger.LegacyPrintf("service.openai_gateway", "[OpenAI] Stripped /responses image_generation tool for Codex client by account policy")
}
}
@@ -123,7 +127,18 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
reasoningEffort := extractOpenAIReasoningEffortFromBody(body, mappedModel)
// 国产模型默认 effort 补充:也要用 mappedModel 判定是否是 passback-required 上游。
reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, mappedModel)
return s.forwardOpenAIPassthrough(ctx, c, account, originalBody, reqModel, reasoningEffort, reqStream, startTime)
return s.forwardOpenAIPassthrough(
ctx,
c,
account,
originalBody,
canonicalImageIntentBody,
reqModel,
attemptImageIntentInvalidated,
reasoningEffort,
reqStream,
startTime,
)
}
bodyModified := false
@@ -188,6 +203,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
codexImageGenerationExplicitToolPolicy != codexImageGenerationExplicitToolPolicyStrip &&
s.isCodexImageGenerationBridgeEnabled(ctx, account, apiKey)
var imageIntent bool
canonicalImageIntent := resolveOpenAIImageIntentHint(c, reqModel, canonicalImageIntentBody, IsImageGenerationIntent)
if isCodexCLI && codexImageGenerationExplicitToolPolicy == codexImageGenerationExplicitToolPolicyStrip {
decoded, decodeErr := ensureReqBody()
if decodeErr != nil {
@@ -199,7 +215,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
}
imageIntent = IsImageGenerationIntentMap(openAIResponsesEndpoint, reqModel, decoded)
} else {
imageIntent = IsImageGenerationIntent(openAIResponsesEndpoint, reqModel, body)
imageIntent = canonicalImageIntent
}
if imageIntent && !imageGenerationAllowed {
MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalFeatureGate)
@@ -1,8 +1,6 @@
package service
// 本文件由 openai_gateway_service.go 纯移动拆分而来:/v1/responses 直通
// (passthrough)转发路径及其流式/非流式响应处理与错误处理。仅做代码搬迁,
// 无任何行为变更。
// 本文件承载 /v1/responses 透传转发及其流式、非流式响应与错误处理。
import (
"bufio"
@@ -31,7 +29,9 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough(
c *gin.Context,
account *Account,
body []byte,
canonicalImageIntentBody []byte,
reqModel string,
attemptImageIntentInvalidated bool,
reasoningEffort *string,
reqStream bool,
startTime time.Time,
@@ -46,6 +46,7 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough(
}
body = nextBody
upstreamPassthroughModel = compactMappedModel
attemptImageIntentInvalidated = true
}
}
@@ -102,7 +103,17 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough(
body = updatedBody
apiKey := getAPIKeyFromContext(c)
if IsImageGenerationIntent(openAIResponsesEndpoint, reqModel, body) && !GroupAllowsImageGeneration(apiKeyGroup(apiKey)) {
// 同一 attempt 的最终 model/body 只判定一次,权限检查与后续图片状态设置共用该结果。
imageIntent := resolveOpenAIPassthroughImageIntent(
c,
reqModel,
canonicalImageIntentBody,
policyModel,
body,
attemptImageIntentInvalidated,
IsImageGenerationIntent,
)
if imageIntent && !GroupAllowsImageGeneration(apiKeyGroup(apiKey)) {
MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalFeatureGate)
c.JSON(http.StatusForbidden, gin.H{
"error": gin.H{
@@ -115,7 +126,7 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough(
imageBillingModel := ""
imageSizeTier := ""
imageInputSize := ""
if IsImageGenerationIntent(openAIResponsesEndpoint, reqModel, body) {
if imageIntent {
var imageCfgErr error
imageCfg, imageCfgErr := resolveOpenAIResponsesImageBillingConfigDetailedFromBody(body, reqModel)
if imageCfgErr != nil {
@@ -950,7 +961,18 @@ func (s *OpenAIGatewayService) handleStreamingResponsePassthrough(
failedMessage := ""
clientOutputStarted := false
upstreamRequestID := strings.TrimSpace(resp.Header.Get("x-request-id"))
// pendingLines 在首个可见输出前保留前导事件,确保无输出失败仍可安全 failover。
pendingLines := make([]string, 0, 8)
// flushPending 表示已写入但未到 SSE 空行边界的脏状态;defer 兜底函数退出前的残留,断连后不再 Flush。
flushPending := false
flushPendingOutput := func() {
if clientDisconnected || !flushPending {
return
}
flusher.Flush()
flushPending = false
}
defer flushPendingOutput()
writePendingLines := func() bool {
for _, pending := range pendingLines {
if _, err := fmt.Fprintln(w, pending); err != nil {
@@ -1101,7 +1123,10 @@ func (s *OpenAIGatewayService) handleStreamingResponsePassthrough(
logger.LegacyPrintf("service.openai_gateway", "[OpenAI passthrough] Client disconnected during streaming, continue draining upstream for usage: account=%d", account.ID)
} else {
clientOutputStarted = true
flusher.Flush()
flushPending = true
if line == "" {
flushPendingOutput()
}
}
}
}
@@ -0,0 +1,284 @@
package service
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
type passthroughFlushTestWriter struct {
gin.ResponseWriter
recorder *httptest.ResponseRecorder
failAfterWrites int
successfulWrites int
failedWrites int
flushBodyLengths []int
}
func (w *passthroughFlushTestWriter) Write(data []byte) (int, error) {
if w.failAfterWrites >= 0 && w.successfulWrites >= w.failAfterWrites {
w.failedWrites++
return 0, errors.New("client disconnected")
}
n, err := w.ResponseWriter.Write(data)
if err == nil {
w.successfulWrites++
}
return n, err
}
func (w *passthroughFlushTestWriter) WriteString(data string) (int, error) {
return w.Write([]byte(data))
}
func (w *passthroughFlushTestWriter) Flush() {
w.ResponseWriter.Flush()
w.flushBodyLengths = append(w.flushBodyLengths, w.recorder.Body.Len())
}
type passthroughFlushTestErrorBody struct {
payload []byte
err error
sent bool
}
func (r *passthroughFlushTestErrorBody) Read(p []byte) (int, error) {
if !r.sent {
r.sent = true
return copy(p, r.payload), nil
}
return 0, r.err
}
func (r *passthroughFlushTestErrorBody) Close() error { return nil }
func runPassthroughFlushTest(
t *testing.T,
body io.ReadCloser,
failAfterWrites int,
setups ...func(*gin.Context),
) (*openaiStreamingResultPassthrough, *httptest.ResponseRecorder, *passthroughFlushTestWriter, error) {
t.Helper()
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
writer := &passthroughFlushTestWriter{
ResponseWriter: c.Writer,
recorder: recorder,
failAfterWrites: failAfterWrites,
}
c.Writer = writer
for _, setup := range setups {
setup(c)
}
svc := &OpenAIGatewayService{cfg: &config.Config{
Gateway: config.GatewayConfig{MaxLineSize: defaultMaxLineSize},
}}
resp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
Body: body,
}
result, err := svc.handleStreamingResponsePassthrough(
context.Background(),
resp,
c,
&Account{ID: 1, Platform: PlatformOpenAI, Name: "flush-test"},
time.Now(),
"",
"",
)
return result, recorder, writer, err
}
func TestOpenAIStreamingPassthroughFlushesAtCompleteEventBoundaries(t *testing.T) {
firstEvent := "event: response.output_text.delta\n" +
"id: event-1\n" +
`data: {"type":"response.output_text.delta","delta":"hello"}` + "\n\n"
heartbeat := ": keepalive\n\n"
terminalEvent := "event: response.completed\n" +
`data: {"type":"response.completed","response":{"id":"resp_flush","usage":{"input_tokens":3,"output_tokens":2,"total_tokens":5}}}` + "\n\n"
upstream := firstEvent + heartbeat + terminalEvent
result, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, upstream, recorder.Body.String())
require.Equal(t, []int{
len(firstEvent),
len(firstEvent) + len(heartbeat),
len(upstream),
}, writer.flushBodyLengths)
require.Equal(t, 3, result.usage.InputTokens)
require.Equal(t, 2, result.usage.OutputTokens)
}
func TestOpenAIStreamingPassthroughKeepsPreamblePendingUntilFirstOutputBoundary(t *testing.T) {
preamble := "event: response.created\n" +
`data: {"type":"response.created","response":{"id":"resp_pending"}}` + "\n\n" +
": waiting\n\n"
firstOutput := `data: {"type":"response.output_text.delta","delta":"ready"}` + "\n\n"
terminalEvent := `data: {"type":"response.completed","response":{"id":"resp_pending","usage":{"input_tokens":4,"output_tokens":1,"total_tokens":5}}}` + "\n\n"
upstream := preamble + firstOutput + terminalEvent
_, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
require.NoError(t, err)
require.Equal(t, upstream, recorder.Body.String())
require.Equal(t, []int{
len(preamble) + len(firstOutput),
len(upstream),
}, writer.flushBodyLengths)
}
func TestOpenAIStreamingPassthroughFlushesTerminalEventAtEOFWithoutBlankLine(t *testing.T) {
upstream := "event: response.completed\n" +
`data: {"type":"response.completed","response":{"id":"resp_eof","usage":{"input_tokens":5,"output_tokens":2,"total_tokens":7}}}`
wantBody := upstream + "\n"
result, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, wantBody, recorder.Body.String())
require.Equal(t, []int{len(wantBody)}, writer.flushBodyLengths)
require.Equal(t, 5, result.usage.InputTokens)
require.Equal(t, 2, result.usage.OutputTokens)
}
func TestOpenAIStreamingPassthroughFailedBeforeOutputCanStillFailOverWithoutFlush(t *testing.T) {
upstream := "event: response.created\n" +
`data: {"type":"response.created","response":{"id":"resp_failover"}}` + "\n\n" +
"event: response.failed\n" +
`data: {"type":"response.failed","error":{"code":"server_error","message":"upstream processing failed"}}` + "\n\n"
_, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
require.Error(t, err)
var failoverErr *UpstreamFailoverError
require.ErrorAs(t, err, &failoverErr)
require.Empty(t, recorder.Body.String())
require.Empty(t, writer.flushBodyLengths)
}
func TestOpenAIStreamingPassthroughNonRetryableFailedBeforeOutputFlushesAtBoundary(t *testing.T) {
upstream := "event: response.failed\n" +
`data: {"type":"response.failed","error":{"code":"content_policy","message":"request blocked by policy"},"usage":{"input_tokens":6,"output_tokens":0,"total_tokens":6}}` + "\n\n"
result, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
require.Error(t, err)
var failoverErr *UpstreamFailoverError
require.False(t, errors.As(err, &failoverErr))
require.NotNil(t, result)
require.Equal(t, upstream, recorder.Body.String())
require.Equal(t, []int{len(upstream)}, writer.flushBodyLengths)
require.Equal(t, 6, result.usage.InputTokens)
require.Zero(t, result.usage.OutputTokens)
}
func TestOpenAIStreamingPassthroughFailedAfterOutputFlushesAtBoundaryAndKeepsUsage(t *testing.T) {
firstOutput := `data: {"type":"response.output_text.delta","delta":"partial"}` + "\n\n"
failedEvent := "event: response.failed\n" +
`data: {"type":"response.failed","error":{"code":"server_error","message":"upstream processing failed"},"usage":{"input_tokens":7,"output_tokens":2,"total_tokens":9}}` + "\n\n"
upstream := firstOutput + failedEvent
result, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
require.Error(t, err)
var failoverErr *UpstreamFailoverError
require.False(t, errors.As(err, &failoverErr))
require.NotNil(t, result)
require.Equal(t, upstream, recorder.Body.String())
require.Equal(t, []int{len(firstOutput), len(upstream)}, writer.flushBodyLengths)
require.Equal(t, 7, result.usage.InputTokens)
require.Equal(t, 2, result.usage.OutputTokens)
}
func TestOpenAIStreamingPassthroughClientDisconnectStillDrainsTerminalUsage(t *testing.T) {
firstOutput := `data: {"type":"response.output_text.delta","delta":"partial"}` + "\n\n"
terminalEvent := `data: {"type":"response.completed","response":{"id":"resp_drain","usage":{"input_tokens":11,"output_tokens":4,"total_tokens":15}}}` + "\n\n"
result, recorder, writer, err := runPassthroughFlushTest(
t,
io.NopCloser(strings.NewReader(firstOutput+terminalEvent)),
2,
)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, firstOutput, recorder.Body.String())
require.Equal(t, []int{len(firstOutput)}, writer.flushBodyLengths)
require.Equal(t, 1, writer.failedWrites)
require.Equal(t, 11, result.usage.InputTokens)
require.Equal(t, 4, result.usage.OutputTokens)
}
func TestOpenAIStreamingPassthroughScannerErrorFlushesWrittenResidual(t *testing.T) {
upstream := []byte(`data: {"type":"response.output_text.delta","delta":"partial"}`)
readErr := errors.New("upstream read failed")
_, recorder, writer, err := runPassthroughFlushTest(t, &passthroughFlushTestErrorBody{
payload: upstream,
err: readErr,
}, -1)
require.ErrorIs(t, err, readErr)
wantBody := string(upstream) + "\n"
require.Equal(t, wantBody, recorder.Body.String())
require.Equal(t, []int{len(wantBody)}, writer.flushBodyLengths)
}
func TestOpenAIStreamingPassthroughNamespaceRestoreErrorFlushesWrittenResidualOnce(t *testing.T) {
writtenPrefix := `data: {"type":"response.output_text.delta","delta":"prefix"}` + "\n"
overflowData := `data: {"type":"response.output_text.delta","delta":"not-written","overflow":1e1000}`
_, recorder, writer, err := runPassthroughFlushTest(
t,
io.NopCloser(strings.NewReader(writtenPrefix+overflowData)),
-1,
func(c *gin.Context) {
setOpenAIResponsesNamespaceNames(c, map[string]apicompat.ResponsesNamespaceName{
"collaboration__spawn_agent": {Namespace: "collaboration", Name: "spawn_agent"},
})
},
)
require.ErrorContains(t, err, "restore OpenAI passthrough namespace response")
require.Equal(t, writtenPrefix, recorder.Body.String())
require.Equal(t, []int{len(writtenPrefix)}, writer.flushBodyLengths)
}
func TestOpenAIStreamingPassthroughBlankWriteFailureDoesNotFlushAndStillDrainsUsage(t *testing.T) {
writtenDataLine := `data: {"type":"response.output_text.delta","delta":"partial"}` + "\n"
terminalEvent := `data: {"type":"response.completed","response":{"id":"resp_blank_failure","usage":{"input_tokens":13,"output_tokens":5,"total_tokens":18}}}` + "\n\n"
result, recorder, writer, err := runPassthroughFlushTest(
t,
io.NopCloser(strings.NewReader(writtenDataLine+"\n"+terminalEvent)),
1,
)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, writtenDataLine, recorder.Body.String())
require.Empty(t, writer.flushBodyLengths)
require.Equal(t, 1, writer.successfulWrites)
require.Equal(t, 1, writer.failedWrites)
require.Equal(t, 13, result.usage.InputTokens)
require.Equal(t, 5, result.usage.OutputTokens)
}
@@ -0,0 +1,27 @@
package service
import "testing"
var passthroughImageIntentBenchmarkSink bool
func BenchmarkOpenAIPassthroughImageIntentReuse_LargeBody(b *testing.B) {
body := buildLargeOpenAIResponsesImageToolBody(32 << 20)
b.Run("Once", func(b *testing.B) {
b.SetBytes(int64(len(body)))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
passthroughImageIntentBenchmarkSink = IsImageGenerationIntent(openAIResponsesEndpoint, "gpt-5.4", body)
}
})
b.Run("Twice", func(b *testing.B) {
b.SetBytes(int64(len(body)))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
permissionIntent := IsImageGenerationIntent(openAIResponsesEndpoint, "gpt-5.4", body)
billingIntent := IsImageGenerationIntent(openAIResponsesEndpoint, "gpt-5.4", body)
passthroughImageIntentBenchmarkSink = permissionIntent && billingIntent
}
})
}
@@ -0,0 +1,59 @@
package service
import (
"context"
"io"
"net/http"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestOpenAIGatewayService_APIKeyPassthrough_ImageIntentPreservesGateAndBilling(t *testing.T) {
gin.SetMode(gin.TestMode)
body := []byte(`{"model":"gpt-5.4","stream":false,"tools":[{"type":"image_generation","model":"gpt-image-2","size":"2048x1152"}],"input":"draw"}`)
t.Run("disabled group rejects before upstream", func(t *testing.T) {
upstream := &httpUpstreamRecorder{}
svc := newOpenAIImageGenerationControlTestService(upstream)
c, recorder := newOpenAIImageGenerationControlTestContext(false, "curl/8.0")
account := newOpenAIImageGenerationControlTestAccount()
account.Extra = map[string]any{"openai_passthrough": true}
result, err := svc.Forward(context.Background(), c, account, body)
require.Error(t, err)
require.Nil(t, result)
require.Equal(t, http.StatusForbidden, recorder.Code)
require.Equal(t, "permission_error", gjson.GetBytes(recorder.Body.Bytes(), "error.type").String())
require.Nil(t, upstream.lastReq)
})
t.Run("allowed group keeps image billing", func(t *testing.T) {
upstream := &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(
`{"output":[{"id":"ig_1","type":"image_generation_call","result":"final-image","size":"2048x1152"}],"usage":{"input_tokens":1,"output_tokens":2}}`,
)),
}}
svc := newOpenAIImageGenerationControlTestService(upstream)
c, _ := newOpenAIImageGenerationControlTestContext(true, "curl/8.0")
account := newOpenAIImageGenerationControlTestAccount()
account.Extra = map[string]any{"openai_passthrough": true}
result, err := svc.Forward(context.Background(), c, account, body)
require.NoError(t, err)
require.NotNil(t, result)
require.NotNil(t, upstream.lastReq)
require.Equal(t, body, upstream.lastBody)
require.Equal(t, 1, result.ImageCount)
require.Equal(t, "gpt-image-2", result.BillingModel)
require.Equal(t, "2K", result.ImageSize)
require.Equal(t, "2048x1152", result.ImageInputSize)
})
}
@@ -221,6 +221,24 @@ func TestOpenAIGatewayService_Forward_MappedImageModelUsesImageGate(t *testing.T
require.Nil(t, result)
require.Nil(t, upstream.lastReq)
require.Equal(t, http.StatusForbidden, rec.Code)
cached, known := getOpenAIImageIntentHint(c)
require.True(t, known)
require.False(t, cached)
textAccount := *account
textAccount.ID = 4
textAccount.Credentials = map[string]any{
"api_key": "sk-test",
"base_url": "https://example.com",
}
result, err = svc.Forward(context.Background(), c, &textAccount, body)
require.NoError(t, err)
require.NotNil(t, result)
require.NotNil(t, upstream.lastReq)
require.Len(t, upstream.bodies, 1)
cached, known = getOpenAIImageIntentHint(c)
require.True(t, known)
require.False(t, cached)
}
func TestOpenAIGatewayService_Forward_TextResponsesSetsBillingModelToMappedModel(t *testing.T) {
@@ -242,6 +242,7 @@ func TestOpenAIGatewayServiceForward_AccountPolicyStripsImageNamespaceTools(t *t
}
svc := newOpenAIImageGenerationControlTestService(upstream)
c, _ := newOpenAIImageGenerationControlTestContext(false, "codex_cli_rs/0.144.1")
SetOpenAIClientTransport(c, OpenAIClientTransportHTTP)
account := newOpenAIImageGenerationControlTestAccount()
account.Extra = map[string]any{
featureKeyCodexImageGenerationExplicitToolPolicy: codexImageGenerationExplicitToolPolicyStrip,
@@ -274,6 +275,9 @@ func TestOpenAIGatewayServiceForward_AccountPolicyStripsImageNamespaceTools(t *t
require.True(t, gjson.GetBytes(upstream.lastBody, `tools.#(name=="shell")`).Exists())
require.True(t, gjson.GetBytes(upstream.lastBody, `tools.#(name=="code_tools")`).Exists())
require.Equal(t, "write code", gjson.GetBytes(upstream.lastBody, "input.0.content.0.text").String())
cached, known := getOpenAIImageIntentHint(c)
require.True(t, known)
require.True(t, cached)
})
}
}
@@ -0,0 +1,59 @@
package service
import "github.com/gin-gonic/gin"
// 请求级 hint 仅限 HTTP:缺失表示 unknown,false/true 都表示已完成 canonical 判定。
const openAIImageIntentHintContextKey = "openai_image_intent_hint"
type openAIImageIntentClassifier func(endpoint string, requestedModel string, body []byte) bool
// SetOpenAIImageIntentHint 只写入请求级 canonical 判定,不记录 attempt-local 结果。
func SetOpenAIImageIntentHint(c *gin.Context, imageIntent bool) {
if c == nil || GetOpenAIClientTransport(c) != OpenAIClientTransportHTTP {
return
}
c.Set(openAIImageIntentHintContextKey, imageIntent)
}
func getOpenAIImageIntentHint(c *gin.Context) (imageIntent bool, known bool) {
if c == nil || GetOpenAIClientTransport(c) != OpenAIClientTransportHTTP {
return false, false
}
value, ok := c.Get(openAIImageIntentHintContextKey)
if !ok {
return false, false
}
imageIntent, ok = value.(bool)
return imageIntent, ok
}
func resolveOpenAIImageIntentHint(
c *gin.Context,
requestedModel string,
canonicalBody []byte,
classify openAIImageIntentClassifier,
) bool {
if imageIntent, known := getOpenAIImageIntentHint(c); known {
return imageIntent
}
imageIntent := classify(openAIResponsesEndpoint, requestedModel, canonicalBody)
SetOpenAIImageIntentHint(c, imageIntent)
return imageIntent
}
func resolveOpenAIPassthroughImageIntent(
c *gin.Context,
canonicalRequestedModel string,
canonicalBody []byte,
attemptRequestedModel string,
attemptBody []byte,
attemptInvalidated bool,
classify openAIImageIntentClassifier,
) bool {
imageIntent := resolveOpenAIImageIntentHint(c, canonicalRequestedModel, canonicalBody, classify)
if attemptInvalidated {
// strip/compact 改写只重算当前 attempt,不得把变换后的结果写回请求级 canonical hint。
imageIntent = classify(openAIResponsesEndpoint, attemptRequestedModel, attemptBody)
}
return imageIntent
}
@@ -0,0 +1,338 @@
package service
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"sync/atomic"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func newOpenAIImageIntentHintTestContext(transport OpenAIClientTransport) *gin.Context {
c := &gin.Context{}
SetOpenAIClientTransport(c, transport)
return c
}
func countingOpenAIImageIntentClassifier(calls *atomic.Int64) openAIImageIntentClassifier {
return func(endpoint string, requestedModel string, body []byte) bool {
calls.Add(1)
return IsImageGenerationIntent(endpoint, requestedModel, body)
}
}
func TestResolveOpenAIImageIntentHintCachesTrueAndFalse(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
body []byte
want bool
}{
{name: "true", body: []byte(`{"model":"gpt-5.4","tools":[{"type":"image_generation"}]}`), want: true},
{name: "false is known", body: []byte(`{"model":"gpt-5.4","input":"write code"}`), want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := newOpenAIImageIntentHintTestContext(OpenAIClientTransportHTTP)
var calls atomic.Int64
classify := countingOpenAIImageIntentClassifier(&calls)
require.Equal(t, tt.want, resolveOpenAIImageIntentHint(c, "gpt-5.4", tt.body, classify))
require.Equal(t, tt.want, resolveOpenAIImageIntentHint(c, "gpt-5.4", tt.body, classify))
require.Equal(t, int64(1), calls.Load())
cached, known := getOpenAIImageIntentHint(c)
require.True(t, known)
require.Equal(t, tt.want, cached)
})
}
}
func TestResolveOpenAIImageIntentHintUsesHandlerSeed(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, seeded := range []bool{false, true} {
c := newOpenAIImageIntentHintTestContext(OpenAIClientTransportHTTP)
SetOpenAIImageIntentHint(c, seeded)
var calls atomic.Int64
got := resolveOpenAIImageIntentHint(c, "gpt-5.4", []byte(`{"model":"gpt-5.4"}`), countingOpenAIImageIntentClassifier(&calls))
require.Equal(t, seeded, got)
require.Zero(t, calls.Load())
}
}
func TestResolveOpenAIPassthroughImageIntentReusesCanonicalAcrossFailover(t *testing.T) {
gin.SetMode(gin.TestMode)
c := newOpenAIImageIntentHintTestContext(OpenAIClientTransportHTTP)
body := []byte(`{"model":"gpt-5.4","input":"write code"}`)
var calls atomic.Int64
classify := countingOpenAIImageIntentClassifier(&calls)
for range 3 {
require.False(t, resolveOpenAIPassthroughImageIntent(c, "gpt-5.4", body, "gpt-5.4", body, false, classify))
}
require.Equal(t, int64(1), calls.Load())
}
func TestResolveOpenAIPassthroughImageIntentKeepsCompactMappingAttemptLocal(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Run("text to image", func(t *testing.T) {
c := newOpenAIImageIntentHintTestContext(OpenAIClientTransportHTTP)
body := []byte(`{"model":"draw-alias","input":"draw"}`)
compactBody := []byte(`{"model":"gpt-image-2","input":"draw"}`)
var calls atomic.Int64
classify := countingOpenAIImageIntentClassifier(&calls)
require.True(t, resolveOpenAIPassthroughImageIntent(c, "draw-alias", body, "gpt-image-2", compactBody, true, classify))
cached, known := getOpenAIImageIntentHint(c)
require.True(t, known)
require.False(t, cached)
require.False(t, resolveOpenAIPassthroughImageIntent(c, "draw-alias", body, "draw-alias", body, false, classify))
require.Equal(t, int64(2), calls.Load())
cached, known = getOpenAIImageIntentHint(c)
require.True(t, known)
require.False(t, cached)
})
t.Run("image to text", func(t *testing.T) {
c := newOpenAIImageIntentHintTestContext(OpenAIClientTransportHTTP)
body := []byte(`{"model":"gpt-image-2","input":"draw"}`)
compactBody := []byte(`{"model":"gpt-5.4","input":"draw"}`)
var calls atomic.Int64
classify := countingOpenAIImageIntentClassifier(&calls)
require.False(t, resolveOpenAIPassthroughImageIntent(c, "gpt-image-2", body, "gpt-5.4", compactBody, true, classify))
cached, known := getOpenAIImageIntentHint(c)
require.True(t, known)
require.True(t, cached)
require.True(t, resolveOpenAIPassthroughImageIntent(c, "gpt-image-2", body, "gpt-image-2", body, false, classify))
require.Equal(t, int64(2), calls.Load())
})
}
func TestResolveOpenAIPassthroughImageIntentInvalidationDoesNotPolluteCanonical(t *testing.T) {
gin.SetMode(gin.TestMode)
c := newOpenAIImageIntentHintTestContext(OpenAIClientTransportHTTP)
canonicalBody := []byte(`{"model":"gpt-5.4","tools":[{"type":"image_generation"}]}`)
strippedBody := []byte(`{"model":"gpt-5.4","tools":[]}`)
var calls atomic.Int64
classify := countingOpenAIImageIntentClassifier(&calls)
require.False(t, resolveOpenAIPassthroughImageIntent(c, "gpt-5.4", canonicalBody, "gpt-5.4", strippedBody, true, classify))
require.Equal(t, int64(2), calls.Load(), "unknown canonical and invalidated attempt are classified independently")
cached, known := getOpenAIImageIntentHint(c)
require.True(t, known)
require.True(t, cached)
require.True(t, resolveOpenAIPassthroughImageIntent(c, "gpt-5.4", canonicalBody, "gpt-5.4", canonicalBody, false, classify))
require.Equal(t, int64(2), calls.Load())
}
func TestResolveOpenAIPassthroughImageIntentMappedBodyStartsUnknownThenSeedsCanonical(t *testing.T) {
gin.SetMode(gin.TestMode)
c := newOpenAIImageIntentHintTestContext(OpenAIClientTransportHTTP)
canonicalBody := []byte(`{"model":"gpt-image-2","input":"draw"}`)
strippedAttemptBody := []byte(`{"model":"gpt-5.4","input":"draw"}`)
_, known := getOpenAIImageIntentHint(c)
require.False(t, known)
var calls atomic.Int64
classify := countingOpenAIImageIntentClassifier(&calls)
require.False(t, resolveOpenAIPassthroughImageIntent(c, "gpt-image-2", canonicalBody, "gpt-5.4", strippedAttemptBody, true, classify))
require.Equal(t, int64(2), calls.Load())
cached, known := getOpenAIImageIntentHint(c)
require.True(t, known)
require.True(t, cached)
}
func TestResolveOpenAIPassthroughImageIntentReusesAcrossInvariantMutations(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
canonicalBody []byte
attemptBody []byte
want bool
}{
{
name: "oauth sanitize fast policy and reasoning",
canonicalBody: []byte(`{"model":"gpt-5.4","input":[{"type":"input_image","image_url":"data:image/png;base64,"}],"service_tier":"fast","reasoning":{"effort":"minimal"}}`),
attemptBody: []byte(`{"model":"gpt-5.4","input":[],"service_tier":"priority","reasoning":{"effort":"none"},"store":false,"stream":true}`),
want: false,
},
{
name: "namespace flatten",
canonicalBody: []byte(`{"model":"gpt-5.4","tools":[{"type":"namespace","name":"code_tools"}]}`),
attemptBody: []byte(`{"model":"gpt-5.4","tools":[{"type":"function","name":"code_tools.run"}]}`),
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := newOpenAIImageIntentHintTestContext(OpenAIClientTransportHTTP)
var calls atomic.Int64
classify := countingOpenAIImageIntentClassifier(&calls)
require.Equal(t, tt.want, resolveOpenAIPassthroughImageIntent(c, "gpt-5.4", tt.canonicalBody, "gpt-5.4", tt.attemptBody, false, classify))
require.Equal(t, tt.want, resolveOpenAIPassthroughImageIntent(c, "gpt-5.4", tt.canonicalBody, "gpt-5.4", tt.attemptBody, false, classify))
require.Equal(t, int64(1), calls.Load())
})
}
}
func TestOpenAIGatewayServicePassthroughCompactImageIntentIsAttemptLocal(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
canonicalModel string
compactModel string
wantRejected bool
wantCanonical bool
}{
{
name: "text to image rejects",
canonicalModel: "gpt-5.4",
compactModel: "gpt-image-2",
wantRejected: true,
},
{
name: "image to text reaches upstream",
canonicalModel: "gpt-image-2",
compactModel: "gpt-5.4",
wantCanonical: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
upstream := &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"id":"resp_compact","model":"` + tt.compactModel + `","usage":{"input_tokens":1,"output_tokens":1}}`)),
}}
svc := newOpenAIImageGenerationControlTestService(upstream)
c, recorder := newOpenAIImageGenerationControlTestContext(false, "unit-test-agent/1.0")
c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses/compact", nil)
SetOpenAIClientTransport(c, OpenAIClientTransportHTTP)
account := newOpenAIImageGenerationControlTestAccount()
account.Extra = map[string]any{"openai_passthrough": true}
account.Credentials = map[string]any{
"api_key": "sk-test",
"compact_model_mapping": map[string]any{
tt.canonicalModel: tt.compactModel,
},
}
body := []byte(`{"model":"` + tt.canonicalModel + `","stream":false,"input":"draw"}`)
result, err := svc.Forward(context.Background(), c, account, body)
cached, known := getOpenAIImageIntentHint(c)
require.True(t, known)
require.Equal(t, tt.wantCanonical, cached)
if tt.wantRejected {
require.Error(t, err)
require.Nil(t, result)
require.Equal(t, http.StatusForbidden, recorder.Code)
require.Nil(t, upstream.lastReq)
return
}
require.NoError(t, err)
require.NotNil(t, result)
require.NotNil(t, upstream.lastReq)
require.Equal(t, tt.compactModel, gjson.GetBytes(upstream.lastBody, "model").String())
})
}
}
func TestResolveOpenAIImageIntentHintExcludesWebSocketAndUnknownTransport(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, transport := range []OpenAIClientTransport{OpenAIClientTransportWS, OpenAIClientTransportUnknown} {
c := newOpenAIImageIntentHintTestContext(transport)
var calls atomic.Int64
classify := countingOpenAIImageIntentClassifier(&calls)
body := []byte(`{"model":"gpt-5.4","input":"write code"}`)
require.False(t, resolveOpenAIImageIntentHint(c, "gpt-5.4", body, classify))
require.False(t, resolveOpenAIImageIntentHint(c, "gpt-5.4", body, classify))
require.Equal(t, int64(2), calls.Load())
_, known := getOpenAIImageIntentHint(c)
require.False(t, known)
}
}
func TestResolveOpenAIImageIntentHintConcurrentRequestsAreIsolated(t *testing.T) {
gin.SetMode(gin.TestMode)
const requests = 32
var calls atomic.Int64
classify := countingOpenAIImageIntentClassifier(&calls)
var wg sync.WaitGroup
results := make([][2]bool, requests)
for i := range requests {
wg.Add(1)
go func(index int, image bool) {
defer wg.Done()
c := newOpenAIImageIntentHintTestContext(OpenAIClientTransportHTTP)
body := []byte(`{"model":"gpt-5.4","input":"write code"}`)
if image {
body = []byte(`{"model":"gpt-5.4","tools":[{"type":"image_generation"}]}`)
}
results[index][0] = resolveOpenAIImageIntentHint(c, "gpt-5.4", body, classify)
results[index][1] = resolveOpenAIImageIntentHint(c, "gpt-5.4", body, classify)
}(i, i%2 == 0)
}
wg.Wait()
for i, result := range results {
require.Equal(t, i%2 == 0, result[0])
require.Equal(t, result[0], result[1])
}
require.Equal(t, int64(requests), calls.Load())
}
var openAIImageIntentHintBenchmarkSink bool
func BenchmarkOpenAIPassthroughImageIntentHintLargeBody(b *testing.B) {
body := []byte(`{"model":"gpt-5.4","input":"` + strings.Repeat("x", 4<<20) + `"}`)
const attempts = 4
b.Run("scan_each_attempt", func(b *testing.B) {
c := newOpenAIImageIntentHintTestContext(OpenAIClientTransportHTTP)
b.ReportAllocs()
calls := 0
for range b.N {
c.Set(openAIImageIntentHintContextKey, struct{}{})
for range attempts {
calls++
openAIImageIntentHintBenchmarkSink = IsImageGenerationIntent(openAIResponsesEndpoint, "gpt-5.4", body)
}
}
b.ReportMetric(float64(calls)/float64(b.N), "classifier_calls/op")
})
b.Run("request_scoped_hint", func(b *testing.B) {
c := newOpenAIImageIntentHintTestContext(OpenAIClientTransportHTTP)
b.ReportAllocs()
calls := 0
classify := func(endpoint string, requestedModel string, candidate []byte) bool {
calls++
return IsImageGenerationIntent(endpoint, requestedModel, candidate)
}
for range b.N {
c.Set(openAIImageIntentHintContextKey, struct{}{})
for range attempts {
openAIImageIntentHintBenchmarkSink = resolveOpenAIPassthroughImageIntent(c, "gpt-5.4", body, "gpt-5.4", body, false, classify)
}
}
b.ReportMetric(float64(calls)/float64(b.N), "classifier_calls/op")
})
}