fix(deepseek): backfill v4 assistant reasoning replay

This commit is contained in:
Peter Steinberger
2026-04-28 10:07:15 +01:00
parent 0876ff481b
commit 62997f7fce
9 changed files with 176 additions and 15 deletions

View File

@@ -176,4 +176,59 @@ describeLive("deepseek plugin live", () => {
});
expect(extractNonEmptyAssistantText(result.content).length).toBeGreaterThan(0);
}, 60_000);
it("accepts V4 thinking replay after a prior plain assistant message", async () => {
const context: Context = {
messages: [
{
role: "user",
content: "Say hello.",
timestamp: Date.now() - 2,
},
{
role: "assistant",
api: "openai-completions",
provider: "openai",
model: "gpt-5.4",
content: [{ type: "text", text: "Hello." }],
usage: ZERO_USAGE,
stopReason: "stop",
timestamp: Date.now() - 1,
},
{
role: "user",
content: "Reply with exactly: ok",
timestamp: Date.now(),
},
],
};
let capturedPayload: Record<string, unknown> | undefined;
const streamFn = createDeepSeekV4ThinkingWrapper(streamSimple, "high");
expect(streamFn).toBeDefined();
const stream = streamFn?.(resolveDeepSeekV4LiveModel(), context, {
apiKey: DEEPSEEK_KEY,
maxTokens: 64,
onPayload: (payload) => {
capturedPayload = payload as Record<string, unknown>;
},
});
expect(stream).toBeDefined();
const result = await (await stream!).result();
if (result.stopReason === "error") {
throw new Error(
result.errorMessage || "DeepSeek V4 plain replay returned error with no message",
);
}
const messages = capturedPayload?.messages;
expect(Array.isArray(messages)).toBe(true);
expect((messages as Array<Record<string, unknown>>)[1]).toMatchObject({
role: "assistant",
content: "Hello.",
reasoning_content: "",
});
expect(extractNonEmptyAssistantText(result.content).length).toBeGreaterThan(0);
}, 60_000);
});

View File

@@ -309,6 +309,73 @@ describe("deepseek provider plugin", () => {
});
});
it("adds blank reasoning_content for replayed plain assistant messages", async () => {
let capturedPayload: Record<string, unknown> | undefined;
const model = {
provider: "deepseek",
id: "deepseek-v4-pro",
name: "DeepSeek V4 Pro",
api: "openai-completions",
baseUrl: "https://api.deepseek.com",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_000_000,
maxTokens: 384_000,
compat: {
supportsUsageInStreaming: true,
supportsReasoningEffort: true,
maxTokensField: "max_tokens",
},
} as Model<"openai-completions">;
const context = {
messages: [
{ role: "user", content: "hi", timestamp: 1 },
{
role: "assistant",
api: "openai-completions",
provider: "openai",
model: "gpt-5.4",
content: [{ type: "text", text: "Hello." }],
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: 2,
},
{ role: "user", content: "next", timestamp: 3 },
],
} as Context;
const baseStreamFn = (
streamModel: Model<"openai-completions">,
streamContext: Context,
options?: { onPayload?: (payload: unknown, model: unknown) => unknown },
) => {
capturedPayload = buildOpenAICompletionsParams(streamModel, streamContext, {
reasoning: "high",
} as never);
options?.onPayload?.(capturedPayload, streamModel);
const stream = createAssistantMessageEventStream();
queueMicrotask(() => stream.end());
return stream;
};
const wrapThinkingHigh = createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high");
expect(wrapThinkingHigh).toBeDefined();
await wrapThinkingHigh?.(model, context, {});
expect((capturedPayload?.messages as Array<Record<string, unknown>>)[1]).toMatchObject({
role: "assistant",
content: "Hello.",
reasoning_content: "",
});
});
it("strips replayed reasoning_content when DeepSeek V4 thinking is disabled", async () => {
let capturedPayload: Record<string, unknown> | undefined;
const model = {