Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/poor-jeans-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ai-sdk/deepseek': patch
---

fix: ensure reasoning_content is included in multi-turn tool chains
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('convertToDeepSeekChatMessages', () => {
},
],
responseFormat: undefined,
thinkingMode: false,
});

expect(result).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -43,6 +44,7 @@ describe('convertToDeepSeekChatMessages', () => {
},
],
responseFormat: undefined,
thinkingMode: false,
});

expect(result).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -92,6 +94,7 @@ describe('convertToDeepSeekChatMessages', () => {
},
],
responseFormat: undefined,
thinkingMode: false,
});

expect(result).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -150,6 +153,7 @@ describe('convertToDeepSeekChatMessages', () => {
},
],
responseFormat: undefined,
thinkingMode: false,
});

expect(result).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -216,6 +220,7 @@ describe('convertToDeepSeekChatMessages', () => {
},
],
responseFormat: undefined,
thinkingMode: true,
});

expect(result).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -290,6 +295,7 @@ describe('convertToDeepSeekChatMessages', () => {
},
],
responseFormat: undefined,
thinkingMode: true,
});

expect(result).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -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);
});
});
});
16 changes: 15 additions & 1 deletion packages/deepseek/src/chat/convert-to-deepseek-chat-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SharedV4Warning>;
Expand Down Expand Up @@ -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,
});

Expand Down
7 changes: 7 additions & 0 deletions packages/deepseek/src/chat/deepseek-chat-language-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down