From 073a92ef206541a6956b73078307a6f7751ffdd3 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Sat, 2 Aug 2025 23:08:33 -0700 Subject: [PATCH 1/3] feat(chatcompletions): add handling of reasoning for third party providers --- packages/agents-core/src/types/protocol.ts | 15 +++++++++ .../src/openaiChatCompletionsConverter.ts | 9 +++--- .../src/openaiChatCompletionsModel.ts | 31 ++++++++++++++++++- .../src/openaiChatCompletionsStreaming.ts | 19 ++++++++++++ 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/packages/agents-core/src/types/protocol.ts b/packages/agents-core/src/types/protocol.ts index 95a5a98c..365e7f68 100644 --- a/packages/agents-core/src/types/protocol.ts +++ b/packages/agents-core/src/types/protocol.ts @@ -65,6 +65,16 @@ export const InputText = SharedBase.extend({ export type InputText = z.infer; +export const ReasoningText = SharedBase.extend({ + type: z.literal('reasoning_text'), + /** + * A text input for example a message from a user + */ + text: z.string(), +}); + +export type ReasoningText = z.infer; + export const InputImage = SharedBase.extend({ type: z.literal('input_image'), @@ -452,6 +462,11 @@ export const ReasoningItem = SharedBase.extend({ * The user facing representation of the reasoning. Additional information might be in the `providerData` field. */ content: z.array(InputText), + + /** + * The raw reasoning text from the model. + */ + rawContent: z.array(ReasoningText).optional(), }); export type ReasoningItem = z.infer; diff --git a/packages/agents-openai/src/openaiChatCompletionsConverter.ts b/packages/agents-openai/src/openaiChatCompletionsConverter.ts index d7108aa1..bfa44525 100644 --- a/packages/agents-openai/src/openaiChatCompletionsConverter.ts +++ b/packages/agents-openai/src/openaiChatCompletionsConverter.ts @@ -182,10 +182,11 @@ export function itemsToMessages( }); } } else if (item.type === 'reasoning') { - throw new UserError( - 'Reasoning is not supported for chat completions. Got item: ' + - JSON.stringify(item), - ); + const asst = ensureAssistantMessage(); + // @ts-expect-error - reasoning is not supported in the official Chat Completion API spec + // this is handling third party providers that support reasoning + asst.reasoning = item.rawContent?.[0]?.text; + continue; } else if (item.type === 'hosted_tool_call') { if (item.name === 'file_search_call') { const asst = ensureAssistantMessage(); diff --git a/packages/agents-openai/src/openaiChatCompletionsModel.ts b/packages/agents-openai/src/openaiChatCompletionsModel.ts index e4829f84..fcd94d2e 100644 --- a/packages/agents-openai/src/openaiChatCompletionsModel.ts +++ b/packages/agents-openai/src/openaiChatCompletionsModel.ts @@ -35,6 +35,23 @@ import { protocol } from '@openai/agents-core'; export const FAKE_ID = 'FAKE_ID'; +// Some Chat Completions API compatible providers return a reasoning property on the message +// If that's the case we handle them separately +type OpenAIMessageWithReasoning = + OpenAI.Chat.Completions.ChatCompletionMessage & { + reasoning: string; + }; + +function hasReasoningContent( + message: OpenAI.Chat.Completions.ChatCompletionMessage, +): message is OpenAIMessageWithReasoning { + return ( + 'reasoning' in message && + typeof message.reasoning === 'string' && + message.reasoning !== '' + ); +} + /** * A model that uses (or is compatible with) OpenAI's Chat Completions API. */ @@ -67,7 +84,19 @@ export class OpenAIChatCompletionsModel implements Model { const output: protocol.OutputModelItem[] = []; if (response.choices && response.choices[0]) { const message = response.choices[0].message; - + + if (hasReasoningContent(message)) { + output.push({ + type: 'reasoning', + content: [], + rawContent: [ + { + type: 'reasoning_text', + text: message.reasoning, + }, + ], + }); + } if ( message.content !== undefined && message.content !== null && diff --git a/packages/agents-openai/src/openaiChatCompletionsStreaming.ts b/packages/agents-openai/src/openaiChatCompletionsStreaming.ts index 832d6601..e9d16a62 100644 --- a/packages/agents-openai/src/openaiChatCompletionsStreaming.ts +++ b/packages/agents-openai/src/openaiChatCompletionsStreaming.ts @@ -9,6 +9,7 @@ type StreamingState = { text_content_index_and_output: [number, protocol.OutputText] | null; refusal_content_index_and_output: [number, protocol.Refusal] | null; function_calls: Record; + reasoning: string; }; export async function* convertChatCompletionsStreamToResponses( @@ -21,6 +22,7 @@ export async function* convertChatCompletionsStreamToResponses( text_content_index_and_output: null, refusal_content_index_and_output: null, function_calls: {}, + reasoning: '', }; for await (const chunk of stream) { @@ -64,6 +66,14 @@ export async function* convertChatCompletionsStreamToResponses( state.text_content_index_and_output[1].text += delta.content; } + if ( + 'reasoning' in delta && + delta.reasoning && + typeof delta.reasoning === 'string' + ) { + state.reasoning += delta.reasoning; + } + // Handle refusals if ('refusal' in delta && delta.refusal) { if (!state.refusal_content_index_and_output) { @@ -98,6 +108,15 @@ export async function* convertChatCompletionsStreamToResponses( // Final output message const outputs: protocol.OutputModelItem[] = []; + + if (state.reasoning) { + outputs.push({ + type: 'reasoning', + content: [], + rawContent: [{ type: 'reasoning_text', text: state.reasoning }], + }); + } + if ( state.text_content_index_and_output || state.refusal_content_index_and_output From dcb93f96261066162a8823df63739a7d00cc5380 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Sun, 3 Aug 2025 00:14:21 -0700 Subject: [PATCH 2/3] add test coverage --- .../openaiChatCompletionsConverter.test.ts | 17 ++++++ .../test/openaiChatCompletionsModel.test.ts | 55 ++++++++++++++++++- .../openaiChatCompletionsStreaming.test.ts | 32 +++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/packages/agents-openai/test/openaiChatCompletionsConverter.test.ts b/packages/agents-openai/test/openaiChatCompletionsConverter.test.ts index 571b50a1..b85c2867 100644 --- a/packages/agents-openai/test/openaiChatCompletionsConverter.test.ts +++ b/packages/agents-openai/test/openaiChatCompletionsConverter.test.ts @@ -204,6 +204,23 @@ describe('itemsToMessages', () => { ]; expect(() => itemsToMessages(bad)).toThrow(UserError); }); + + test('converts reasoning items into assistant reasoning', () => { + const items: protocol.ModelItem[] = [ + { + type: 'reasoning', + content: [], + rawContent: [{ type: 'reasoning_text', text: 'why' }], + } as protocol.ReasoningItem, + ]; + const msgs = itemsToMessages(items); + expect(msgs).toEqual([ + { + role: 'assistant', + reasoning: 'why', + }, + ]); + }); }); describe('tool helpers', () => { diff --git a/packages/agents-openai/test/openaiChatCompletionsModel.test.ts b/packages/agents-openai/test/openaiChatCompletionsModel.test.ts index 7ed7fdce..18ed14de 100644 --- a/packages/agents-openai/test/openaiChatCompletionsModel.test.ts +++ b/packages/agents-openai/test/openaiChatCompletionsModel.test.ts @@ -68,7 +68,13 @@ describe('OpenAIChatCompletionsModel', () => { type: 'message', role: 'assistant', status: 'completed', - content: [{ type: 'output_text', text: 'hi', providerData: {} }], + content: [ + { + type: 'output_text', + text: 'hi', + providerData: {}, + }, + ], }, ]); }); @@ -171,6 +177,53 @@ describe('OpenAIChatCompletionsModel', () => { ]); }); + it('handles reasoning messages from third-party providers', async () => { + const client = new FakeClient(); + const response = { + id: 'r', + choices: [ + { + message: { reasoning: 'because', content: 'hi' }, + }, + ], + usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }, + } as any; + client.chat.completions.create.mockResolvedValue(response); + + const model = new OpenAIChatCompletionsModel(client as any, 'gpt'); + const req: any = { + input: 'u', + modelSettings: {}, + tools: [], + outputType: 'text', + handoffs: [], + tracing: false, + }; + + const result = await withTrace('t', () => model.getResponse(req)); + + expect(result.output).toEqual([ + { + type: 'reasoning', + content: [], + rawContent: [{ type: 'reasoning_text', text: 'because' }], + }, + { + id: 'r', + type: 'message', + role: 'assistant', + status: 'completed', + content: [ + { + type: 'output_text', + text: 'hi', + providerData: { reasoning: 'because' }, + }, + ], + }, + ]); + }); + it('handles function tool calls', async () => { const client = new FakeClient(); const response = { diff --git a/packages/agents-openai/test/openaiChatCompletionsStreaming.test.ts b/packages/agents-openai/test/openaiChatCompletionsStreaming.test.ts index 27a4304f..8ee957b1 100644 --- a/packages/agents-openai/test/openaiChatCompletionsStreaming.test.ts +++ b/packages/agents-openai/test/openaiChatCompletionsStreaming.test.ts @@ -233,4 +233,36 @@ describe('convertChatCompletionsStreamToResponses', () => { expect(deltas).toHaveLength(1); expect(deltas[0].delta).toBe('hi'); }); + + it('accumulates reasoning deltas into a reasoning item', async () => { + const resp: ChatCompletion = { + id: 'r1', + created: 0, + model: 'gpt-test', + object: 'chat.completion', + choices: [], + usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }, + } as any; + + async function* stream() { + yield makeChunk({ reasoning: 'foo' }); + yield makeChunk({ reasoning: 'bar' }); + } + + const events: any[] = []; + for await (const e of convertChatCompletionsStreamToResponses( + resp, + stream() as any, + )) { + events.push(e); + } + + const final = events[events.length - 1]; + expect(final.type).toBe('response_done'); + expect(final.response.output[0]).toEqual({ + type: 'reasoning', + content: [], + rawContent: [{ type: 'reasoning_text', text: 'foobar' }], + }); + }); }); From 19fcc61abaf52a62bf4ddca7416642b05a678de3 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Sun, 3 Aug 2025 00:15:09 -0700 Subject: [PATCH 3/3] add changeset --- .changeset/three-moles-strive.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/three-moles-strive.md diff --git a/.changeset/three-moles-strive.md b/.changeset/three-moles-strive.md new file mode 100644 index 00000000..0f73bdde --- /dev/null +++ b/.changeset/three-moles-strive.md @@ -0,0 +1,6 @@ +--- +'@openai/agents-openai': patch +'@openai/agents-core': patch +--- + +feat: add reasoning handling in chat completions