fix(responses): correct streaming detection so SSE events are emitted#1
Open
lunan0320 wants to merge 1 commit into
Open
Conversation
The previous check !Object.hasOwn(response, Symbol.asyncIterator) always
returned true because Symbol.asyncIterator on async generators lives on the
prototype, not as an own property. As a result, every streaming Responses
request was incorrectly routed through the non-streaming branch, where
c.json(asyncGenerator) serialized to an empty object {}. Clients (e.g.
the Codex desktop app) then reported stream closed before response.completed.
Mirror the chat-completions handler convention by detecting the non-streaming
shape via the id own property on ResponsesApiResponse instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This patch fixes a streaming-detection bug in the Responses API handler introduced together with the new
/responsesand/v1/responsesroutes. With the bug, every request that setsstream: trueis silently downgraded to a non-streaming code path that serializes an async generator withc.json(...), producing an empty{}body and a closed connection.Root cause
In
src/routes/responses/handler.ts:ts const isNonStreaming = ( response: Awaited<ReturnType<typeof createResponses>>, ): response is ResponsesApiResponse => !Object.hasOwn(response, Symbol.asyncIterator)For streaming requests,
createResponsesreturnsevents(response)fromfetch-event-stream, i.e. an async generator.Symbol.asyncIteratoron async generators is defined on%AsyncGeneratorPrototype%, not as an own property, soObject.hasOwn(generator, Symbol.asyncIterator)is alwaysfalse. The negation therefore always returnstrue, causing the streaming branch to be unreachable.The non-streaming branch then runs
c.json(asyncGenerator), which JSON-stringifies the generator object as{}(no enumerable own properties). Downstream OpenAI Responses API consumers react accordingly:Reconnecting... 3/5followed bystream disconnected before completion: stream closed before response.completed.curl -Nagainst/v1/responseswithstream: trueprints just{}and exits.Fix
Mirror the convention used by the chat-completions handler — detect the non-streaming response by checking for an own property that only the buffered
ResponsesApiResponseshape has (id):ts const isNonStreaming = ( response: Awaited<ReturnType<typeof createResponses>>, ): response is ResponsesApiResponse => Object.hasOwn(response, "id")This is the same pattern used in
src/routes/chat-completions/handler.ts:ts ): response is ChatCompletionResponse => Object.hasOwn(response, "choices")Async generators do not have an own
idproperty, so streaming requests now correctly fall through tostreamSSEand forwardresponse.created/response.in_progress/response.output_text.delta/response.completedevents to the client.Validation
Tested against this branch with
--account-type enterpriseon port 4141, modelgpt-5.5:Before fix (
stream: true):console $ curl -sN -X POST http://localhost:4141/v1/responses \ -H 'Content-Type: application/json' \ -d '{"model":"gpt-5.5","input":"Say hi","stream":true}' {}After fix (
stream: true):`console
event: response.created
data: {"response":{...,"status":"in_progress",...},"sequence_number":0,"type":"response.created"}
event: response.in_progress
data: {"response":{...},"type":"response.in_progress"}
event: response.output_text.delta
data: {...}
...
event: response.completed
data: {...}
`
Codex desktop now connects to
http://localhost:4141/v1withwire_api = "responses"and successfully streams responses end-to-end without theReconnecting…retry loop.Non-streaming requests (
stream: falseor omitted) are unaffected — theResponsesApiResponseobject still ownsid, so it is still routed toc.json(response)as before.Credit
Discovered while integrating this branch with the Codex desktop app. Big thanks to @toreleon for adding Responses API support — this is a one-line follow-up to make streaming work for clients like Codex that require the full SSE event stream.