GlexirDocs
Dashboard

SDK

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 a ResponseStream and glexir.chat.completions.stream() returns a ChatCompletionStream. You iterate typed events, and the helper builds the final object for you.
  • Raw streams: pass stream: true to create(). You get a Stream of parsed events and nothing else.

Quick start

import Glexir from "@glexir/sdk";

const glexir = new Glexir({ apiKey: process.env.GLEXIR_API_KEY });

const stream = glexir.responses.stream({
  model: "gpt-5.4-mini",
  input: "Write a haiku about the sea.",
});

for await (const event of stream) {
  if (event.type === "response.output_text.delta") process.stdout.write(event.delta);
}

const response = await stream.finalResponse();
console.log("\n", response.usage);

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.

TypeScript
import Glexir from "@glexir/sdk";

const glexir = new Glexir();

const stream = glexir.responses.stream({
  model: "gpt-5.4-mini",
  instructions: "Answer in one sentence.",
  input: "Why is the sky blue?",
});

for await (const event of stream) {
  switch (event.type) {
    case "response.created":
      console.log("id:", event.response.id);
      break;
    case "response.output_text.delta":
      process.stdout.write(event.delta);
      break;
    case "response.completed":
      console.log("\ntokens:", event.response.usage?.total_tokens);
      break;
  }
}

console.log(stream.outputText);
ResponseStream
[Symbol.asyncIterator]()AsyncGenerator<ResponseStreamEvent>optional
Iterate with for await. Yields every event in order. You can iterate a stream only once; a second loop throws GlexirError.
finalResponse()Promise<Response>optional
Waits for the stream to end and returns the final Response, with output_text and usage. If nothing is iterating the stream, it reads the stream itself.
finalText()Promise<string>optional
Same as finalResponse(), but returns only output_text.
outputTextstringoptional
Getter. The text from every response.output_text.delta event received so far.
currentResponse()Response | undefinedoptional
The response built from the events received so far. undefined before the first response.created, response.queued or response.in_progress event.
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:

EventEffect on the snapshot
response.created, response.queued, response.in_progressReplaces the snapshot with event.response.
response.completed, response.incomplete, response.failedStores event.response as the final response.
response.output_item.added, response.output_item.doneSets output[output_index] to event.item.
response.content_part.addedAdds the text or refusal part to the message.
response.output_text.deltaAppends delta to the text part and to outputText.
response.function_call_arguments.deltaAppends delta to the function call's arguments.
response.custom_tool_call_input.deltaAppends 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.

TypeScript
import Glexir from "@glexir/sdk";

const glexir = new Glexir();

const stream = await glexir.responses.create({
  model: "gpt-5.4-mini",
  input: "List three prime numbers.",
  stream: true,
});

for await (const event of stream) {
  if (event.type === "response.output_text.delta") process.stdout.write(event.delta);
  if (event.type === "response.completed") console.log("\n", event.response.usage);
}

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.

typeSDK typeKey fields
response.queuedResponseQueuedEventresponse
response.createdResponseCreatedEventresponse
response.in_progressResponseInProgressEventresponse
response.completedResponseCompletedEventresponse (with usage)
response.incompleteResponseIncompleteEventresponse (see incomplete_details)
response.failedResponseFailedEventresponse (see error)
response.output_item.addedResponseOutputItemAddedEventoutput_index, item
response.output_item.doneResponseOutputItemDoneEventoutput_index, item
response.content_part.addedResponseContentPartAddedEventitem_id, output_index, content_index, part
response.content_part.doneResponseContentPartDoneEventitem_id, output_index, content_index, part
response.output_text.deltaResponseTextDeltaEventdelta, logprobs, obfuscation?
response.output_text.doneResponseTextDoneEventtext, logprobs
response.output_text.annotation.addedResponseOutputTextAnnotationAddedEventannotation_index, annotation
response.refusal.deltaResponseRefusalDeltaEventdelta
response.refusal.doneResponseRefusalDoneEventrefusal
response.reasoning_summary_part.addedResponseReasoningSummaryPartAddedEventsummary_index, part
response.reasoning_summary_part.doneResponseReasoningSummaryPartDoneEventsummary_index, part, status?
response.reasoning_summary_text.deltaResponseReasoningSummaryTextDeltaEventsummary_index, delta
response.reasoning_summary_text.doneResponseReasoningSummaryTextDoneEventsummary_index, text
response.reasoning_text.deltaResponseReasoningTextDeltaEventcontent_index, delta
response.reasoning_text.doneResponseReasoningTextDoneEventcontent_index, text
response.function_call_arguments.deltaResponseFunctionCallArgumentsDeltaEventitem_id, output_index, delta
response.function_call_arguments.doneResponseFunctionCallArgumentsDoneEventitem_id, output_index, name?, arguments
response.custom_tool_call_input.deltaResponseCustomToolCallInputDeltaEventitem_id, output_index, delta
response.custom_tool_call_input.doneResponseCustomToolCallInputDoneEventitem_id, output_index, input
response.compaction.compactingResponseCompactionCompactingEventoutput_index?, item_id?
errorResponseErrorEventcode, message, param
keepaliveResponseKeepaliveEventnone; 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.

TypeScript
import Glexir from "@glexir/sdk";

const glexir = new Glexir();

const stream = glexir.responses.stream({
  model: "gpt-5.4-mini",
  input: "What's the weather in Lagos?",
  tools: [
    {
      type: "function",
      name: "get_weather",
      parameters: {
        type: "object",
        properties: { city: { type: "string" } },
        required: ["city"],
        additionalProperties: false,
      },
      strict: true,
    },
  ],
});

