From f0dc77892a3a36f7bcf6a11756a7595fda3a9545 Mon Sep 17 00:00:00 2001 From: Mr White <42571711+Neuron-Mr-White@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:39:07 +0800 Subject: [PATCH] fix(api): reach DeepSeek V4's native max reasoning tier (#10230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(api): reach DeepSeek V4's native max reasoning tier DeepSeek V4 accepts reasoning_effort low | high | max, defaults to high, and maps medium and xhigh down to high (https://api-docs.deepseek.com/api/create-chat-completion; the upstream 400 on an invalid value enumerates none, minimal, low, medium, high, xhigh, max). OmniRoute's canonical vocabulary is none|low|medium|high|xhigh, where `max` is an alias collapsing onto `xhigh`. DeepSeek then maps `xhigh` back down to `high`, so a client sending {"effort":"max"} silently got high — the model's top reasoning tier was unreachable through the canonical field, and the catalog never advertised `max` as an available tier. Mirror the existing extendCodexGpt56EffortValues precedent: expose the provider-native tier for these models only, without widening the global request vocabulary. CANONICAL_EFFORT_VALUES and normalizeEffort() are unchanged, so every other provider keeps collapsing max -> xhigh. Scoped to the native `deepseek`/`ds` provider. Routed namespaces that merely carry "deepseek" in the id (openrouter/deepseek/..., tllm/deepseek_v4, oc/deepseek-v4-flash-free) terminate at a different upstream whose effort vocabulary we do not control, so they keep the canonical behavior. The provider is not resolved yet where the canonical params are folded in (chat.ts), so the check also accepts a `/` id. An explicit client reasoning_effort / reasoning.effort still wins, as before. Covered by tests/unit/deepseek-native-max-effort.test.ts: 4 of its 6 cases fail without this change. * chore(changelog): add fragment for #10230 --- .../fixes/10230-deepseek-native-max-effort.md | 1 + src/app/api/v1/models/catalogHelpers.ts | 10 +- src/shared/reasoning/effortStandardization.ts | 67 ++++++++++- tests/unit/deepseek-native-max-effort.test.ts | 104 ++++++++++++++++++ 4 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 changelog.d/fixes/10230-deepseek-native-max-effort.md create mode 100644 tests/unit/deepseek-native-max-effort.test.ts diff --git a/changelog.d/fixes/10230-deepseek-native-max-effort.md b/changelog.d/fixes/10230-deepseek-native-max-effort.md new file mode 100644 index 0000000000..3a681d3190 --- /dev/null +++ b/changelog.d/fixes/10230-deepseek-native-max-effort.md @@ -0,0 +1 @@ +- **fix(api):** DeepSeek V4's native `max` reasoning tier is now reachable. DeepSeek accepts `reasoning_effort` `low`/`high`/`max` and maps `medium`/`xhigh` down to `high`, while OmniRoute's canonical vocabulary collapses `max` onto `xhigh` — so `{"effort":"max"}` silently resolved to `high` and the catalog never advertised a `max` tier (or its `-max` variant). Following the existing `extendCodexGpt56EffortValues` precedent, the native tier is now preserved for `deepseek`/`ds` V4 models only; the global effort vocabulary is unchanged, routed namespaces (`openrouter/deepseek/…`, `tllm/deepseek_v4`, `oc/deepseek-v4-flash-free`) keep the canonical behavior, and an explicit client `reasoning_effort` still wins ([#10230](https://github.com/diegosouzapw/OmniRoute/pull/10230)) — thanks @Neuron-Mr-White diff --git a/src/app/api/v1/models/catalogHelpers.ts b/src/app/api/v1/models/catalogHelpers.ts index 151d5ce733..bb1c600a16 100644 --- a/src/app/api/v1/models/catalogHelpers.ts +++ b/src/app/api/v1/models/catalogHelpers.ts @@ -6,6 +6,7 @@ import { CANONICAL_EFFORT_VALUES, extendCodexGpt56EffortValues, + extendDeepSeekEffortValues, } from "@/shared/reasoning/effortStandardization"; export interface CustomModelEntry { @@ -93,8 +94,7 @@ export function getThinkingCapabilityFields( ): Record { const supportsThinking = resolvedThinking; if (typeof supportsThinking !== "boolean") return {}; - const hasDeclaredTiers = - supportedThinkingEfforts && supportedThinkingEfforts.length > 0; + const hasDeclaredTiers = supportedThinkingEfforts && supportedThinkingEfforts.length > 0; return { thinking: supportsThinking, supportsThinking, @@ -102,7 +102,11 @@ export function getThinkingCapabilityFields( ? { effort_tiers: hasDeclaredTiers ? [...supportedThinkingEfforts!] - : extendCodexGpt56EffortValues(providerId, modelId, CANONICAL_EFFORT_VALUES), + : extendDeepSeekEffortValues( + providerId, + modelId, + extendCodexGpt56EffortValues(providerId, modelId, CANONICAL_EFFORT_VALUES) + ), } : {}), }; diff --git a/src/shared/reasoning/effortStandardization.ts b/src/shared/reasoning/effortStandardization.ts index 21a336556c..7f0edcd41c 100644 --- a/src/shared/reasoning/effortStandardization.ts +++ b/src/shared/reasoning/effortStandardization.ts @@ -63,6 +63,57 @@ const EFFORT_TIER_ALIASES: Record = { max: "xhigh", }; +/** + * DeepSeek V4 exposes a native `max` reasoning tier ABOVE its `high` tier. + * + * Per https://api-docs.deepseek.com/api/create-chat-completion the accepted + * `reasoning_effort` values are `low`, `high` and `max`, the default is `high`, + * and **`medium` / `xhigh` are both mapped to `high` upstream**. Canonical + * `max` collapses to `xhigh` (see EFFORT_TIER_ALIASES), so without this the + * top tier is unreachable: `{"effort":"max"}` → `xhigh` → upstream `high`. + * + * Mirrors extendCodexGpt56EffortValues: expose the provider-native tier for + * these models only, without widening the global request vocabulary. + */ +export function extendDeepSeekEffortValues( + provider: string | null | undefined, + model: string | null | undefined, + baseValues: readonly string[] +): string[] { + const values = [...baseValues]; + if (!isDeepSeekNativeMaxModel(provider, model)) return values; + return values.includes("max") ? values : [...values, "max"]; +} + +/** + * Whether `/` is a DeepSeek V4 model served by the native + * DeepSeek provider (registry id `deepseek`, alias `ds`). + * + * Deliberately scoped to the native provider: routed namespaces such as + * `openrouter/deepseek/...` or `tllm/deepseek_v4` terminate at a different + * upstream whose accepted effort vocabulary we do not control. + */ +export function isDeepSeekNativeMaxModel( + provider: string | null | undefined, + model: string | null | undefined +): boolean { + const rawModel = model?.trim().toLowerCase(); + if (!rawModel) return false; + + // The provider is not always resolved yet at the point the canonical request + // params are folded in (see chat.ts), so accept either an explicit provider or + // a `/` id carrying the native DeepSeek prefix. + const prefixMatch = rawModel.match(/^(deepseek|ds)\//); + const normalizedProvider = provider?.trim().toLowerCase() || prefixMatch?.[1]; + if (normalizedProvider !== "deepseek" && normalizedProvider !== "ds") return false; + + const normalizedModel = rawModel.replace(/^(?:deepseek|ds)\//, ""); + if (!normalizedModel) return false; + return /^deepseek-v4-(?:pro|flash)(?:-(?:none|minimal|low|medium|high|xhigh|max))?$/.test( + normalizedModel + ); +} + /** * Normalize an arbitrary effort value onto the canonical vocabulary. Accepts the canonical * values plus the UI tier synonyms (`extra`/`max` → `xhigh`), case-insensitively. Returns @@ -100,6 +151,11 @@ function isPlainObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } +/** Read a request body's `model` field when it is a usable string. */ +function asModelId(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value : undefined; +} + /** * Fold the canonical `effort` / `thinking` request params onto the per-provider reasoning * fields the existing translators already consume (`reasoning_effort`, `reasoning.effort`, @@ -112,10 +168,17 @@ function isPlainObject(value: unknown): value is Record { * - An explicit object-shaped `thinking` (the Anthropic `{ type, budget_tokens }` config) * is never overwritten by the canonical boolean `thinking`. */ -export function normalizeReasoningRequest(body: T): T { +export function normalizeReasoningRequest(body: T, provider?: string | null): T { if (!isPlainObject(body)) return body; - const canonicalEffort = normalizeEffort(body.effort); + // DeepSeek V4 has a native `max` tier above `high`. Canonical `max` normally + // collapses to `xhigh`, which DeepSeek maps back down to `high` — so preserve + // the literal value for those models instead of round-tripping it away. + const rawEffort = typeof body.effort === "string" ? body.effort.trim().toLowerCase() : undefined; + const canonicalEffort = + rawEffort === "max" && isDeepSeekNativeMaxModel(provider, asModelId(body.model)) + ? ("max" as const) + : normalizeEffort(body.effort); const canonicalThinking = body.thinking; const hasCanonicalThinkingBool = typeof canonicalThinking === "boolean"; diff --git a/tests/unit/deepseek-native-max-effort.test.ts b/tests/unit/deepseek-native-max-effort.test.ts new file mode 100644 index 0000000000..2910cb70e2 --- /dev/null +++ b/tests/unit/deepseek-native-max-effort.test.ts @@ -0,0 +1,104 @@ +/** + * DeepSeek V4 exposes a native `max` reasoning tier that the canonical vocabulary erases. + * + * Per https://api-docs.deepseek.com/api/create-chat-completion the accepted + * `reasoning_effort` values are `low`, `high` and `max`, the default is `high`, and + * **`medium` / `xhigh` are both mapped to `high`** upstream. (The live API's 400 on an + * invalid value enumerates the full accepted set: + * `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`.) + * + * OmniRoute's canonical vocabulary is `none|low|medium|high|xhigh`, and `max` is an alias + * that collapses onto `xhigh` (EFFORT_TIER_ALIASES). Since DeepSeek then maps `xhigh` back + * down to `high`, a client sending `{"effort":"max"}` silently received **high** — the top + * tier was unreachable through the canonical field. + * + * The fix mirrors the existing `extendCodexGpt56EffortValues` precedent: expose the + * provider-native tier for these models only, without widening the global request + * vocabulary for every other provider. + * + * Guards: A = `max` survives for native DeepSeek models; B = every other provider still + * collapses `max`→`xhigh`; C = routed DeepSeek namespaces (openrouter/tllm) are NOT treated + * as native; D = an explicit client `reasoning_effort` still wins; E = the catalog offers + * `max` as an effort tier for native DeepSeek models. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { + normalizeReasoningRequest, + normalizeEffort, + isDeepSeekNativeMaxModel, + extendDeepSeekEffortValues, + CANONICAL_EFFORT_VALUES, +} = await import("../../src/shared/reasoning/effortStandardization.ts"); + +test("A: canonical effort `max` survives for native DeepSeek V4 models", () => { + for (const model of [ + "ds/deepseek-v4-pro", + "ds/deepseek-v4-flash", + "deepseek/deepseek-v4-pro", + "deepseek/deepseek-v4-flash", + ]) { + const out = normalizeReasoningRequest({ model, effort: "max" }) as Record; + assert.equal( + out.reasoning_effort, + "max", + `${model} must reach DeepSeek's native max tier, not the down-mapped xhigh` + ); + assert.deepEqual((out.reasoning as Record).effort, "max"); + } +}); + +test("A2: the provider can also be supplied explicitly (model id without prefix)", () => { + const out = normalizeReasoningRequest( + { model: "deepseek-v4-pro", effort: "max" }, + "deepseek" + ) as Record; + assert.equal(out.reasoning_effort, "max"); +}); + +test("B: `max` still collapses to `xhigh` for every other provider", () => { + for (const model of ["openai/gpt-5", "anthropic/claude-opus-4-8", "z-ai/glm-5.2"]) { + const out = normalizeReasoningRequest({ model, effort: "max" }) as Record; + assert.equal(out.reasoning_effort, "xhigh", `${model} must keep the canonical collapse`); + } + // The global vocabulary itself is unchanged. + assert.deepEqual([...CANONICAL_EFFORT_VALUES], ["none", "low", "medium", "high", "xhigh"]); + assert.equal(normalizeEffort("max"), "xhigh"); +}); + +test("C: routed DeepSeek namespaces are not treated as the native provider", () => { + // These terminate at a different upstream whose effort vocabulary we do not control. + for (const model of [ + "openrouter/deepseek/deepseek-v4-flash-0731", + "tllm/deepseek_v4", + "oc/deepseek-v4-flash-free", + ]) { + assert.equal(isDeepSeekNativeMaxModel(null, model), false, `${model} is not native`); + const out = normalizeReasoningRequest({ model, effort: "max" }) as Record; + assert.equal(out.reasoning_effort, "xhigh"); + } +}); + +test("D: an explicit client reasoning_effort still wins over canonical effort", () => { + const out = normalizeReasoningRequest({ + model: "ds/deepseek-v4-flash", + effort: "max", + reasoning_effort: "low", + }) as Record; + assert.equal(out.reasoning_effort, "low", "explicit client intent must be preserved"); +}); + +test("E: catalog effort tiers advertise `max` for native DeepSeek models only", () => { + const base = [...CANONICAL_EFFORT_VALUES]; + + const deepseekTiers = extendDeepSeekEffortValues("deepseek", "deepseek-v4-pro", base); + assert.ok(deepseekTiers.includes("max"), "native DeepSeek must advertise the max tier"); + + const otherTiers = extendDeepSeekEffortValues("openai", "gpt-5", base); + assert.ok(!otherTiers.includes("max"), "other providers must be untouched"); + + // Idempotent: never duplicate an already-present tier. + const twice = extendDeepSeekEffortValues("ds", "deepseek-v4-flash", deepseekTiers); + assert.equal(twice.filter((t) => t === "max").length, 1); +});