diff --git a/.changeset/nervous-lemons-admire.md b/.changeset/nervous-lemons-admire.md new file mode 100644 index 00000000..8fbcb54a --- /dev/null +++ b/.changeset/nervous-lemons-admire.md @@ -0,0 +1,5 @@ +--- +"@openai/agents-extensions": patch +--- + +fix: #791 support DeepSeek thinking tool calls diff --git a/packages/agents-extensions/src/aiSdk.ts b/packages/agents-extensions/src/aiSdk.ts index 627dce24..318ff226 100644 --- a/packages/agents-extensions/src/aiSdk.ts +++ b/packages/agents-extensions/src/aiSdk.ts @@ -42,13 +42,22 @@ import { encodeUint8ArrayToBase64 } from '@openai/agents/utils'; export function itemsToLanguageV2Messages( model: LanguageModelV2, items: protocol.ModelItem[], + modelSettingsProviderData?: Record, ): LanguageModelV2Message[] { const messages: LanguageModelV2Message[] = []; let currentAssistantMessage: LanguageModelV2Message | undefined; + const isDeepSeekThinkingMode = isDeepSeekModel( + model, + modelSettingsProviderData, + ); for (const item of items) { if (item.type === 'message' || typeof item.type === 'undefined') { const { role, content, providerData } = item; + if (currentAssistantMessage && role !== 'assistant') { + messages.push(currentAssistantMessage); + currentAssistantMessage = undefined; + } if (role === 'system') { messages.push({ role: 'system', @@ -226,6 +235,29 @@ export function itemsToLanguageV2Messages( item.content.length > 0 && typeof item.content[0].text === 'string' ) { + if (isDeepSeekThinkingMode) { + if (!currentAssistantMessage) { + currentAssistantMessage = { + role: 'assistant', + content: [], + providerOptions: { + ...(item.providerData ?? {}), + }, + }; + } + if ( + Array.isArray(currentAssistantMessage.content) && + currentAssistantMessage.role === 'assistant' + ) { + currentAssistantMessage.content.push({ + type: 'reasoning', + text: item.content[0].text, + providerOptions: { ...(item.providerData ?? {}) }, + } as any); + } + continue; + } + messages.push({ role: 'assistant', content: [ @@ -262,6 +294,26 @@ export function itemsToLanguageV2Messages( return messages; } +function isDeepSeekModel( + model: LanguageModelV2, + modelSettingsProviderData?: Record, +): boolean { + const provider = typeof model.provider === 'string' ? model.provider : ''; + if (!provider.toLowerCase().startsWith('deepseek')) { + return false; + } + + const modelId = typeof model.modelId === 'string' ? model.modelId : ''; + if (modelId === 'deepseek-reasoner') { + return true; + } + + return ( + modelSettingsProviderData?.providerOptions?.deepseek?.thinking?.type === + 'enabled' + ); +} + /** * @internal * Converts a handoff to a language model V2 tool. @@ -612,7 +664,11 @@ export class AiSdkModel implements Model { content: [{ type: 'text', text: request.input }], }, ] - : itemsToLanguageV2Messages(this.#model, request.input); + : itemsToLanguageV2Messages( + this.#model, + request.input, + request.modelSettings.providerData, + ); if (request.systemInstructions) { input = [ @@ -867,7 +923,11 @@ export class AiSdkModel implements Model { content: [{ type: 'text', text: request.input }], }, ] - : itemsToLanguageV2Messages(this.#model, request.input); + : itemsToLanguageV2Messages( + this.#model, + request.input, + request.modelSettings.providerData, + ); if (request.systemInstructions) { input = [ diff --git a/packages/agents-extensions/test/aiSdk.test.ts b/packages/agents-extensions/test/aiSdk.test.ts index 89714015..e51f2507 100644 --- a/packages/agents-extensions/test/aiSdk.test.ts +++ b/packages/agents-extensions/test/aiSdk.test.ts @@ -155,6 +155,176 @@ describe('itemsToLanguageV2Messages', () => { ]); }); + test('merges reasoning into assistant tool-call message for DeepSeek thinking mode (deepseek-reasoner)', () => { + const deepseekModel = { + ...stubModel({}), + provider: 'deepseek.chat', + modelId: 'deepseek-reasoner', + } as any; + const items: protocol.ModelItem[] = [ + { type: 'reasoning', content: [{ text: 'thinking' }] } as any, + { + type: 'function_call', + callId: '1', + name: 'foo', + arguments: '{}', + } as any, + { + type: 'function_call_result', + callId: '1', + name: 'foo', + output: { type: 'text', text: 'out' }, + } as any, + ]; + + const msgs = itemsToLanguageV2Messages(deepseekModel, items); + expect(msgs).toEqual([ + { + role: 'assistant', + content: [ + { type: 'reasoning', text: 'thinking', providerOptions: {} }, + { + type: 'tool-call', + toolCallId: '1', + toolName: 'foo', + input: {}, + providerOptions: {}, + }, + ], + providerOptions: {}, + }, + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: '1', + toolName: 'foo', + output: { type: 'text', value: 'out' }, + providerOptions: {}, + }, + ], + providerOptions: {}, + }, + ]); + }); + + test('does not merge reasoning into tool-call message for DeepSeek without thinking mode', () => { + const deepseekModel = { + ...stubModel({}), + provider: 'deepseek.chat', + modelId: 'deepseek-chat', + } as any; + const items: protocol.ModelItem[] = [ + { type: 'reasoning', content: [{ text: 'thinking' }] } as any, + { + type: 'function_call', + callId: '1', + name: 'foo', + arguments: '{}', + } as any, + { + type: 'function_call_result', + callId: '1', + name: 'foo', + output: { type: 'text', text: 'out' }, + } as any, + ]; + + const msgs = itemsToLanguageV2Messages(deepseekModel, items); + expect(msgs).toEqual([ + { + role: 'assistant', + content: [{ type: 'reasoning', text: 'thinking', providerOptions: {} }], + providerOptions: {}, + }, + { + role: 'assistant', + content: [ + { + type: 'tool-call', + toolCallId: '1', + toolName: 'foo', + input: {}, + providerOptions: {}, + }, + ], + providerOptions: {}, + }, + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: '1', + toolName: 'foo', + output: { type: 'text', value: 'out' }, + providerOptions: {}, + }, + ], + providerOptions: {}, + }, + ]); + }); + + test('merges reasoning into tool-call message for DeepSeek thinking mode (providerOptions)', () => { + const deepseekModel = { + ...stubModel({}), + provider: 'deepseek.chat', + modelId: 'deepseek-chat', + } as any; + const items: protocol.ModelItem[] = [ + { type: 'reasoning', content: [{ text: 'thinking' }] } as any, + { + type: 'function_call', + callId: '1', + name: 'foo', + arguments: '{}', + } as any, + { + type: 'function_call_result', + callId: '1', + name: 'foo', + output: { type: 'text', text: 'out' }, + } as any, + ]; + + const providerData = { + providerOptions: { deepseek: { thinking: { type: 'enabled' } } }, + }; + + const msgs = itemsToLanguageV2Messages(deepseekModel, items, providerData); + expect(msgs).toEqual([ + { + role: 'assistant', + content: [ + { type: 'reasoning', text: 'thinking', providerOptions: {} }, + { + type: 'tool-call', + toolCallId: '1', + toolName: 'foo', + input: {}, + providerOptions: {}, + }, + ], + providerOptions: {}, + }, + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: '1', + toolName: 'foo', + output: { type: 'text', value: 'out' }, + providerOptions: {}, + }, + ], + providerOptions: {}, + }, + ]); + }); + test('throws on built-in tool calls', () => { const items: protocol.ModelItem[] = [ { type: 'hosted_tool_call', name: 'search' } as any,