From 53a0b08afe10306429c924eff3da6e6afd9793cf Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 8 Oct 2026 17:23:28 -0700 Subject: [PATCH 1/3] feat(provider): adapt deepseek-v4-flash wire params and family detection DeepSeek V4 Flash is the exception to the historical DeepSeek 400 rule: its encoder keeps every turn's reasoning when tools are present, so stripping it rendered prior tool turns empty and the model stopped thinking. Add the deepseek-v4-flash family (provider-family.ts) and apply the model-card wire shape for that family in the openai-compatible adapter: chat_template_kwargs.thinking, effort folded onto low/high/max, agentic temperature 1.0 / top_p 0.95, and stream_options.include_usage. The family regex keeps other DeepSeek models on the old strip behavior. Added 6 unit tests (15 total pass). CL-10241. --- .../openai-compatible-adapter.test.ts | 73 +++++++++++++++++++ src/provider/openai-compatible-adapter.ts | 69 ++++++++++++++++-- src/subagent/provider-family.ts | 18 +++++ 3 files changed, 155 insertions(+), 5 deletions(-) diff --git a/src/provider/openai-compatible-adapter.test.ts b/src/provider/openai-compatible-adapter.test.ts index 518ff29b5..23b8706b1 100644 --- a/src/provider/openai-compatible-adapter.test.ts +++ b/src/provider/openai-compatible-adapter.test.ts @@ -164,3 +164,76 @@ describe("openai-compatible adapter reasoning_content handling", () => { expect(assistant?.["reasoning_content"]).toBe("ponder"); }); }); + +describe("openai-compatible adapter DeepSeek V4 Flash wire params", () => { + const V4 = "deepseek-ai/DeepSeek-V4-Flash-0731"; + const history: ConversationTurn[] = [ + { role: "user", content: [{ type: "text", text: "hi" }] }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "ponder" }, + { type: "text", text: "hello" }, + ], + }, + { role: "user", content: [{ type: "text", text: "again" }] }, + ] as unknown as ConversationTurn[]; + + function v4Body( + providerOptions?: Record, + model = V4, + ): Record { + const adapter = createOpenAICompatibleAdapter(source); + const built = adapter.buildRequest(history, model, { + maxTokens: 100, + ...(providerOptions !== undefined ? { providerOptions } : {}), + } as InferenceOptions); + return JSON.parse(built.body) as Record; + } + + test("turns thinking on and applies agentic sampling at high effort", () => { + const body = v4Body({ reasoning_effort: "high" }); + expect(body["chat_template_kwargs"]).toEqual({ thinking: true }); + expect(body["reasoning_effort"]).toBe("high"); + expect(body["temperature"]).toBe(1); + expect(body["top_p"]).toBe(0.95); + expect(body["stream_options"]).toEqual({ include_usage: true }); + }); + + test("folds Corbits' ladder onto low/high/max", () => { + expect(v4Body({ reasoning_effort: "minimal" })["reasoning_effort"]).toBe("low"); + expect(v4Body({ reasoning_effort: "medium" })["reasoning_effort"]).toBe("high"); + expect(v4Body({ reasoning_effort: "max" })["reasoning_effort"]).toBe("max"); + }); + + test("effort none sends chat mode explicitly and no effort", () => { + const body = v4Body({ reasoning_effort: "none" }); + expect(body["chat_template_kwargs"]).toEqual({ thinking: false }); + expect("reasoning_effort" in body).toBe(false); + expect("top_p" in body).toBe(false); + }); + + test("an explicitly configured temperature/top_p wins", () => { + const body = v4Body({ reasoning_effort: "high", temperature: 0.6, top_p: 0.9 }); + expect(body["temperature"]).toBe(0.6); + expect(body["top_p"]).toBe(0.9); + }); + + test("replays reasoning_content on prior assistant turns", () => { + const msgs = v4Body({ reasoning_effort: "high" })["messages"] as Record< + string, + unknown + >[]; + const assistant = msgs.find((m) => m["role"] === "assistant"); + expect(assistant?.["reasoning_content"]).toBe("ponder"); + }); + + test("other DeepSeek models keep the old strip and get no extra params", () => { + const body = v4Body({ reasoning_effort: "high" }, "deepseek-v3.2"); + expect("chat_template_kwargs" in body).toBe(false); + expect("top_p" in body).toBe(false); + const msgs = body["messages"] as Record[]; + const assistant = msgs.find((m) => m["role"] === "assistant"); + expect("reasoning_content" in defined(assistant)).toBe(false); + }); +}); diff --git a/src/provider/openai-compatible-adapter.ts b/src/provider/openai-compatible-adapter.ts index d869af801..679a914ca 100644 --- a/src/provider/openai-compatible-adapter.ts +++ b/src/provider/openai-compatible-adapter.ts @@ -45,11 +45,19 @@ export function createOpenAICompatibleAdapter( const providerOptions = options.providerOptions; const hasProviderOptions = providerOptions !== undefined && Object.keys(providerOptions).length > 0; - // 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"); - needsDeepSeekPatch = stripReasoning; - if (!hasProviderOptions && !stripReasoning) return ensureAccept(built); + // DeepSeek's hosted API historically returned HTTP 400 if + // `reasoning_content` appeared in input messages, whereas the base adapter + // emits it for any model with thinking enabled. DeepSeek V4 Flash is the + // exception: its encoder keeps every turn's reasoning when tools are + // present, so stripping it renders each prior tool turn as an empty + // thinking response and the model copies that and stops thinking. + const v4Flash = isDeepSeekV4Flash(model); + const stripReasoning = + model.toLowerCase().includes("deepseek") && !v4Flash; + needsDeepSeekPatch = model.toLowerCase().includes("deepseek"); + const v4FlashParams = v4Flash; + if (!hasProviderOptions && !stripReasoning && !v4FlashParams) + return ensureAccept(built); const body = JSON.parse(built.body) as Record; if (hasProviderOptions) Object.assign(body, providerOptions); @@ -59,6 +67,7 @@ export function createOpenAICompatibleAdapter( delete (msg as Record)["reasoning_content"]; } } + if (v4FlashParams) applyDeepSeekV4FlashParams(body); const merged: BuiltRequest = { ...built, body: JSON.stringify(body) }; return ensureAccept(merged); }; @@ -73,3 +82,53 @@ export function createOpenAICompatibleAdapter( return { ...base, buildRequest, parseResponse }; } + +/** DeepSeek V4 Flash on any OpenAI-compatible host (SGLang, vLLM, gateway). */ +export function isDeepSeekV4Flash(model: string): boolean { + return /(^|\/)deepseek-v4-flash/i.test(model.trim()); +} + +// DeepSeek V4 understands exactly three reasoning_effort levels (low, high, +// max); its reference encoder asserts on anything else. Corbits' generic +// ladder (minimal/low/medium/high/xhigh/max) is folded onto those three. +const DSV4_EFFORT: Record = { + minimal: "low", + low: "low", + medium: "high", + high: "high", + xhigh: "max", + max: "max", +}; + +/** + * Wire shape the DeepSeek V4 Flash model card asks for, applied to an + * already-built Chat Completions body: + * - thinking is driven by `chat_template_kwargs.thinking` (SGLang and vLLM + * both read it); `reasoning_effort` "none"/"off" or absent means chat mode. + * - effort folds onto low/high/max. + * - thinking mode samples at temperature 1.0 / top_p 0.95 (agentic); an + * explicitly configured temperature/top_p still wins. + * - stream usage is requested so token counts are observable. + */ +export function applyDeepSeekV4FlashParams(body: Record): void { + const raw = body["reasoning_effort"]; + const effort = typeof raw === "string" ? raw.toLowerCase() : undefined; + const thinking = + effort !== undefined && effort !== "none" && effort !== "off"; + const existingKwargs = + body["chat_template_kwargs"] !== null && + typeof body["chat_template_kwargs"] === "object" + ? (body["chat_template_kwargs"] as Record) + : {}; + body["chat_template_kwargs"] = { ...existingKwargs, thinking }; + if (thinking) { + body["reasoning_effort"] = DSV4_EFFORT[effort] ?? "high"; + if (body["temperature"] === undefined) body["temperature"] = 1.0; + if (body["top_p"] === undefined) body["top_p"] = 0.95; + } else { + delete body["reasoning_effort"]; + } + if (body["stream"] === true && body["stream_options"] === undefined) { + body["stream_options"] = { include_usage: true }; + } +} diff --git a/src/subagent/provider-family.ts b/src/subagent/provider-family.ts index 8a42497e4..97388579b 100644 --- a/src/subagent/provider-family.ts +++ b/src/subagent/provider-family.ts @@ -83,8 +83,25 @@ export function isGptProvider(input: { return false; } +/** + * True when the model id is DeepSeek V4 Flash (any provider: self-hosted + * SGLang/vLLM or a gateway). Scoped to the Flash cell on purpose: the + * residual it unlocks was tuned against that model only, so other DeepSeek + * ids keep the default policy. + */ +export function isDeepSeekV4FlashProvider(input: { + providerName: string; + model?: string; +}): boolean { + return ( + input.model !== undefined && + /(^|\/)deepseek-v4-flash/i.test(input.model.trim()) + ); +} + /** Model families the shared directors branch on via ModelFamilyPolicy. */ export type ModelFamily = + | "deepseek-v4-flash" | "grok" | "kimi" | "muse" @@ -103,6 +120,7 @@ export function detectModelFamily(input: { providerName: string; model?: string; }): ModelFamily { + if (isDeepSeekV4FlashProvider(input)) return "deepseek-v4-flash"; if (isXaiGrokLeafProvider(input)) return "grok"; if (isKimiLeafProvider(input)) return "kimi"; if (isMuseSparkLeafProvider(input)) return "muse"; From 9061eeb02d45422c0937c2a584f75e269350d66d Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 8 Oct 2026 19:53:38 -0700 Subject: [PATCH 2/3] style(provider): format openai-compatible adapter --- src/provider/openai-compatible-adapter.test.ts | 14 +++++++++++--- src/provider/openai-compatible-adapter.ts | 7 ++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/provider/openai-compatible-adapter.test.ts b/src/provider/openai-compatible-adapter.test.ts index 23b8706b1..41ec48e69 100644 --- a/src/provider/openai-compatible-adapter.test.ts +++ b/src/provider/openai-compatible-adapter.test.ts @@ -201,8 +201,12 @@ describe("openai-compatible adapter DeepSeek V4 Flash wire params", () => { }); test("folds Corbits' ladder onto low/high/max", () => { - expect(v4Body({ reasoning_effort: "minimal" })["reasoning_effort"]).toBe("low"); - expect(v4Body({ reasoning_effort: "medium" })["reasoning_effort"]).toBe("high"); + expect(v4Body({ reasoning_effort: "minimal" })["reasoning_effort"]).toBe( + "low", + ); + expect(v4Body({ reasoning_effort: "medium" })["reasoning_effort"]).toBe( + "high", + ); expect(v4Body({ reasoning_effort: "max" })["reasoning_effort"]).toBe("max"); }); @@ -214,7 +218,11 @@ describe("openai-compatible adapter DeepSeek V4 Flash wire params", () => { }); test("an explicitly configured temperature/top_p wins", () => { - const body = v4Body({ reasoning_effort: "high", temperature: 0.6, top_p: 0.9 }); + const body = v4Body({ + reasoning_effort: "high", + temperature: 0.6, + top_p: 0.9, + }); expect(body["temperature"]).toBe(0.6); expect(body["top_p"]).toBe(0.9); }); diff --git a/src/provider/openai-compatible-adapter.ts b/src/provider/openai-compatible-adapter.ts index 679a914ca..2538f386a 100644 --- a/src/provider/openai-compatible-adapter.ts +++ b/src/provider/openai-compatible-adapter.ts @@ -52,8 +52,7 @@ export function createOpenAICompatibleAdapter( // present, so stripping it renders each prior tool turn as an empty // thinking response and the model copies that and stops thinking. const v4Flash = isDeepSeekV4Flash(model); - const stripReasoning = - model.toLowerCase().includes("deepseek") && !v4Flash; + const stripReasoning = model.toLowerCase().includes("deepseek") && !v4Flash; needsDeepSeekPatch = model.toLowerCase().includes("deepseek"); const v4FlashParams = v4Flash; if (!hasProviderOptions && !stripReasoning && !v4FlashParams) @@ -110,7 +109,9 @@ const DSV4_EFFORT: Record = { * explicitly configured temperature/top_p still wins. * - stream usage is requested so token counts are observable. */ -export function applyDeepSeekV4FlashParams(body: Record): void { +export function applyDeepSeekV4FlashParams( + body: Record, +): void { const raw = body["reasoning_effort"]; const effort = typeof raw === "string" ? raw.toLowerCase() : undefined; const thinking = From f801c7af25a98b16628aa29612bc57d08b216be5 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 8 Oct 2026 20:24:36 -0700 Subject: [PATCH 3/3] refactor(provider): trim adapter and family-policy comments --- src/provider/openai-compatible-adapter.ts | 26 ++++++----------------- src/subagent/provider-family.ts | 6 ++---- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/src/provider/openai-compatible-adapter.ts b/src/provider/openai-compatible-adapter.ts index 2538f386a..4ac3518b0 100644 --- a/src/provider/openai-compatible-adapter.ts +++ b/src/provider/openai-compatible-adapter.ts @@ -45,17 +45,12 @@ export function createOpenAICompatibleAdapter( const providerOptions = options.providerOptions; const hasProviderOptions = providerOptions !== undefined && Object.keys(providerOptions).length > 0; - // DeepSeek's hosted API historically returned HTTP 400 if - // `reasoning_content` appeared in input messages, whereas the base adapter - // emits it for any model with thinking enabled. DeepSeek V4 Flash is the - // exception: its encoder keeps every turn's reasoning when tools are - // present, so stripping it renders each prior tool turn as an empty - // thinking response and the model copies that and stops thinking. + // Earlier DeepSeek models 400 on `reasoning_content` in input messages; + // V4 Flash keeps it because its encoder preserves each turn's thinking. const v4Flash = isDeepSeekV4Flash(model); const stripReasoning = model.toLowerCase().includes("deepseek") && !v4Flash; needsDeepSeekPatch = model.toLowerCase().includes("deepseek"); - const v4FlashParams = v4Flash; - if (!hasProviderOptions && !stripReasoning && !v4FlashParams) + if (!hasProviderOptions && !stripReasoning && !v4Flash) return ensureAccept(built); const body = JSON.parse(built.body) as Record; @@ -66,7 +61,7 @@ export function createOpenAICompatibleAdapter( delete (msg as Record)["reasoning_content"]; } } - if (v4FlashParams) applyDeepSeekV4FlashParams(body); + if (v4Flash) applyDeepSeekV4FlashParams(body); const merged: BuiltRequest = { ...built, body: JSON.stringify(body) }; return ensureAccept(merged); }; @@ -82,7 +77,7 @@ export function createOpenAICompatibleAdapter( return { ...base, buildRequest, parseResponse }; } -/** DeepSeek V4 Flash on any OpenAI-compatible host (SGLang, vLLM, gateway). */ +/** True for DeepSeek V4 Flash on any OpenAI-compatible host. */ export function isDeepSeekV4Flash(model: string): boolean { return /(^|\/)deepseek-v4-flash/i.test(model.trim()); } @@ -99,16 +94,7 @@ const DSV4_EFFORT: Record = { max: "max", }; -/** - * Wire shape the DeepSeek V4 Flash model card asks for, applied to an - * already-built Chat Completions body: - * - thinking is driven by `chat_template_kwargs.thinking` (SGLang and vLLM - * both read it); `reasoning_effort` "none"/"off" or absent means chat mode. - * - effort folds onto low/high/max. - * - thinking mode samples at temperature 1.0 / top_p 0.95 (agentic); an - * explicitly configured temperature/top_p still wins. - * - stream usage is requested so token counts are observable. - */ +/** Apply the DeepSeek V4 Flash wire shape to an already-built chat body. */ export function applyDeepSeekV4FlashParams( body: Record, ): void { diff --git a/src/subagent/provider-family.ts b/src/subagent/provider-family.ts index 97388579b..48adefad7 100644 --- a/src/subagent/provider-family.ts +++ b/src/subagent/provider-family.ts @@ -84,10 +84,8 @@ export function isGptProvider(input: { } /** - * True when the model id is DeepSeek V4 Flash (any provider: self-hosted - * SGLang/vLLM or a gateway). Scoped to the Flash cell on purpose: the - * residual it unlocks was tuned against that model only, so other DeepSeek - * ids keep the default policy. + * True when the model id is DeepSeek V4 Flash (any provider). Scoped to the + * Flash cell only: the residual it unlocks was tuned against that model. */ export function isDeepSeekV4FlashProvider(input: { providerName: string;