TanStack AI version
@tanstack/ai 0.53.0, @tanstack/ai-openai 0.22.5, @tanstack/openai-base 0.10.10, @tanstack/ai-persistence 0.5.6
Framework/Library version
Node.js (server-side, no UI framework); OpenAI Responses API, model gpt-5.6-terra, store:false
Describe the bug and the steps to reproduce it
This is uncommon, but unrecoverable when it hits. It needs the model to emit two or more reasoning items in a single response, which most turns don't do (turns that reason once per message replay fine). But when it does happen, that thread can never be sent again.
OpenAI wants each tool call sent back right after the reasoning item that produced it. When @tanstack/ai replays a stored assistant message, it groups all the reasoning items first and then all the tool calls. A message with a single reasoning item still has it right before its call, so it works. But when the model produced two or more reasoning items in one response, replaying them grouped separates a tool call from its reasoning, and OpenAI rejects the whole request:
400 Item 'fc_…' of type 'function_call' was provided without its required 'reasoning' item: 'rs_…'
It stays broken because the regrouped order gets saved, so every later turn resends it and fails again.
convertMessagesToInput is where the regrouping happens (adapters/responses-text.ts, ~L1946). The fix is to keep the reasoning items and tool calls in the order they actually occurred; if the stored message can't preserve that order, drop the saved tool-call id so OpenAI stops requiring its paired reasoning item.
Repro: the linked sandbox (standalone: npm install then OPENAI_API_KEY=sk-… node repro.mjs; deps are just @tanstack/ai + @tanstack/ai-openai + @tanstack/ai-persistence + zod). A static fixture can't work, because OpenAI rejects reasoning it didn't mint ("encrypted content could not be decrypted"), so it mints two real reasoning→call pairs with your key, replays one turn holding both, and prints the input order it produces:
CONTROL (pair A alone) -> OK
BUG: input order convertMessagesToInput produces for the two-pair turn:
message user
reasoning …rs_A
reasoning …rs_B
function_call …fc_A
function_call …fc_B
function_call_output (call_A)
function_call_output (call_B)
message user
-> 400 Item 'fc_A' of type 'function_call' was provided without its required 'reasoning' item: 'rs_A'.
rs_A is no longer next to fc_A (rs_B is between them), so OpenAI rejects it.
This is the case @harshlocham raised reviewing #1290 ("convertMessagesToInput() currently puts all reasoning items before the function calls, so … reasoning A → function_call A → reasoning B → function_call B wouldn't be replayed in the same order"), and asked to cover with "a regression test for two interleaved reasoning/function-call pairs." That's exactly this repro (CodeRabbit flagged the same, citing OpenAI's docs that the order must be preserved). Our own broken thread, a real transcript with 3 reasonings and 5 calls, fails 15/15; deleting the saved call ids makes it pass.
There's a second, rarer failure on the same turns: Duplicate item found with id rs_…. The same reasoning id lands on two assistant messages and gets replayed twice.
It's rare, and we couldn't catch OpenAI producing it live: we ran 255 stateless legs watching for a reused reasoning id and every single one came back with a fresh id. So we don't have the raw OpenAI response from the moment it happened, only our stored transcript after the fact, included in the sandbox as raw-message.json (sanitized: text, tool args/outputs and tool-call ids removed, tool names genericized, and the encrypted blobs truncated to their timestamp header).
Our evidence that OpenAI actually produced this (and it isn't a duplicate introduced on our side): the two copies of the id carry two different encrypted contents (distinct Fernet blobs). Decoding the timestamp embedded in each blob gives 19:04:48 and 19:04:52: the same id, encrypted by OpenAI 4 seconds apart. The client never encrypts these (only OpenAI does), and a client-side copy would be byte-identical. Two different signatures for one id means OpenAI emitted it twice; it's not a simple copy on our end. Deduping reasoning ids on rebuild fixes it, the same way openai/openai-agents-js#556 did.
Versions: @tanstack/ai@0.53.0, @tanstack/ai-openai@0.22.5, @tanstack/ai-persistence@0.5.6; gpt-5.6-terra, store:false.
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
https://codesandbox.io/s/2qvxr9
Screenshots or Videos (Optional)
No response
Do you intend to try to help solve this bug with your own PR?
No, because I do not have time to dig into it
Terms & Code of Conduct
TanStack AI version
@tanstack/ai 0.53.0, @tanstack/ai-openai 0.22.5, @tanstack/openai-base 0.10.10, @tanstack/ai-persistence 0.5.6
Framework/Library version
Node.js (server-side, no UI framework); OpenAI Responses API, model gpt-5.6-terra, store:false
Describe the bug and the steps to reproduce it
This is uncommon, but unrecoverable when it hits. It needs the model to emit two or more reasoning items in a single response, which most turns don't do (turns that reason once per message replay fine). But when it does happen, that thread can never be sent again.
OpenAI wants each tool call sent back right after the reasoning item that produced it. When
@tanstack/aireplays a stored assistant message, it groups all the reasoning items first and then all the tool calls. A message with a single reasoning item still has it right before its call, so it works. But when the model produced two or more reasoning items in one response, replaying them grouped separates a tool call from its reasoning, and OpenAI rejects the whole request:It stays broken because the regrouped order gets saved, so every later turn resends it and fails again.
convertMessagesToInputis where the regrouping happens (adapters/responses-text.ts, ~L1946). The fix is to keep the reasoning items and tool calls in the order they actually occurred; if the stored message can't preserve that order, drop the saved tool-call id so OpenAI stops requiring its paired reasoning item.Repro: the linked sandbox (standalone:
npm installthenOPENAI_API_KEY=sk-… node repro.mjs; deps are just@tanstack/ai+@tanstack/ai-openai+@tanstack/ai-persistence+zod). A static fixture can't work, because OpenAI rejects reasoning it didn't mint ("encrypted content could not be decrypted"), so it mints two real reasoning→call pairs with your key, replays one turn holding both, and prints the input order it produces:rs_Ais no longer next tofc_A(rs_Bis between them), so OpenAI rejects it.This is the case @harshlocham raised reviewing #1290 ("convertMessagesToInput() currently puts all reasoning items before the function calls, so … reasoning A → function_call A → reasoning B → function_call B wouldn't be replayed in the same order"), and asked to cover with "a regression test for two interleaved reasoning/function-call pairs." That's exactly this repro (CodeRabbit flagged the same, citing OpenAI's docs that the order must be preserved). Our own broken thread, a real transcript with 3 reasonings and 5 calls, fails 15/15; deleting the saved call ids makes it pass.
There's a second, rarer failure on the same turns:
Duplicate item found with id rs_…. The same reasoning id lands on two assistant messages and gets replayed twice.It's rare, and we couldn't catch OpenAI producing it live: we ran 255 stateless legs watching for a reused reasoning id and every single one came back with a fresh id. So we don't have the raw OpenAI response from the moment it happened, only our stored transcript after the fact, included in the sandbox as
raw-message.json(sanitized: text, tool args/outputs and tool-call ids removed, tool names genericized, and the encrypted blobs truncated to their timestamp header).Our evidence that OpenAI actually produced this (and it isn't a duplicate introduced on our side): the two copies of the id carry two different encrypted contents (distinct Fernet blobs). Decoding the timestamp embedded in each blob gives 19:04:48 and 19:04:52: the same id, encrypted by OpenAI 4 seconds apart. The client never encrypts these (only OpenAI does), and a client-side copy would be byte-identical. Two different signatures for one id means OpenAI emitted it twice; it's not a simple copy on our end. Deduping reasoning ids on rebuild fixes it, the same way
openai/openai-agents-js#556did.Versions:
@tanstack/ai@0.53.0,@tanstack/ai-openai@0.22.5,@tanstack/ai-persistence@0.5.6;gpt-5.6-terra,store:false.Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
https://codesandbox.io/s/2qvxr9
Screenshots or Videos (Optional)
No response
Do you intend to try to help solve this bug with your own PR?
No, because I do not have time to dig into it
Terms & Code of Conduct