From f19d4b585b927816b37ad4e95ce190c5cd26d135 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:25:04 -0300 Subject: [PATCH] fix(sse): map reasoning_effort to DeepSeek V4's native {high, max} vocabulary (#4219) Integrated into release/v3.8.29 (DeepSeek V4 native reasoning_effort {high,max} mapping). --- config/quality/file-size-baseline.json | 2 +- open-sse/executors/base.ts | 24 +++++ .../base-executor-sanitize-effort.test.ts | 99 +++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 552d41029c..b371b7df87 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -58,7 +58,7 @@ "frozen": { "open-sse/config/providerRegistry.ts": 4731, "open-sse/executors/antigravity.ts": 1664, - "open-sse/executors/base.ts": 1334, + "open-sse/executors/base.ts": 1358, "open-sse/executors/chatgpt-web.ts": 2870, "open-sse/executors/claude-web.ts": 1057, "open-sse/executors/codex.ts": 1447, diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 46ac640e34..900117470c 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -288,6 +288,30 @@ export function sanitizeReasoningEffortForProvider( return next; } + // 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}` + ); + const next: Record = { ...b }; + if (hasTopLevelReasoningEffort) next.reasoning_effort = mapped; + if (reasoning) next.reasoning = { ...reasoning, effort: mapped }; + return next; + } + return body; + } + const supportsXHigh = supportsXHighEffort(provider, modelStr); const shouldDowngradeXHigh = effortStr === "xhigh" && !supportsXHigh; const supportsXHighForMax = supportsXHigh; diff --git a/tests/unit/base-executor-sanitize-effort.test.ts b/tests/unit/base-executor-sanitize-effort.test.ts index 81d6dc51d7..bcdbacff60 100644 --- a/tests/unit/base-executor-sanitize-effort.test.ts +++ b/tests/unit/base-executor-sanitize-effort.test.ts @@ -337,3 +337,102 @@ test("sanitizeReasoningEffortForProvider: non-object body returns unchanged", () const arr: unknown[] = []; assert.equal(sanitizeReasoningEffortForProvider(arr, "xiaomi-mimo", "x", null), arr); }); + +// ── 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 +// onto DeepSeek's native vocabulary so the client's requested effort is honored +// instead of silently dropped to the default. This is the INVERSE of the +// OpenRouter-DeepSeek path, whose normalized API expects xhigh, not max. + +test("sanitizeReasoningEffortForProvider: native deepseek maps xhigh → max", () => { + const log = makeLog(); + const body = { + model: "deepseek-v4-pro", + reasoning_effort: "xhigh", + messages: [{ role: "user", content: "hi" }], + }; + 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.ok( + log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /xhigh → max/.test(m)), + "logs the xhigh → max mapping" + ); +}); + +test("sanitizeReasoningEffortForProvider: native deepseek preserves max", () => { + const log = makeLog(); + const body = { + model: "deepseek-v4-flash", + reasoning_effort: "max", + messages: [{ role: "user", content: "hi" }], + }; + 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(log.messages.length, 0); +}); + +test("sanitizeReasoningEffortForProvider: native deepseek clamps low → high", () => { + const body = { + model: "deepseek-v4-pro", + reasoning_effort: "low", + messages: [{ role: "user", content: "hi" }], + }; + 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"); +}); + +test("sanitizeReasoningEffortForProvider: native deepseek clamps medium → high", () => { + const body = { + model: "deepseek-v4-pro", + reasoning_effort: "medium", + messages: [{ role: "user", content: "hi" }], + }; + const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-pro", null); + assert.equal((result as any).reasoning_effort, "high"); +}); + +test("sanitizeReasoningEffortForProvider: native deepseek preserves high unchanged", () => { + const body = { + model: "deepseek-v4-pro", + reasoning_effort: "high", + messages: [{ role: "user", content: "hi" }], + }; + 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"); +}); + +test("sanitizeReasoningEffortForProvider: native deepseek maps nested reasoning.effort xhigh → max", () => { + const body = { + model: "deepseek-v4-pro", + reasoning: { effort: "xhigh", summary: "auto" }, + 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); +}); + +test("sanitizeReasoningEffortForProvider: OpenRouter DeepSeek still preserves xhigh (not native)", () => { + // Regression guard: the native-deepseek mapping must NOT touch openrouter, + // whose normalized API expects xhigh (issue earendil-works/pi#4055). + const body = { + model: "deepseek/deepseek-v4-pro", + reasoning_effort: "xhigh", + messages: [{ role: "user", content: "hi" }], + }; + const result = sanitizeReasoningEffortForProvider( + body, + "openrouter", + "deepseek/deepseek-v4-pro", + null + ); + assert.equal(result, body); + assert.equal((result as any).reasoning_effort, "xhigh"); +});