Skip to content

OpenAI Responses: EOF without a terminal event is reported as a successful stop #1447

Description

@L-1ngg

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.

  1. Run bun install --frozen-lockfile in the reproduction directory.
  2. Run node repro.mjs (or bun run repro.mjs).
  3. 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 ".
  4. In the successful control, it sends the remaining text, the corresponding done events, and response.completed, then closes the body.
  5. 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.
  6. 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

  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions