mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #6507 from wucm667/fix/issue-6501-glm53-thinking-effort
fix(gateway): normalize GLM-5.3 thinking effort
This commit is contained in:
@@ -1388,6 +1388,9 @@ func NormalizeGLMOpenAIReasoningEffort(body []byte, mappedModel string) ([]byte,
|
||||
}
|
||||
|
||||
mapped := normalizeGLMOpenAIReasoningEffort(raw)
|
||||
if isGLM53Model(mappedModel) && mapped == "high" && normalizeEffortToken(raw) == "low" {
|
||||
mapped = "low"
|
||||
}
|
||||
if mapped == "" || mapped == raw {
|
||||
return body, false
|
||||
}
|
||||
@@ -1399,12 +1402,20 @@ func NormalizeGLMOpenAIReasoningEffort(body []byte, mappedModel string) ([]byte,
|
||||
return modified, true
|
||||
}
|
||||
|
||||
func normalizeGLMOpenAIReasoningEffort(raw string) string {
|
||||
func normalizeEffortToken(raw string) string {
|
||||
value := strings.ToLower(strings.TrimSpace(raw))
|
||||
return strings.NewReplacer("-", "", "_", "", " ", "").Replace(value)
|
||||
}
|
||||
|
||||
func isGLM53Model(model string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(model), "glm-5.3")
|
||||
}
|
||||
|
||||
func normalizeGLMOpenAIReasoningEffort(raw string) string {
|
||||
value := normalizeEffortToken(raw)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
value = strings.NewReplacer("-", "", "_", "", " ", "").Replace(value)
|
||||
|
||||
switch value {
|
||||
case "low", "medium", "high":
|
||||
@@ -1416,6 +1427,42 @@ func normalizeGLMOpenAIReasoningEffort(raw string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeGLM53AnthropicThinking maps explicit client thinking effort onto the
|
||||
// GLM-5.3 Anthropic-compatible scale. Requests without an effort or thinking
|
||||
// preference are left unchanged so the upstream default remains in effect.
|
||||
func NormalizeGLM53AnthropicThinking(body []byte, mappedModel string) ([]byte, bool) {
|
||||
if !isGLM53Model(mappedModel) {
|
||||
return body, false
|
||||
}
|
||||
|
||||
raw := gjson.GetBytes(body, "output_config.effort").String()
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
raw = gjson.GetBytes(body, "thinking.type").String()
|
||||
}
|
||||
|
||||
var effort string
|
||||
switch normalizeEffortToken(raw) {
|
||||
case "disabled", "off", "none", "minimal", "low":
|
||||
effort = "low"
|
||||
case "enabled", "adaptive", "medium", "high":
|
||||
effort = "high"
|
||||
case "xhigh", "max", "ultra":
|
||||
effort = "max"
|
||||
default:
|
||||
return body, false
|
||||
}
|
||||
|
||||
modified, err := sjson.SetBytes(body, "thinking.type", "enabled")
|
||||
if err != nil {
|
||||
return body, false
|
||||
}
|
||||
modified, err = sjson.SetBytes(modified, "output_config.effort", effort)
|
||||
if err != nil {
|
||||
return body, false
|
||||
}
|
||||
return modified, true
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Thinking Budget Rectifier
|
||||
// =========================
|
||||
|
||||
@@ -1605,6 +1605,14 @@ func TestNormalizeGLMOpenAIReasoningEffort(t *testing.T) {
|
||||
wantPath: "reasoning_effort",
|
||||
wantValue: "high",
|
||||
},
|
||||
{
|
||||
name: "glm 5.2 low maps to high",
|
||||
model: "glm-5.2",
|
||||
input: `{"model":"glm-5.2","reasoning_effort":"low","messages":[]}`,
|
||||
wantApplied: true,
|
||||
wantPath: "reasoning_effort",
|
||||
wantValue: "high",
|
||||
},
|
||||
{
|
||||
name: "nested high case-normalizes",
|
||||
model: "glm-5.2",
|
||||
@@ -1613,6 +1621,13 @@ func TestNormalizeGLMOpenAIReasoningEffort(t *testing.T) {
|
||||
wantPath: "reasoning.effort",
|
||||
wantValue: "high",
|
||||
},
|
||||
{
|
||||
name: "glm 5.3 native low unchanged",
|
||||
model: "glm-5.3",
|
||||
input: `{"model":"glm-5.3","reasoning_effort":"low","messages":[]}`,
|
||||
wantApplied: false,
|
||||
wantUnchanged: true,
|
||||
},
|
||||
{
|
||||
name: "native max unchanged",
|
||||
model: "glm-5.2",
|
||||
|
||||
@@ -57,6 +57,9 @@ func (s *OpenAIGatewayService) forwardAnthropicViaNativeAnthropicEndpoint(
|
||||
}
|
||||
body = rewritten
|
||||
}
|
||||
if normalized, changed := NormalizeGLM53AnthropicThinking(body, upstreamModel); changed {
|
||||
body = normalized
|
||||
}
|
||||
|
||||
// 记录客户端请求的推理强度:优先 Claude 协议的 output_config.effort;
|
||||
// 缺失且 thinking 已启用时,按国产 passback-required 模型兜底为 high
|
||||
|
||||
@@ -4,6 +4,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// 国产供应商原生 Anthropic 直通路径(api_protocol=anthropic)的 reasoning_effort 记录。
|
||||
@@ -35,6 +37,13 @@ func nativeAnthropicTestAccount() *Account {
|
||||
}
|
||||
}
|
||||
|
||||
func nativeAnthropicGLMTestAccount() *Account {
|
||||
account := nativeAnthropicTestAccount()
|
||||
account.Name = "zhipu-native"
|
||||
account.Platform = PlatformZhipu
|
||||
return account
|
||||
}
|
||||
|
||||
func nativeAnthropicBufferedResponse() *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
@@ -137,3 +146,70 @@ func TestNativeAnthropicPassthroughNoEffortStaysNil(t *testing.T) {
|
||||
require.NotNil(t, result)
|
||||
require.Nil(t, result.ReasoningEffort)
|
||||
}
|
||||
|
||||
func TestNativeAnthropicPassthroughNormalizesGLM53Thinking(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
tests := []struct {
|
||||
name string
|
||||
stream bool
|
||||
preference string
|
||||
wantEffort string
|
||||
}{
|
||||
{name: "disabled buffered", preference: `"thinking":{"type":"disabled"},`, wantEffort: "low"},
|
||||
{name: "off buffered", preference: `"thinking":{"type":"off"},`, wantEffort: "low"},
|
||||
{name: "none buffered", preference: `"thinking":{"type":"none"},`, wantEffort: "low"},
|
||||
{name: "enabled buffered", preference: `"thinking":{"type":"enabled"},`, wantEffort: "high"},
|
||||
{name: "enabled streaming", stream: true, preference: `"thinking":{"type":"enabled"},`, wantEffort: "high"},
|
||||
{name: "adaptive buffered", preference: `"thinking":{"type":"adaptive"},`, wantEffort: "high"},
|
||||
{name: "adaptive streaming", stream: true, preference: `"thinking":{"type":"adaptive"},`, wantEffort: "high"},
|
||||
{name: "minimal buffered", preference: `"output_config":{"effort":"minimal"},`, wantEffort: "low"},
|
||||
{name: "low buffered", preference: `"output_config":{"effort":"low"},`, wantEffort: "low"},
|
||||
{name: "medium streaming", stream: true, preference: `"output_config":{"effort":"medium"},`, wantEffort: "high"},
|
||||
{name: "high buffered", preference: `"output_config":{"effort":"high"},`, wantEffort: "high"},
|
||||
{name: "xhigh buffered", preference: `"output_config":{"effort":"xhigh"},`, wantEffort: "max"},
|
||||
{name: "max buffered", preference: `"output_config":{"effort":"max"},`, wantEffort: "max"},
|
||||
{name: "ultra buffered", preference: `"output_config":{"effort":"ultra"},`, wantEffort: "max"},
|
||||
{name: "output effort wins over thinking", preference: `"thinking":{"type":"adaptive"},"output_config":{"effort":"low"},`, wantEffort: "low"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
body := []byte(fmt.Sprintf(`{"model":"glm-5.3","max_tokens":32,"stream":%t,%s"messages":[{"role":"user","content":"hi"}]}`, tt.stream, tt.preference))
|
||||
response := nativeAnthropicBufferedResponse()
|
||||
if tt.stream {
|
||||
response = nativeAnthropicStreamResponse()
|
||||
}
|
||||
upstream := &httpUpstreamRecorder{resp: response}
|
||||
svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
|
||||
|
||||
_, err := svc.ForwardAsAnthropic(context.Background(),
|
||||
adaptiveProtocolTestContext("/v1/messages", body), nativeAnthropicGLMTestAccount(), body, "", "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "enabled", gjson.GetBytes(upstream.lastBody, "thinking.type").String())
|
||||
require.Equal(t, tt.wantEffort, gjson.GetBytes(upstream.lastBody, "output_config.effort").String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeAnthropicPassthroughLeavesOtherThinkingUntouched(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
}{
|
||||
{name: "glm 5.3 unspecified", body: `{"model":"glm-5.3","max_tokens":32,"stream":false,"messages":[]}`},
|
||||
{name: "glm 5.2 disabled", body: `{"model":"glm-5.2","max_tokens":32,"stream":false,"thinking":{"type":"disabled"},"messages":[]}`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
body := []byte(tt.body)
|
||||
upstream := &httpUpstreamRecorder{resp: nativeAnthropicBufferedResponse()}
|
||||
svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
|
||||
_, err := svc.ForwardAsAnthropic(context.Background(),
|
||||
adaptiveProtocolTestContext("/v1/messages", body), nativeAnthropicGLMTestAccount(), body, "", "")
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, tt.body, string(upstream.lastBody))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user