mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:08:03 +08:00
fix: preserve native max reasoning effort
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user