From e44748b6d24405163000869fe459deaa24946669 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 8 Oct 2026 19:41:43 -0700 Subject: [PATCH 1/3] feat(provider): add v4 effort ladder and wire mapping --- src/provider/deepseek-v4-effort.test.ts | 57 +++++++++++++++++ src/provider/deepseek-v4-effort.ts | 40 ++++++++++++ .../openai-compatible-adapter.test.ts | 64 +++++++++++++++++++ src/provider/openai-compatible-adapter.ts | 31 +++++++++ src/provider/reasoning-effort.test.ts | 24 +++++++ src/provider/reasoning-effort.ts | 8 +++ 6 files changed, 224 insertions(+) create mode 100644 src/provider/deepseek-v4-effort.test.ts create mode 100644 src/provider/deepseek-v4-effort.ts diff --git a/src/provider/deepseek-v4-effort.test.ts b/src/provider/deepseek-v4-effort.test.ts new file mode 100644 index 000000000..cebfb5cb3 --- /dev/null +++ b/src/provider/deepseek-v4-effort.test.ts @@ -0,0 +1,57 @@ +import { describe, test, expect } from "bun:test"; +import { + DEEPSEEK_V4_EFFORTS, + isDeepSeekV4Model, + mapV4Effort, + v4Thinking, +} from "./deepseek-v4-effort.js"; + +describe("isDeepSeekV4Model", () => { + test.each([ + "deepseek-v4", + "deepseek-v4-pro", + "deepseek-v4-flash", + "deepseek-v4-flash-vision-exp", + ])("true for V4 id %s", (model) => { + expect(isDeepSeekV4Model(model)).toBe(true); + }); + test.each([ + "deepseek-v3", + "deepseek-r1", + "gpt-5", + "kimi-k2", + "claude-sonnet-4.5", + "glm-5.3", + "grok-4.6", + "some-proxy-deepseek-v4-pro", + ])("false for non-V4 id %s", (model) => { + expect(isDeepSeekV4Model(model)).toBe(false); + }); +}); + +describe("DEEPSEEK_V4_EFFORTS", () => { + test("ladder is none/xhigh/max", () => { + expect(DEEPSEEK_V4_EFFORTS).toEqual(["none", "xhigh", "max"]); + }); +}); + +describe("mapV4Effort", () => { + test.each([ + ["xhigh", "xhigh"], + ["max", "max"], + ] as const)("maps %s raw to %s", (effort, wire) => { + expect(mapV4Effort(effort)).toBe(wire); + }); + test("maps none to null (drop the wire effort)", () => { + expect(mapV4Effort("none")).toBeNull(); + }); +}); + +describe("v4Thinking", () => { + test("thinking off for none", () => { + expect(v4Thinking("none")).toEqual({ thinking: false }); + }); + test.each(["xhigh", "max"] as const)("thinking on for %s", (effort) => { + expect(v4Thinking(effort)).toEqual({ thinking: true }); + }); +}); diff --git a/src/provider/deepseek-v4-effort.ts b/src/provider/deepseek-v4-effort.ts new file mode 100644 index 000000000..0792732d8 --- /dev/null +++ b/src/provider/deepseek-v4-effort.ts @@ -0,0 +1,40 @@ +import type { ReasoningEffort } from "../agent/profile-types.js"; + +// DeepSeek V4 effort translator shared by the adapter and the effort picker. +// V4 ships under several ids across two catalogs — deepseek-v4-pro/-flash/ +// -flash-vision-exp — so the family is matched by prefix rather than an exact +// id list; the `^` anchor keeps hypothetical spaced/pre-V4 ids out. This shape +// mirrors the grok/kimi/muse-spark prefix predicates elsewhere in the tree. +export function isDeepSeekV4Model(model: string): boolean { + return /^deepseek-v4/i.test(model.trim()); +} + +// The operator-confirmed native V4 rungs. `none` is a toggle, never a wire +// effort; `xhigh`/`max` are the wire rungs sent raw (the API docs' low/high/max +// is the gateway normalization layer, not what the model/encoder honors). +export const DEEPSEEK_V4_EFFORTS: readonly ReasoningEffort[] = [ + "none", + "xhigh", + "max", +]; + +/** + * Translate a V4 effort to its raw wire value. `xhigh`/`max` pass through + * verbatim; `none` maps to null — the caller drops `reasoning_effort` and + * signals thinking off instead, because `reasoning_effort:"none"` is illegal + * on the wire. + */ +export function mapV4Effort(effort: ReasoningEffort): "xhigh" | "max" | null { + if (effort === "none") return null; + return effort === "max" ? "max" : "xhigh"; +} + +/** + * chat_template_kwargs toggle for a V4 effort: thinking off for `none`, + * thinking on for any wire effort. When the caller drops `reasoning_effort` + * for `none` it supplies `{thinking:false}` so the encoder still knows to + * bypass the reasoning pass. + */ +export function v4Thinking(effort: ReasoningEffort): { thinking: boolean } { + return { thinking: effort !== "none" }; +} diff --git a/src/provider/openai-compatible-adapter.test.ts b/src/provider/openai-compatible-adapter.test.ts index 518ff29b5..8f4596f91 100644 --- a/src/provider/openai-compatible-adapter.test.ts +++ b/src/provider/openai-compatible-adapter.test.ts @@ -164,3 +164,67 @@ describe("openai-compatible adapter reasoning_content handling", () => { expect(assistant?.["reasoning_content"]).toBe("ponder"); }); }); + +describe("openai-compatible adapter V4 effort wiring", () => { + function bodyForModel( + model: string, + options: InferenceOptions, + ): Record { + const adapter = createOpenAICompatibleAdapter({ + ...source, + model, + } as typeof source); + const built = adapter.buildRequest(messages, model, options); + return JSON.parse(built.body) as Record; + } + + test("none drops reasoning_effort and sets thinking off", () => { + const body = bodyForModel("deepseek-v4-pro", { + providerOptions: { reasoning_effort: "none" }, + } as InferenceOptions); + expect("reasoning_effort" in body).toBe(false); + expect(body["chat_template_kwargs"]).toEqual({ thinking: false }); + }); + + describe("v4-effort is no-op for non-V4", () => { + const models = [ + "gpt-5", + "gpt-5.1", + "kimi-k2", + "grok-4.6", + "claude-sonnet-4.5", + "glm-5.3", + "some-openai-compatible-model", + "deepseek-r1", + "deepseek-v3", + ]; + + test.each(models)( + "merged body for %s is byte-identical to a manual base+merge (V4 step no-op)", + (model) => { + const base = bodyForModel(model, {} as InferenceOptions); + const withEffort = bodyForModel(model, { + providerOptions: { reasoning_effort: "high" }, + } as InferenceOptions); + // For a non-V4 model the V4 step must not run: the output body equals + // the base body shallow-merged with providerOptions verbatim (no + // effort mapping, no chat_template_kwargs injection). + const expected = { + ...base, + ...({ reasoning_effort: "high" } as const), + }; + expect(withEffort).toEqual(expected); + }, + ); + + test.each(models)( + "merged body for %s carries reasoning_effort verbatim", + (model) => { + const body = bodyForModel(model, { + providerOptions: { reasoning_effort: "high" }, + } as InferenceOptions); + expect(body["reasoning_effort"]).toBe("high"); + }, + ); + }); +}); diff --git a/src/provider/openai-compatible-adapter.ts b/src/provider/openai-compatible-adapter.ts index d869af801..a598c2a0d 100644 --- a/src/provider/openai-compatible-adapter.ts +++ b/src/provider/openai-compatible-adapter.ts @@ -1,5 +1,7 @@ import { type BuiltRequest, type ProviderAdapter } from "@intx/inference"; import { createOpenAIAdapter } from "@intx/inference/providers"; +import type { ReasoningEffort } from "../agent/profile-types.js"; +import { isDeepSeekV4Model, mapV4Effort } from "./deepseek-v4-effort.js"; import { normalizeNullDeltaFields } from "./null-delta-fields.js"; // The stock OpenAI adapter builds the request body from a fixed set of fields @@ -53,6 +55,35 @@ export function createOpenAICompatibleAdapter( const body = JSON.parse(built.body) as Record; if (hasProviderOptions) Object.assign(body, providerOptions); + // V4 drives its own effort ladder (none/xhigh/max) and encodes thinking via + // chat_template_kwargs. `none` is a toggle, never a wire effort — drop + // reasoning_effort and tell the encoder to skip the reasoning pass; xhigh/ + // max go out raw (the API docs' low/high/max is gateway normalization, not + // what the model/encoder honors). Non-V4 models keep the merged body as-is. + if (isDeepSeekV4Model(model)) { + // PR3's effort translation only applies when a reasoning_effort is + // actually carried (none/xhigh/max) — an absent one stays absent, so an + // unset default under the V4 ladder is never invented here. + let wire: "xhigh" | "max" | null = null; + if ("reasoning_effort" in body) { + const raw = body["reasoning_effort"] as ReasoningEffort; + wire = mapV4Effort(raw); + if (wire === null) { + delete body["reasoning_effort"]; + // Merge the thinking-off toggle into any existing kwargs rather than + // replacing the object, so provider-supplied keys survive. + const kwargs = + body["chat_template_kwargs"] !== null && + typeof body["chat_template_kwargs"] === "object" + ? (body["chat_template_kwargs"] as Record) + : {}; + kwargs.thinking = false; + Object.assign(body, { chat_template_kwargs: kwargs }); + } else { + body["reasoning_effort"] = wire; + } + } + } if (stripReasoning && Array.isArray(body["messages"])) { for (const msg of body["messages"]) { if (msg !== null && typeof msg === "object") diff --git a/src/provider/reasoning-effort.test.ts b/src/provider/reasoning-effort.test.ts index b3bcc699e..390cdba1d 100644 --- a/src/provider/reasoning-effort.test.ts +++ b/src/provider/reasoning-effort.test.ts @@ -116,6 +116,17 @@ describe("supportedEfforts", () => { expect(supportedEfforts("glm-5.3")).toEqual(["low", "high", "max"]); }); + test("V4 model gets the native none/xhigh/max ladder", () => { + expect(supportedEfforts("deepseek-v4-pro")).toEqual([ + "none", + "xhigh", + "max", + ]); + }); + test("pre-V4 deepseek-v3 stays on the unknown-model subset", () => { + expect(supportedEfforts("deepseek-v3")).toEqual(["low", "medium", "high"]); + }); + // Every catalog Muse Spark id reaches supportedEfforts unnormalized, so // they must all land on the same ladder — two ids pin the family rule. test.each(["muse-spark-1.3-contributor", "muse-spark-1.3-contributor-free"])( @@ -175,6 +186,15 @@ describe("validateEffort", () => { expect(validateEffort("glm-5.3", "medium").ok).toBe(false); expect(validateEffort("glm-5.3", "max")).toEqual({ ok: true }); }); + + test("V4 accepts none/xhigh/max and rejects medium (and low/high)", () => { + for (const effort of ["none", "xhigh", "max"] as const) { + expect(validateEffort("deepseek-v4-pro", effort)).toEqual({ ok: true }); + } + for (const effort of ["medium", "low", "high"] as const) { + expect(validateEffort("deepseek-v4-pro", effort).ok).toBe(false); + } + }); }); describe("cycleReasoningEffort", () => { @@ -448,6 +468,10 @@ describe("defaultEffortForModel", () => { expect(defaultEffortForModel("glm-5.3")).toBe("max"); }); + test("V4 model defaults to max", () => { + expect(defaultEffortForModel("deepseek-v4-pro")).toBe("max"); + }); + test("gpt-5 and o-series default to medium", () => { expect(defaultEffortForModel("gpt-5")).toBe("medium"); expect(defaultEffortForModel("o4-mini")).toBe("medium"); diff --git a/src/provider/reasoning-effort.ts b/src/provider/reasoning-effort.ts index 44f3a694d..ebe2af2ca 100644 --- a/src/provider/reasoning-effort.ts +++ b/src/provider/reasoning-effort.ts @@ -7,6 +7,10 @@ // profile schema and the runtime cannot drift. Re-exported here for callers // that already import from this module. import { REASONING_EFFORTS as CANONICAL_EFFORTS } from "../agent/profile-types.js"; +import { + DEEPSEEK_V4_EFFORTS, + isDeepSeekV4Model, +} from "./deepseek-v4-effort.js"; export const REASONING_EFFORTS = CANONICAL_EFFORTS; export type ReasoningEffort = (typeof REASONING_EFFORTS)[number]; @@ -186,6 +190,9 @@ export function supportedEfforts( if (GLM_53_MODELS.includes(model)) { return [...GLM_53_EFFORTS]; } + if (isDeepSeekV4Model(model)) { + return [...DEEPSEEK_V4_EFFORTS]; + } if (isMuseSparkModel(model)) { return [...MUSE_SPARK_EFFORTS]; } @@ -287,6 +294,7 @@ export function defaultEffortForModel( } if (model.startsWith("grok")) return pick("high"); if (GLM_53_MODELS.includes(model)) return pick("max"); + if (isDeepSeekV4Model(model)) return "max"; if (isMuseSparkModel(model)) return pick("low"); if (!isCodex && supported.includes("none")) return "none"; if (isCodex || isKnownOpenAIReasoningModel(model)) return pick("medium"); From d6fdd4ae017319b76ceb2e6067dee659bb52425b Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 8 Oct 2026 20:29:09 -0700 Subject: [PATCH 2/3] style(provider): tighten v4 effort comments --- src/provider/deepseek-v4-effort.ts | 27 +++++++------------ .../openai-compatible-adapter.test.ts | 3 --- src/provider/openai-compatible-adapter.ts | 18 +++++-------- 3 files changed, 16 insertions(+), 32 deletions(-) diff --git a/src/provider/deepseek-v4-effort.ts b/src/provider/deepseek-v4-effort.ts index 0792732d8..737ed9d05 100644 --- a/src/provider/deepseek-v4-effort.ts +++ b/src/provider/deepseek-v4-effort.ts @@ -3,38 +3,31 @@ import type { ReasoningEffort } from "../agent/profile-types.js"; // DeepSeek V4 effort translator shared by the adapter and the effort picker. // V4 ships under several ids across two catalogs — deepseek-v4-pro/-flash/ // -flash-vision-exp — so the family is matched by prefix rather than an exact -// id list; the `^` anchor keeps hypothetical spaced/pre-V4 ids out. This shape -// mirrors the grok/kimi/muse-spark prefix predicates elsewhere in the tree. +// id list, mirroring the grok/kimi/muse-spark prefix predicates elsewhere. export function isDeepSeekV4Model(model: string): boolean { return /^deepseek-v4/i.test(model.trim()); } -// The operator-confirmed native V4 rungs. `none` is a toggle, never a wire -// effort; `xhigh`/`max` are the wire rungs sent raw (the API docs' low/high/max -// is the gateway normalization layer, not what the model/encoder honors). +// The native V4 rungs. `none` is a toggle, never a wire effort; `xhigh`/`max` +// are the wire rungs sent raw (the API docs' low/high/max is the gateway +// normalization layer, not what the model/encoder honors). export const DEEPSEEK_V4_EFFORTS: readonly ReasoningEffort[] = [ "none", "xhigh", "max", ]; -/** - * Translate a V4 effort to its raw wire value. `xhigh`/`max` pass through - * verbatim; `none` maps to null — the caller drops `reasoning_effort` and - * signals thinking off instead, because `reasoning_effort:"none"` is illegal - * on the wire. - */ +// Translate a V4 effort to its raw wire value: `none` maps to null (the caller +// drops `reasoning_effort`, since `reasoning_effort:"none"` is illegal on the +// wire), otherwise `xhigh`/`max` pass through verbatim. export function mapV4Effort(effort: ReasoningEffort): "xhigh" | "max" | null { if (effort === "none") return null; return effort === "max" ? "max" : "xhigh"; } -/** - * chat_template_kwargs toggle for a V4 effort: thinking off for `none`, - * thinking on for any wire effort. When the caller drops `reasoning_effort` - * for `none` it supplies `{thinking:false}` so the encoder still knows to - * bypass the reasoning pass. - */ +// chat_template_kwargs thinking toggle for a V4 effort: off for `none`, on for +// any wire effort. The caller pairs the dropped reasoning_effort with +// `{thinking:false}` so the encoder still bypasses the reasoning pass. export function v4Thinking(effort: ReasoningEffort): { thinking: boolean } { return { thinking: effort !== "none" }; } diff --git a/src/provider/openai-compatible-adapter.test.ts b/src/provider/openai-compatible-adapter.test.ts index 8f4596f91..4f3872dd3 100644 --- a/src/provider/openai-compatible-adapter.test.ts +++ b/src/provider/openai-compatible-adapter.test.ts @@ -206,9 +206,6 @@ describe("openai-compatible adapter V4 effort wiring", () => { const withEffort = bodyForModel(model, { providerOptions: { reasoning_effort: "high" }, } as InferenceOptions); - // For a non-V4 model the V4 step must not run: the output body equals - // the base body shallow-merged with providerOptions verbatim (no - // effort mapping, no chat_template_kwargs injection). const expected = { ...base, ...({ reasoning_effort: "high" } as const), diff --git a/src/provider/openai-compatible-adapter.ts b/src/provider/openai-compatible-adapter.ts index a598c2a0d..7a32a2619 100644 --- a/src/provider/openai-compatible-adapter.ts +++ b/src/provider/openai-compatible-adapter.ts @@ -55,23 +55,17 @@ export function createOpenAICompatibleAdapter( const body = JSON.parse(built.body) as Record; if (hasProviderOptions) Object.assign(body, providerOptions); - // V4 drives its own effort ladder (none/xhigh/max) and encodes thinking via - // chat_template_kwargs. `none` is a toggle, never a wire effort — drop - // reasoning_effort and tell the encoder to skip the reasoning pass; xhigh/ - // max go out raw (the API docs' low/high/max is gateway normalization, not - // what the model/encoder honors). Non-V4 models keep the merged body as-is. + // V4's ladder (none/xhigh/max) is translated here: `none` drops + // reasoning_effort and toggles the encoder's thinking off (it is illegal + // on the wire), while `xhigh`/`max` go out raw. Effort absent under V4 is + // left absent. if (isDeepSeekV4Model(model)) { - // PR3's effort translation only applies when a reasoning_effort is - // actually carried (none/xhigh/max) — an absent one stays absent, so an - // unset default under the V4 ladder is never invented here. - let wire: "xhigh" | "max" | null = null; if ("reasoning_effort" in body) { const raw = body["reasoning_effort"] as ReasoningEffort; - wire = mapV4Effort(raw); + const wire = mapV4Effort(raw); if (wire === null) { delete body["reasoning_effort"]; - // Merge the thinking-off toggle into any existing kwargs rather than - // replacing the object, so provider-supplied keys survive. + // Merge into any existing kwargs so provider-supplied keys survive. const kwargs = body["chat_template_kwargs"] !== null && typeof body["chat_template_kwargs"] === "object" From 2b9d6c24d30c46633c561d1b02707d59636d3e0b Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 8 Oct 2026 20:39:29 -0700 Subject: [PATCH 3/3] refactor(provider): remove dead v4 helper and tighten effort mapping --- src/provider/deepseek-v4-effort.test.ts | 16 ++++++--------- src/provider/deepseek-v4-effort.ts | 17 +++++++--------- .../openai-compatible-adapter.test.ts | 20 +++++++++++++++++++ src/provider/openai-compatible-adapter.ts | 18 ++++++++++------- src/provider/reasoning-effort.ts | 2 +- 5 files changed, 45 insertions(+), 28 deletions(-) diff --git a/src/provider/deepseek-v4-effort.test.ts b/src/provider/deepseek-v4-effort.test.ts index cebfb5cb3..faec39f1b 100644 --- a/src/provider/deepseek-v4-effort.test.ts +++ b/src/provider/deepseek-v4-effort.test.ts @@ -3,7 +3,6 @@ import { DEEPSEEK_V4_EFFORTS, isDeepSeekV4Model, mapV4Effort, - v4Thinking, } from "./deepseek-v4-effort.js"; describe("isDeepSeekV4Model", () => { @@ -45,13 +44,10 @@ describe("mapV4Effort", () => { test("maps none to null (drop the wire effort)", () => { expect(mapV4Effort("none")).toBeNull(); }); -}); - -describe("v4Thinking", () => { - test("thinking off for none", () => { - expect(v4Thinking("none")).toEqual({ thinking: false }); - }); - test.each(["xhigh", "max"] as const)("thinking on for %s", (effort) => { - expect(v4Thinking(effort)).toEqual({ thinking: true }); - }); + test.each(["low", "medium", "high"] as const)( + "maps off-ladder %s to undefined", + (effort) => { + expect(mapV4Effort(effort)).toBeUndefined(); + }, + ); }); diff --git a/src/provider/deepseek-v4-effort.ts b/src/provider/deepseek-v4-effort.ts index 737ed9d05..e9f58ecba 100644 --- a/src/provider/deepseek-v4-effort.ts +++ b/src/provider/deepseek-v4-effort.ts @@ -19,15 +19,12 @@ export const DEEPSEEK_V4_EFFORTS: readonly ReasoningEffort[] = [ // Translate a V4 effort to its raw wire value: `none` maps to null (the caller // drops `reasoning_effort`, since `reasoning_effort:"none"` is illegal on the -// wire), otherwise `xhigh`/`max` pass through verbatim. -export function mapV4Effort(effort: ReasoningEffort): "xhigh" | "max" | null { +// wire), `xhigh`/`max` pass through verbatim, and any off-ladder value returns +// undefined so the caller leaves effort untouched. +export function mapV4Effort( + effort: ReasoningEffort, +): "xhigh" | "max" | null | undefined { if (effort === "none") return null; - return effort === "max" ? "max" : "xhigh"; -} - -// chat_template_kwargs thinking toggle for a V4 effort: off for `none`, on for -// any wire effort. The caller pairs the dropped reasoning_effort with -// `{thinking:false}` so the encoder still bypasses the reasoning pass. -export function v4Thinking(effort: ReasoningEffort): { thinking: boolean } { - return { thinking: effort !== "none" }; + if (effort === "xhigh" || effort === "max") return effort; + return undefined; } diff --git a/src/provider/openai-compatible-adapter.test.ts b/src/provider/openai-compatible-adapter.test.ts index 4f3872dd3..4b5d3d704 100644 --- a/src/provider/openai-compatible-adapter.test.ts +++ b/src/provider/openai-compatible-adapter.test.ts @@ -186,6 +186,26 @@ describe("openai-compatible adapter V4 effort wiring", () => { expect(body["chat_template_kwargs"]).toEqual({ thinking: false }); }); + test.each(["xhigh", "max"] as const)( + "maps ladder %s through verbatim", + (effort) => { + const body = bodyForModel("deepseek-v4-pro", { + providerOptions: { reasoning_effort: effort }, + } as InferenceOptions); + expect(body["reasoning_effort"]).toBe(effort); + }, + ); + + test.each(["low", "medium", "high"] as const)( + "leaves off-ladder %s untouched", + (effort) => { + const body = bodyForModel("deepseek-v4-pro", { + providerOptions: { reasoning_effort: effort }, + } as InferenceOptions); + expect(body["reasoning_effort"]).toBe(effort); + }, + ); + describe("v4-effort is no-op for non-V4", () => { const models = [ "gpt-5", diff --git a/src/provider/openai-compatible-adapter.ts b/src/provider/openai-compatible-adapter.ts index 7a32a2619..9d0bbe61a 100644 --- a/src/provider/openai-compatible-adapter.ts +++ b/src/provider/openai-compatible-adapter.ts @@ -47,10 +47,14 @@ export function createOpenAICompatibleAdapter( const providerOptions = options.providerOptions; const hasProviderOptions = providerOptions !== undefined && Object.keys(providerOptions).length > 0; + // Normalize the model once so every predicate below reads from the same + // lower-cased, trimmed string rather than re-normalizing per call. + const m = model.toLowerCase().trim(); // DeepSeek returns HTTP 400 if `reasoning_content` appears in input messages, // whereas the base adapter emits it for any model with thinking enabled. - const stripReasoning = model.toLowerCase().includes("deepseek"); + const stripReasoning = m.includes("deepseek"); needsDeepSeekPatch = stripReasoning; + const isV4 = isDeepSeekV4Model(m); if (!hasProviderOptions && !stripReasoning) return ensureAccept(built); const body = JSON.parse(built.body) as Record; @@ -58,12 +62,12 @@ export function createOpenAICompatibleAdapter( // V4's ladder (none/xhigh/max) is translated here: `none` drops // reasoning_effort and toggles the encoder's thinking off (it is illegal // on the wire), while `xhigh`/`max` go out raw. Effort absent under V4 is - // left absent. - if (isDeepSeekV4Model(model)) { + // left absent. Off-ladder efforts (low/medium/high) are left untouched. + if (isV4) { if ("reasoning_effort" in body) { const raw = body["reasoning_effort"] as ReasoningEffort; - const wire = mapV4Effort(raw); - if (wire === null) { + const wireEffort = mapV4Effort(raw); + if (wireEffort === null) { delete body["reasoning_effort"]; // Merge into any existing kwargs so provider-supplied keys survive. const kwargs = @@ -73,8 +77,8 @@ export function createOpenAICompatibleAdapter( : {}; kwargs.thinking = false; Object.assign(body, { chat_template_kwargs: kwargs }); - } else { - body["reasoning_effort"] = wire; + } else if (wireEffort !== undefined) { + body["reasoning_effort"] = wireEffort; } } } diff --git a/src/provider/reasoning-effort.ts b/src/provider/reasoning-effort.ts index ebe2af2ca..b9ac32a88 100644 --- a/src/provider/reasoning-effort.ts +++ b/src/provider/reasoning-effort.ts @@ -294,7 +294,7 @@ export function defaultEffortForModel( } if (model.startsWith("grok")) return pick("high"); if (GLM_53_MODELS.includes(model)) return pick("max"); - if (isDeepSeekV4Model(model)) return "max"; + if (isDeepSeekV4Model(model)) return pick("max"); if (isMuseSparkModel(model)) return pick("low"); if (!isCodex && supported.includes("none")) return "none"; if (isCodex || isKnownOpenAIReasoningModel(model)) return pick("medium");