diff --git a/.changeset/poor-jeans-allow.md b/.changeset/poor-jeans-allow.md new file mode 100644 index 000000000000..9d083ca22add --- /dev/null +++ b/.changeset/poor-jeans-allow.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/deepseek': patch +--- + +fix: ensure reasoning_content is included in multi-turn tool chains diff --git a/packages/deepseek/src/chat/convert-to-deepseek-chat-messages.test.ts b/packages/deepseek/src/chat/convert-to-deepseek-chat-messages.test.ts index 9e5103c685da..c18ef7f82e6f 100644 --- a/packages/deepseek/src/chat/convert-to-deepseek-chat-messages.test.ts +++ b/packages/deepseek/src/chat/convert-to-deepseek-chat-messages.test.ts @@ -12,6 +12,7 @@ describe('convertToDeepSeekChatMessages', () => { }, ], responseFormat: undefined, + thinkingMode: false, }); expect(result).toMatchInlineSnapshot(` @@ -43,6 +44,7 @@ describe('convertToDeepSeekChatMessages', () => { }, ], responseFormat: undefined, + thinkingMode: false, }); expect(result).toMatchInlineSnapshot(` @@ -92,6 +94,7 @@ describe('convertToDeepSeekChatMessages', () => { }, ], responseFormat: undefined, + thinkingMode: false, }); expect(result).toMatchInlineSnapshot(` @@ -150,6 +153,7 @@ describe('convertToDeepSeekChatMessages', () => { }, ], responseFormat: undefined, + thinkingMode: false, }); expect(result).toMatchInlineSnapshot(` @@ -216,6 +220,7 @@ describe('convertToDeepSeekChatMessages', () => { }, ], responseFormat: undefined, + thinkingMode: true, }); expect(result).toMatchInlineSnapshot(` @@ -290,6 +295,7 @@ describe('convertToDeepSeekChatMessages', () => { }, ], responseFormat: undefined, + thinkingMode: true, }); expect(result).toMatchInlineSnapshot(` @@ -328,5 +334,83 @@ describe('convertToDeepSeekChatMessages', () => { } `); }); + it('should include empty reasoning_content in assistant turns that are part of a multi-turn tool chain', () => { + const { messages } = convertToDeepSeekChatMessages({ + prompt: [ + // User question + { + role: 'user', + content: [{ type: 'text', text: 'What is the weather in Berlin?' }], + }, + // First assistant turn: reasoning + tool call + { + role: 'assistant', + content: [ + { type: 'reasoning', text: 'I need to call the weather API.' }, + { + type: 'tool-call', + toolCallId: 'call_1', + toolName: 'get_weather', + input: { city: 'Berlin' }, + }, + ], + }, + // Tool response + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: 'call_1', + toolName: 'get_weather', + output: { type: 'text', value: 'Sunny, 20°C' }, + }, + ], + }, + // Second assistant turn: only a tool call (no reasoning) + { + role: 'assistant', + content: [ + { + type: 'tool-call', + toolCallId: 'call_2', + toolName: 'get_uv_index', + input: { city: 'Berlin' }, + }, + ], + }, + ], + responseFormat: undefined, + thinkingMode: true, + }); + + // There should be 4 messages in the DeepSeek format: + // user, assistant (with reasoning + tool), tool, assistant (with tool only) + expect(messages).toHaveLength(4); + + // Check the first assistant message + expect(messages[1]).toMatchObject({ + role: 'assistant', + content: '', + reasoning_content: 'I need to call the weather API.', + tool_calls: [ + { + id: 'call_1', + type: 'function', + function: { name: 'get_weather', arguments: '{"city":"Berlin"}' }, + }, + ], + }); + + // Check the second assistant message (the one without reasoning) + const secondAssistant = + messages[3] as import('./deepseek-chat-api-types').DeepSeekAssistantMessage; + expect(secondAssistant.role).toBe('assistant'); + expect(secondAssistant.content).toBe(''); + // According to DeepSeek docs, during a multi-turn tool chain, + // reasoning_content must be present (even if empty) to avoid 400 errors. + expect(secondAssistant.reasoning_content).toBe(''); + expect(secondAssistant.tool_calls).toHaveLength(1); + }); }); }); diff --git a/packages/deepseek/src/chat/convert-to-deepseek-chat-messages.ts b/packages/deepseek/src/chat/convert-to-deepseek-chat-messages.ts index 1ffdbeb391dd..288e83de30aa 100644 --- a/packages/deepseek/src/chat/convert-to-deepseek-chat-messages.ts +++ b/packages/deepseek/src/chat/convert-to-deepseek-chat-messages.ts @@ -8,9 +8,11 @@ import { DeepSeekChatPrompt } from './deepseek-chat-api-types'; export function convertToDeepSeekChatMessages({ prompt, responseFormat, + thinkingMode = false, }: { prompt: LanguageModelV4Prompt; responseFormat: LanguageModelV4CallOptions['responseFormat']; + thinkingMode?: boolean; }): { messages: DeepSeekChatPrompt; warnings: Array; @@ -121,10 +123,22 @@ export function convertToDeepSeekChatMessages({ } } + // Only include reasoning_content if thinking mode is enabled + // then reasoning content *must* be provided + // for every assistant message in current thinking chain even if empty + const reasoningContent = thinkingMode + ? toolCalls.length > 0 && + reasoning === undefined && + index > lastUserMessageIndex + ? '' + : reasoning + : undefined; + messages.push({ role: 'assistant', content: text, - reasoning_content: reasoning, + + reasoning_content: reasoningContent, tool_calls: toolCalls.length > 0 ? toolCalls : undefined, }); diff --git a/packages/deepseek/src/chat/deepseek-chat-language-model.ts b/packages/deepseek/src/chat/deepseek-chat-language-model.ts index 9620f4f191d6..6ee0ee36391d 100644 --- a/packages/deepseek/src/chat/deepseek-chat-language-model.ts +++ b/packages/deepseek/src/chat/deepseek-chat-language-model.ts @@ -98,9 +98,16 @@ export class DeepSeekChatLanguageModel implements LanguageModelV4 { schema: deepseekLanguageModelOptions, })) ?? {}; + // Determine if thinking mode is enabled + const thinkingMode = + deepseekOptions.thinking?.type === 'enabled' || + (this.modelId === 'deepseek-reasoner' && + deepseekOptions.thinking?.type !== 'disabled'); + const { messages, warnings } = convertToDeepSeekChatMessages({ prompt, responseFormat, + thinkingMode, }); if (topK != null) {