From 80b3d4c1f8a0b708e4f40757a66ecc428b714c4d Mon Sep 17 00:00:00 2001 From: psyche <123316733+githubbzxs@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:39:16 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=BC=E5=AE=B9=20GPT-5.6=20max=20?= =?UTF-8?q?=E6=8E=A8=E7=90=86=E5=BC=BA=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../openai_gateway_chat_completions_raw.go | 4 +- ...penai_gateway_chat_completions_raw_test.go | 33 +++ .../service/openai_gateway_forward.go | 15 +- .../openai_gateway_messages_chat_fallback.go | 2 +- .../service/openai_gateway_request_body.go | 55 +++- .../openai_gateway_responses_chat_fallback.go | 2 +- .../internal/service/openai_gpt56_max_test.go | 266 ++++++++++++++++++ .../service/openai_ws_forwarder_ingress.go | 2 +- .../service/openai_ws_forwarder_v2.go | 2 +- .../internal/service/openai_ws_http_bridge.go | 2 +- 10 files changed, 367 insertions(+), 16 deletions(-) create mode 100644 backend/internal/service/openai_gpt56_max_test.go diff --git a/backend/internal/service/openai_gateway_chat_completions_raw.go b/backend/internal/service/openai_gateway_chat_completions_raw.go index 9b31b803d2..2f46a1c76c 100644 --- a/backend/internal/service/openai_gateway_chat_completions_raw.go +++ b/backend/internal/service/openai_gateway_chat_completions_raw.go @@ -72,13 +72,13 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions( } clientStream := gjson.GetBytes(body, "stream").Bool() - // 1b. Extract reasoning effort and service tier from the raw body before any transformation. - reasoningEffort := extractOpenAIReasoningEffortFromBody(body, originalModel) + // 1b. Extract service tier from the raw body before any transformation. serviceTier := extractOpenAIServiceTierFromBody(body) // 2. Resolve model mapping (same as ForwardAsChatCompletions) billingModel := resolveOpenAIForwardModel(account, originalModel, defaultMappedModel) upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel) + reasoningEffort := extractOpenAIReasoningEffortFromBody(body, firstNonEmpty(upstreamModel, billingModel, originalModel)) // 国产模型默认 effort 补充:需要 mappedModel 判定,推迟到 billingModel 算出之后。 reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, billingModel) diff --git a/backend/internal/service/openai_gateway_chat_completions_raw_test.go b/backend/internal/service/openai_gateway_chat_completions_raw_test.go index 5a6ee3f9b9..5f4b5bcd2d 100644 --- a/backend/internal/service/openai_gateway_chat_completions_raw_test.go +++ b/backend/internal/service/openai_gateway_chat_completions_raw_test.go @@ -122,6 +122,39 @@ func TestForwardAsRawChatCompletions_ForcesStreamUsageUpstreamAndPassesUsageDown require.Contains(t, rec.Body.String(), "data: [DONE]") } +func TestForwardAsRawChatCompletions_PreservesMappedGPT56MaxEffort(t *testing.T) { + gin.SetMode(gin.TestMode) + + body := []byte(`{"model":"sol","messages":[{"role":"user","content":"hello"}],"reasoning_effort":"max","stream":false}`) + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + upstream := &httpUpstreamRecorder{resp: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader( + `{"id":"chatcmpl_max","object":"chat.completion","model":"gpt-5.6-sol","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":2,"total_tokens":5}}`, + )), + }} + svc := &OpenAIGatewayService{ + cfg: rawChatCompletionsTestConfig(), + httpUpstream: upstream, + } + account := rawChatCompletionsTestAccount() + account.Credentials["model_mapping"] = map[string]any{"sol": "gpt-5.6-sol"} + + result, err := svc.forwardAsRawChatCompletions(context.Background(), c, account, body, "") + + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, "gpt-5.6-sol", gjson.GetBytes(upstream.lastBody, "model").String()) + require.Equal(t, "max", gjson.GetBytes(upstream.lastBody, "reasoning_effort").String()) + require.NotNil(t, result.ReasoningEffort) + require.Equal(t, "max", *result.ReasoningEffort) +} + func TestForwardAsRawChatCompletions_PreservesDeepSeekReasoningContentNonStreaming(t *testing.T) { gin.SetMode(gin.TestMode) diff --git a/backend/internal/service/openai_gateway_forward.go b/backend/internal/service/openai_gateway_forward.go index 6fdabeb90f..2b19578bd8 100644 --- a/backend/internal/service/openai_gateway_forward.go +++ b/backend/internal/service/openai_gateway_forward.go @@ -35,6 +35,14 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco return nil, errors.New("codex_cli_only restriction: only codex official clients are allowed") } + normalizedBody, normalized, err := normalizeOpenAICodexCompactReasoningEffortForAccount(c, account, body) + if err != nil { + return nil, err + } + if normalized { + body = normalizedBody + } + originalBody := body requestView := newOpenAIRequestView(body) reqModel, reqStream, promptCacheKey := requestView.Model, requestView.Stream, requestView.PromptCacheKey @@ -88,9 +96,10 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco passthroughEnabled := account.IsOpenAIPassthroughEnabled() if passthroughEnabled { // 透传分支只需要轻量提取字段,避免热路径全量 Unmarshal。 - reasoningEffort := extractOpenAIReasoningEffortFromBody(body, reqModel) + mappedModel := account.GetMappedModel(reqModel) + reasoningEffort := extractOpenAIReasoningEffortFromBody(body, mappedModel) // 国产模型默认 effort 补充:也要用 mappedModel 判定是否是 passback-required 上游。 - reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, account.GetMappedModel(reqModel)) + reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, mappedModel) return s.forwardOpenAIPassthrough(ctx, c, account, originalBody, reqModel, reasoningEffort, reqStream, startTime) } @@ -746,7 +755,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco } defer func() { _ = resp.Body.Close() }() - reasoningEffort := extractOpenAIReasoningEffortFromBody(body, originalModel) + reasoningEffort := extractOpenAIReasoningEffortFromBody(body, firstNonEmpty(upstreamModel, billingModel, originalModel)) // 国产模型默认 effort 补充:此处 reqModel 已被 mapping 重写为 billingModel(见 // line 2510-2515 的 GetMappedModel + reqModel 赋值),可直接作为 mappedModel。 reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, reqModel) diff --git a/backend/internal/service/openai_gateway_messages_chat_fallback.go b/backend/internal/service/openai_gateway_messages_chat_fallback.go index 4596177468..97eecf014b 100644 --- a/backend/internal/service/openai_gateway_messages_chat_fallback.go +++ b/backend/internal/service/openai_gateway_messages_chat_fallback.go @@ -72,7 +72,7 @@ func (s *OpenAIGatewayService) forwardAnthropicViaRawChatCompletions( chatReq.StreamOptions = &apicompat.ChatStreamOptions{IncludeUsage: true} } - reasoningEffort := extractOpenAIReasoningEffortFromBody(body, originalModel) + reasoningEffort := extractOpenAIReasoningEffortFromBody(body, firstNonEmpty(upstreamModel, billingModel, originalModel)) reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, billingModel) serviceTier := extractOpenAIServiceTierFromBody(body) diff --git a/backend/internal/service/openai_gateway_request_body.go b/backend/internal/service/openai_gateway_request_body.go index b48b7510eb..cead23d262 100644 --- a/backend/internal/service/openai_gateway_request_body.go +++ b/backend/internal/service/openai_gateway_request_body.go @@ -199,6 +199,32 @@ func normalizeOpenAICompactRequestBody(body []byte) ([]byte, bool, error) { return normalized, true, nil } +func normalizeOpenAICodexCompactReasoningEffortForAccount(c *gin.Context, account *Account, body []byte) ([]byte, bool, error) { + if account == nil || !account.IsOpenAIOAuth() || !isOpenAIResponsesCompactPath(c) { + return body, false, nil + } + + requestedModel := strings.TrimSpace(gjson.GetBytes(body, "model").String()) + effectiveModel := account.GetMappedModel(requestedModel) + return normalizeOpenAICodexCompactReasoningEffort(body, effectiveModel) +} + +func normalizeOpenAICodexCompactReasoningEffort(body []byte, effectiveModel string) ([]byte, bool, error) { + if !isOpenAIGPT56Model(effectiveModel) || + !strings.EqualFold(strings.TrimSpace(gjson.GetBytes(body, "reasoning.effort").String()), "max") { + return body, false, nil + } + + // Codex Ultra 在客户端编排层会下发 max;ChatGPT compact 端点目前只接受到 + // xhigh。这里只降级 OpenAI OAuth 的 GPT-5.6 compact 子请求,普通 Responses、 + // API Key 请求和其他平台的 OAuth 请求保留 max。 + normalized, err := sjson.SetBytes(body, "reasoning.effort", "xhigh") + if err != nil { + return body, false, fmt.Errorf("normalize codex compact reasoning effort: %w", err) + } + return normalized, true, nil +} + func resolveOpenAICompactSessionID(c *gin.Context) string { if c != nil { if sessionID := strings.TrimSpace(c.GetHeader("session_id")); sessionID != "" { @@ -259,7 +285,7 @@ func (s *OpenAIGatewayService) replaceModelInResponseBody(body []byte, fromModel return body } -func getOpenAIReasoningEffortFromReqBody(reqBody map[string]any) (value string, present bool) { +func getOpenAIReasoningEffortFromReqBody(reqBody map[string]any, requestedModel string) (value string, present bool) { if reqBody == nil { return "", false } @@ -267,13 +293,13 @@ func getOpenAIReasoningEffortFromReqBody(reqBody map[string]any) (value string, // Primary: reasoning.effort if reasoning, ok := reqBody["reasoning"].(map[string]any); ok { if effort, ok := reasoning["effort"].(string); ok { - return normalizeOpenAIReasoningEffort(effort), true + return normalizeOpenAIReasoningEffortForModel(effort, requestedModel), true } } // Fallback: some clients may use a flat field. if effort, ok := reqBody["reasoning_effort"].(string); ok { - return normalizeOpenAIReasoningEffort(effort), true + return normalizeOpenAIReasoningEffortForModel(effort, requestedModel), true } return "", false @@ -302,7 +328,7 @@ func deriveOpenAIReasoningEffortFromModel(model string) string { return "" } - return normalizeOpenAIReasoningEffort(parts[len(parts)-1]) + return normalizeOpenAIReasoningEffortForModel(parts[len(parts)-1], modelID) } type openAIRequestView struct { @@ -551,7 +577,7 @@ func extractOpenAIReasoningEffortFromBody(body []byte, requestedModel string) *s reasoningEffort = strings.TrimSpace(gjson.GetBytes(body, "reasoning_effort").String()) } if reasoningEffort != "" { - normalized := normalizeOpenAIReasoningEffort(reasoningEffort) + normalized := normalizeOpenAIReasoningEffortForModel(reasoningEffort, requestedModel) if normalized == "" { return nil } @@ -1127,7 +1153,7 @@ func getOpenAIRequestBodyMap(_ *gin.Context, body []byte) (map[string]any, error } func extractOpenAIReasoningEffort(reqBody map[string]any, requestedModel string) *string { - if value, present := getOpenAIReasoningEffortFromReqBody(reqBody); present { + if value, present := getOpenAIReasoningEffortFromReqBody(reqBody, requestedModel); present { if value == "" { return nil } @@ -1162,3 +1188,20 @@ func normalizeOpenAIReasoningEffort(raw string) string { return "" } } + +func normalizeOpenAIReasoningEffortForModel(raw, model string) string { + if strings.EqualFold(strings.TrimSpace(raw), "max") && isOpenAIGPT56Model(model) { + return "max" + } + return normalizeOpenAIReasoningEffort(raw) +} + +func isOpenAIGPT56Model(model string) bool { + normalized := canonicalizeOpenAIModelAliasSpelling(model) + for _, prefix := range []string{"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"} { + if normalized == prefix || strings.HasPrefix(normalized, prefix+"-") { + return true + } + } + return false +} diff --git a/backend/internal/service/openai_gateway_responses_chat_fallback.go b/backend/internal/service/openai_gateway_responses_chat_fallback.go index 16e552f21f..9f2cb84864 100644 --- a/backend/internal/service/openai_gateway_responses_chat_fallback.go +++ b/backend/internal/service/openai_gateway_responses_chat_fallback.go @@ -38,7 +38,6 @@ func (s *OpenAIGatewayService) forwardResponsesViaRawChatCompletions( } clientStream := responsesReq.Stream - reasoningEffort := extractOpenAIReasoningEffortFromBody(body, originalModel) serviceTier := extractOpenAIServiceTierFromBody(body) chatReq, err := apicompat.ResponsesToChatCompletionsRequest(&responsesReq) @@ -49,6 +48,7 @@ func (s *OpenAIGatewayService) forwardResponsesViaRawChatCompletions( billingModel := resolveOpenAIForwardModel(account, originalModel, "") upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel) + reasoningEffort := extractOpenAIReasoningEffortFromBody(body, firstNonEmpty(upstreamModel, billingModel, originalModel)) // 国产模型默认 effort 补充:需要 mappedModel 判定,推迟到 billingModel 算出之后。 reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, billingModel) chatReq.Model = upstreamModel diff --git a/backend/internal/service/openai_gpt56_max_test.go b/backend/internal/service/openai_gpt56_max_test.go new file mode 100644 index 0000000000..272eb16ff0 --- /dev/null +++ b/backend/internal/service/openai_gpt56_max_test.go @@ -0,0 +1,266 @@ +package service + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestNormalizeOpenAIReasoningEffortForGPT56(t *testing.T) { + tests := []struct { + name string + raw string + model string + want string + }{ + {name: "Sol 保留 max", raw: "max", model: "gpt-5.6-sol", want: "max"}, + {name: "Terra 保留 max", raw: "max", model: "openai/gpt-5.6-terra", want: "max"}, + {name: "Luna 后缀保留 max", raw: "max", model: "gpt-5.6-luna-2026-07-09", want: "max"}, + {name: "其他模型沿用 xhigh", raw: "max", model: "deepseek-v4-pro", want: "xhigh"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, normalizeOpenAIReasoningEffortForModel(tt.raw, tt.model)) + }) + } +} + +func TestNormalizeOpenAICodexCompactReasoningEffortDowngradesMax(t *testing.T) { + body := []byte(`{"model":"gpt-5.6-sol","input":"compact me","reasoning":{"effort":"max","summary":"auto"}}`) + + normalized, changed, err := normalizeOpenAICodexCompactReasoningEffort(body, "gpt-5.6-sol") + + require.NoError(t, err) + require.True(t, changed) + require.Equal(t, "gpt-5.6-sol", gjson.GetBytes(normalized, "model").String()) + require.Equal(t, "xhigh", gjson.GetBytes(normalized, "reasoning.effort").String()) + require.Equal(t, "auto", gjson.GetBytes(normalized, "reasoning.summary").String()) +} + +func TestNormalizeOpenAICodexCompactReasoningEffortForAccountScopesCompatibility(t *testing.T) { + gin.SetMode(gin.TestMode) + body := []byte(`{"model":"gpt-5.6-sol","input":"compact me","reasoning":{"effort":"max"}}`) + + tests := []struct { + name string + path string + account *Account + changed bool + want string + }{ + { + name: "OpenAI OAuth compact 降级", + path: "/openai/v1/responses/compact", + account: &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}, + changed: true, + want: "xhigh", + }, + { + name: "OpenAI OAuth 普通请求保留", + path: "/openai/v1/responses", + account: &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}, + want: "max", + }, + { + name: "OpenAI API Key compact 保留", + path: "/openai/v1/responses/compact", + account: &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}, + want: "max", + }, + { + name: "Grok OAuth compact 保留", + path: "/openai/v1/responses/compact", + account: &Account{Platform: PlatformGrok, Type: AccountTypeOAuth}, + want: "max", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodPost, tt.path, nil) + + normalized, changed, err := normalizeOpenAICodexCompactReasoningEffortForAccount(c, tt.account, body) + + require.NoError(t, err) + require.Equal(t, tt.changed, changed) + require.Equal(t, tt.want, gjson.GetBytes(normalized, "reasoning.effort").String()) + }) + } +} + +func TestOpenAIGatewayServiceForwardPreservesGPT56MaxEffort(t *testing.T) { + gin.SetMode(gin.TestMode) + upstream := &httpUpstreamRecorder{ + resp: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{"usage":{"input_tokens":1,"output_tokens":2}}`)), + }, + } + cfg := &config.Config{} + cfg.Security.URLAllowlist.Enabled = false + svc := &OpenAIGatewayService{cfg: cfg, httpUpstream: upstream} + account := &Account{ + ID: 7, + Name: "openai-apikey", + Platform: PlatformOpenAI, + Type: AccountTypeAPIKey, + Concurrency: 1, + Credentials: map[string]any{ + "api_key": "sk-test", + "base_url": "https://example.com", + }, + Extra: map[string]any{"use_responses_api": true}, + } + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses", nil) + SetOpenAIClientTransport(c, OpenAIClientTransportHTTP) + + body := []byte(`{"model":"gpt-5.6-sol","stream":false,"reasoning":{"effort":"max"},"input":"hello"}`) + result, err := svc.Forward(context.Background(), c, account, body) + + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, "max", gjson.GetBytes(upstream.lastBody, "reasoning.effort").String()) + require.NotNil(t, result.ReasoningEffort) + require.Equal(t, "max", *result.ReasoningEffort) +} + +func TestOpenAIGatewayServiceForwardPreservesMappedGPT56MaxEffort(t *testing.T) { + gin.SetMode(gin.TestMode) + upstream := &httpUpstreamRecorder{ + resp: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{"usage":{"input_tokens":1,"output_tokens":2}}`)), + }, + } + cfg := &config.Config{} + cfg.Security.URLAllowlist.Enabled = false + svc := &OpenAIGatewayService{cfg: cfg, httpUpstream: upstream} + account := &Account{ + ID: 9, + Name: "openai-apikey-mapped", + Platform: PlatformOpenAI, + Type: AccountTypeAPIKey, + Concurrency: 1, + Credentials: map[string]any{ + "api_key": "sk-test", + "base_url": "https://example.com", + "model_mapping": map[string]any{ + "sol": "gpt-5.6-sol", + }, + }, + Extra: map[string]any{"use_responses_api": true}, + } + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses", nil) + SetOpenAIClientTransport(c, OpenAIClientTransportHTTP) + + body := []byte(`{"model":"sol","stream":false,"reasoning":{"effort":"max"},"input":"hello"}`) + result, err := svc.Forward(context.Background(), c, account, body) + + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, "gpt-5.6-sol", gjson.GetBytes(upstream.lastBody, "model").String()) + require.Equal(t, "max", gjson.GetBytes(upstream.lastBody, "reasoning.effort").String()) + require.NotNil(t, result.ReasoningEffort) + require.Equal(t, "max", *result.ReasoningEffort) +} + +func TestOpenAIGatewayServiceForwardOAuthCompactDowngradesMaxEffort(t *testing.T) { + gin.SetMode(gin.TestMode) + upstream := &httpUpstreamRecorder{ + resp: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{"usage":{"input_tokens":1,"output_tokens":2}}`)), + }, + } + cfg := &config.Config{} + cfg.Security.URLAllowlist.Enabled = false + svc := &OpenAIGatewayService{cfg: cfg, httpUpstream: upstream} + account := &Account{ + ID: 8, + Name: "openai-oauth", + Platform: PlatformOpenAI, + Type: AccountTypeOAuth, + Concurrency: 1, + Credentials: map[string]any{ + "access_token": "oauth-token", + "chatgpt_account_id": "chatgpt-acc", + }, + Status: StatusActive, + Schedulable: true, + } + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses/compact", nil) + SetOpenAIClientTransport(c, OpenAIClientTransportHTTP) + + body := []byte(`{"model":"gpt-5.6-sol","instructions":"compact-test","input":"hello","reasoning":{"effort":"max"}}`) + 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, chatgptCodexURL+"/compact", upstream.lastReq.URL.String()) + require.Equal(t, "xhigh", gjson.GetBytes(upstream.lastBody, "reasoning.effort").String()) + require.NotNil(t, result.ReasoningEffort) + require.Equal(t, "xhigh", *result.ReasoningEffort) +} + +func TestOpenAIGatewayServiceForwardOAuthResponsesPreservesMaxEffort(t *testing.T) { + gin.SetMode(gin.TestMode) + upstream := &httpUpstreamRecorder{ + resp: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{"usage":{"input_tokens":1,"output_tokens":2}}`)), + }, + } + cfg := &config.Config{} + cfg.Security.URLAllowlist.Enabled = false + svc := &OpenAIGatewayService{cfg: cfg, httpUpstream: upstream} + account := &Account{ + ID: 10, + Name: "openai-oauth-responses", + Platform: PlatformOpenAI, + Type: AccountTypeOAuth, + Concurrency: 1, + Credentials: map[string]any{ + "access_token": "oauth-token", + "chatgpt_account_id": "chatgpt-acc", + }, + Status: StatusActive, + Schedulable: true, + } + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses", nil) + SetOpenAIClientTransport(c, OpenAIClientTransportHTTP) + + body := []byte(`{"model":"gpt-5.6-sol","instructions":"response-test","input":"hello","reasoning":{"effort":"max"}}`) + 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, chatgptCodexURL, upstream.lastReq.URL.String()) + require.Equal(t, "max", gjson.GetBytes(upstream.lastBody, "reasoning.effort").String()) + require.NotNil(t, result.ReasoningEffort) + require.Equal(t, "max", *result.ReasoningEffort) +} diff --git a/backend/internal/service/openai_ws_forwarder_ingress.go b/backend/internal/service/openai_ws_forwarder_ingress.go index 5fb9395461..9dea4118da 100644 --- a/backend/internal/service/openai_ws_forwarder_ingress.go +++ b/backend/internal/service/openai_ws_forwarder_ingress.go @@ -920,7 +920,7 @@ func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient( Model: originalModel, UpstreamModel: mappedModel, ServiceTier: extractOpenAIServiceTierFromBody(payload), - ReasoningEffort: ApplyThinkingEnabledFallback(extractOpenAIReasoningEffortFromBody(payload, originalModel), payload, mappedModel), + ReasoningEffort: ApplyThinkingEnabledFallback(extractOpenAIReasoningEffortFromBody(payload, firstNonEmpty(mappedModel, originalModel)), payload, mappedModel), Stream: reqStream, OpenAIWSMode: true, ResponseHeaders: lease.HandshakeHeaders(), diff --git a/backend/internal/service/openai_ws_forwarder_v2.go b/backend/internal/service/openai_ws_forwarder_v2.go index ea8c0a0722..40a3cc0194 100644 --- a/backend/internal/service/openai_ws_forwarder_v2.go +++ b/backend/internal/service/openai_ws_forwarder_v2.go @@ -693,7 +693,7 @@ func (s *OpenAIGatewayService) forwardOpenAIWSV2( ImageCount: imageCounter.Count(), ImageOutputSizes: imageCounter.Sizes(), ServiceTier: extractOpenAIServiceTier(reqBody), - ReasoningEffort: extractOpenAIReasoningEffort(reqBody, originalModel), + ReasoningEffort: extractOpenAIReasoningEffort(reqBody, firstNonEmpty(mappedModel, originalModel)), Stream: reqStream, OpenAIWSMode: true, ResponseHeaders: lease.HandshakeHeaders(), diff --git a/backend/internal/service/openai_ws_http_bridge.go b/backend/internal/service/openai_ws_http_bridge.go index 0879d7e8fb..df1a8067cb 100644 --- a/backend/internal/service/openai_ws_http_bridge.go +++ b/backend/internal/service/openai_ws_http_bridge.go @@ -263,7 +263,7 @@ func (s *OpenAIGatewayService) proxyOpenAIWSHTTPBridgeTurn( Model: originalModel, UpstreamModel: mappedModel, ServiceTier: extractOpenAIServiceTierFromBody(body), - ReasoningEffort: ApplyThinkingEnabledFallback(extractOpenAIReasoningEffortFromBody(body, originalModel), body, mappedModel), + ReasoningEffort: ApplyThinkingEnabledFallback(extractOpenAIReasoningEffortFromBody(body, firstNonEmpty(mappedModel, originalModel)), body, mappedModel), Stream: reqStream, OpenAIWSMode: true, ResponseHeaders: cloneHeader(resp.Header),