From e763ad0ad48fa4149b1b8d1e990c3afdba707e41 Mon Sep 17 00:00:00 2001 From: ai-sdk-factory <308175966+ai-sdk-factory@users.noreply.github.com> Date: Thu, 6 Aug 2026 10:48:40 +0000 Subject: [PATCH 1/3] reproduction (reproduced) --- .ai-sdk-factory/reproduction-replay.json | 12 ++ ...513-open-responses-reasoning-round-trip.ts | 134 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 .ai-sdk-factory/reproduction-replay.json create mode 100644 examples/ai-functions/src/reproduction/issue-18513-open-responses-reasoning-round-trip.ts diff --git a/.ai-sdk-factory/reproduction-replay.json b/.ai-sdk-factory/reproduction-replay.json new file mode 100644 index 000000000000..34ed2b27eb93 --- /dev/null +++ b/.ai-sdk-factory/reproduction-replay.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "issueId": 18513, + "command": "pnpm -C examples/ai-functions exec tsx src/reproduction/issue-18513-open-responses-reasoning-round-trip.ts", + "artifactPaths": [ + "examples/ai-functions/src/reproduction/issue-18513-open-responses-reasoning-round-trip.ts" + ], + "expectedFailure": { + "exitCode": 1, + "signal": "asymmetric round trip: the SDK produced 1 reasoning part(s) from the response, but serialized 0 back into the next request." + } +} diff --git a/examples/ai-functions/src/reproduction/issue-18513-open-responses-reasoning-round-trip.ts b/examples/ai-functions/src/reproduction/issue-18513-open-responses-reasoning-round-trip.ts new file mode 100644 index 000000000000..eae9da694700 --- /dev/null +++ b/examples/ai-functions/src/reproduction/issue-18513-open-responses-reasoning-round-trip.ts @@ -0,0 +1,134 @@ +import assert from 'node:assert/strict'; +import { createOpenResponses } from '@ai-sdk/open-responses'; +import type { LanguageModelV4Prompt } from '@ai-sdk/provider'; + +const reasoningText = 'REASONING THAT MUST SURVIVE THE ROUND TRIP'; + +type AssistantContent = Extract< + LanguageModelV4Prompt[number], + { role: 'assistant' } +>['content']; + +type RequestBody = { + input: Array<{ type?: string; role?: string }>; +}; + +async function main() { + const requestBodies: RequestBody[] = []; + + function modelReturning(output: unknown[]) { + return createOpenResponses({ + name: 'reproduction', + apiKey: 'not-used', + url: 'https://example.invalid/v1/responses', + fetch: async (_url, init) => { + requestBodies.push(JSON.parse(String(init?.body)) as RequestBody); + + return new Response( + JSON.stringify({ + id: 'r', + object: 'response', + created_at: 0, + model: 'm', + status: 'completed', + output, + usage: { input_tokens: 1, output_tokens: 1 }, + }), + { + status: 200, + headers: { 'content-type': 'application/json' }, + }, + ); + }, + })('any-model'); + } + + const step1 = await modelReturning([ + { + type: 'reasoning', + id: 'rs_1', + content: [{ type: 'reasoning_text', text: reasoningText }], + summary: [], + }, + { + type: 'function_call', + id: 'fc_1', + call_id: 'call_1', + name: 't', + arguments: '{}', + }, + ]).doGenerate({ + prompt: [{ role: 'user', content: [{ type: 'text', text: 'q' }] }], + tools: [ + { + type: 'function', + name: 't', + inputSchema: { type: 'object', properties: {} }, + }, + ], + }); + + const reasoningRead = step1.content.filter(part => part.type === 'reasoning'); + console.log( + 'step 1 — reasoning parts the SDK produced:', + reasoningRead.length, + JSON.stringify(reasoningRead.map(part => part.text)), + ); + + await modelReturning([]).doGenerate({ + prompt: [ + { role: 'user', content: [{ type: 'text', text: 'q' }] }, + { + role: 'assistant', + content: step1.content as AssistantContent, + }, + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: 'call_1', + toolName: 't', + output: { type: 'json', value: { ok: true } }, + }, + ], + }, + ], + tools: [ + { + type: 'function', + name: 't', + inputSchema: { type: 'object', properties: {} }, + }, + ], + }); + + const lastRequestBody = requestBodies.at(-1); + assert.ok(lastRequestBody != null, 'no second request body was captured'); + + const sentReasoning = lastRequestBody.input.filter( + item => item.type === 'reasoning', + ); + console.log( + 'step 2 — request input items:', + lastRequestBody.input + .map(item => item.type ?? `message:${item.role}`) + .join(', '), + ); + console.log('step 2 — reasoning items sent:', sentReasoning.length); + + assert.ok( + reasoningRead.length > 0, + 'precondition: the SDK did not read the reasoning item back', + ); + assert.equal( + sentReasoning.length, + reasoningRead.length, + `asymmetric round trip: the SDK produced ${reasoningRead.length} reasoning part(s) from the response, but serialized ${sentReasoning.length} back into the next request.`, + ); +} + +main().catch(error => { + console.error(error); + process.exitCode = 1; +}); From 34b7764195842570406d95cebcc7d926de34387a Mon Sep 17 00:00:00 2001 From: ai-sdk-factory <308175966+ai-sdk-factory@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:11:05 +0000 Subject: [PATCH 2/3] failing tests Co-authored-by: Astro-Han <255364436+Astro-Han@users.noreply.github.com> --- .../convert-to-open-responses-input.test.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/open-responses/src/responses/convert-to-open-responses-input.test.ts b/packages/open-responses/src/responses/convert-to-open-responses-input.test.ts index d21c4a4306a0..91d8a6731b54 100644 --- a/packages/open-responses/src/responses/convert-to-open-responses-input.test.ts +++ b/packages/open-responses/src/responses/convert-to-open-responses-input.test.ts @@ -359,6 +359,35 @@ describe('convertToOpenResponsesInput', () => { ] `); }); + + it('should convert reasoning parts to reasoning items', async () => { + const result = await convertToOpenResponsesInput({ + prompt: [ + { + role: 'assistant', + content: [ + { + type: 'reasoning', + text: 'Analyzing the problem step by step', + }, + ], + }, + ], + }); + + expect(result.input).toEqual([ + { + type: 'reasoning', + summary: [], + content: [ + { + type: 'reasoning_text', + text: 'Analyzing the problem step by step', + }, + ], + }, + ]); + }); }); describe('assistant messages with tool calls', () => { From 530ecaa21f029ac65cfed0072d4e843a409254ca Mon Sep 17 00:00:00 2001 From: ai-sdk-factory <308175966+ai-sdk-factory@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:11:05 +0000 Subject: [PATCH 3/3] fix #18513 Co-authored-by: Astro-Han <255364436+Astro-Han@users.noreply.github.com> --- .ai-sdk-factory/reproduction-replay.json | 12 -- .changeset/calm-dogs-reason.md | 5 + ...513-open-responses-reasoning-round-trip.ts | 134 ------------------ .../convert-to-open-responses-input.ts | 15 ++ .../src/responses/open-responses-api.ts | 2 +- 5 files changed, 21 insertions(+), 147 deletions(-) delete mode 100644 .ai-sdk-factory/reproduction-replay.json create mode 100644 .changeset/calm-dogs-reason.md delete mode 100644 examples/ai-functions/src/reproduction/issue-18513-open-responses-reasoning-round-trip.ts diff --git a/.ai-sdk-factory/reproduction-replay.json b/.ai-sdk-factory/reproduction-replay.json deleted file mode 100644 index 34ed2b27eb93..000000000000 --- a/.ai-sdk-factory/reproduction-replay.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": 1, - "issueId": 18513, - "command": "pnpm -C examples/ai-functions exec tsx src/reproduction/issue-18513-open-responses-reasoning-round-trip.ts", - "artifactPaths": [ - "examples/ai-functions/src/reproduction/issue-18513-open-responses-reasoning-round-trip.ts" - ], - "expectedFailure": { - "exitCode": 1, - "signal": "asymmetric round trip: the SDK produced 1 reasoning part(s) from the response, but serialized 0 back into the next request." - } -} diff --git a/.changeset/calm-dogs-reason.md b/.changeset/calm-dogs-reason.md new file mode 100644 index 000000000000..e2494dd50e3c --- /dev/null +++ b/.changeset/calm-dogs-reason.md @@ -0,0 +1,5 @@ +--- +"@ai-sdk/open-responses": patch +--- + +Preserve assistant reasoning parts when converting prompts to Open Responses input. diff --git a/examples/ai-functions/src/reproduction/issue-18513-open-responses-reasoning-round-trip.ts b/examples/ai-functions/src/reproduction/issue-18513-open-responses-reasoning-round-trip.ts deleted file mode 100644 index eae9da694700..000000000000 --- a/examples/ai-functions/src/reproduction/issue-18513-open-responses-reasoning-round-trip.ts +++ /dev/null @@ -1,134 +0,0 @@ -import assert from 'node:assert/strict'; -import { createOpenResponses } from '@ai-sdk/open-responses'; -import type { LanguageModelV4Prompt } from '@ai-sdk/provider'; - -const reasoningText = 'REASONING THAT MUST SURVIVE THE ROUND TRIP'; - -type AssistantContent = Extract< - LanguageModelV4Prompt[number], - { role: 'assistant' } ->['content']; - -type RequestBody = { - input: Array<{ type?: string; role?: string }>; -}; - -async function main() { - const requestBodies: RequestBody[] = []; - - function modelReturning(output: unknown[]) { - return createOpenResponses({ - name: 'reproduction', - apiKey: 'not-used', - url: 'https://example.invalid/v1/responses', - fetch: async (_url, init) => { - requestBodies.push(JSON.parse(String(init?.body)) as RequestBody); - - return new Response( - JSON.stringify({ - id: 'r', - object: 'response', - created_at: 0, - model: 'm', - status: 'completed', - output, - usage: { input_tokens: 1, output_tokens: 1 }, - }), - { - status: 200, - headers: { 'content-type': 'application/json' }, - }, - ); - }, - })('any-model'); - } - - const step1 = await modelReturning([ - { - type: 'reasoning', - id: 'rs_1', - content: [{ type: 'reasoning_text', text: reasoningText }], - summary: [], - }, - { - type: 'function_call', - id: 'fc_1', - call_id: 'call_1', - name: 't', - arguments: '{}', - }, - ]).doGenerate({ - prompt: [{ role: 'user', content: [{ type: 'text', text: 'q' }] }], - tools: [ - { - type: 'function', - name: 't', - inputSchema: { type: 'object', properties: {} }, - }, - ], - }); - - const reasoningRead = step1.content.filter(part => part.type === 'reasoning'); - console.log( - 'step 1 — reasoning parts the SDK produced:', - reasoningRead.length, - JSON.stringify(reasoningRead.map(part => part.text)), - ); - - await modelReturning([]).doGenerate({ - prompt: [ - { role: 'user', content: [{ type: 'text', text: 'q' }] }, - { - role: 'assistant', - content: step1.content as AssistantContent, - }, - { - role: 'tool', - content: [ - { - type: 'tool-result', - toolCallId: 'call_1', - toolName: 't', - output: { type: 'json', value: { ok: true } }, - }, - ], - }, - ], - tools: [ - { - type: 'function', - name: 't', - inputSchema: { type: 'object', properties: {} }, - }, - ], - }); - - const lastRequestBody = requestBodies.at(-1); - assert.ok(lastRequestBody != null, 'no second request body was captured'); - - const sentReasoning = lastRequestBody.input.filter( - item => item.type === 'reasoning', - ); - console.log( - 'step 2 — request input items:', - lastRequestBody.input - .map(item => item.type ?? `message:${item.role}`) - .join(', '), - ); - console.log('step 2 — reasoning items sent:', sentReasoning.length); - - assert.ok( - reasoningRead.length > 0, - 'precondition: the SDK did not read the reasoning item back', - ); - assert.equal( - sentReasoning.length, - reasoningRead.length, - `asymmetric round trip: the SDK produced ${reasoningRead.length} reasoning part(s) from the response, but serialized ${sentReasoning.length} back into the next request.`, - ); -} - -main().catch(error => { - console.error(error); - process.exitCode = 1; -}); diff --git a/packages/open-responses/src/responses/convert-to-open-responses-input.ts b/packages/open-responses/src/responses/convert-to-open-responses-input.ts index a20485284799..fd03c2921de0 100644 --- a/packages/open-responses/src/responses/convert-to-open-responses-input.ts +++ b/packages/open-responses/src/responses/convert-to-open-responses-input.ts @@ -16,6 +16,7 @@ import type { InputTextContentParam, OpenResponsesRequestBody, OutputTextContentParam, + ReasoningItemParam, RefusalContentParam, } from './open-responses-api'; @@ -105,10 +106,19 @@ export async function convertToOpenResponsesInput({ const assistantContent: Array< OutputTextContentParam | RefusalContentParam > = []; + const reasoningItems: Array = []; const toolCalls: Array = []; for (const part of content) { switch (part.type) { + case 'reasoning': { + reasoningItems.push({ + type: 'reasoning', + summary: [], + content: [{ type: 'reasoning_text', text: part.text }], + }); + break; + } case 'text': { assistantContent.push({ type: 'output_text', text: part.text }); break; @@ -129,6 +139,11 @@ export async function convertToOpenResponsesInput({ } } + // Push reasoning as separate items + for (const reasoningItem of reasoningItems) { + input.push(reasoningItem); + } + // Push assistant message with text content if any if (assistantContent.length > 0) { input.push({ diff --git a/packages/open-responses/src/responses/open-responses-api.ts b/packages/open-responses/src/responses/open-responses-api.ts index 08cd2cde60a6..8b2caad9ba34 100644 --- a/packages/open-responses/src/responses/open-responses-api.ts +++ b/packages/open-responses/src/responses/open-responses-api.ts @@ -143,7 +143,7 @@ export type ReasoningItemParam = { id?: string; type: 'reasoning'; summary: ReasoningSummaryContentParam[]; - content?: unknown; + content?: ReasoningTextContent[]; encrypted_content?: string; };