Skip to content
Merged
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
42 changes: 40 additions & 2 deletions src/provider/openai-compatible-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,20 +150,58 @@ describe("openai-compatible adapter reasoning_content handling", () => {
.messages;
}

test("strips reasoning_content from input messages for DeepSeek models", () => {
const assistant = messagesFor("deepseek-v4").find(
test("strips reasoning_content from input messages for pre-V4 DeepSeek models", () => {
const assistant = messagesFor("deepseek-v3").find(
(m) => m["role"] === "assistant",
);
expect(assistant).toBeDefined();
expect("reasoning_content" in defined(assistant)).toBe(false);
});

test.each([
"deepseek-v4",
"deepseek-v4-pro",
"deepseek-v4-flash",
"deepseek-v4-flash-vision-exp",
])("V4 keeps reasoning_content on replayed assistant turns (%s)", (model) => {
const assistant = messagesFor(model).find((m) => m["role"] === "assistant");
expect(assistant).toBeDefined();
expect(assistant?.["reasoning_content"]).toBe("ponder");
});

test("keeps reasoning_content for non-DeepSeek models", () => {
const assistant = messagesFor("kimi-k2").find(
(m) => m["role"] === "assistant",
);
expect(assistant?.["reasoning_content"]).toBe("ponder");
});

test("non-V4/non-DeepSeek models are byte-identical on reasoning_content", () => {
for (const model of [
"gpt-5",
"gpt-5.1",
"kimi-k2",
"grok-4.6",
"claude-sonnet-4.5",
"glm-5.3",
"openai-compatible-generic",
]) {
const assistant = messagesFor(model).find(
(m) => m["role"] === "assistant",
);
expect(assistant?.["reasoning_content"]).toBe("ponder");
}
});

test("pre-V4 DeepSeek ids still strip reasoning_content (byte-identical)", () => {
for (const model of ["deepseek-v3", "deepseek-r1"]) {
const assistant = messagesFor(model).find(
(m) => m["role"] === "assistant",
);
expect(assistant).toBeDefined();
expect("reasoning_content" in defined(assistant)).toBe(false);
}
});
});

describe("openai-compatible adapter V4 effort wiring", () => {
Expand Down
14 changes: 10 additions & 4 deletions src/provider/openai-compatible-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,17 @@ export function createOpenAICompatibleAdapter(
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.
// whereas the base adapter emits it for any model with thinking enabled. V4
// keeps each turn's reasoning on replay, so only pre-V4 DeepSeek strips it.
const m = model.toLowerCase().trim();
const stripReasoning = m.includes("deepseek");
needsDeepSeekPatch = stripReasoning;
if (!hasProviderOptions && !stripReasoning) return ensureAccept(built);
const isDeepSeek = m.includes("deepseek");
const stripReasoning = isDeepSeek && !/^deepseek-v4/i.test(m);
needsDeepSeekPatch = isDeepSeek;
// V4 always runs the settings step below, even with no providerOptions, so
// its sampling/stream defaults reach the wire; non-V4 models without
// providerOptions short-circuit here and stay byte-identical.
if (!hasProviderOptions && !stripReasoning && !/^deepseek-v4/i.test(m))
return ensureAccept(built);

const body = JSON.parse(built.body) as Record<string, unknown>;
if (hasProviderOptions) Object.assign(body, providerOptions);
Expand Down
Loading