Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/provider/openai-compatible-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,84 @@ 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<string, unknown>,
model = V4,
): Record<string, unknown> {
const adapter = createOpenAICompatibleAdapter(source);
const built = adapter.buildRequest(history, model, {
maxTokens: 100,
...(providerOptions !== undefined ? { providerOptions } : {}),
} as InferenceOptions);
return JSON.parse(built.body) as Record<string, unknown>;
}

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<string, unknown>[];
const assistant = msgs.find((m) => m["role"] === "assistant");
expect("reasoning_content" in defined(assistant)).toBe(false);
});
});
56 changes: 51 additions & 5 deletions src/provider/openai-compatible-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ 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);
// 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");
if (!hasProviderOptions && !stripReasoning && !v4Flash)
return ensureAccept(built);

const body = JSON.parse(built.body) as Record<string, unknown>;
if (hasProviderOptions) Object.assign(body, providerOptions);
Expand All @@ -59,6 +61,7 @@ export function createOpenAICompatibleAdapter(
delete (msg as Record<string, unknown>)["reasoning_content"];
}
}
if (v4Flash) applyDeepSeekV4FlashParams(body);
const merged: BuiltRequest = { ...built, body: JSON.stringify(body) };
return ensureAccept(merged);
};
Expand All @@ -73,3 +76,46 @@ export function createOpenAICompatibleAdapter(

return { ...base, buildRequest, parseResponse };
}

/** True for DeepSeek V4 Flash on any OpenAI-compatible host. */
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<string, "low" | "high" | "max"> = {
minimal: "low",
low: "low",
medium: "high",
high: "high",
xhigh: "max",
max: "max",
};

/** Apply the DeepSeek V4 Flash wire shape to an already-built chat body. */
export function applyDeepSeekV4FlashParams(
body: Record<string, unknown>,
): 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<string, unknown>)
: {};
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 };
}
}
16 changes: 16 additions & 0 deletions src/subagent/provider-family.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,23 @@ export function isGptProvider(input: {
return false;
}

/**
* 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;
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"
Expand All @@ -103,6 +118,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";
Expand Down
Loading