From 92edda2f6e2d239b02c312787d9e7eb3c57fd35c Mon Sep 17 00:00:00 2001 From: jjaw Date: Tue, 14 Jul 2026 21:57:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=80=8F=E4=BC=A0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E6=84=8F=E5=9B=BE=E5=88=A4=E5=AE=9A=E5=A4=8D=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/openai_gateway_passthrough.go | 5 +- ...passthrough_image_intent_benchmark_test.go | 27 +++++++++ ...i_gateway_passthrough_image_intent_test.go | 59 +++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 backend/internal/service/openai_gateway_passthrough_image_intent_benchmark_test.go create mode 100644 backend/internal/service/openai_gateway_passthrough_image_intent_test.go diff --git a/backend/internal/service/openai_gateway_passthrough.go b/backend/internal/service/openai_gateway_passthrough.go index 0a19a9d28..8d8c6c96f 100644 --- a/backend/internal/service/openai_gateway_passthrough.go +++ b/backend/internal/service/openai_gateway_passthrough.go @@ -100,7 +100,8 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough( body = updatedBody apiKey := getAPIKeyFromContext(c) - if IsImageGenerationIntent(openAIResponsesEndpoint, reqModel, body) && !GroupAllowsImageGeneration(apiKeyGroup(apiKey)) { + imageIntent := IsImageGenerationIntent(openAIResponsesEndpoint, reqModel, body) + if imageIntent && !GroupAllowsImageGeneration(apiKeyGroup(apiKey)) { MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalFeatureGate) c.JSON(http.StatusForbidden, gin.H{ "error": gin.H{ @@ -113,7 +114,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 { diff --git a/backend/internal/service/openai_gateway_passthrough_image_intent_benchmark_test.go b/backend/internal/service/openai_gateway_passthrough_image_intent_benchmark_test.go new file mode 100644 index 000000000..9335a4e43 --- /dev/null +++ b/backend/internal/service/openai_gateway_passthrough_image_intent_benchmark_test.go @@ -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 + } + }) +} diff --git a/backend/internal/service/openai_gateway_passthrough_image_intent_test.go b/backend/internal/service/openai_gateway_passthrough_image_intent_test.go new file mode 100644 index 000000000..1720ef6e1 --- /dev/null +++ b/backend/internal/service/openai_gateway_passthrough_image_intent_test.go @@ -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) + }) +}