Skip to content

@ai-sdk/open-responses: reasoning items are never replayed in assistant messages, breaking tool loops on providers that require them #18511

Description

@Astro-Han

Description

convertToOpenResponsesInput() handles only text and tool-call parts in the assistant branch — there is no reasoning case. Reasoning received from the provider is parsed correctly on the response side but is silently dropped when the conversation is sent back, so the round trip is asymmetric.

Reproduction

Deterministic, and needs no API key, no network and no particular provider:

import { createOpenResponses } from '@ai-sdk/open-responses';

let body;
const fetch = async (_url, init) => {
  body = JSON.parse(String(init.body));
  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' } });
};

const model = createOpenResponses({
  apiKey: 'test', baseURL: 'https://example.invalid/v1', fetch,
})('any-model');

await model.doGenerate({
  prompt: [
    { role: 'user', content: [{ type: 'text', text: 'q' }] },
    { role: 'assistant', content: [
      { type: 'reasoning', text: 'REASONING THAT MUST BE REPLAYED' },
      { type: 'tool-call', toolCallId: 'call_1', toolName: 't', input: {} },
    ]},
    { 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: {} } }],
});

console.log('reasoning items sent:', body.input.filter((i) => i.type === 'reasoning').length);
console.log(body.input.map((i) => i.type ?? `message:${i.role}`).join(', '));
reasoning items sent: 0
message, function_call, function_call_output

A reasoning part goes in; no reasoning item comes out.

Why it matters

For providers that merely tolerate missing reasoning this is invisible. For providers that require reasoning to be replayed inside a tool loop it makes multi-step tool use impossible.

DeepSeek (deepseek-v4-flash, https://api.deepseek.com/v1/responses) requires it — thinking mode is on by default there, so any tool loop is affected. Omitting the reasoning item returns:

The `reasoning_text` in the thinking mode must be passed back to the API.

Replaying it by hand as {"type":"reasoning","content":[{"type":"reasoning_text","text":...}]}, using a call_id the server cannot associate with a record of its own, and varying only the replayed length:

replayed reasoning HTTP usage.input_tokens
omitted 400
empty string 400
1 char 200 472
3653 chars 200 1388
7307 chars 200 2189
14615 chars 200 4022

Input token accounting scales with the replayed payload, so it is loaded into context rather than accepted and ignored.

Note on server-side recovery

A tool loop run end to end against DeepSeek will often not surface the 400. DeepSeek recovers the reasoning associated with a call_id it issued itself, so as long as the SDK forwards that id unchanged, the omission is masked and the request succeeds — while still being billed for reasoning the SDK never sent.

That recovery is DeepSeek's own behaviour, not part of the Open Responses shape, and another provider implementing that shape need not offer anything comparable. Isolating it with an unassociated call_id is what makes the omission observable, which is why the table above uses one. The serialization gap itself is independent of all of this — see the reproduction above.

Expected

The assistant branch should emit reasoning parts as Open Responses reasoning items, positioned with the function_call they belong to:

{"type": "reasoning", "content": [{"type": "reasoning_text", "text": "..."}]}

This is the same shape the package already parses on the response side (case "reasoning": for (const contentPart of part.content) ...).

Notes

Environment

@ai-sdk/open-responses 2.0.23

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions