Description
Using @ai-sdk/openai@4.0.29 with ai@7.0.51. A multi-turn conversation via the OpenAI Responses API, chaining turns with providerOptions.openai.previousResponseId, and using a plain client-executed tool (tool({ inputSchema, execute }), not a provider-defined tool like local_shell/shell/apply_patch/computer).
First turn (no previousResponseId): the model calls the tool, everything works, response streams normally.
Any follow-up turn (a previousResponseId from the prior turn is set) where the model makes a new tool call: the request fails with
AI_APICallError: No tool call found for function call output with call_id call_XXXXXXXX.
Minimal repro shape:
import { streamText, tool, stepCountIs } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const myTool = tool({
description: 'a plain client-executed tool',
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => ({ result: `results for ${query}` }),
});
// Turn 1 — works
const r1 = await streamText({
model: openai.responses('gpt-4.1-mini'), // or a reasoning model, e.g. gpt-5.x
prompt: 'search for red shoes',
tools: { myTool },
stopWhen: stepCountIs(3),
});
for await (const _ of r1.fullStream) {}
const responseId = (await r1.finalStep).providerMetadata?.openai?.responseId;
// Turn 2 — FAILS: "No tool call found for function call output with call_id ..."
const r2 = await streamText({
model: openai.responses('gpt-4.1-mini'),
prompt: 'now search for blue shoes',
tools: { myTool },
stopWhen: stepCountIs(3),
providerOptions: { openai: { previousResponseId: responseId, store: true } },
});
for await (const part of r2.fullStream) {
if (part.type === 'error') console.error(part.error); // <-- the 400 surfaces here
}
What I found tracing the root cause
In packages/openai/src/responses/convert-to-openai-responses-input.ts, case 'tool-call':, there's an early break:
if (hasPreviousResponseId && store && id != null) {
break;
}
This drops the assistant's function_call item on a follow-up turn whenever the tool call carries a provider itemId (i.e. id != null) — which includes plain client-executed tool calls, not just provider-defined ones. The matching function_call_output for that call is still sent (it's only referenced by call_id, unconditionally), so the request ends up with a function_call_output whose function_call was never included → OpenAI 400s.
Below that break, there's already a guard for provider-defined tools:
const isProviderDefinedToolCall = /* local_shell | shell | apply_patch | computer | custom */;
if (store && id != null && isProviderDefinedToolCall) {
input.push({ type: 'item_reference', id });
break;
}
It looks like the intent (per #15795's own description — "Plain client-executed function calls must NOT be [sent as item_reference]… the matching function_call_output can only reference the call by call_id") was that plain client tool calls should fall through to the unconditional function_call push further down, so they pair with their output by call_id regardless of store/previousResponseId. But the earlier hasPreviousResponseId && store && id != null break fires first and drops them before that fallthrough is reached.
Guarding that break with the same isProviderDefinedToolCall check (and hoisting that const above it) seems to resolve it in local testing — plain client tool calls then fall through and pair correctly.
Environment
ai: 7.0.51
@ai-sdk/openai: 4.0.29 (also reproduces on 4.0.33, latest — confirmed same guard ordering on main)
- Node: 24
- Model: reproduces with both
gpt-4.1-mini and reasoning models (gpt-5.x)
Related
Impact
Any multi-turn conversation that chains via previousResponseId (rather than resending full message history) and uses a plain client-executed tool cannot make a new tool call on any turn after the first. This affects any app using server-side conversation chaining + tools with the Responses API — a fairly standard combination.
Reproduction
No response
AI SDK Version
ai: 7.0.51
@ai-sdk/openai: 4.0.29 (also reproduces on 4.0.33, latest — confirmed same guard ordering on main)
Code of Conduct
Description
Using
@ai-sdk/openai@4.0.29withai@7.0.51. A multi-turn conversation via the OpenAI Responses API, chaining turns withproviderOptions.openai.previousResponseId, and using a plain client-executed tool (tool({ inputSchema, execute }), not a provider-defined tool likelocal_shell/shell/apply_patch/computer).First turn (no
previousResponseId): the model calls the tool, everything works, response streams normally.Any follow-up turn (a
previousResponseIdfrom the prior turn is set) where the model makes a new tool call: the request fails withMinimal repro shape:
What I found tracing the root cause
In
packages/openai/src/responses/convert-to-openai-responses-input.ts,case 'tool-call':, there's an early break:This drops the assistant's
function_callitem on a follow-up turn whenever the tool call carries a provideritemId(i.e.id != null) — which includes plain client-executed tool calls, not just provider-defined ones. The matchingfunction_call_outputfor that call is still sent (it's only referenced bycall_id, unconditionally), so the request ends up with afunction_call_outputwhosefunction_callwas never included → OpenAI 400s.Below that break, there's already a guard for provider-defined tools:
It looks like the intent (per #15795's own description — "Plain client-executed function calls must NOT be [sent as item_reference]… the matching function_call_output can only reference the call by call_id") was that plain client tool calls should fall through to the unconditional
function_callpush further down, so they pair with their output bycall_idregardless ofstore/previousResponseId. But the earlierhasPreviousResponseId && store && id != nullbreak fires first and drops them before that fallthrough is reached.Guarding that break with the same
isProviderDefinedToolCallcheck (and hoisting that const above it) seems to resolve it in local testing — plain client tool calls then fall through and pair correctly.Environment
ai: 7.0.51@ai-sdk/openai: 4.0.29 (also reproduces on 4.0.33, latest — confirmed same guard ordering onmain)gpt-4.1-miniand reasoning models (gpt-5.x)Related
Impact
Any multi-turn conversation that chains via
previousResponseId(rather than resending full message history) and uses a plain client-executed tool cannot make a new tool call on any turn after the first. This affects any app using server-side conversation chaining + tools with the Responses API — a fairly standard combination.Reproduction
No response
AI SDK Version
ai: 7.0.51@ai-sdk/openai: 4.0.29 (also reproduces on 4.0.33, latest — confirmed same guard ordering onmain)Code of Conduct