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:
- Every chunk that carries a choice also carries
usage: null. The field is always declared, sochunk.usagereads asnull, neverundefined. - After the finish-reason chunk, one extra chunk with an empty
choicesarray and a populatedusage(prompt_tokens,completion_tokens,total_tokens). - Then
data: [DONE].
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_reasonislengthand 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().