Preserve empty DeepSeek reasoning chunks

This commit is contained in:
avaritiachaos 2026-05-01 14:14:38 +08:00
parent 67457a30ff
commit 65e92304ce
4 changed files with 100 additions and 4 deletions

View file

@ -439,6 +439,51 @@ describe("DeepSeekHandler", () => {
expect(reasoningChunks[1].text).toBe(" I'll analyze step by step.")
})
it("should preserve empty reasoning_content chunks for DeepSeek V4", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: { reasoning_content: "" },
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: {},
index: 0,
finish_reason: "stop",
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}
},
}))
const reasonerHandler = new DeepSeekHandler({
...mockOptions,
apiModelId: "deepseek-v4-pro",
})
const chunks: any[] = []
for await (const chunk of reasonerHandler.createMessage(systemPrompt, messages)) {
chunks.push(chunk)
}
expect(chunks.filter((chunk) => chunk.type === "reasoning")).toContainEqual({
type: "reasoning",
text: "",
})
})
it("should pass thinking parameter for DeepSeek V4 models", async () => {
const reasonerHandler = new DeepSeekHandler({
...mockOptions,

View file

@ -463,6 +463,57 @@ describe("OpenAiHandler", () => {
expect(callArgs.reasoning_effort).toBeUndefined()
})
it("should preserve empty DeepSeek V4 reasoning_content chunks for OpenAI-compatible endpoints", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: { reasoning_content: "" },
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: {},
index: 0,
finish_reason: "stop",
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}
},
}))
const deepSeekV4Handler = new OpenAiHandler({
...mockOptions,
openAiBaseUrl: "https://api.deepseek.com",
openAiModelId: "deepseek-v4-flash",
openAiCustomModelInfo: {
contextWindow: 1_000_000,
supportsPromptCache: true,
preserveReasoning: true,
},
})
const chunks: any[] = []
for await (const chunk of deepSeekV4Handler.createMessage(systemPrompt, messages)) {
chunks.push(chunk)
}
expect(chunks.filter((chunk) => chunk.type === "reasoning")).toContainEqual({
type: "reasoning",
text: "",
})
})
it("should include max_tokens when includeMaxTokens is true", async () => {
const optionsWithMaxTokens: ApiHandlerOptions = {
...mockOptions,

View file

@ -125,10 +125,10 @@ export class DeepSeekHandler extends OpenAiHandler {
// Handle reasoning_content from DeepSeek's interleaved thinking
// This is the proper way DeepSeek sends thinking content in streaming
if ("reasoning_content" in delta && delta.reasoning_content) {
if ("reasoning_content" in delta && typeof delta.reasoning_content === "string") {
yield {
type: "reasoning",
text: (delta.reasoning_content as string) || "",
text: delta.reasoning_content,
}
}

View file

@ -231,10 +231,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
}
}
if ("reasoning_content" in delta && delta.reasoning_content) {
if ("reasoning_content" in delta && typeof delta.reasoning_content === "string") {
yield {
type: "reasoning",
text: (delta.reasoning_content as string | undefined) || "",
text: delta.reasoning_content,
}
}