TanStack AI version
@tanstack/ai: 0.58.0
@tanstack/ai-openai: 0.23.1
@tanstack/openai-base: 0.10.15
- Underlying
openai SDK: 6.41.0
Also reproduced with the underlying openai SDK pinned to 6.49.0, with both @tanstack/ai-openai and @tanstack/openai-base verified to resolve that version. The shared reproduction keeps its original 6.41.0 lockfile.
Framework/Library version
No UI framework. Reproduced with Node.js 24.14.1 and Bun 1.3.12 on Linux.
Describe the bug and the steps to reproduce it
When an OpenAI Responses stream reaches EOF after text deltas but before any protocol terminal event, chat() reports a successful completion: it emits RUN_FINISHED with metadata.tanstack.finishReason: "stop" and invokes the middleware's onFinish. No RUN_ERROR or onError is produced, although the client has received no confirmation that generation finished.
This conflicts with the documented success semantics: RunFinishedEvent is described as "Emitted when a run completes successfully." The onFinish contract says it is called when the chat run "completes normally," and that exactly one of onFinish / onAbort / onError is called per run.
For example, an incomplete answer containing only "The answer is " receives the same successful lifecycle outcome as the completed answer "The answer is 42.". Callers using the terminal event or middleware callback to decide whether generation succeeded can consequently accept partial output as complete.
Steps to reproduce
The reproduction uses the public chat() API and a custom fetch injected into createOpenaiChat(). The fetch returns a controlled SSE Response; no network requests or real API key are needed.
- Run
bun install --frozen-lockfile in the reproduction directory.
- Run
node repro.mjs (or bun run repro.mjs).
- The script compares three cases sharing the same initial events:
response.created with status: "in_progress", a message/content part, and a text delta containing "The answer is ".
- In the successful control, it sends the remaining text, the corresponding done events, and
response.completed, then closes the body.
- In the premature-EOF case, it closes the body immediately after the initial text delta. It sends no
response.completed, response.failed, response.incomplete, or error event.
- In the read-error control, it errors the body instead of closing it normally.
Actual behavior
| Case |
Text received |
Public terminal |
Finish reason |
onFinish / onError calls |
| Completed response + EOF |
"The answer is 42." |
RUN_FINISHED |
stop |
1 / 0 |
| EOF before any protocol terminal |
"The answer is " |
RUN_FINISHED |
stop |
1 / 0 |
| Response-body read failure |
"The answer is " |
RUN_ERROR |
— |
0 / 1 |
Both runtimes reproduce these results with SDK versions 6.41.0 and 6.49.0. The script prints each result and asserts the behavior. The read-error control also produces an expected diagnostic log for the simulated failure.
The failing case is a normally exhausted HTTP response body with an incomplete application-level stream. It does not require a thrown socket/read error, user cancellation, or a model output-token limit.
This is a controlled transport reproduction; it does not claim that the official OpenAI endpoint normally omits terminal events. Socket resets, truncated HTTP bodies, or read failures may instead throw and take the error path, as the third control demonstrates. A missing terminal means the client cannot confirm completion; it does not establish whether generation failed on the server or completed there without the final events reaching the client.
Expected behavior
EOF before a recognized protocol terminal should not be reported as a confirmed successful stop. It should surface an error or another explicitly distinguishable incomplete outcome, while preserving any partial text for callers that want it.
Ensuring that every started run has a terminal event is useful, but a synthetic terminal should retain the fact that completion was never confirmed by the provider.
Relevant source
The underlying SDK's ordinary SSE iterator, used by responses.create({ stream: true }), can exhaust normally without confirming a Responses terminal event. The adapter then maps that exhaustion to RUN_FINISHED with finishReason: "stop", even though completion has not been confirmed.
The fallback in OpenAIBaseResponsesTextAdapter.processStreamChunks() explicitly handles a stream ending without response.completed; its comment even names "truncated upstream connection" as an example. If no run terminal was emitted, it synthesizes RUN_FINISHED and uses stop when there are no tool calls. Normal iterator exhaustion explains why this fallback is reached, but does not itself confirm successful generation.
I searched existing issues, PRs, and discussions. Related issue #1445 covers the opposite ordering: response.completed has arrived but EOF has not. This report covers EOF arriving without any protocol terminal. Issue #1426 concerns non-streaming structured output with an explicit finish_reason: "length", which is also a different case.
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
https://gist.github.com/L-1ngg/cbd231c5ca9bccbe82ad387580ed6bfd
Screenshots or Videos (Optional)
Not applicable; the reproduction prints the results summarized above.
Do you intend to try to help solve this bug with your own PR?
Maybe, I'll investigate and start debugging
Terms & Code of Conduct
TanStack AI version
@tanstack/ai:0.58.0@tanstack/ai-openai:0.23.1@tanstack/openai-base:0.10.15openaiSDK:6.41.0Also reproduced with the underlying
openaiSDK pinned to6.49.0, with both@tanstack/ai-openaiand@tanstack/openai-baseverified to resolve that version. The shared reproduction keeps its original6.41.0lockfile.Framework/Library version
No UI framework. Reproduced with Node.js
24.14.1and Bun1.3.12on Linux.Describe the bug and the steps to reproduce it
When an OpenAI Responses stream reaches EOF after text deltas but before any protocol terminal event,
chat()reports a successful completion: it emitsRUN_FINISHEDwithmetadata.tanstack.finishReason: "stop"and invokes the middleware'sonFinish. NoRUN_ERRORoronErroris produced, although the client has received no confirmation that generation finished.This conflicts with the documented success semantics:
RunFinishedEventis described as "Emitted when a run completes successfully." TheonFinishcontract says it is called when the chat run "completes normally," and that exactly one ofonFinish/onAbort/onErroris called per run.For example, an incomplete answer containing only
"The answer is "receives the same successful lifecycle outcome as the completed answer"The answer is 42.". Callers using the terminal event or middleware callback to decide whether generation succeeded can consequently accept partial output as complete.Steps to reproduce
The reproduction uses the public
chat()API and a customfetchinjected intocreateOpenaiChat(). The fetch returns a controlled SSEResponse; no network requests or real API key are needed.bun install --frozen-lockfilein the reproduction directory.node repro.mjs(orbun run repro.mjs).response.createdwithstatus: "in_progress", a message/content part, and a text delta containing"The answer is ".response.completed, then closes the body.response.completed,response.failed,response.incomplete, or error event.Actual behavior
onFinish/onErrorcalls"The answer is 42."RUN_FINISHEDstop"The answer is "RUN_FINISHEDstop"The answer is "RUN_ERRORBoth runtimes reproduce these results with SDK versions
6.41.0and6.49.0. The script prints each result and asserts the behavior. The read-error control also produces an expected diagnostic log for the simulated failure.The failing case is a normally exhausted HTTP response body with an incomplete application-level stream. It does not require a thrown socket/read error, user cancellation, or a model output-token limit.
This is a controlled transport reproduction; it does not claim that the official OpenAI endpoint normally omits terminal events. Socket resets, truncated HTTP bodies, or read failures may instead throw and take the error path, as the third control demonstrates. A missing terminal means the client cannot confirm completion; it does not establish whether generation failed on the server or completed there without the final events reaching the client.
Expected behavior
EOF before a recognized protocol terminal should not be reported as a confirmed successful
stop. It should surface an error or another explicitly distinguishable incomplete outcome, while preserving any partial text for callers that want it.Ensuring that every started run has a terminal event is useful, but a synthetic terminal should retain the fact that completion was never confirmed by the provider.
Relevant source
The underlying SDK's ordinary SSE iterator, used by
responses.create({ stream: true }), can exhaust normally without confirming a Responses terminal event. The adapter then maps that exhaustion toRUN_FINISHEDwithfinishReason: "stop", even though completion has not been confirmed.The fallback in
OpenAIBaseResponsesTextAdapter.processStreamChunks()explicitly handles a stream ending withoutresponse.completed; its comment even names "truncated upstream connection" as an example. If no run terminal was emitted, it synthesizesRUN_FINISHEDand usesstopwhen there are no tool calls. Normal iterator exhaustion explains why this fallback is reached, but does not itself confirm successful generation.I searched existing issues, PRs, and discussions. Related issue #1445 covers the opposite ordering:
response.completedhas arrived but EOF has not. This report covers EOF arriving without any protocol terminal. Issue #1426 concerns non-streaming structured output with an explicitfinish_reason: "length", which is also a different case.Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
https://gist.github.com/L-1ngg/cbd231c5ca9bccbe82ad387580ed6bfd
Screenshots or Videos (Optional)
Not applicable; the reproduction prints the results summarized above.
Do you intend to try to help solve this bug with your own PR?
Maybe, I'll investigate and start debugging
Terms & Code of Conduct