Files
OmniRoute/open-sse/executors/base/reasoningEffort.ts
backryun eebf15f3d0 fix(nvidia): restore GLM-5.2 reasoning on NIM (#7215) (#7296)
* 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 <diegosouza.pw@gmail.com>

* 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 <diegosouza.pw@gmail.com>

* fix(nvidia): restore default executor file-size gate

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-20 10:07:55 -03:00

316 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Provider-aware reasoning_effort sanitation (xhigh/max normalization + reject strip).
// Extracted verbatim from base.ts. Deps are config/services only (no host import → no cycle).
import { PROVIDER_CLAUDE } from "../../services/systemTransforms.ts";
import { isClaudeCodeCompatible } from "../../services/provider.ts";
import { supportsClaudeMaxEffort, supportsXHighEffort } from "../../config/providerModels.ts";
/**
* Sanitize reasoning_effort for providers that don't accept all values.
*
* The claude→openai translator may emit reasoning_effort=max/xhigh when the
* client sends output_config.effort=max on a Claude-shape request. Combined with
* runtime alias remapping (e.g. claude-opus-4-6 → mimo/mimo-v2.5-pro), this
* routes xhigh to OpenAI-shape providers that don't accept the value:
*
* xiaomi-mimo : low|medium|high only — 400 literal_error on xhigh
* mistral : devstral models reject reasoning_effort entirely
* github : claude/haiku/oswe models reject reasoning_effort entirely
*
* Each rejection burns a combo fallback attempt before reaching a working
* provider. Apply provider-aware sanitation here (after transformRequest, so
* reintroductions by per-provider transforms are also caught) before fetch.
* xhigh support is opt-out: pass through unchanged unless the registry marks
* a model as unsupported. Literal max support is provider-specific and
* intentionally separate: some upstreams accept max even when they do not
* accept xhigh. For OpenAI-shape providers, max normalizes to xhigh by default
* and falls back to high only for explicit xhigh opt-outs.
*/
export const MISTRAL_NO_REASONING_EFFORT_PATTERN = /devstral/i;
// GitHub Copilot Claude routing is granular (upstream port: decolua/9router#791):
// ✅ Pass through — Claude Opus 4.6, Claude Sonnet 4.6. Copilot routes both to
// Anthropic's chat/completions surface, which honors reasoning_effort and
// emits visible reasoning tokens (verified upstream: 3× token increase
// between low/medium/high).
// ❌ Strip — Claude Haiku 4.5 and Claude Opus 4.7 (rejected upstream by
// Copilot's Claude backend), older Claude variants, all `haiku`-named
// models, and the `oswe-*` family (Raptor) which still rejects
// reasoning_effort.
// 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<string, unknown> | null;
effortStr: string;
};
/** Pulls a normalized (lowercased) effort string out of top-level or nested `reasoning.effort`. */
function extractNvidiaGlm52Effort(b: Record<string, unknown>): NvidiaGlm52EffortInfo | null {
const reasoning =
b.reasoning && typeof b.reasoning === "object" && !Array.isArray(b.reasoning)
? (b.reasoning as Record<string, unknown>)
: 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<string, unknown> | null {
if (
rawTemplateKwargs !== undefined &&
(!rawTemplateKwargs ||
typeof rawTemplateKwargs !== "object" ||
Array.isArray(rawTemplateKwargs))
) {
return null;
}
const templateKwargs = {
...((rawTemplateKwargs as Record<string, unknown> | 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<string, unknown>,
templateKwargs: Record<string, unknown>,
reasoning: Record<string, unknown> | null
): Record<string, unknown> {
const next: Record<string, unknown> = { ...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<string, unknown>;
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 =
(provider === PROVIDER_CLAUDE || isClaudeCodeCompatible(provider)) &&
supportsClaudeMaxEffort(model);
// opencode-go proxies DeepSeek with the native DeepSeek API contract, which
// accepts {high, max} literally. Without this opt-in, max would be
// normalized to xhigh (the OmniRoute-internal top tier) and rejected by the
// upstream. Scoped to opencode-go deliberately: OpenRouter's DeepSeek path
// (pi#4055) is the documented inverse and expects xhigh, not max.
// Ollama Cloud also accepts literal max (for example GLM 5.2 supports
// low|medium|high|max|none) and rejects xhigh.
const isOpencodeGoDeepSeek =
provider === "opencode-go" && model.toLowerCase().includes("deepseek");
const isOllamaCloud = provider === "ollama-cloud";
const isMoonshotK3 =
(provider === "moonshot" || provider === "kimi") && /^kimi-k3(?:$|-)/i.test(model);
return isClaude || isOpencodeGoDeepSeek || isOllamaCloud || isMoonshotK3;
}
// ── Effort carrier helpers (#7044) ──────────────────────────────────────────
// OmniRoute carries the requested effort on up to three shapes:
// 1. top-level `reasoning_effort` — OpenAI / OmniRoute-internal
// 2. `reasoning.effort` — OpenAI Responses shape
// 3. `output_config.effort` — Anthropic Messages native (Claude Code / Claude passthrough)
// Carrier (3) was previously invisible to this sanitizer, so a native Claude request
// carrying `output_config.effort: "xhigh"` reached providers that don't accept xhigh
// (e.g. claude-sonnet-4-6, supportsXHighEffort=false) unchanged → HTTP 400 (#7044).
interface EffortCarriers {
reasoning: Record<string, unknown> | null;
outputConfig: Record<string, unknown> | null;
hasTopLevelReasoningEffort: boolean;
hasReasoningEffort: boolean;
hasOutputConfigEffort: boolean;
effort: unknown;
}
function readEffortCarriers(b: Record<string, unknown>): EffortCarriers {
const reasoning =
b.reasoning && typeof b.reasoning === "object" && !Array.isArray(b.reasoning)
? (b.reasoning as Record<string, unknown>)
: null;
const outputConfig =
b.output_config && typeof b.output_config === "object" && !Array.isArray(b.output_config)
? (b.output_config as Record<string, unknown>)
: null;
const hasTopLevelReasoningEffort = Object.prototype.hasOwnProperty.call(b, "reasoning_effort");
const hasReasoningEffort = !!(
reasoning && Object.prototype.hasOwnProperty.call(reasoning, "effort")
);
const hasOutputConfigEffort = !!(
outputConfig && Object.prototype.hasOwnProperty.call(outputConfig, "effort")
);
const effort = b.reasoning_effort ?? reasoning?.effort ?? outputConfig?.effort;
return {
reasoning,
outputConfig,
hasTopLevelReasoningEffort,
hasReasoningEffort,
hasOutputConfigEffort,
effort,
};
}
/** Write a normalized effort value back to every carrier that was present. */
function writeEffortValue(
b: Record<string, unknown>,
value: string,
c: EffortCarriers
): Record<string, unknown> {
const next: Record<string, unknown> = { ...b };
if (c.hasTopLevelReasoningEffort) next.reasoning_effort = value;
if (c.hasReasoningEffort && c.reasoning) next.reasoning = { ...c.reasoning, effort: value };
if (c.hasOutputConfigEffort && c.outputConfig)
next.output_config = { ...c.outputConfig, effort: value };
return next;
}
/** Strip the effort field from every carrier that was present. */
function stripEffortValue(
b: Record<string, unknown>,
c: EffortCarriers
): Record<string, unknown> {
const next: Record<string, unknown> = { ...b };
if (c.hasTopLevelReasoningEffort) delete next.reasoning_effort;
if (c.hasReasoningEffort && c.reasoning) {
const r: Record<string, unknown> = { ...c.reasoning };
delete r.effort;
if (Object.keys(r).length === 0) delete next.reasoning;
else next.reasoning = r;
}
if (c.hasOutputConfigEffort && c.outputConfig) {
const oc: Record<string, unknown> = { ...c.outputConfig };
delete oc.effort;
if (Object.keys(oc).length === 0) delete next.output_config;
else next.output_config = oc;
}
return next;
}
export function sanitizeReasoningEffortForProvider(
body: unknown,
provider: string,
model: string | undefined,
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<string, unknown>;
const c = readEffortCarriers(b);
if (c.effort === undefined) return body;
const effortStr = typeof c.effort === "string" ? c.effort.toLowerCase() : "";
const modelStr = model || "";
const githubOptIn =
provider === "github" && GITHUB_REASONING_EFFORT_OPT_IN_PATTERN.test(modelStr);
const rejecting =
(provider === "mistral" && MISTRAL_NO_REASONING_EFFORT_PATTERN.test(modelStr)) ||
(provider === "github" && !githubOptIn && GITHUB_NO_REASONING_EFFORT_PATTERN.test(modelStr));
if (rejecting) {
log?.info?.(
"REASONING_SANITIZE",
`${provider}/${modelStr}: removed unsupported reasoning_effort`
);
return stripEffortValue(b, c);
}
// Native DeepSeek (api.deepseek.com) — V4 thinking mode accepts reasoning_effort
// ONLY as {high, max} (its own top tier is literally "max"). OmniRoute's internal
// scale is low|medium|high|xhigh where xhigh is the top, so map onto DeepSeek's
// vocabulary: xhigh → max (top→top), low|medium → high (below the enum floor).
// high/max pass through unchanged. Without this, the claude→openai translator's
// xhigh (and max-normalized-to-xhigh below) reaches DeepSeek as an unknown value,
// silently dropping the client's requested effort. This is the INVERSE of the
// OpenRouter-DeepSeek path, whose normalized API expects xhigh, not max (pi#4055).
if (provider === "deepseek") {
const mapped =
effortStr === "xhigh" ? "max" : effortStr === "low" || effortStr === "medium" ? "high" : null;
if (mapped && mapped !== effortStr) {
log?.info?.(
"REASONING_SANITIZE",
`deepseek/${modelStr}: normalized reasoning_effort ${effortStr}${mapped}`
);
return writeEffortValue(b, mapped, c);
}
return body;
}
const supportsXHigh = supportsXHighEffort(provider, modelStr);
const shouldDowngradeXHigh = effortStr === "xhigh" && !supportsXHigh;
const supportsXHighForMax = supportsXHigh;
const supportsMax = supportsMaxEffortForProvider(provider, modelStr);
const shouldNormalizeMaxToXHigh = effortStr === "max" && !supportsMax && supportsXHighForMax;
const shouldDowngradeMax = effortStr === "max" && !supportsMax && !supportsXHighForMax;
if (shouldNormalizeMaxToXHigh) {
log?.info?.(
"REASONING_SANITIZE",
`${provider}/${modelStr}: normalized reasoning_effort max → xhigh`
);
return writeEffortValue(b, "xhigh", c);
}
if (shouldDowngradeXHigh || shouldDowngradeMax) {
log?.info?.(
"REASONING_SANITIZE",
`${provider}/${modelStr}: downgraded reasoning_effort ${effortStr} → high`
);
return writeEffortValue(b, "high", c);
}
return body;
}