From 56a40765e930f008277cef055a061c717f7bd7ce Mon Sep 17 00:00:00 2001 From: qingyong Date: Fri, 28 Aug 2026 12:32:26 +0700 Subject: [PATCH] fix(translator): zai thinkingFormat sends reasoning.effort object Z.ai / GLM-5.2+ require a top-level reasoning_effort (low/high/max) alongside thinking:{type:"enabled"} to control reasoning depth; the zai branch previously only set thinking and dropped reasoning_effort, so every GLM-5.x request ran at the model default (max). Gate the field behind GLM-5.2+ (thinkingEffortSupported in capabilities.js) since older GLM (4.x, 5.0, 5.1, 5-turbo, 5v-turbo) do not read it, and map client levels to the exact low/high/max values z.ai accepts. extractThinking now checks reasoning_effort/reasoning.effort before the thinking object so a client-supplied effort is not overwritten by thinking:{type:"enabled"} mapping to mode:auto. Fixes #2721 --- open-sse/providers/capabilities.js | 5 +++ .../translator/concerns/thinkingUnified.js | 30 ++++++++++++----- tests/translator/thinking-unified.test.js | 33 +++++++++++++++++++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/open-sse/providers/capabilities.js b/open-sse/providers/capabilities.js index 6ecf216d..dcdac7ea 100644 --- a/open-sse/providers/capabilities.js +++ b/open-sse/providers/capabilities.js @@ -57,6 +57,7 @@ export const DEFAULT_CAPABILITIES = { thinkingFormat: null, thinkingCanDisable: true, // false → model cannot turn thinking off (clamp to min instead of disable) thinkingRange: null, // { min, max } for budget formats; null = no clamp + thinkingEffortSupported: false, // zai format only: model accepts a reasoning_effort level (GLM-5.2+; older GLM ignores it) // limits (tokens) contextWindow: 200000, maxOutput: 64000, @@ -282,6 +283,10 @@ export const PATTERN_CAPABILITIES = [ { pattern: "*kimi*", caps: { reasoning: true, thinkingFormat: "kimi", contextWindow: 262144 } }, // ── GLM / Z.ai (thinking.enabled; disable via enable_thinking:false) ─ + // reasoning_effort is only read by z.ai from GLM-5.2 onward (docs.z.ai/guides/capabilities/thinking) — + // older GLM (4.x, 5.0, 5.1, 5-turbo, 5v-turbo) ignore it, so gate it per exact version, not the "*glm-5*" catch-all. + { pattern: "*glm-5.3*", caps: { reasoning: true, thinkingFormat: "zai", thinkingEffortSupported: true, contextWindow: 200000, maxOutput: 128000 } }, + { pattern: "*glm-5.2*", caps: { reasoning: true, thinkingFormat: "zai", thinkingEffortSupported: true, contextWindow: 200000, maxOutput: 128000 } }, { pattern: "*glm-5*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } }, { pattern: "*glm-4.7*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } }, { pattern: "*glm-4*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000 } }, diff --git a/open-sse/translator/concerns/thinkingUnified.js b/open-sse/translator/concerns/thinkingUnified.js index d18f47b6..9902c314 100644 --- a/open-sse/translator/concerns/thinkingUnified.js +++ b/open-sse/translator/concerns/thinkingUnified.js @@ -58,6 +58,15 @@ export function extractThinking(body) { return { mode: "level", level: e }; } + // OpenAI chat / Responses shape — check effort first (zai sends both thinking object and reasoning.effort) + const effort = body.reasoning_effort ?? (typeof body.reasoning === "object" ? body.reasoning?.effort : null); + if (typeof effort === "string" && effort) { + const e = effort.toLowerCase(); + if (e === "none" || e === "off") return { mode: "none" }; + if (e === "auto") return { mode: "auto" }; + return { mode: "level", level: e }; + } + // Claude shape const t = body.thinking; if (t && typeof t === "object") { @@ -69,15 +78,6 @@ export function extractThinking(body) { } } - // OpenAI chat / Responses shape - const effort = body.reasoning_effort ?? (typeof body.reasoning === "object" ? body.reasoning?.effort : null); - if (typeof effort === "string" && effort) { - const e = effort.toLowerCase(); - if (e === "none" || e === "off") return { mode: "none" }; - if (e === "auto") return { mode: "auto" }; - return { mode: "level", level: e }; - } - // Gemini shape (top-level, generationConfig, or request envelope) const tc = body.thinkingConfig || body.generationConfig?.thinkingConfig || body.request?.generationConfig?.thinkingConfig; if (tc && typeof tc === "object") { @@ -270,6 +270,18 @@ function applyFormat(fmt, body, cfg, caps, supportedLevels) { // Z.ai ignores thinking.disabled → must use enable_thinking:false to turn off. if (none && canDisable) { body.enable_thinking = false; delete body.thinking; break; } body.thinking = { type: "enabled" }; + // reasoning_effort is only read by z.ai from GLM-5.2 onward — older GLM ignores it + // (see thinkingEffortSupported in capabilities.js). Skip on unsupported models so we + // don't send a field the API doesn't recognize. + if (caps.thinkingEffortSupported) { + const zaiLvl = toLevel(eff); + // GLM-5.3 only accepts exactly low|high|max (anything else errors); GLM-5.2 accepts + // a wider set but z.ai maps low/medium->high and xhigh->max server-side anyway, so + // this 3-value mapping matches both. + body.reasoning_effort = (zaiLvl === "low" || zaiLvl === "minimal") ? "low" + : (zaiLvl === "high" || zaiLvl === "medium") ? "high" + : "max"; + } break; } case "qwen": { diff --git a/tests/translator/thinking-unified.test.js b/tests/translator/thinking-unified.test.js index ee48e3bb..e6214008 100644 --- a/tests/translator/thinking-unified.test.js +++ b/tests/translator/thinking-unified.test.js @@ -58,6 +58,18 @@ describe("extractThinking", () => { it("no intent → null", () => { expect(extractThinking({ messages: [] })).toBeNull(); }); + it("reasoning_effort wins over thinking:{type:enabled} (no budget)", () => { + expect(extractThinking({ + thinking: { type: "enabled" }, + reasoning_effort: "high", + })).toEqual({ mode: "level", level: "high" }); + }); + it("reasoning.effort wins over thinking:{type:enabled} (no budget)", () => { + expect(extractThinking({ + thinking: { type: "enabled" }, + reasoning: { effort: "medium" }, + })).toEqual({ mode: "level", level: "medium" }); + }); }); describe("applyThinking per provider format", () => { @@ -114,6 +126,27 @@ describe("applyThinking per provider format", () => { expect(out.enable_thinking).toBe(false); expect(out.thinking).toBeUndefined(); }); + it.each([ + ["high", "high"], + ["max", "max"], + ["xhigh", "max"], + ["low", "low"], + ["medium", "high"], + ["minimal", "low"], + ])("GLM-5.3 %s → reasoning_effort=%s (low|high|max only, per z.ai docs)", (input, expected) => { + const out = apply("openai", "glm-5.3", { reasoning_effort: input }, "glm-cn"); + expect(out.thinking).toEqual({ type: "enabled" }); + expect(out.reasoning_effort).toBe(expected); + }); + it("GLM-5.2 also gets reasoning_effort (supported from 5.2 onward)", () => { + const out = apply("openai", "glm-5.2", { reasoning_effort: "low" }, "glm-cn"); + expect(out.reasoning_effort).toBe("low"); + }); + it("GLM-4.7 (pre-5.2) does not get reasoning_effort — z.ai ignores it", () => { + const out = apply("openai", "glm-4.7", { reasoning_effort: "low" }, "glm-cn"); + expect(out.thinking).toEqual({ type: "enabled" }); + expect(out.reasoning_effort).toBeUndefined(); + }); it("Qwen on → enable_thinking + thinking_budget", () => { const out = apply("openai", "qwen3-max", { reasoning_effort: "medium" }, "qwen"); expect(out.enable_thinking).toBe(true);