Merge pull request #3359 from alfadb/fix/glm-effort-mapping

修复 GLM 推理强度映射
This commit is contained in:
Wesley Liddick
2026-06-21 21:12:46 +08:00
committed by GitHub
4 changed files with 170 additions and 0 deletions
@@ -1269,6 +1269,53 @@ func ApplyThinkingEnabledFallback(effort *string, body []byte, mappedModel strin
return DefaultEffortForThinkingEnabled(mappedModel)
}
// NormalizeGLMOpenAIReasoningEffort rewrites OpenAI Chat Completions
// reasoning_effort values to the GLM native scale used by z.ai: high/max.
// It only applies to glm-* mapped models and leaves all other providers untouched.
func NormalizeGLMOpenAIReasoningEffort(body []byte, mappedModel string) ([]byte, bool) {
if !strings.HasPrefix(strings.ToLower(strings.TrimSpace(mappedModel)), "glm-") {
return body, false
}
path := "reasoning.effort"
raw := strings.TrimSpace(gjson.GetBytes(body, path).String())
if raw == "" {
path = "reasoning_effort"
raw = strings.TrimSpace(gjson.GetBytes(body, path).String())
}
if raw == "" {
return body, false
}
mapped := normalizeGLMOpenAIReasoningEffort(raw)
if mapped == "" || mapped == raw {
return body, false
}
modified, err := sjson.SetBytes(body, path, mapped)
if err != nil {
return body, false
}
return modified, true
}
func normalizeGLMOpenAIReasoningEffort(raw string) string {
value := strings.ToLower(strings.TrimSpace(raw))
if value == "" {
return ""
}
value = strings.NewReplacer("-", "", "_", "", " ", "").Replace(value)
switch value {
case "low", "medium", "high":
return "high"
case "xhigh", "extrahigh", "max", "ultracode":
return "max"
default:
return ""
}
}
// =========================
// Thinking Budget Rectifier
// =========================
@@ -1512,3 +1512,96 @@ func TestApplyThinkingEnabledFallback(t *testing.T) {
})
}
}
func TestNormalizeGLMOpenAIReasoningEffort(t *testing.T) {
tests := []struct {
name string
model string
input string
wantApplied bool
wantPath string
wantValue string
wantUnchanged bool
}{
{
name: "flat xhigh maps to max",
model: "glm-5.2",
input: `{"model":"glm-5.2","reasoning_effort":"xhigh","messages":[]}`,
wantApplied: true,
wantPath: "reasoning_effort",
wantValue: "max",
},
{
name: "flat x-high maps to max",
model: "GLM-5.2",
input: `{"model":"glm-5.2","reasoning_effort":"x-high","messages":[]}`,
wantApplied: true,
wantPath: "reasoning_effort",
wantValue: "max",
},
{
name: "flat ultracode maps to max",
model: "glm-5.2",
input: `{"model":"glm-5.2","reasoning_effort":"ultracode","messages":[]}`,
wantApplied: true,
wantPath: "reasoning_effort",
wantValue: "max",
},
{
name: "flat medium maps to high",
model: "glm-5.2",
input: `{"model":"glm-5.2","reasoning_effort":"medium","messages":[]}`,
wantApplied: true,
wantPath: "reasoning_effort",
wantValue: "high",
},
{
name: "nested high case-normalizes",
model: "glm-5.2",
input: `{"model":"glm-5.2","reasoning":{"effort":"HIGH"},"messages":[]}`,
wantApplied: true,
wantPath: "reasoning.effort",
wantValue: "high",
},
{
name: "native max unchanged",
model: "glm-5.2",
input: `{"model":"glm-5.2","reasoning_effort":"max","messages":[]}`,
wantApplied: false,
wantUnchanged: true,
},
{
name: "non glm unchanged",
model: "deepseek-v4-pro",
input: `{"model":"deepseek-v4-pro","reasoning_effort":"xhigh","messages":[]}`,
wantApplied: false,
wantUnchanged: true,
},
{
name: "missing effort unchanged",
model: "glm-5.2",
input: `{"model":"glm-5.2","messages":[]}`,
wantApplied: false,
wantUnchanged: true,
},
{
name: "unknown effort unchanged",
model: "glm-5.2",
input: `{"model":"glm-5.2","reasoning_effort":"banana","messages":[]}`,
wantApplied: false,
wantUnchanged: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, applied := NormalizeGLMOpenAIReasoningEffort([]byte(tt.input), tt.model)
require.Equal(t, tt.wantApplied, applied)
if tt.wantUnchanged {
require.Equal(t, tt.input, string(got))
return
}
require.Equal(t, tt.wantValue, gjson.GetBytes(got, tt.wantPath).String())
})
}
}
@@ -89,6 +89,9 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions(
if upstreamModel != originalModel {
upstreamBody = ReplaceModelInBody(body, upstreamModel)
}
if normalizedBody, normalized := NormalizeGLMOpenAIReasoningEffort(upstreamBody, upstreamModel); normalized {
upstreamBody = normalizedBody
}
// 4. Apply OpenAI fast policy on the CC body
updatedBody, policyErr := s.applyOpenAIFastPolicyToBody(ctx, account, upstreamModel, upstreamBody)
@@ -224,6 +224,33 @@ func TestForwardAsRawChatCompletions_PreservesDeepSeekReasoningContentInRequest(
require.Equal(t, "get_weather", gjson.GetBytes(upstream.lastBody, "messages.1.tool_calls.0.function.name").String())
}
func TestForwardAsRawChatCompletions_NormalizesGLMReasoningEffortForUpstream(t *testing.T) {
gin.SetMode(gin.TestMode)
body := []byte(`{"model":"glm-5.2","messages":[{"role":"user","content":"hello"}],"reasoning_effort":"xhigh","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"}, "x-request-id": []string{"rid_glm_effort"}},
Body: io.NopCloser(strings.NewReader(`{"id":"chatcmpl_glm","object":"chat.completion","model":"glm-5.2","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3}}`)),
}}
svc := &OpenAIGatewayService{
cfg: rawChatCompletionsTestConfig(),
httpUpstream: upstream,
}
account := rawChatCompletionsTestAccount()
result, err := svc.forwardAsRawChatCompletions(context.Background(), c, account, body, "")
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, "max", gjson.GetBytes(upstream.lastBody, "reasoning_effort").String())
}
func TestForwardAsRawChatCompletions_SilentRefusalTriggersFailover(t *testing.T) {
gin.SetMode(gin.TestMode)