Streaming
Streaming sends output to you as the model generates it, over server-sent events (SSE). The Responses API and Chat Completions both stream. The SDK gives you two ways to read a stream:
- Helpers:
glexir.responses.stream()returns aResponseStreamandglexir.chat.completions.stream()returns aChatCompletionStream. You iterate typed events, and the helper builds the final object for you. - Raw streams: pass
stream: truetocreate(). You get aStreamof parsed events and nothing else.
Quick start
Responses streams
ResponseStream helper
glexir.responses.stream(params, options?) sends the request with stream: true and returns a
ResponseStream right away. It does not return a promise. The request is sent immediately; you
read the events by iterating the stream or calling one of its final* methods.
It accepts the same parameters as responses.create().
You don't need to pass stream.
[Symbol.asyncIterator]()AsyncGenerator<ResponseStreamEvent>optional- Iterate with
for await. Yields every event in order. You can iterate a stream only once; a second loop throwsGlexirError. finalResponse()Promise<Response>optional- Waits for the stream to end and returns the final
Response, withoutput_textandusage. If nothing is iterating the stream, it reads the stream itself. finalText()Promise<string>optional- Same as
finalResponse(), but returns onlyoutput_text. outputTextstringoptional- Getter. The text from every
response.output_text.deltaevent received so far. currentResponse()Response | undefinedoptional- The response built from the events received so far.
undefinedbefore the firstresponse.created,response.queuedorresponse.in_progressevent. abort()voidoptional- Stops the stream and cancels the HTTP request. See Aborting a stream.
The helper keeps a running snapshot. On each event it does this:
| Event | Effect on the snapshot |
|---|---|
response.created, response.queued, response.in_progress | Replaces the snapshot with event.response. |
response.completed, response.incomplete, response.failed | Stores event.response as the final response. |
response.output_item.added, response.output_item.done | Sets output[output_index] to event.item. |
response.content_part.added | Adds the text or refusal part to the message. |
response.output_text.delta | Appends delta to the text part and to outputText. |
response.function_call_arguments.delta | Appends delta to the function call's arguments. |
response.custom_tool_call_input.delta | Appends delta to the custom tool call's input. |
finalResponse() returns the response from the terminal event when there is one. If the stream
ends without a terminal event, it returns the last snapshot. If no response arrived at all, it
rejects with GlexirError ("Stream ended before a response was received.").
Raw event stream
responses.create({ ..., stream: true }) returns APIPromise<Stream<ResponseStreamEvent>>. Await it,
then iterate. There is no snapshot and no finalResponse(); read what you need from the events.
Event types
Every Responses event has a type and a sequence_number. ResponseStreamEvent is the union of
all of them, so switch (event.type) narrows the event to its exact shape.
type | SDK type | Key fields |
|---|---|---|
response.queued | ResponseQueuedEvent | response |
response.created | ResponseCreatedEvent | response |
response.in_progress | ResponseInProgressEvent | response |
response.completed | ResponseCompletedEvent | response (with usage) |
response.incomplete | ResponseIncompleteEvent | response (see incomplete_details) |
response.failed | ResponseFailedEvent | response (see error) |
response.output_item.added | ResponseOutputItemAddedEvent | output_index, item |
response.output_item.done | ResponseOutputItemDoneEvent | output_index, item |
response.content_part.added | ResponseContentPartAddedEvent | item_id, output_index, content_index, part |
response.content_part.done | ResponseContentPartDoneEvent | item_id, output_index, content_index, part |
response.output_text.delta | ResponseTextDeltaEvent | delta, logprobs, obfuscation? |
response.output_text.done | ResponseTextDoneEvent | text, logprobs |
response.output_text.annotation.added | ResponseOutputTextAnnotationAddedEvent | annotation_index, annotation |
response.refusal.delta | ResponseRefusalDeltaEvent | delta |
response.refusal.done | ResponseRefusalDoneEvent | refusal |
response.reasoning_summary_part.added | ResponseReasoningSummaryPartAddedEvent | summary_index, part |
response.reasoning_summary_part.done | ResponseReasoningSummaryPartDoneEvent | summary_index, part, status? |
response.reasoning_summary_text.delta | ResponseReasoningSummaryTextDeltaEvent | summary_index, delta |
response.reasoning_summary_text.done | ResponseReasoningSummaryTextDoneEvent | summary_index, text |
response.reasoning_text.delta | ResponseReasoningTextDeltaEvent | content_index, delta |
response.reasoning_text.done | ResponseReasoningTextDoneEvent | content_index, text |
response.function_call_arguments.delta | ResponseFunctionCallArgumentsDeltaEvent | item_id, output_index, delta |
response.function_call_arguments.done | ResponseFunctionCallArgumentsDoneEvent | item_id, output_index, name?, arguments |
response.custom_tool_call_input.delta | ResponseCustomToolCallInputDeltaEvent | item_id, output_index, delta |
response.custom_tool_call_input.done | ResponseCustomToolCallInputDoneEvent | item_id, output_index, input |
response.compaction.compacting | ResponseCompactionCompactingEvent | output_index?, item_id? |
error | ResponseErrorEvent | code, message, param |
keepalive | ResponseKeepaliveEvent | none; sent to keep long streams open |
Take a reasoning item's encrypted_content from response.output_item.done, not from
response.output_item.added.
You never receive an error event from the iterator in practice: the SDK throws it as an
APIError instead. See Errors during a stream.
Streaming function calls
Function call arguments arrive as response.function_call_arguments.delta events. The helper joins
them, so the simplest approach is to wait for the final response and read the function_call items.
Resuming a background stream
A background response created with background: true and stream: true keeps running on the
server if your connection drops. Pass { response_id, starting_after } to
responses.stream() to pick the stream up again. The SDK sends
GET /responses/{response_id}?stream=true&starting_after=N and replays events after sequence
number N.
ResponseStreamResumeParams is { response_id: string; starting_after?: number }. You can resume only
responses created with your Glexir account. Others return 404.
Chat Completions streams
ChatCompletionStream helper
glexir.chat.completions.stream(params, options?) returns a ChatCompletionStream. It accepts the
same parameters as chat.completions.create(), without stream.
Each chunk is a ChatCompletionChunk. The helper joins the chunks into a ChatCompletion: it
concatenates content and refusal, merges logprobs, and merges tool-call fragments by index
(the id and name arrive only on the first fragment).
[Symbol.asyncIterator]()AsyncGenerator<ChatCompletionChunk>optional- Iterate with
for await. Iterable once; a second loop throwsGlexirError. finalChatCompletion()Promise<ChatCompletion>optional- Waits for the stream to end and returns the assembled completion. Reads the stream itself if nothing is iterating it.
finalContent()Promise<string | null>optional- The first choice's
message.contentonce the stream ends. snapshot()ChatCompletionoptional- The completion built from the chunks so far. Throws
GlexirError("No chunks received yet.") before the first chunk. A choice with nofinish_reasonyet shows"stop". abort()voidoptional- Stops the stream and cancels the HTTP request.
The helper throws GlexirError if a chunk has a choice index or tool-call index outside 0 to 127,
or if a finished tool call has no id.
Usage in Chat streams
Chat Completions sends token usage in one extra chunk at the end of the stream, after the
content. That chunk has an empty choices array and a usage object.
- Set
stream_options: { include_usage: true }to receive it.finalChatCompletion()then hasusage. - Without it, you don't get the usage chunk. Glexir still asks OpenAI for usage on every stream so it can bill you accurately, then removes that chunk before it reaches you.
In a raw stream, guard against the empty choices array:
Final usage and billing
- Responses: usage is on the terminal event's
response.usage, sofinalResponse()has it. You don't need to opt in. - Chat Completions: usage is in the last chunk, only with
stream_options.include_usage.
Glexir bills a stream from the usage OpenAI reports at the end. If a stream ends without usage (for example, the upstream connection fails), Glexir bills an estimate of the input tokens only.
Aborting a stream
You can stop a stream in three ways. Each one cancels the HTTP request.
abort() takes effect only after iteration has started and the response headers have arrived.
Called earlier, it does nothing. To cancel a request that may not have started yet, pass a
signal.
What happens next:
- After
abort()or a signal fires mid-stream, the loop ends without an error. A helper then resolvesfinalResponse()/finalChatCompletion()with what it received so far. A partial Responses snapshot has nousageand itsstatusis notcompleted. If nothing arrived yet, thefinal*method rejects withGlexirError. - If the signal fires before the response headers arrive, the call throws
APIUserAbortError. - If you abort a signal mid-stream with your own reason (
controller.abort(reason)) that is not anAbortError, the loop throws that reason instead of ending quietly. - Breaking out of a
for awaitloop aborts the request. Do not callfinalResponse()orfinalChatCompletion()after abreak: it never settles. Useabort()instead if you need the partial result, or readcurrentResponse()/snapshot().
Aborting does not cancel billing for generated tokens
When you disconnect, the gateway keeps reading the upstream stream so it can record the final usage. You pay for the tokens the model generated, which can include tokens produced after you stopped reading.
Errors during a stream
An error that happens after the stream has started arrives as an SSE event, not as an HTTP status.
The SDK throws it from the loop as an APIError whose status is undefined. Branch on code,
not on the error class.
The SDK treats these as errors: an event: error SSE event, a data payload with an error
field, and a data payload with type: "error".
The gateway itself can end a stream with one of these codes (type server_error):
code | When |
|---|---|
stream_timeout | The upstream stream sent nothing for the gateway's idle timeout. |
upstream_stream_error | Reading the upstream stream failed. |
Errors before the stream starts (invalid parameters, 401, 402, 429) are thrown by the call itself, with an HTTP status, like any other request. See Errors & retries.
The SDK retries a stream request only before the response starts. Once a stream is open it has no
timeout, and a dropped connection is not retried. Your signal still cancels it.
The Stream class
Raw streams are instances of Stream<Item>, exported from @glexir/sdk. It is also what the helpers
wrap.
[Symbol.asyncIterator]()AsyncIterator<Item>optional- Yields parsed events. Stops at a
[DONE]message. Iterable once; iterating again throwsGlexirError. controllerAbortControlleroptional- Aborts the HTTP request:
stream.controller.abort(). responseResponse | undefinedoptional- The underlying fetch Response, when the stream came from one.
tee()[Stream<Item>, Stream<Item>]optional- Splits the stream into two that each see every event.
toReadableStream()ReadableStream<Uint8Array>optional- Converts the stream to newline-delimited JSON bytes, one event per line. Use it to relay a stream to a browser.
Stream.fromReadableStream(readable, controller?)Stream<Item>optional- Static. Turns the output of
toReadableStream()back into aStream. Stream.fromSSEResponse(response, controller)Stream<Item>optional- Static. Parses a fetch Response with an SSE body. The SDK uses this internally.
ServerSentEvent is also exported: { event: string | null; data: string; raw: string[] }.
Relay a stream to a browser
Keep your API key on the server. Convert the stream to NDJSON there, and read it back in the browser
with Stream.fromReadableStream().
Wire format
If you don't use the SDK, read the response as text/event-stream. Each event is one or more
data: lines followed by a blank line. Responses events carry event: <type> too. Chat
Completions streams end with data: [DONE]. Send Accept: text/event-stream and, with curl, use
-N to turn off buffering.
Idempotency-Key works differently for streams: the gateway remembers the key but can't replay a
streamed response. Retrying a finished streamed request with the same key returns
409 idempotency_stream_replay.