diff --git a/backend/internal/service/gateway_forward_as_chat_completions.go b/backend/internal/service/gateway_forward_as_chat_completions.go index 93da85a7b2..d03cf53531 100644 --- a/backend/internal/service/gateway_forward_as_chat_completions.go +++ b/backend/internal/service/gateway_forward_as_chat_completions.go @@ -179,7 +179,7 @@ func (s *GatewayService) ForwardAsChatCompletions( } // 13. Extract reasoning effort from CC request body - reasoningEffort := extractCCReasoningEffortFromBody(body) + reasoningEffort := extractCCReasoningEffortFromBody(body, mappedModel, originalModel) // 国产模型默认 effort 补充:本路径是客户端 CC 请求 → Anthropic 上游, // 如果上游是 passback-required 国产模型 (Kimi-anthropic / GLM-anthropic / MiniMax) // 且客户端在 body 里传了 thinking.type=enabled,补中默认 effort。 @@ -201,7 +201,7 @@ func (s *GatewayService) ForwardAsChatCompletions( // extractCCReasoningEffortFromBody reads reasoning effort from a Chat Completions // request body. It checks both nested (reasoning.effort) and flat (reasoning_effort) // formats used by OpenAI-compatible clients. -func extractCCReasoningEffortFromBody(body []byte) *string { +func extractCCReasoningEffortFromBody(body []byte, modelCandidates ...string) *string { raw := strings.TrimSpace(gjson.GetBytes(body, "reasoning.effort").String()) if raw == "" { raw = strings.TrimSpace(gjson.GetBytes(body, "reasoning_effort").String()) @@ -209,7 +209,11 @@ func extractCCReasoningEffortFromBody(body []byte) *string { if raw == "" { return nil } - normalized := normalizeOpenAIReasoningEffort(raw) + model := firstNonEmpty(modelCandidates...) + if model == "" { + model = strings.TrimSpace(gjson.GetBytes(body, "model").String()) + } + normalized := normalizeOpenAIReasoningEffortForModel(raw, model) if normalized == "" { return nil } diff --git a/backend/internal/service/gateway_forward_as_chat_completions_test.go b/backend/internal/service/gateway_forward_as_chat_completions_test.go index d1047bdc9b..a5a5cbf913 100644 --- a/backend/internal/service/gateway_forward_as_chat_completions_test.go +++ b/backend/internal/service/gateway_forward_as_chat_completions_test.go @@ -30,7 +30,23 @@ func TestExtractCCReasoningEffortFromBody(t *testing.T) { }) t.Run("DeepSeek max", func(t *testing.T) { - got := extractCCReasoningEffortFromBody([]byte(`{"reasoning_effort":"Max"}`)) + got := extractCCReasoningEffortFromBody([]byte(`{"model":"deepseek-v4-flash","reasoning_effort":"Max"}`)) + require.NotNil(t, got) + require.Equal(t, "max", *got) + }) + + t.Run("mapped Kimi alias max", func(t *testing.T) { + got := extractCCReasoningEffortFromBody( + []byte(`{"model":"public-alias","reasoning_effort":"max"}`), + "kimi-k3", + "public-alias", + ) + require.NotNil(t, got) + require.Equal(t, "max", *got) + }) + + t.Run("legacy model max", func(t *testing.T) { + got := extractCCReasoningEffortFromBody([]byte(`{"model":"gpt-5.5","reasoning_effort":"max"}`)) require.NotNil(t, got) require.Equal(t, "xhigh", *got) }) diff --git a/backend/internal/service/gateway_forward_as_responses.go b/backend/internal/service/gateway_forward_as_responses.go index 2aa5255c29..b0ca29c1c2 100644 --- a/backend/internal/service/gateway_forward_as_responses.go +++ b/backend/internal/service/gateway_forward_as_responses.go @@ -63,7 +63,6 @@ func (s *GatewayService) ForwardAsResponses( // 4. Model mapping mappedModel := originalModel - reasoningEffort := ExtractResponsesReasoningEffortFromBody(body) if account.Type == AccountTypeAPIKey || account.Type == AccountTypeServiceAccount { mappedModel = account.GetMappedModel(originalModel) } @@ -78,6 +77,7 @@ func (s *GatewayService) ForwardAsResponses( mappedModel = normalized } } + reasoningEffort := ExtractResponsesReasoningEffortFromBody(body, mappedModel, originalModel) // 国产模型默认 effort 补充:需要 mappedModel 判定,推迟到 mapping 完成之后。 reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, mappedModel) anthropicReq.Model = mappedModel @@ -255,12 +255,16 @@ func liftResponsesAdditionalTools(requestBody map[string]any) (bool, error) { // ExtractResponsesReasoningEffortFromBody reads Responses API reasoning.effort // and normalizes it for usage logging. -func ExtractResponsesReasoningEffortFromBody(body []byte) *string { +func ExtractResponsesReasoningEffortFromBody(body []byte, modelCandidates ...string) *string { raw := strings.TrimSpace(gjson.GetBytes(body, "reasoning.effort").String()) if raw == "" { return nil } - normalized := normalizeOpenAIReasoningEffort(raw) + model := firstNonEmpty(modelCandidates...) + if model == "" { + model = strings.TrimSpace(gjson.GetBytes(body, "model").String()) + } + normalized := normalizeOpenAIReasoningEffortForModel(raw, model) if normalized == "" { return nil } diff --git a/backend/internal/service/gateway_forward_as_responses_test.go b/backend/internal/service/gateway_forward_as_responses_test.go index 6784e7c7a5..262d588629 100644 --- a/backend/internal/service/gateway_forward_as_responses_test.go +++ b/backend/internal/service/gateway_forward_as_responses_test.go @@ -143,7 +143,19 @@ func TestExtractResponsesReasoningEffortFromBody(t *testing.T) { maxGot := ExtractResponsesReasoningEffortFromBody([]byte(`{"model":"deepseek-v4-pro","reasoning":{"effort":"max"}}`)) require.NotNil(t, maxGot) - require.Equal(t, "xhigh", *maxGot) + require.Equal(t, "max", *maxGot) + + mappedMax := ExtractResponsesReasoningEffortFromBody( + []byte(`{"model":"public-alias","reasoning":{"effort":"max"}}`), + "provider/glm-5.2", + "public-alias", + ) + require.NotNil(t, mappedMax) + require.Equal(t, "max", *mappedMax) + + legacyMax := ExtractResponsesReasoningEffortFromBody([]byte(`{"model":"gpt-5.5","reasoning":{"effort":"max"}}`)) + require.NotNil(t, legacyMax) + require.Equal(t, "xhigh", *legacyMax) require.Nil(t, ExtractResponsesReasoningEffortFromBody([]byte(`{"model":"claude-sonnet-4.5"}`))) } diff --git a/backend/internal/service/gemini_chat_completions_compat_service.go b/backend/internal/service/gemini_chat_completions_compat_service.go index 3ee7a4af9e..a56a1b3ebc 100644 --- a/backend/internal/service/gemini_chat_completions_compat_service.go +++ b/backend/internal/service/gemini_chat_completions_compat_service.go @@ -204,7 +204,7 @@ func (s *GeminiMessagesCompatService) forwardClaudeBodyAsChatCompletions( c.Header("x-request-id", requestID) } - reasoningEffort := extractCCReasoningEffortFromBody(originalChatBody) + reasoningEffort := extractCCReasoningEffortFromBody(originalChatBody, mappedModel) // 国产模型默认 effort 补充(本路径上游是 Gemini,不会命中 passback-required)。 // 保持与 OpenAI 网关路径调用模式一致,便于未来上游变异时语义一致。 reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, originalChatBody, mappedModel) diff --git a/backend/internal/service/openai_gateway_chat_completions_anthropic_native.go b/backend/internal/service/openai_gateway_chat_completions_anthropic_native.go index b41bc644c6..d4000f9289 100644 --- a/backend/internal/service/openai_gateway_chat_completions_anthropic_native.go +++ b/backend/internal/service/openai_gateway_chat_completions_anthropic_native.go @@ -132,7 +132,7 @@ func (s *OpenAIGatewayService) forwardChatCompletionsViaNativeAnthropic( return nil, fmt.Errorf("upstream error: %d %s", resp.StatusCode, upstreamMsg) } - reasoningEffort := extractCCReasoningEffortFromBody(body) + reasoningEffort := extractCCReasoningEffortFromBody(body, upstreamModel, billingModel, originalModel) reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, billingModel) if clientStream { diff --git a/backend/internal/service/openai_gateway_request_body.go b/backend/internal/service/openai_gateway_request_body.go index 59199cea05..ad9b6abaea 100644 --- a/backend/internal/service/openai_gateway_request_body.go +++ b/backend/internal/service/openai_gateway_request_body.go @@ -1507,8 +1507,31 @@ func normalizeOpenAIReasoningEffort(raw string) string { } func normalizeOpenAIReasoningEffortForModel(raw, model string) string { - if strings.EqualFold(strings.TrimSpace(raw), "max") && isOpenAIGPT56Model(model) { + if strings.EqualFold(strings.TrimSpace(raw), "max") && supportsOpenAIReasoningEffortMax(model) { return "max" } return normalizeOpenAIReasoningEffort(raw) } + +// supportsOpenAIReasoningEffortMax reports model families whose upstream scale +// has a distinct max level. Other models keep the legacy max -> xhigh behavior. +func supportsOpenAIReasoningEffortMax(model string) bool { + if isOpenAIGPT56Model(model) { + return true + } + + normalized := strings.ToLower(lastOpenAIModelSegment(model)) + normalized = strings.ReplaceAll(normalized, "_", "-") + switch { + case strings.HasPrefix(normalized, "deepseek-v4"): + return true + case strings.HasPrefix(normalized, "glm-"): + return true + case strings.HasPrefix(normalized, "kimi-"), strings.HasPrefix(normalized, "moonshot-"): + return true + case normalized == "k3" || strings.HasPrefix(normalized, "k3-"): + return true + default: + return false + } +} diff --git a/backend/internal/service/openai_gateway_responses_anthropic_native.go b/backend/internal/service/openai_gateway_responses_anthropic_native.go index 04cd7fc274..88cf83310a 100644 --- a/backend/internal/service/openai_gateway_responses_anthropic_native.go +++ b/backend/internal/service/openai_gateway_responses_anthropic_native.go @@ -75,7 +75,7 @@ func (s *OpenAIGatewayService) forwardResponsesViaNativeAnthropic( upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel) anthropicReq.Model = upstreamModel - reasoningEffort := ExtractResponsesReasoningEffortFromBody(body) + reasoningEffort := ExtractResponsesReasoningEffortFromBody(body, upstreamModel, billingModel, originalModel) reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, billingModel) // 5. Force upstream streaming(客户端原始终决定响应格式; diff --git a/backend/internal/service/openai_gateway_service_hotpath_test.go b/backend/internal/service/openai_gateway_service_hotpath_test.go index a51f58bf56..b6c3abca35 100644 --- a/backend/internal/service/openai_gateway_service_hotpath_test.go +++ b/backend/internal/service/openai_gateway_service_hotpath_test.go @@ -920,10 +920,17 @@ func TestExtractOpenAIReasoningEffortFromBody(t *testing.T) { wantValue: "xhigh", }, { - name: "DeepSeek max 归一化为 xhigh", + name: "DeepSeek V4 保留 max", body: []byte(`{"reasoning_effort":"max"}`), model: "deepseek-v4-pro", wantNil: false, + wantValue: "max", + }, + { + name: "旧模型仍将 max 归一化为 xhigh", + body: []byte(`{"reasoning_effort":"max"}`), + model: "gpt-5.5", + wantNil: false, wantValue: "xhigh", }, { diff --git a/backend/internal/service/openai_gpt56_max_test.go b/backend/internal/service/openai_gpt56_max_test.go index cbca2ff3ee..9dc865295c 100644 --- a/backend/internal/service/openai_gpt56_max_test.go +++ b/backend/internal/service/openai_gpt56_max_test.go @@ -24,7 +24,8 @@ func TestNormalizeOpenAIReasoningEffortForGPT56(t *testing.T) { {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"}, + {name: "DeepSeek V4 保留 max", raw: "max", model: "deepseek-v4-pro", want: "max"}, + {name: "旧 GPT 模型沿用 xhigh", raw: "max", model: "gpt-5.5", want: "xhigh"}, } for _, tt := range tests {