Core

Streaming

Server-sent events, token usage in the stream, tool calls while streaming, and exactly what you are billed when a client disconnects.

Pass stream: true. Frames are chat.completion.chunk, terminated by data: [DONE].

stream = client.chat.completions.create(
    model="llama-3.1-8b",
    messages=[{"role": "user", "content": "Count to five."}],
    stream=True,
)

for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)
const stream = await client.chat.completions.create({
  model: "llama-3.1-8b",
  messages: [{ role: "user", content: "Count to five." }],
  stream: true,
});

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

The stream opens with a role frame, then content frames, then a frame carrying finish_reason:

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"One"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]

Token counts in the stream

Ask for them with stream_options:

stream = client.chat.completions.create(
    model="llama-3.1-8b",
    messages=[{"role": "user", "content": "Count to five."}],
    stream=True,
    stream_options={"include_usage": True},
)

You then get one extra frame, after the final choice frame, with an empty choices array:

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":9,"total_tokens":23}}

If your loop reads chunk.choices[0] unconditionally it will find nothing on that frame. That is OpenAI’s shape, and the SDKs handle it; hand-rolled parsers usually need a guard.

A stream that failed part way sends no usage frame. A usage block on a broken stream would read as a completed request.

Tool calls

They stream. delta.tool_calls accumulates by index: the first frame carries the id, type and function name, and later frames append a few characters of function.arguments at a time. Reassembling them is the client’s job, and every SDK already does it. See Tool calling.

When a stream fails part way

Once the response headers are sent there is no status code left to fail with, so a mid-stream failure closes the stream with finish_reason: "error" rather than "stop".

Check it. A parser that reads only content cannot tell a truncated answer from a complete one, and that is the whole reason this exists.

Disconnecting

Billing is otherwise identical whether you stream or not. Same reservation, same settle, same ledger entry, same usage record. Send the same prompt at temperature: 0 with and without stream and the two cost the same.

Not available for every model

A small number of models are served through a path where streaming is not implemented. They are filtered out of anywhere that offers streaming, and asking for it explicitly is a clean error rather than a reservation followed by a failure.

Idempotency and streaming do not combine

Idempotency-Key with stream: true is a 400. Replaying a stream from cache would mean buffering every response on the chance of a retry. Refusing is honest; accepting the header and ignoring it would let you believe you were protected from a double charge when you were not. See Idempotency.