From eebf15f3d01932b4ee5073c84291b8d64920d880 Mon Sep 17 00:00:00 2001 From: backryun Date: Mon, 20 Jul 2026 22:07:55 +0900 Subject: [PATCH] fix(nvidia): restore GLM-5.2 reasoning on NIM (#7215) (#7296) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(nvidia): map GLM-5.2 reasoning to thinking toggle Fixes #7215. * fix(nvidia): shrink default.ts under the file-size ratchet The GLM-5.2 reasoning-mapping call in requestBodyDefaults() pushed open-sse/executors/default.ts from 877 to 881 lines, tripping the frozen check:file-size ceiling (Fast Quality Gates). withDefaults is typed unknown, so the `as typeof withDefaults` cast added by the multi-line call was unnecessary — collapsing to a single-line call removes the cast and the line-wrap, landing the file at 876 lines. Co-authored-by: diegosouzapw * refactor(nvidia): extract mapNvidiaGlm52ReasoningParams helpers to clear complexity ratchet mapNvidiaGlm52ReasoningParams landed at cyclomatic complexity 24 (limit 15), a brand-new violation that pushed the project-wide complexity ratchet from 2056 to 2057 (Fast Quality Gates: check:complexity-ratchets). It was previously masked by the file-size failure aborting the job before this step ran. Split the function into three single-purpose helpers — effort extraction, chat_template_kwargs construction, and the reasoning_effort/reasoning.effort strip — bringing the orchestrating function's complexity back under threshold with no behavior change (all 41 cases in tests/unit/base-executor-sanitize-effort.test.ts still pass unchanged). Co-authored-by: diegosouzapw * fix(nvidia): restore default executor file-size gate --------- Co-authored-by: diegosouzapw --- open-sse/executors/base/reasoningEffort.ts | 105 ++++++++++++++++- open-sse/executors/default.ts | 10 +- .../base-executor-sanitize-effort.test.ts | 109 ++++++++++++++++++ 3 files changed, 218 insertions(+), 6 deletions(-) diff --git a/open-sse/executors/base/reasoningEffort.ts b/open-sse/executors/base/reasoningEffort.ts index 9f9e39e497..b613f0c3c0 100644 --- a/open-sse/executors/base/reasoningEffort.ts +++ b/open-sse/executors/base/reasoningEffort.ts @@ -38,6 +38,105 @@ export const MISTRAL_NO_REASONING_EFFORT_PATTERN = /devstral/i; // Order matters: the opt-in check must run BEFORE the broad Claude/haiku/oswe strip. export const GITHUB_REASONING_EFFORT_OPT_IN_PATTERN = /claude[-_.]?(?:opus|sonnet)[-_.]?4[-_.]6/i; export const GITHUB_NO_REASONING_EFFORT_PATTERN = /(claude|haiku|oswe)/i; +const NVIDIA_GLM_52_PATTERN = /z-ai\/glm-5\.2\b/i; + +type ReasoningSanitizeLog = { + info?: (tag: string, msg: string) => void; +}; + +function isNvidiaGlm52(provider: string, model: string | undefined): boolean { + return provider === "nvidia" && NVIDIA_GLM_52_PATTERN.test(model || ""); +} + +type NvidiaGlm52EffortInfo = { + reasoning: Record | null; + effortStr: string; +}; + +/** Pulls a normalized (lowercased) effort string out of top-level or nested `reasoning.effort`. */ +function extractNvidiaGlm52Effort(b: Record): NvidiaGlm52EffortInfo | null { + const reasoning = + b.reasoning && typeof b.reasoning === "object" && !Array.isArray(b.reasoning) + ? (b.reasoning as Record) + : null; + const effort = b.reasoning_effort ?? reasoning?.effort; + if (effort === undefined) return null; + + const effortStr = typeof effort === "string" ? effort.toLowerCase() : ""; + if (!effortStr) return null; + + return { reasoning, effortStr }; +} + +/** Builds `chat_template_kwargs.enable_thinking`, or null when the existing kwargs shape is unusable. */ +function buildNvidiaGlm52TemplateKwargs( + rawTemplateKwargs: unknown, + effortStr: string +): Record | null { + if ( + rawTemplateKwargs !== undefined && + (!rawTemplateKwargs || + typeof rawTemplateKwargs !== "object" || + Array.isArray(rawTemplateKwargs)) + ) { + return null; + } + + const templateKwargs = { + ...((rawTemplateKwargs as Record | undefined) ?? {}), + }; + if (!Object.prototype.hasOwnProperty.call(templateKwargs, "enable_thinking")) { + templateKwargs.enable_thinking = effortStr !== "none"; + } + return templateKwargs; +} + +/** Returns a copy of `b` with `reasoning_effort`/`reasoning.effort` replaced by `templateKwargs`. */ +function withNvidiaGlm52TemplateKwargs( + b: Record, + templateKwargs: Record, + reasoning: Record | null +): Record { + const next: Record = { ...b, chat_template_kwargs: templateKwargs }; + delete next.reasoning_effort; + if (reasoning) { + const nextReasoning = { ...reasoning }; + delete nextReasoning.effort; + if (Object.keys(nextReasoning).length === 0) delete next.reasoning; + else next.reasoning = nextReasoning; + } + return next; +} + +/** + * Map OmniRoute's reasoning-effort inputs onto the binary thinking switch exposed by + * NVIDIA's hosted GLM-5.2 chat template. This runs before DefaultExecutor's unsupported + * parameter stripping so a nested `reasoning.effort` is not discarded first, and is also + * reused by the final provider sanitizer for non-default execution paths. + */ +export function mapNvidiaGlm52ReasoningParams( + body: unknown, + provider: string, + model: string | undefined, + log?: ReasoningSanitizeLog | null +): unknown { + if (!isNvidiaGlm52(provider, model)) return body; + if (!body || typeof body !== "object" || Array.isArray(body)) return body; + + const b = body as Record; + const info = extractNvidiaGlm52Effort(b); + if (!info) return body; + + const templateKwargs = buildNvidiaGlm52TemplateKwargs(b.chat_template_kwargs, info.effortStr); + if (!templateKwargs) return body; + + const next = withNvidiaGlm52TemplateKwargs(b, templateKwargs, info.reasoning); + log?.info?.( + "REASONING_SANITIZE", + `nvidia/${model || ""}: mapped reasoning effort to enable_thinking` + ); + return next; +} export function supportsMaxEffortForProvider(provider: string, model: string): boolean { const isClaude = @@ -142,8 +241,12 @@ export function sanitizeReasoningEffortForProvider( body: unknown, provider: string, model: string | undefined, - log?: { info?: (tag: string, msg: string) => void } | null + log?: ReasoningSanitizeLog | null ): unknown { + if (isNvidiaGlm52(provider, model)) { + return mapNvidiaGlm52ReasoningParams(body, provider, model, log); + } + if (!body || typeof body !== "object" || Array.isArray(body)) return body; const b = body as Record; const c = readEffortCarriers(b); diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 0065625af6..bc5685f521 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -1,4 +1,5 @@ import { BaseExecutor, type ExecuteInput } from "./base.ts"; +import { mapNvidiaGlm52ReasoningParams } from "./base/reasoningEffort.ts"; import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts"; import { getAccessToken } from "../services/tokenRefresh.ts"; @@ -701,14 +702,13 @@ export class DefaultExecutor extends BaseExecutor { ); } - // Config-driven strip of params unsupported by the target provider/model - // (e.g. claude-opus-4 deprecated `temperature` → Anthropic 400). Port from - // 9router#7ae9fff6 (fixes upstream #1748). Rules live in - // ../translator/paramSupport.ts so adding one means editing one table. + // Strip params unsupported by the target provider/model before sending upstream. + // Rules live in ../translator/paramSupport.ts (9router#7ae9fff6; fixes #1748). if (typeof withDefaults === "object" && withDefaults !== null) { const bodyRecord = withDefaults as Record; const outboundModel = typeof bodyRecord.model === "string" ? bodyRecord.model : model; - stripUnsupportedParams(this.provider, outboundModel, bodyRecord); + withDefaults = mapNvidiaGlm52ReasoningParams(bodyRecord, this.provider, outboundModel); + stripUnsupportedParams(this.provider, outboundModel, withDefaults as Record); } // Apply modelIdPrefix from RegistryEntry (e.g. "accounts/fireworks/models/") diff --git a/tests/unit/base-executor-sanitize-effort.test.ts b/tests/unit/base-executor-sanitize-effort.test.ts index 943e09b95a..2fda8aab10 100644 --- a/tests/unit/base-executor-sanitize-effort.test.ts +++ b/tests/unit/base-executor-sanitize-effort.test.ts @@ -2,6 +2,7 @@ import test from "node:test"; import assert from "node:assert/strict"; const { sanitizeReasoningEffortForProvider } = await import("../../open-sse/executors/base.ts"); +const { DefaultExecutor } = await import("../../open-sse/executors/default.ts"); function makeLog() { const messages: Array<[string, string]> = []; @@ -394,6 +395,114 @@ test("sanitizeReasoningEffortForProvider: non-object body returns unchanged", () assert.equal(sanitizeReasoningEffortForProvider(arr, "xiaomi-mimo", "x", null), arr); }); +// ── NVIDIA NIM GLM-5.2 (#7215) ───────────────────────────────────────────── + +test("sanitizeReasoningEffortForProvider: NVIDIA GLM-5.2 enables thinking for active effort", () => { + for (const effort of ["low", "medium", "high", "xhigh", "max"]) { + const body = { reasoning_effort: effort, messages: [] }; + const result = sanitizeReasoningEffortForProvider( + body, + "nvidia", + "z-ai/glm-5.2", + null + ) as Record; + + assert.notEqual(result, body); + assert.equal(result.reasoning_effort, undefined); + assert.deepEqual(result.chat_template_kwargs, { enable_thinking: true }); + } +}); + +test("sanitizeReasoningEffortForProvider: NVIDIA GLM-5.2 maps none to thinking off", () => { + const result = sanitizeReasoningEffortForProvider( + { reasoning_effort: "none", messages: [] }, + "nvidia", + "z-ai/glm-5.2", + null + ) as Record; + + assert.equal(result.reasoning_effort, undefined); + assert.deepEqual(result.chat_template_kwargs, { enable_thinking: false }); +}); + +test("sanitizeReasoningEffortForProvider: NVIDIA GLM-5.2 maps nested reasoning.effort", () => { + const result = sanitizeReasoningEffortForProvider( + { reasoning: { effort: "high" }, messages: [] }, + "nvidia", + "z-ai/glm-5.2", + null + ) as Record; + + assert.equal(result.reasoning, undefined); + assert.deepEqual(result.chat_template_kwargs, { enable_thinking: true }); +}); + +test("DefaultExecutor: NVIDIA GLM-5.2 maps nested effort before unsupported-param stripping", () => { + const result = new DefaultExecutor("nvidia").transformRequest( + "z-ai/glm-5.2", + { + model: "z-ai/glm-5.2", + reasoning: { effort: "high", summary: "auto" }, + messages: [], + }, + false, + {} + ) as Record; + + assert.equal(result.reasoning, undefined); + assert.equal(result.reasoning_effort, undefined); + assert.deepEqual(result.chat_template_kwargs, { enable_thinking: true }); +}); + +test("DefaultExecutor: NVIDIA reasoning levels remain intact for GPT-OSS", () => { + const result = new DefaultExecutor("nvidia").transformRequest( + "openai/gpt-oss-120b", + { + model: "openai/gpt-oss-120b", + reasoning_effort: "low", + messages: [], + }, + false, + {} + ) as Record; + + assert.equal(result.reasoning_effort, "low"); + assert.equal(result.chat_template_kwargs, undefined); +}); + +test("sanitizeReasoningEffortForProvider: NVIDIA GLM-5.2 preserves a native thinking switch", () => { + const result = sanitizeReasoningEffortForProvider( + { + reasoning_effort: "xhigh", + chat_template_kwargs: { enable_thinking: false, custom_flag: true }, + messages: [], + }, + "nvidia", + "z-ai/glm-5.2", + null + ) as Record; + + assert.equal(result.reasoning_effort, undefined); + assert.deepEqual(result.chat_template_kwargs, { + enable_thinking: false, + custom_flag: true, + }); +}); + +test("sanitizeReasoningEffortForProvider: NVIDIA GLM-5.2 mapping is narrowly scoped", () => { + const otherModel = { reasoning_effort: "high", messages: [] }; + const otherProvider = { reasoning_effort: "high", messages: [] }; + + assert.equal( + sanitizeReasoningEffortForProvider(otherModel, "nvidia", "z-ai/glm-5.1", null), + otherModel + ); + assert.equal( + sanitizeReasoningEffortForProvider(otherProvider, "openai", "z-ai/glm-5.2", null), + otherProvider + ); +}); + // ── Native DeepSeek (api.deepseek.com) ─────────────────────────────────────── // DeepSeek V4 thinking mode accepts reasoning_effort ONLY as {high, max}. The // internal OmniRoute scale (low|medium|high|xhigh, xhigh = top) must be mapped