Fleeexdocs

Streaming usage

stream_options.include_usage, the exact wire shape of the trailing usage chunk, and why its numbers are the ones you were billed.

A streaming caller otherwise has no way to know what it consumed. Add stream_options: { include_usage: true } and the stream ends with an extra chunk carrying the token counts.

const stream = await client.chat.completions.create({
  model: "nova-lite",
  messages: [{ role: "user", content: "Write a haiku." }],
  stream: true,
  stream_options: { include_usage: true },
});
 
for await (const chunk of stream) {
  // The final usage chunk has an EMPTY choices array, so guard the index.
  const delta = chunk.choices[0]?.delta?.content;
  if (delta) process.stdout.write(delta);
 
  if (chunk.usage) {
    console.log("\nbilled:", chunk.usage.total_tokens, "tokens");
  }
}

The wire, exactly

The shape is OpenAI's, and it matters because the empty-choices chunk is the one chunk shape a client only ever meets at the end of a stream:

  1. Every chunk that carries a choice also carries usage: null. The field is always declared, so chunk.usage reads as null, never undefined.
  2. After the finish-reason chunk, one extra chunk with an empty choices array and a populated usage (prompt_tokens, completion_tokens, total_tokens).
  3. Then data: [DONE].
the tail of the stream
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":null}
 
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":21,"total_tokens":35}}
 
data: [DONE]

Without the option, the wire has no usage key at all, byte-identical to a stream that predates the option. And stream_options without stream: true is a 400: it is the one honored parameter that exists only in the streaming wire.

The numbers are the ones you were billed

They are read back from the recorded usage event, the counts the charge was settled on, never a fresh estimate. That fixes the ordering: the settle happens first, the chunk second. A chunk written before the settle could report a number the books never agree with, and you'd have no way to tell.

Two consequences worth designing for:

  • No charge recorded means no usage chunk. If nothing was billable, the reservation was gone at settle, or the settle failed, the stream ends with the finish chunk and [DONE] and nothing else. Reporting the provider's raw figures there would tell you that you consumed tokens nobody billed you for.
  • A cut stream reports what it was charged for. If the stream is stopped early, because the reserved ceiling or the wall-clock ceiling was reached, finish_reason is length and the counts are the ones written on the event, not provider figures that never arrived.

The chunk is best-effort, as it is upstream: OpenAI documents the same "if the stream is interrupted or cancelled, you may not receive the final usage chunk". So treat it as the convenient answer, not the authority.

There is no money on the wire

usage has no cost field, and won't get one. It's the object every SDK deserializes into a generated typed model, and a strict decoder fails on an unknown property, which is exactly the compatibility guarantee this feature has to keep. The stream is also structurally the wrong place for an amount: this chunk may legitimately never arrive, whereas a charge is authoritative.

The tokens reported here are the billed ones. The amount lives where it's auditable: getBalance() and getUsageSummary().