Skip to content

fix(responses): correct streaming detection so SSE events are emitted#1

Open
lunan0320 wants to merge 1 commit into
toreleon:add-responses-api-supportfrom
lunan0320:fix/responses-streaming-detection
Open

fix(responses): correct streaming detection so SSE events are emitted#1
lunan0320 wants to merge 1 commit into
toreleon:add-responses-api-supportfrom
lunan0320:fix/responses-streaming-detection

Conversation

@lunan0320
Copy link
Copy Markdown

Summary

This patch fixes a streaming-detection bug in the Responses API handler introduced together with the new /responses and /v1/responses routes. With the bug, every request that sets stream: true is silently downgraded to a non-streaming code path that serializes an async generator with c.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, createResponses returns events(response) from fetch-event-stream, i.e. an async generator. Symbol.asyncIterator on async generators is defined on %AsyncGeneratorPrototype%, not as an own property, so Object.hasOwn(generator, Symbol.asyncIterator) is always false. The negation therefore always returns true, 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:

  • Codex desktop: Reconnecting... 3/5 followed by stream disconnected before completion: stream closed before response.completed.
  • curl -N against /v1/responses with stream: true prints 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 ResponsesApiResponse shape 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 id property, so streaming requests now correctly fall through to streamSSE and forward response.created / response.in_progress / response.output_text.delta / response.completed events to the client.

Validation

Tested against this branch with --account-type enterprise on port 4141, model gpt-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/v1 with wire_api = "responses" and successfully streams responses end-to-end without the Reconnecting… retry loop.

Non-streaming requests (stream: false or omitted) are unaffected — the ResponsesApiResponse object still owns id, so it is still routed to c.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.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant