fix: preserve native max reasoning effort

This commit is contained in:
haruka
2026-08-21 14:24:32 +08:00
parent 3548256745
commit d5824f6a5b
10 changed files with 81 additions and 14 deletions
@@ -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
}
@@ -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)
})
@@ -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
}
@@ -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"}`)))
}
@@ -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)
@@ -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 {
@@ -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
}
}
@@ -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(客户端原始终决定响应格式;
@@ -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",
},
{
@@ -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 {