for await (const event of stream) {
  if (event.type === "response.function_call_arguments.done") {
    console.log("arguments ready:", event.arguments);
  }
}

const response = await stream.finalResponse();
for (const item of response.output) {
  if (item.type === "function_call") console.log(item.name, JSON.parse(item.arguments));
}

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.

TypeScript
import Glexir from "@glexir/sdk";

const glexir = new Glexir();

let responseId: string | undefined;
let lastSequence: number | undefined;

const stream = glexir.responses.stream({
  model: "gpt-5.4",
  input: "Write a detailed project plan.",
  background: true,
  store: true,
});

try {
  for await (const event of stream) {
    lastSequence = event.sequence_number;
    if (event.type === "response.created") responseId = event.response.id;
    if (event.type === "response.output_text.delta") process.stdout.write(event.delta);
  }
} catch (err) {
  if (!responseId) throw err;
  const resumed = glexir.responses.stream({ response_id: responseId, starting_after: lastSequence });
  console.log(await resumed.finalText());
}

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).

TypeScript
import Glexir from "@glexir/sdk";

const glexir = new Glexir();

const stream = glexir.chat.completions.stream({
  model: "gpt-5.4-mini",
  messages: [{ role: "user", content: "Count to five." }],
  stream_options: { include_usage: true },
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta.content ?? "");
}

const completion = await stream.finalChatCompletion();
console.log("\n", completion.usage);
ChatCompletionStream
[Symbol.asyncIterator]()AsyncGenerator<ChatCompletionChunk>optional
Iterate with for await. Iterable once; a second loop throws GlexirError.
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.content once 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 no finish_reason yet 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 has usage.
  • 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:

TypeScript
import Glexir from "@glexir/sdk";

const glexir = new Glexir();

const stream = await glexir.chat.completions.create({
  model: "gpt-5.4-mini",
  messages: [{ role: "user", content: "Say hello." }],
  stream: true,
  stream_options: { include_usage: true },
});

for await (const chunk of stream) {
  const delta = chunk.choices[0]?.delta.content;
  if (delta) process.stdout.write(delta);
  if (chunk.usage) console.log("\ntokens:", chunk.usage.total_tokens);
}

Final usage and billing

  • Responses: usage is on the terminal event's response.usage, so finalResponse() 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.

TypeScript
import Glexir from "@glexir/sdk";

const glexir = new Glexir();

// 1. Call abort() on a helper.
const stream = glexir.responses.stream({ model: "gpt-5.4-mini", input: "Tell me a long story." });
setTimeout(() => stream.abort(), 2000);
for await (const event of stream) {
  if (event.type === "response.output_text.delta") process.stdout.write(event.delta);
}
const partial = await stream.finalResponse();
console.log("\nstatus:", partial.status);

// 2. Pass an AbortSignal in the request options.
const controller = new AbortController();
const chat = glexir.chat.completions.stream(
  { model: "gpt-5.4-mini", messages: [{ role: "user", content: "Tell me a long story." }] },
  { signal: controller.signal },
);
setTimeout(() => controller.abort(), 2000);
for await (const chunk of chat) process.stdout.write(chunk.choices[0]?.delta.content ?? "");

// 3. Break out of the loop.
const raw = await glexir.responses.create({ model: "gpt-5.4-mini", input: "Count to 100.", stream: true });
for await (const event of raw) {
  if (event.type === "response.output_text.delta" && event.delta.includes("10")) break;
}

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 resolves finalResponse() / finalChatCompletion() with what it received so far. A partial Responses snapshot has no usage and its status is not completed. If nothing arrived yet, the final* method rejects with GlexirError.
  • 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 an AbortError, the loop throws that reason instead of ending quietly.
  • Breaking out of a for await loop aborts the request. Do not call finalResponse() or finalChatCompletion() after a break: it never settles. Use abort() instead if you need the partial result, or read currentResponse() / 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".

TypeScript
import Glexir, { APIError } from "@glexir/sdk";

const glexir = new Glexir();

try {
  const stream = glexir.responses.stream({ model: "gpt-5.4-mini", input: "Hello" });
  for await (const event of stream) {
    if (event.type === "response.output_text.delta") process.stdout.write(event.delta);
  }
} catch (err) {
  if (err instanceof APIError && err.status === undefined) {
    console.error("stream error:", err.code, err.message);
  } else {
    throw err;
  }
}

The gateway itself can end a stream with one of these codes (type server_error):

codeWhen
stream_timeoutThe upstream stream sent nothing for the gateway's idle timeout.
upstream_stream_errorReading 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.

Stream<Item>
[Symbol.asyncIterator]()AsyncIterator<Item>optional
Yields parsed events. Stops at a [DONE] message. Iterable once; iterating again throws GlexirError.
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 a Stream.
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().

TypeScript
// Server: a fetch-style route handler.
import Glexir from "@glexir/sdk";

const glexir = new Glexir();

export async function POST(request: Request): Promise<Response> {
  const { prompt } = (await request.json()) as { prompt: string };
  const stream = await glexir.responses.create({ model: "gpt-5.4-mini", input: prompt, stream: true });
  return new Response(stream.toReadableStream(), {
    headers: { "content-type": "application/x-ndjson" },
  });
}
TypeScript
// Browser
import { Stream, type ResponseStreamEvent } from "@glexir/sdk";

const res = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ prompt: "Hi" }) });
if (!res.body) throw new Error("No response body");

for await (const event of Stream.fromReadableStream<ResponseStreamEvent>(res.body)) {
  if (event.type === "response.output_text.delta") console.log(event.delta);
}

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.

Output
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_123","output_index":0,"content_index":0,"delta":"Hello","logprobs":[]}

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.