mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix(openai): normalize glm reasoning effort
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user