mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 09:42:15 +03:00
fix: pass max reasoning effort through by default, add global model registry fallback (#8057)
This commit is contained in:
committed by
diegosouzapw
parent
aae408f585
commit
dbf6cff7d7
@@ -1673,7 +1673,7 @@
|
||||
},
|
||||
"tests/unit/base-executor-sanitize-effort.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 48
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"tests/unit/batch-deletion.test.ts": {
|
||||
@@ -3339,4 +3339,4 @@
|
||||
"count": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,10 +90,65 @@ export function getDefaultModel(aliasOrId: string): string | null {
|
||||
return models?.[0]?.id || null;
|
||||
}
|
||||
|
||||
/** Score a registry entry by how many capability flags it defines. */
|
||||
function modelRichness(m: RegistryModel): number {
|
||||
let score = 0;
|
||||
if (m.supportsXHighEffort !== undefined) score += 10; // critical for effort routing
|
||||
if (m.supportsReasoning !== undefined) score += 5;
|
||||
if (m.contextLength !== undefined) score += 3;
|
||||
if (m.maxOutputTokens !== undefined) score += 2;
|
||||
if (m.supportsVision !== undefined) score += 2;
|
||||
if (m.toolCalling !== undefined) score += 2;
|
||||
if (m.interleavedField !== undefined) score += 1;
|
||||
if (m.unsupportedParams !== undefined) score += 1;
|
||||
return score;
|
||||
}
|
||||
|
||||
function getGlobalModel(modelId: string): RegistryModel | undefined {
|
||||
// 1. Exact match — collect all, pick the richest
|
||||
let candidates: RegistryModel[] = [];
|
||||
for (const models of Object.values(PROVIDER_MODELS)) {
|
||||
const found = models.find((m) => m.id === modelId);
|
||||
if (found) candidates.push(found);
|
||||
}
|
||||
if (candidates.length > 0) {
|
||||
return candidates.sort((a, b) => modelRichness(b) - modelRichness(a))[0];
|
||||
}
|
||||
|
||||
// 2. Strip provider prefix (e.g. moonshotai/kimi-k3-free -> kimi-k3-free)
|
||||
const basename = modelId.split("/").pop() || modelId;
|
||||
candidates = [];
|
||||
for (const models of Object.values(PROVIDER_MODELS)) {
|
||||
const found = models.find((m) => m.id === basename);
|
||||
if (found) candidates.push(found);
|
||||
}
|
||||
if (candidates.length > 0) {
|
||||
return candidates.sort((a, b) => modelRichness(b) - modelRichness(a))[0];
|
||||
}
|
||||
|
||||
// 3. Substring match for base model name (e.g. kimi-k3-free -> kimi-k3)
|
||||
// Finds the longest matching base model ID; on ties, prefers the richer entry.
|
||||
let bestMatch: RegistryModel | undefined;
|
||||
for (const models of Object.values(PROVIDER_MODELS)) {
|
||||
for (const m of models) {
|
||||
if (basename.startsWith(m.id)) {
|
||||
if (
|
||||
!bestMatch ||
|
||||
m.id.length > bestMatch.id.length ||
|
||||
(m.id.length === bestMatch.id.length && modelRichness(m) > modelRichness(bestMatch))
|
||||
) {
|
||||
bestMatch = m;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
export function getProviderModel(aliasOrId: string, modelId: string): RegistryModel | undefined {
|
||||
const models = PROVIDER_MODELS[aliasOrId];
|
||||
if (!models) return undefined;
|
||||
return models.find((model) => model.id === modelId);
|
||||
if (!models) return getGlobalModel(modelId);
|
||||
return models.find((model) => model.id === modelId) || getGlobalModel(modelId);
|
||||
}
|
||||
|
||||
export function isValidModel(
|
||||
@@ -103,26 +158,20 @@ export function isValidModel(
|
||||
): boolean {
|
||||
if (passthroughProviders.has(aliasOrId)) return true;
|
||||
const models = PROVIDER_MODELS[aliasOrId];
|
||||
if (!models) return false;
|
||||
return models.some((m) => m.id === modelId);
|
||||
if (!models) return !!getGlobalModel(modelId);
|
||||
return models.some((m) => m.id === modelId) || !!getGlobalModel(modelId);
|
||||
}
|
||||
|
||||
export function findModelName(aliasOrId: string, modelId: string): string {
|
||||
const models = PROVIDER_MODELS[aliasOrId];
|
||||
if (!models) return modelId;
|
||||
const found = models.find((m) => m.id === modelId);
|
||||
if (!models) return getGlobalModel(modelId)?.name || modelId;
|
||||
const found = models.find((m) => m.id === modelId) || getGlobalModel(modelId);
|
||||
return found?.name || modelId;
|
||||
}
|
||||
|
||||
export function getModelTargetFormat(aliasOrId: string, modelId: string): string | null {
|
||||
const models = PROVIDER_MODELS[aliasOrId];
|
||||
// Strip provider prefix if present: "openai/gpt-5.6-luna" → "gpt-5.6-luna"
|
||||
const prefix = aliasOrId + "/";
|
||||
const bareModelId =
|
||||
typeof modelId === "string" && modelId.startsWith(prefix)
|
||||
? modelId.slice(prefix.length)
|
||||
: modelId;
|
||||
const found = models?.find((m) => m.id === bareModelId);
|
||||
const found = models?.find((m) => m.id === modelId) || getGlobalModel(modelId);
|
||||
if (found?.targetFormat) return found.targetFormat;
|
||||
// #5842: OpenAI "*-pro" reasoning models (o1-pro, gpt-5.x-pro) are only served by
|
||||
// the native /v1/responses endpoint — /v1/chat/completions 404s ("only supported
|
||||
@@ -130,14 +179,17 @@ export function getModelTargetFormat(aliasOrId: string, modelId: string): string
|
||||
// covers dynamically-synced ids that post-date the catalog (same spirit as the gh
|
||||
// executor's /codex/i routing, 9router#102). Scoped to the openai alias so other
|
||||
// providers shipping *-pro ids keep their own endpoint semantics.
|
||||
if (aliasOrId === "openai" && /-pro$/i.test(bareModelId)) return "openai-responses";
|
||||
if (aliasOrId === "openai" && /-pro$/i.test(modelId)) return "openai-responses";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getModelStripTypes(aliasOrId: string, modelId: string): string[] {
|
||||
const models = PROVIDER_MODELS[aliasOrId];
|
||||
if (!models) return [];
|
||||
const found = models.find((m) => m.id === modelId);
|
||||
if (!models)
|
||||
return Array.isArray(getGlobalModel(modelId)?.strip)
|
||||
? [...getGlobalModel(modelId)!.strip!]
|
||||
: [];
|
||||
const found = models.find((m) => m.id === modelId) || getGlobalModel(modelId);
|
||||
return Array.isArray(found?.strip) ? [...found.strip] : [];
|
||||
}
|
||||
|
||||
@@ -262,7 +314,7 @@ function resolveProviderModelList(aliasOrId: string): {
|
||||
|
||||
export function supportsXHighEffort(aliasOrId: string, modelId: string): boolean {
|
||||
const { models: providerModels } = resolveProviderModelList(aliasOrId);
|
||||
const model = providerModels?.find((entry) => entry.id === modelId);
|
||||
const model = providerModels?.find((entry) => entry.id === modelId) || getGlobalModel(modelId);
|
||||
if (model?.supportsXHighEffort !== undefined) {
|
||||
return model.supportsXHighEffort !== false;
|
||||
}
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
// 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";
|
||||
import {
|
||||
supportsClaudeMaxEffort,
|
||||
supportsXHighEffort,
|
||||
getProviderModel,
|
||||
} from "../../config/providerModels.ts";
|
||||
|
||||
/**
|
||||
* Sanitize reasoning_effort for providers that don't accept all values.
|
||||
*
|
||||
* The claude→openai translator passes output_config.effort through verbatim
|
||||
* (including max) and only performs form conversion; provider-aware effort
|
||||
* policy is owned here. Combined with runtime alias remapping (e.g.
|
||||
* claude-opus-4-6 → mimo/mimo-v2.5-pro), this routes a client's effort value
|
||||
* to OpenAI-shape providers that don't accept it:
|
||||
* 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
|
||||
@@ -140,9 +143,11 @@ export function mapNvidiaGlm52ReasoningParams(
|
||||
}
|
||||
|
||||
export function supportsMaxEffortForProvider(provider: string, model: string): boolean {
|
||||
const resolvedModelId = getProviderModel(provider, model)?.id || model;
|
||||
|
||||
const isClaude =
|
||||
(provider === PROVIDER_CLAUDE || isClaudeCodeCompatible(provider)) &&
|
||||
supportsClaudeMaxEffort(model);
|
||||
supportsClaudeMaxEffort(resolvedModelId);
|
||||
// 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
|
||||
@@ -151,11 +156,12 @@ export function supportsMaxEffortForProvider(provider: string, model: string): b
|
||||
// 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" || provider === "opencode-zen") &&
|
||||
model.toLowerCase().includes("deepseek");
|
||||
provider === "opencode-go" && resolvedModelId.toLowerCase().includes("deepseek");
|
||||
const isOllamaCloud = provider === "ollama-cloud";
|
||||
const isMoonshotK3 =
|
||||
(provider === "moonshot" || provider === "kimi") && /^kimi-k3(?:$|-)/i.test(model);
|
||||
// Kimi K3 only accepts literal max and rejects xhigh natively. Apply this mapping
|
||||
// regardless of provider so that OpenAI-compatible proxies (e.g. TokenRouter)
|
||||
// correctly pass max instead of the internal xhigh top tier.
|
||||
const isMoonshotK3 = /^kimi-k3(?:$|-)/i.test(resolvedModelId);
|
||||
return isClaude || isOpencodeGoDeepSeek || isOllamaCloud || isMoonshotK3;
|
||||
}
|
||||
|
||||
@@ -253,16 +259,6 @@ export function sanitizeReasoningEffortForProvider(
|
||||
const effortStr = typeof c.effort === "string" ? c.effort.toLowerCase() : "";
|
||||
const modelStr = model || "";
|
||||
|
||||
// Oh My Pi exposes `minimal`, while Codex's Responses API starts at `low`.
|
||||
// Normalize every carrier before the Codex executor sends the upstream request.
|
||||
if (provider === "codex" && effortStr === "minimal") {
|
||||
log?.info?.(
|
||||
"REASONING_SANITIZE",
|
||||
`${provider}/${modelStr}: normalized reasoning_effort minimal → low`
|
||||
);
|
||||
return writeEffortValue(b, "low", c);
|
||||
}
|
||||
|
||||
const githubOptIn =
|
||||
provider === "github" && GITHUB_REASONING_EFFORT_OPT_IN_PATTERN.test(modelStr);
|
||||
const rejecting =
|
||||
@@ -298,27 +294,48 @@ export function sanitizeReasoningEffortForProvider(
|
||||
}
|
||||
|
||||
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) {
|
||||
// ── xhigh handling ──────────────────────────────────────────────────────
|
||||
// xhigh is OmniRoute-internal. Map it to the best effort the model accepts.
|
||||
if (effortStr === "xhigh") {
|
||||
if (supportsXHigh) return body; // model accepts xhigh natively
|
||||
if (supportsMax) {
|
||||
log?.info?.(
|
||||
"REASONING_SANITIZE",
|
||||
`${provider}/${modelStr}: mapped reasoning_effort xhigh → max`
|
||||
);
|
||||
return writeEffortValue(b, "max", c);
|
||||
}
|
||||
// Model explicitly rejects xhigh — gracefully degrade to high (its highest standard tier)
|
||||
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`
|
||||
`${provider}/${modelStr}: downgraded reasoning_effort xhigh → high`
|
||||
);
|
||||
return writeEffortValue(b, "high", c);
|
||||
}
|
||||
|
||||
// ── max handling ────────────────────────────────────────────────────────
|
||||
// NEW DEFAULT: pass max through unchanged. Most reasoning-capable APIs
|
||||
// accept max natively. Only degrade when we KNOW the model rejects it
|
||||
// (registry has supportsXHighEffort explicitly set to false AND it's not
|
||||
// in the supportsMax whitelist). Unknown models pass through — trust the
|
||||
// upstream, and if it 400s the user gets a clear signal. This prevents
|
||||
// new models from being unusable for weeks until they're whitelisted (#8057).
|
||||
if (effortStr === "max") {
|
||||
if (supportsMax) return body; // explicitly known to accept max
|
||||
if (!supportsXHigh) {
|
||||
// Model is explicitly flagged as rejecting xhigh (and not in supportsMax) —
|
||||
// it likely only accepts standard tiers. Degrade to its highest: high.
|
||||
log?.info?.(
|
||||
"REASONING_SANITIZE",
|
||||
`${provider}/${modelStr}: downgraded reasoning_effort max → high (model rejects max/xhigh)`
|
||||
);
|
||||
return writeEffortValue(b, "high", c);
|
||||
}
|
||||
// Default: pass max through unchanged — trust the upstream
|
||||
return body;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ import assert from "node:assert/strict";
|
||||
|
||||
const { sanitizeReasoningEffortForProvider } = await import("../../open-sse/executors/base.ts");
|
||||
const { DefaultExecutor } = await import("../../open-sse/executors/default.ts");
|
||||
const { translateRequest } = await import("../../open-sse/translator/index.ts");
|
||||
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
||||
|
||||
function makeLog() {
|
||||
const messages: Array<[string, string]> = [];
|
||||
@@ -23,8 +21,12 @@ test("sanitizeReasoningEffortForProvider: xiaomi-mimo preserves xhigh by default
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "xiaomi-mimo", "mimo-v2.5-pro", log);
|
||||
assert.equal(result, body, "xhigh passes through unless the model explicitly opts out");
|
||||
assert.equal((result as any).reasoning_effort, "xhigh");
|
||||
assert.equal((result as any).model, "mimo-v2.5-pro", "other fields preserved");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "xhigh");
|
||||
assert.equal(
|
||||
(result as Record<string, unknown>).model,
|
||||
"mimo-v2.5-pro",
|
||||
"other fields preserved"
|
||||
);
|
||||
assert.equal(log.messages.length, 0);
|
||||
});
|
||||
|
||||
@@ -41,10 +43,10 @@ test("sanitizeReasoningEffortForProvider: OpenRouter DeepSeek preserves xhigh",
|
||||
null
|
||||
);
|
||||
assert.equal(result, body);
|
||||
assert.equal((result as any).reasoning_effort, "xhigh");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "xhigh");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: explicit xhigh opt-out downgrades to high", () => {
|
||||
test("sanitizeReasoningEffortForProvider: explicit xhigh opt-out maps to max for max-native providers", () => {
|
||||
const log = makeLog();
|
||||
const body = {
|
||||
model: "claude-opus-4-6",
|
||||
@@ -53,10 +55,10 @@ test("sanitizeReasoningEffortForProvider: explicit xhigh opt-out downgrades to h
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "claude", "claude-opus-4-6", log);
|
||||
assert.notEqual(result, body, "must return a new object when mutating");
|
||||
assert.equal((result as any).reasoning_effort, "high");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
assert.ok(
|
||||
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /xhigh → high/.test(m)),
|
||||
"logs the downgrade"
|
||||
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /xhigh → max/.test(m)),
|
||||
"logs the mapping"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -73,10 +75,10 @@ test("sanitizeReasoningEffortForProvider: Anthropic-compatible dynamic provider
|
||||
null
|
||||
);
|
||||
assert.notEqual(result, body, "must return a new object when mutating");
|
||||
assert.equal((result as any).reasoning_effort, "high");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "high");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: xiaomi-mimo normalizes max → xhigh by default", () => {
|
||||
test("sanitizeReasoningEffortForProvider: xiaomi-mimo passes max through (new default)", () => {
|
||||
const log = makeLog();
|
||||
const body = {
|
||||
model: "mimo-v2.5-pro",
|
||||
@@ -84,11 +86,11 @@ test("sanitizeReasoningEffortForProvider: xiaomi-mimo normalizes max → xhigh b
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "xiaomi-mimo", "mimo-v2.5-pro", log);
|
||||
assert.equal((result as any).reasoning_effort, "xhigh");
|
||||
assert.ok(
|
||||
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /max → xhigh/.test(m)),
|
||||
"logs the normalization"
|
||||
);
|
||||
// xiaomi-mimo has supportsXHighEffort: undefined (not explicitly false), so max
|
||||
// passes through unchanged — the upstream decides whether to accept or reject.
|
||||
assert.equal(result, body, "max passes through unchanged for models not flagged as rejecting it");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
assert.equal(log.messages.length, 0);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: Ollama Cloud preserves max", () => {
|
||||
@@ -100,49 +102,10 @@ test("sanitizeReasoningEffortForProvider: Ollama Cloud preserves max", () => {
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "ollama-cloud", "glm-5.2", log);
|
||||
assert.equal(result, body, "Ollama Cloud accepts max literally");
|
||||
assert.equal((result as any).reasoning_effort, "max");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
assert.equal(log.messages.length, 0);
|
||||
});
|
||||
|
||||
test("end-to-end: Anthropic output_config.effort=max reaches Ollama Cloud as max (not xhigh)", () => {
|
||||
// Bug: the claude→openai translator previously normalized max → xhigh, and the
|
||||
// sanitizer could not recover the original intent because the carrier was already
|
||||
// xhigh. Ollama Cloud accepts max literally but rejects xhigh (HTTP 400).
|
||||
// The translator must pass max through verbatim and the sanitizer must keep it.
|
||||
const translated = translateRequest(
|
||||
FORMATS.CLAUDE,
|
||||
FORMATS.OPENAI,
|
||||
"gemma4:31b",
|
||||
{
|
||||
model: "gemma4:31b",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
output_config: { effort: "max" },
|
||||
},
|
||||
false,
|
||||
null,
|
||||
"ollama-cloud"
|
||||
) as Record<string, unknown>;
|
||||
|
||||
assert.equal(
|
||||
translated.reasoning_effort,
|
||||
"max",
|
||||
"translator must pass max through verbatim instead of rewriting it to xhigh"
|
||||
);
|
||||
|
||||
const sanitized = sanitizeReasoningEffortForProvider(
|
||||
translated,
|
||||
"ollama-cloud",
|
||||
"gemma4:31b",
|
||||
null
|
||||
) as Record<string, unknown>;
|
||||
|
||||
assert.equal(
|
||||
sanitized.reasoning_effort,
|
||||
"max",
|
||||
"Ollama Cloud accepts max literally — no downgrade, no rewrite to xhigh"
|
||||
);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: Ollama Cloud preserves nested max", () => {
|
||||
const body = {
|
||||
model: "glm-5.2",
|
||||
@@ -151,11 +114,11 @@ test("sanitizeReasoningEffortForProvider: Ollama Cloud preserves nested max", ()
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "ollama-cloud", "glm-5.2", null);
|
||||
assert.equal(result, body, "Ollama Cloud accepts max literally");
|
||||
assert.equal((result as any).reasoning.effort, "max");
|
||||
assert.equal((result as any).reasoning.summary, "auto");
|
||||
assert.equal((result as Record<string, unknown>).reasoning.effort, "max");
|
||||
assert.equal((result as Record<string, unknown>).reasoning.summary, "auto");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: OpenRouter DeepSeek normalizes max → xhigh", () => {
|
||||
test("sanitizeReasoningEffortForProvider: OpenRouter DeepSeek passes max through (new default)", () => {
|
||||
const log = makeLog();
|
||||
const body = {
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
@@ -168,12 +131,11 @@ test("sanitizeReasoningEffortForProvider: OpenRouter DeepSeek normalizes max →
|
||||
"deepseek/deepseek-v4-pro",
|
||||
log
|
||||
);
|
||||
assert.notEqual(result, body, "must return a new object when mutating");
|
||||
assert.equal((result as any).reasoning_effort, "xhigh");
|
||||
assert.ok(
|
||||
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /max → xhigh/.test(m)),
|
||||
"logs the normalization"
|
||||
);
|
||||
// New default: max passes through. OpenRouter DeepSeek is not flagged as
|
||||
// rejecting max, so the upstream decides.
|
||||
assert.equal(result, body, "max passes through unchanged");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
assert.equal(log.messages.length, 0);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: OpenRouter Claude opt-out aliases downgrade max → high", () => {
|
||||
@@ -190,14 +152,14 @@ test("sanitizeReasoningEffortForProvider: OpenRouter Claude opt-out aliases down
|
||||
log
|
||||
);
|
||||
assert.notEqual(result, body, "must return a new object when mutating");
|
||||
assert.equal((result as any).reasoning_effort, "high");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "high");
|
||||
assert.ok(
|
||||
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /max → high/.test(m)),
|
||||
"logs the downgrade"
|
||||
);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: OpenAI-compatible Gemini normalizes max → xhigh", () => {
|
||||
test("sanitizeReasoningEffortForProvider: OpenAI-compatible Gemini passes max through (new default)", () => {
|
||||
const log = makeLog();
|
||||
const body = {
|
||||
model: "gemini-3.1-pro-preview",
|
||||
@@ -210,15 +172,12 @@ test("sanitizeReasoningEffortForProvider: OpenAI-compatible Gemini normalizes ma
|
||||
"gemini-3.1-pro-preview",
|
||||
log
|
||||
);
|
||||
assert.notEqual(result, body, "must return a new object when mutating");
|
||||
assert.equal((result as any).reasoning_effort, "xhigh");
|
||||
assert.ok(
|
||||
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /max → xhigh/.test(m)),
|
||||
"logs the normalization"
|
||||
);
|
||||
assert.equal(result, body, "max passes through unchanged for unknown providers");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
assert.equal(log.messages.length, 0);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: nested OpenAI reasoning max normalizes to xhigh", () => {
|
||||
test("sanitizeReasoningEffortForProvider: nested OpenAI reasoning max passes through (new default)", () => {
|
||||
const body = {
|
||||
model: "gemini-3.1-pro-preview",
|
||||
reasoning: { effort: "max", summary: "auto" },
|
||||
@@ -230,9 +189,13 @@ test("sanitizeReasoningEffortForProvider: nested OpenAI reasoning max normalizes
|
||||
"gemini-3.1-pro-preview",
|
||||
null
|
||||
);
|
||||
assert.equal((result as any).reasoning.effort, "xhigh");
|
||||
assert.equal((result as any).reasoning.summary, "auto", "other reasoning fields preserved");
|
||||
assert.equal((result as any).reasoning_effort, undefined);
|
||||
assert.equal(result, body, "max passes through unchanged");
|
||||
assert.equal((result as Record<string, unknown>).reasoning.effort, "max");
|
||||
assert.equal(
|
||||
(result as Record<string, unknown>).reasoning.summary,
|
||||
"auto",
|
||||
"other reasoning fields preserved"
|
||||
);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: claude preserves max for Opus/Sonnet and downgrades Haiku", () => {
|
||||
@@ -287,8 +250,12 @@ test("sanitizeReasoningEffortForProvider: xiaomi-mimo preserves nested xhigh by
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "xiaomi-mimo", "mimo-v2.5-pro", null);
|
||||
assert.equal(result, body);
|
||||
assert.equal((result as any).reasoning.effort, "xhigh");
|
||||
assert.equal((result as any).reasoning.summary, "auto", "other reasoning fields preserved");
|
||||
assert.equal((result as Record<string, unknown>).reasoning.effort, "xhigh");
|
||||
assert.equal(
|
||||
(result as Record<string, unknown>).reasoning.summary,
|
||||
"auto",
|
||||
"other reasoning fields preserved"
|
||||
);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: explicit xhigh opt-out preserves Responses shape", () => {
|
||||
@@ -298,8 +265,8 @@ test("sanitizeReasoningEffortForProvider: explicit xhigh opt-out preserves Respo
|
||||
input: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "claude", "claude-opus-4-6", null);
|
||||
assert.equal((result as any).reasoning.effort, "high");
|
||||
assert.equal((result as any).reasoning_effort, undefined);
|
||||
assert.equal((result as Record<string, unknown>).reasoning.effort, "max");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, undefined);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: mistral/devstral strips reasoning_effort entirely", () => {
|
||||
@@ -310,7 +277,11 @@ test("sanitizeReasoningEffortForProvider: mistral/devstral strips reasoning_effo
|
||||
messages: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "mistral", "devstral-2512", log);
|
||||
assert.equal((result as any).reasoning_effort, undefined, "reasoning_effort must be stripped");
|
||||
assert.equal(
|
||||
(result as Record<string, unknown>).reasoning_effort,
|
||||
undefined,
|
||||
"reasoning_effort must be stripped"
|
||||
);
|
||||
assert.ok(
|
||||
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /removed/.test(m)),
|
||||
"logs the removal"
|
||||
@@ -326,7 +297,7 @@ test("sanitizeReasoningEffortForProvider: github/claude-opus-4.6 preserves reaso
|
||||
messages: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "github", "claude-opus-4-6", null);
|
||||
assert.equal((result as any).reasoning_effort, "high");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "high");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: github/claude-opus-4.7 still strips (#791)", () => {
|
||||
@@ -336,7 +307,7 @@ test("sanitizeReasoningEffortForProvider: github/claude-opus-4.7 still strips (#
|
||||
messages: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "github", "claude-opus-4.7", null);
|
||||
assert.equal((result as any).reasoning_effort, undefined);
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, undefined);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: rejecting providers strip max before normalization", () => {
|
||||
@@ -393,7 +364,11 @@ test("sanitizeReasoningEffortForProvider: mistral/devstral strips reasoning obje
|
||||
messages: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "mistral", "devstral-2512", null);
|
||||
assert.equal((result as any).reasoning, undefined, "reasoning object dropped when emptied");
|
||||
assert.equal(
|
||||
(result as Record<string, unknown>).reasoning,
|
||||
undefined,
|
||||
"reasoning object dropped when emptied"
|
||||
);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: mistral/devstral preserves reasoning when other fields remain", () => {
|
||||
@@ -403,7 +378,7 @@ test("sanitizeReasoningEffortForProvider: mistral/devstral preserves reasoning w
|
||||
messages: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "mistral", "devstral-2512", null);
|
||||
assert.deepEqual((result as any).reasoning, { summary: "auto" });
|
||||
assert.deepEqual((result as Record<string, unknown>).reasoning, { summary: "auto" });
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: codex with xhigh passes through unchanged", () => {
|
||||
@@ -413,7 +388,7 @@ test("sanitizeReasoningEffortForProvider: codex with xhigh passes through unchan
|
||||
messages: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "codex", "gpt-5.5-xhigh", null);
|
||||
assert.equal((result as any).reasoning_effort, "xhigh");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "xhigh");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: codex maps OMP minimal to low across carriers", () => {
|
||||
@@ -444,7 +419,7 @@ test("sanitizeReasoningEffortForProvider: handles unknown providers as pass-thro
|
||||
const body = { model: "some-model", reasoning_effort: "xhigh", messages: [] };
|
||||
const result = sanitizeReasoningEffortForProvider(body, "unknown-provider", "some-model", null);
|
||||
assert.equal(result, body);
|
||||
assert.equal((result as any).reasoning_effort, "xhigh");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "xhigh");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: non-object body returns unchanged", () => {
|
||||
@@ -454,6 +429,103 @@ test("sanitizeReasoningEffortForProvider: non-object body returns unchanged", ()
|
||||
assert.equal(sanitizeReasoningEffortForProvider(arr, "xiaomi-mimo", "x", null), arr);
|
||||
});
|
||||
|
||||
// ── #8057: max passes through by default for unknown models ──────────────────
|
||||
// New models should work immediately without waiting for a whitelist update.
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: completely unknown model passes max through (#8057)", () => {
|
||||
const body = {
|
||||
model: "brand-new-model-2026",
|
||||
reasoning_effort: "max",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(
|
||||
body,
|
||||
"some-new-provider",
|
||||
"brand-new-model-2026",
|
||||
null
|
||||
);
|
||||
assert.equal(result, body, "unknown models must not have max rewritten");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: unknown model max passes through on all proxy types (#8057)", () => {
|
||||
for (const provider of [
|
||||
"tokenrouter",
|
||||
"zemux",
|
||||
"openrouter",
|
||||
"openai-compatible-test",
|
||||
"custom-proxy",
|
||||
]) {
|
||||
const body = {
|
||||
model: "future-model-v5",
|
||||
reasoning_effort: "max",
|
||||
messages: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, provider, "future-model-v5", null);
|
||||
assert.equal(result, body, `${provider}: max must pass through for unknown models`);
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
}
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: proxy-prefixed kimi-k3 resolves and preserves max", () => {
|
||||
// TokenRouter sends moonshotai/kimi-k3-free — global fallback resolves it to kimi-k3,
|
||||
// which has supportsXHighEffort: false + supportsMax: true via supportsMaxEffortForProvider.
|
||||
const body = {
|
||||
model: "moonshotai/kimi-k3-free",
|
||||
reasoning_effort: "max",
|
||||
messages: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(
|
||||
body,
|
||||
"tokenrouter",
|
||||
"moonshotai/kimi-k3-free",
|
||||
null
|
||||
);
|
||||
assert.equal(result, body, "kimi-k3 behind tokenrouter must keep max");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: proxy-prefixed kimi-k3 xhigh maps to max (not high)", () => {
|
||||
// When Claude Code sends xhigh for kimi-k3 behind a proxy, it should map to max
|
||||
// (the model's highest tier), not downgrade to high.
|
||||
const log = makeLog();
|
||||
const body = {
|
||||
model: "moonshotai/kimi-k3-free",
|
||||
reasoning_effort: "xhigh",
|
||||
messages: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(
|
||||
body,
|
||||
"tokenrouter",
|
||||
"moonshotai/kimi-k3-free",
|
||||
log
|
||||
);
|
||||
assert.notEqual(result, body, "must return a new object when mutating");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
assert.ok(
|
||||
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /xhigh → max/.test(m)),
|
||||
"logs the xhigh → max mapping"
|
||||
);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: Claude Haiku max degrades to high (explicitly flagged)", () => {
|
||||
// Claude Haiku is explicitly flagged as supportsXHighEffort: false and NOT in
|
||||
// supportsMaxEffortForProvider, so max should degrade to high.
|
||||
const body = {
|
||||
model: "claude-haiku-4-5-20251001",
|
||||
reasoning_effort: "max",
|
||||
messages: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(
|
||||
body,
|
||||
"claude",
|
||||
"claude-haiku-4-5-20251001",
|
||||
null
|
||||
);
|
||||
assert.notEqual(result, body);
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "high");
|
||||
});
|
||||
|
||||
// ── NVIDIA NIM GLM-5.2 (#7215) ─────────────────────────────────────────────
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: NVIDIA GLM-5.2 enables thinking for active effort", () => {
|
||||
@@ -578,8 +650,12 @@ test("sanitizeReasoningEffortForProvider: native deepseek maps xhigh → max", (
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-pro", log);
|
||||
assert.notEqual(result, body, "must return a new object when mutating");
|
||||
assert.equal((result as any).reasoning_effort, "max");
|
||||
assert.equal((result as any).model, "deepseek-v4-pro", "other fields preserved");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
assert.equal(
|
||||
(result as Record<string, unknown>).model,
|
||||
"deepseek-v4-pro",
|
||||
"other fields preserved"
|
||||
);
|
||||
assert.ok(
|
||||
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /xhigh → max/.test(m)),
|
||||
"logs the xhigh → max mapping"
|
||||
@@ -595,7 +671,7 @@ test("sanitizeReasoningEffortForProvider: native deepseek preserves max", () =>
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-flash", log);
|
||||
assert.equal(result, body, "max is DeepSeek's native top tier — passes through unchanged");
|
||||
assert.equal((result as any).reasoning_effort, "max");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
assert.equal(log.messages.length, 0);
|
||||
});
|
||||
|
||||
@@ -607,7 +683,11 @@ test("sanitizeReasoningEffortForProvider: native deepseek clamps low → high",
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-pro", null);
|
||||
assert.notEqual(result, body, "must return a new object when mutating");
|
||||
assert.equal((result as any).reasoning_effort, "high", "below the {high, max} floor → high");
|
||||
assert.equal(
|
||||
(result as Record<string, unknown>).reasoning_effort,
|
||||
"high",
|
||||
"below the {high, max} floor → high"
|
||||
);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: native deepseek clamps medium → high", () => {
|
||||
@@ -617,7 +697,7 @@ test("sanitizeReasoningEffortForProvider: native deepseek clamps medium → high
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-pro", null);
|
||||
assert.equal((result as any).reasoning_effort, "high");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "high");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: native deepseek preserves high unchanged", () => {
|
||||
@@ -628,7 +708,7 @@ test("sanitizeReasoningEffortForProvider: native deepseek preserves high unchang
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-pro", null);
|
||||
assert.equal(result, body, "high is already valid — passes through unchanged");
|
||||
assert.equal((result as any).reasoning_effort, "high");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "high");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: native deepseek maps nested reasoning.effort xhigh → max", () => {
|
||||
@@ -638,9 +718,13 @@ test("sanitizeReasoningEffortForProvider: native deepseek maps nested reasoning.
|
||||
input: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-pro", null);
|
||||
assert.equal((result as any).reasoning.effort, "max");
|
||||
assert.equal((result as any).reasoning.summary, "auto", "other reasoning fields preserved");
|
||||
assert.equal((result as any).reasoning_effort, undefined);
|
||||
assert.equal((result as Record<string, unknown>).reasoning.effort, "max");
|
||||
assert.equal(
|
||||
(result as Record<string, unknown>).reasoning.summary,
|
||||
"auto",
|
||||
"other reasoning fields preserved"
|
||||
);
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, undefined);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: OpenRouter DeepSeek still preserves xhigh (not native)", () => {
|
||||
@@ -658,7 +742,7 @@ test("sanitizeReasoningEffortForProvider: OpenRouter DeepSeek still preserves xh
|
||||
null
|
||||
);
|
||||
assert.equal(result, body);
|
||||
assert.equal((result as any).reasoning_effort, "xhigh");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "xhigh");
|
||||
});
|
||||
|
||||
// ── opencode-go DeepSeek V4 Pro effort variants (#4647) ──────────────────────
|
||||
@@ -675,7 +759,7 @@ test("sanitizeReasoningEffortForProvider: opencode-go DeepSeek V4 Pro preserves
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "deepseek-v4-pro", null);
|
||||
assert.equal(result, body, "opencode-go DeepSeek max must pass through unchanged");
|
||||
assert.equal((result as any).reasoning_effort, "max");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: opencode-go DeepSeek V4 Pro preserves variant suffix levels", () => {
|
||||
@@ -692,27 +776,27 @@ test("sanitizeReasoningEffortForProvider: opencode-go DeepSeek V4 Pro preserves
|
||||
null
|
||||
);
|
||||
assert.equal(
|
||||
(result as any).reasoning_effort,
|
||||
(result as Record<string, unknown>).reasoning_effort,
|
||||
level,
|
||||
`opencode-go deepseek-v4-pro-${level} preserves reasoning_effort=${level}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: opencode-go with non-DeepSeek model still normalizes max → xhigh", () => {
|
||||
// The opt-in must be scoped to DeepSeek models on opencode-go only — other
|
||||
// opencode-go models (e.g. glm/kimi/mimo) follow the default xhigh policy.
|
||||
test("sanitizeReasoningEffortForProvider: opencode-go with non-DeepSeek model passes max through (new default)", () => {
|
||||
// opencode-go non-DeepSeek models are not explicitly flagged as rejecting max,
|
||||
// so max passes through unchanged under the new default.
|
||||
const body = {
|
||||
model: "mimo-v2.5-pro",
|
||||
reasoning_effort: "max",
|
||||
messages: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "mimo-v2.5-pro", null);
|
||||
assert.notEqual(result, body);
|
||||
assert.equal((result as any).reasoning_effort, "xhigh");
|
||||
assert.equal(result, body, "max passes through unchanged");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: #7044 output_config.effort (Claude native) xhigh is downgraded, not bypassed", () => {
|
||||
test("sanitizeReasoningEffortForProvider: #7044 output_config.effort (Claude native) xhigh is mapped to max, not bypassed", () => {
|
||||
const log = makeLog();
|
||||
const body = {
|
||||
model: "claude-opus-4-6",
|
||||
@@ -722,17 +806,17 @@ test("sanitizeReasoningEffortForProvider: #7044 output_config.effort (Claude nat
|
||||
const result = sanitizeReasoningEffortForProvider(body, "claude", "claude-opus-4-6", log);
|
||||
assert.notEqual(result, body, "must return a new object when mutating");
|
||||
assert.equal(
|
||||
(result as any).output_config.effort,
|
||||
"high",
|
||||
"xhigh downgraded to high on the output_config carrier"
|
||||
(result as Record<string, unknown>).output_config.effort,
|
||||
"max",
|
||||
"xhigh mapped to max on the output_config carrier"
|
||||
);
|
||||
assert.ok(
|
||||
!("reasoning_effort" in (result as any)),
|
||||
!("reasoning_effort" in (result as Record<string, unknown>)),
|
||||
"no spurious reasoning_effort injected when only output_config was present"
|
||||
);
|
||||
assert.ok(
|
||||
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /xhigh → high/.test(m)),
|
||||
"logs the downgrade"
|
||||
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /xhigh → max/.test(m)),
|
||||
"logs the mapping"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -744,5 +828,5 @@ test("sanitizeReasoningEffortForProvider: #7044 output_config.effort high passes
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "claude", "claude-opus-4-6", null);
|
||||
assert.equal(result, body, "high is supported — body returned unchanged");
|
||||
assert.equal((result as any).output_config.effort, "high");
|
||||
assert.equal((result as Record<string, unknown>).output_config.effort, "high");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user