From b9b013a0881981fd01f59dec45a2d5f0735aa37a Mon Sep 17 00:00:00 2001 From: li Date: Fri, 10 Jul 2026 11:56:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(usage):=20WS=20passthrough=20effort=20?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E8=A1=A5=E5=85=A5=E6=98=A0=E5=B0=84=E5=90=8E?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=80=99=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #3924 --- .../service/openai_fast_policy_ws_test.go | 4 +- .../openai_ws_v2_passthrough_adapter.go | 12 +++--- ...i_ws_v2_passthrough_adapter_effort_test.go | 42 +++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 backend/internal/service/openai_ws_v2_passthrough_adapter_effort_test.go diff --git a/backend/internal/service/openai_fast_policy_ws_test.go b/backend/internal/service/openai_fast_policy_ws_test.go index ac5b363cc0..a802540879 100644 --- a/backend/internal/service/openai_fast_policy_ws_test.go +++ b/backend/internal/service/openai_fast_policy_ws_test.go @@ -1015,7 +1015,7 @@ func TestPassthroughUsageMeta_TracksReasoningEffortAcrossTurns(t *testing.T) { firstOut, firstBlocked, firstErr := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, capturedSessionModel, firstFrame) require.NoError(t, firstErr) require.Nil(t, firstBlocked) - meta.initFromFirstFrame(firstOut) + meta.initFromFirstFrame(firstOut, capturedSessionModel) require.NotNil(t, meta.reasoningEffort.Load()) require.Equal(t, "medium", *meta.reasoningEffort.Load()) @@ -1032,7 +1032,7 @@ func TestPassthroughUsageMeta_TracksReasoningEffortAcrossTurns(t *testing.T) { out, blocked, policyErr := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, model, payload) if policyErr == nil && blocked == nil && strings.TrimSpace(gjson.GetBytes(payload, "type").String()) == "response.create" { - meta.updateFromResponseCreate(out, requestModelForThisFrame) + meta.updateFromResponseCreate(out, model, requestModelForThisFrame) } return out, blocked, policyErr } diff --git a/backend/internal/service/openai_ws_v2_passthrough_adapter.go b/backend/internal/service/openai_ws_v2_passthrough_adapter.go index 7241f11ed1..d822d181cd 100644 --- a/backend/internal/service/openai_ws_v2_passthrough_adapter.go +++ b/backend/internal/service/openai_ws_v2_passthrough_adapter.go @@ -142,12 +142,12 @@ func newOpenAIWSPassthroughUsageMeta(initialRequestModel string, firstFrame []by return meta } -func (m *openAIWSPassthroughUsageMeta) initFromFirstFrame(policyOutput []byte) { +func (m *openAIWSPassthroughUsageMeta) initFromFirstFrame(policyOutput []byte, mappedModel string) { if m == nil { return } m.serviceTier.Store(extractOpenAIServiceTierFromBody(policyOutput)) - m.reasoningEffort.Store(extractOpenAIReasoningEffortFromBody(policyOutput, m.sessionRequestModel)) + m.reasoningEffort.Store(extractOpenAIReasoningEffortFromBody(policyOutput, mappedModel, m.sessionRequestModel)) } func (m *openAIWSPassthroughUsageMeta) updateSessionRequestModel(payload []byte) { @@ -169,12 +169,12 @@ func (m *openAIWSPassthroughUsageMeta) requestModelForFrame(payload []byte) stri return m.sessionRequestModel } -func (m *openAIWSPassthroughUsageMeta) updateFromResponseCreate(policyOutput []byte, requestModelForFrame string) { +func (m *openAIWSPassthroughUsageMeta) updateFromResponseCreate(policyOutput []byte, mappedModel string, requestModelForFrame string) { if m == nil { return } m.serviceTier.Store(extractOpenAIServiceTierFromBody(policyOutput)) - m.reasoningEffort.Store(extractOpenAIReasoningEffortFromBody(policyOutput, requestModelForFrame)) + m.reasoningEffort.Store(extractOpenAIReasoningEffortFromBody(policyOutput, mappedModel, requestModelForFrame)) } func openAIWSPassthroughRequestModelForFrame(payload []byte) string { @@ -311,7 +311,7 @@ func (s *OpenAIGatewayService) proxyResponsesWebSocketV2Passthrough( // 因此使用 atomic.Pointer[string] 在 filter(runClientToUpstream // goroutine)和 OnTurnComplete / final result(runUpstreamToClient // goroutine)之间同步当前 turn 的 usage metadata。 - usageMeta.initFromFirstFrame(firstClientMessage) + usageMeta.initFromFirstFrame(firstClientMessage, capturedSessionModel) promptCacheKey := strings.TrimSpace(gjson.GetBytes(firstClientMessage, "prompt_cache_key").String()) wsURL, err := s.buildOpenAIResponsesWSURL(account) @@ -455,7 +455,7 @@ func (s *OpenAIGatewayService) proxyResponsesWebSocketV2Passthrough( // service_tier 时按 default 处理,billing 应如实反映。 if policyErr == nil && blocked == nil && strings.TrimSpace(gjson.GetBytes(payload, "type").String()) == "response.create" { - usageMeta.updateFromResponseCreate(out, requestModelForThisFrame) + usageMeta.updateFromResponseCreate(out, model, requestModelForThisFrame) } return out, blocked, policyErr }, diff --git a/backend/internal/service/openai_ws_v2_passthrough_adapter_effort_test.go b/backend/internal/service/openai_ws_v2_passthrough_adapter_effort_test.go new file mode 100644 index 0000000000..82c9eaac85 --- /dev/null +++ b/backend/internal/service/openai_ws_v2_passthrough_adapter_effort_test.go @@ -0,0 +1,42 @@ +//go:build unit + +package service + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWSPassthroughUsageMeta_InitFromFirstFrame_MappedModelCandidate(t *testing.T) { + body := []byte(`{"type":"response.create","model":"sol","reasoning":{"effort":"max"}}`) + + meta := newOpenAIWSPassthroughUsageMeta("sol", body) + meta.initFromFirstFrame(body, "gpt-5.6-sol") + + got := meta.reasoningEffort.Load() + require.NotNil(t, got, "reasoning effort should be set") + require.Equal(t, "max", *got, "mapped model gpt-5.6-sol should preserve max") +} + +func TestWSPassthroughUsageMeta_InitFromFirstFrame_NonGPT56FallsBackToXHigh(t *testing.T) { + body := []byte(`{"type":"response.create","model":"gpt-5.4","reasoning":{"effort":"max"}}`) + + meta := newOpenAIWSPassthroughUsageMeta("gpt-5.4", body) + meta.initFromFirstFrame(body, "gpt-5.4") + + got := meta.reasoningEffort.Load() + require.NotNil(t, got) + require.Equal(t, "xhigh", *got, "non-5.6 model should normalize max to xhigh") +} + +func TestWSPassthroughUsageMeta_UpdateFromResponseCreate_MappedModelCandidate(t *testing.T) { + body := []byte(`{"type":"response.create","model":"sol","reasoning":{"effort":"max"}}`) + + meta := newOpenAIWSPassthroughUsageMeta("sol", body) + meta.updateFromResponseCreate(body, "gpt-5.6-sol", "sol") + + got := meta.reasoningEffort.Load() + require.NotNil(t, got) + require.Equal(t, "max", *got, "mapped model should preserve max on multi-turn update") +}