Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface ExtendedCompletionUsage extends OpenAI.CompletionUsage {
cached_tokens?: number;
}

interface ExtendedChatCompletionAssistantMessageParam
export interface ExtendedChatCompletionAssistantMessageParam
extends OpenAI.Chat.ChatCompletionAssistantMessageParam {
reasoning_content?: string | null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,89 @@ describe('DeepSeekOpenAICompatibleProvider', () => {
content: 'Hello \n\n[Unsupported content type: image_url]',
});
});

// https://github.com/QwenLM/qwen-code/issues/3695 — DeepSeek's thinking
// mode rejects subsequent requests when a prior tool-calling assistant
// turn omits reasoning_content, even if the model itself returned no
// reasoning text. The provider must always send the field.
it('injects empty reasoning_content on tool-calling assistant turns missing it', () => {
const originalRequest: OpenAI.Chat.ChatCompletionCreateParams = {
model: 'deepseek-v4-flash',
messages: [
{ role: 'user', content: 'list markdown files' },
{
role: 'assistant',
content: null,
tool_calls: [
{
id: 'call_1',
type: 'function',
function: { name: 'glob', arguments: '{"pattern":"**/*.md"}' },
},
],
},
{
role: 'tool',
tool_call_id: 'call_1',
content: 'Found 2 matching file(s)',
},
],
};

const result = provider.buildRequest(originalRequest, userPromptId);

const assistant = result.messages?.[1] as {
role: string;
reasoning_content?: string;
};
expect(assistant.role).toBe('assistant');
expect(assistant.reasoning_content).toBe('');
});

it('preserves existing reasoning_content on tool-calling assistant turns', () => {
const originalRequest = {
model: 'deepseek-v4-flash',
messages: [
{ role: 'user' as const, content: 'list markdown files' },
{
role: 'assistant' as const,
content: null,
reasoning_content: 'Let me glob first.',
tool_calls: [
{
id: 'call_1',
type: 'function' as const,
function: { name: 'glob', arguments: '{"pattern":"**/*.md"}' },
},
],
},
],
} as unknown as OpenAI.Chat.ChatCompletionCreateParams;

const result = provider.buildRequest(originalRequest, userPromptId);

const assistant = result.messages?.[1] as {
reasoning_content?: string;
};
expect(assistant.reasoning_content).toBe('Let me glob first.');
});

it('does not add reasoning_content to assistant turns without tool_calls', () => {
const originalRequest: OpenAI.Chat.ChatCompletionCreateParams = {
model: 'deepseek-v4-flash',
messages: [
{ role: 'user', content: 'hi' },
{ role: 'assistant', content: 'hello' },
],
};

const result = provider.buildRequest(originalRequest, userPromptId);

const assistant = result.messages?.[1] as {
reasoning_content?: string;
};
expect(assistant.reasoning_content).toBeUndefined();
});
});

describe('getDefaultGenerationConfig', () => {
Expand Down
103 changes: 69 additions & 34 deletions packages/core/src/core/openaiContentGenerator/provider/deepseek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import type OpenAI from 'openai';
import type { Config } from '../../../config/config.js';
import type { ContentGeneratorConfig } from '../../contentGenerator.js';
import type { ExtendedChatCompletionAssistantMessageParam } from '../converter.js';
import { DefaultOpenAICompatibleProvider } from './default.js';
import type { GenerateContentConfig } from '@google/genai';

Expand Down Expand Up @@ -50,40 +51,8 @@ export class DeepSeekOpenAICompatibleProvider extends DefaultOpenAICompatiblePro
}

const messages = baseRequest.messages.map((message) => {
if (!('content' in message)) {
return message;
}

const { content } = message;

if (
typeof content === 'string' ||
content === null ||
content === undefined
) {
return message;
}

if (!Array.isArray(content)) {
return message;
}

const text = content
.map((part) => {
if (typeof part === 'string') {
return part;
}
if (part.type === 'text') {
return part.text ?? '';
}
return `[Unsupported content type: ${part.type}]`;
})
.join('\n\n');

return {
...message,
content: text,
} as OpenAI.Chat.ChatCompletionMessageParam;
const flattened = flattenContentParts(message);
return ensureReasoningContentOnToolCalls(flattened);
});

return {
Expand All @@ -98,3 +67,69 @@ export class DeepSeekOpenAICompatibleProvider extends DefaultOpenAICompatiblePro
};
}
}

function flattenContentParts(
message: OpenAI.Chat.ChatCompletionMessageParam,
): OpenAI.Chat.ChatCompletionMessageParam {
if (!('content' in message)) {
return message;
}

const { content } = message;

if (
typeof content === 'string' ||
content === null ||
content === undefined
) {
return message;
}

if (!Array.isArray(content)) {
return message;
}

const text = content
.map((part) => {
if (typeof part === 'string') {
return part;
}
if (part.type === 'text') {
return part.text ?? '';
}
return `[Unsupported content type: ${part.type}]`;
})
.join('\n\n');

return {
...message,
content: text,
} as OpenAI.Chat.ChatCompletionMessageParam;
}

// DeepSeek's thinking mode requires reasoning_content to be replayed on every
// prior assistant turn that carried tool_calls. The model may legitimately
// return a tool round without reasoning text, so the field can be missing
// when we rebuild the request. Send an empty string in that case so the API
// contract is satisfied. https://github.com/QwenLM/qwen-code/issues/3695
function ensureReasoningContentOnToolCalls(
message: OpenAI.Chat.ChatCompletionMessageParam,
): OpenAI.Chat.ChatCompletionMessageParam {
if (message.role !== 'assistant') {
return message;
}
if (!Array.isArray(message.tool_calls) || message.tool_calls.length === 0) {
return message;
}
const extended = message as ExtendedChatCompletionAssistantMessageParam;
if (
typeof extended.reasoning_content === 'string' &&
extended.reasoning_content.length > 0
) {
return message;
}
return {
...extended,
reasoning_content: '',
} as OpenAI.Chat.ChatCompletionMessageParam;
}
Loading