diff --git a/src/api/providers/__tests__/deepseek.spec.ts b/src/api/providers/__tests__/deepseek.spec.ts index 52b9e01afb..7545d94c77 100644 --- a/src/api/providers/__tests__/deepseek.spec.ts +++ b/src/api/providers/__tests__/deepseek.spec.ts @@ -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, diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index b26021be77..66bc16453d 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -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, diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index 706c774f79..59fc13d4ae 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -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, } } diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 77cb8a0381..193cb02bc2 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -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, } }