Streaming
Set stream and the response arrives as a text/event-stream instead of one JSON body. Each API streams in its own native format, so the streaming code you already have keeps working: Chat Completions sends chat.completion.chunk frames, Responses sends typed response.* events, Messages sends Anthropic's message_start … message_stop sequence.
Works with
Every generation model, on all three APIs. Per-model differences in tool-call streaming and usage frames are listed below and in the capability matrix.
Request
- Chat Completions
- Responses
- Messages
- Chat Completions
- Responses
- Messages
stream = client.chat.completions.create(
model="mindshub_air",
messages=[{"role": "user", "content": "Count to five."}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
stream = client.responses.create(
model="mindshub_air",
input="Count to five.",
stream=True,
)
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)
with client.messages.stream(
model="mindshub_air",
max_tokens=1024,
messages=[{"role": "user", "content": "Count to five."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
const stream = await client.chat.completions.create({
model: "mindshub_air",
messages: [{ role: "user", content: "Count to five." }],
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);
}
const stream = await client.responses.create({
model: "mindshub_air",
input: "Count to five.",
stream: true,
});
for await (const event of stream) {
if (event.type === "response.output_text.delta") process.stdout.write(event.delta);
}
const stream = client.messages.stream({
model: "mindshub_air",
max_tokens: 1024,
messages: [{ role: "user", content: "Count to five." }],
});
for await (const text of stream.textStream) process.stdout.write(text);
The samples assume a client built as shown in Getting started. Guard on chunk.choices being non-empty in the Chat Completions loop: the stream ends with a usage-bearing chunk that carries no choices.
Response
Chat Completions
A series of data: <json> lines, each carrying a chat.completion.chunk, terminated by data: [DONE]:
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"","role":"assistant"},"index":0}],"created":1785401212,"model":"claude-haiku-4-5-20251001","object":"chat.completion.chunk"}
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"Hello"},"index":0}],"created":1785401212,"model":"claude-haiku-4-5-20251001","object":"chat.completion.chunk"}
data: {"id":"chatcmpl-...","choices":[{"delta":{},"finish_reason":"stop","index":0}],"created":1785401212,"model":"claude-haiku-4-5-20251001","object":"chat.completion.chunk"}
data: [DONE]
Differences from OpenAI's stream that your client has to handle:
- Keys with null values are omitted rather than sent as
null. Non-terminal chunks have nofinish_reasonkey at all. - Usage arrives on request. Send
stream_options: {"include_usage": true}and the stream ends with a usage-bearing chunk whosechoicesarray is empty.kimistreams end with that chunk even without asking, and may carry extra provider fields (system_fingerprint, usage nested in the finish chunk); don't use a strict parser that rejects unknown fields. - Errors before the first token are plain JSON. If the request fails before generation starts, you get an ordinary JSON error response with an error status, not an SSE stream, even though you asked for
stream: true. - A mid-stream error closes the stream without an SSE error event. A stream that ends without a
finish_reasonchunk was a failed generation, except on Gemini models, which always send one; see Errors → Streaming failures.
Responses
Typed events, with event: names matching the payload type:
event: response.created
event: response.in_progress
event: response.output_item.added
event: response.content_part.added
event: response.output_text.delta (repeated)
event: response.output_text.done
event: response.content_part.done
event: response.output_item.done
event: response.completed
Tool calls stream as response.function_call_arguments.delta events between an output_item.added and output_item.done pair for the function_call item. Every event carries a monotonic sequence_number, so a client can detect a gap, and output items are strictly sequential: item N is closed before item N+1 opens. Text that resumes after a tool call opens a second message item rather than reopening the first.
The final response.completed event carries the complete response object, including real token usage. Three behaviors to code around:
- A request that fails before generation starts returns a JSON error, not an SSE stream, even with
stream: true. Check the responseContent-Typebefore parsing. - A mid-stream failure is reported in-band as a
response.failedevent, because the HTTP status is already200by then. It carriesresponse.error.code(rate_limit_exceeded,context_length_exceeded,insufficient_quota,invalid_promptorserver_error) plus whatever output was produced before the failure, markedincomplete.response.failedandresponse.completedare mutually exclusive. - A truncated turn terminates with
response.incomplete, notresponse.completed, withincomplete_details.reasonset tomax_output_tokensorcontent_filter.
Two of those error.code values, context_length_exceeded and insufficient_quota, are ours rather than OpenAI's, which defines neither. They are there because they are actionable. Clients that switch on the OpenAI enum should treat an unrecognized code as a generic retryable error, which is what Codex does. Note that the SDK's stream.get_final_response() raises on anything but response.completed, so read the terminal event directly if you need to handle a truncated turn.
Events we don't emit
The Responses protocol defines 53 event types; this API emits 13: the ones listed above plus response.incomplete and response.failed. Nothing else is ever sent, so a client waiting on one of them waits forever. The absences, and why:
| Events | Why |
|---|---|
response.audio.*, response.image_gen_call.* | Audio and image output aren't supported yet. |
response.file_search_call.*, response.code_interpreter_call.*, response.mcp_*, response.custom_tool_call_input.* | Hosted tools this API doesn't run. Those tool entries are dropped from tools rather than executed. |
response.queued | Nothing is ever queued; background requests are served synchronously. |
response.web_search_call.* | A non-streamed turn reports its searches as web_search_call items in output. The streaming events are not emitted yet, so a streamed search is still silent. |
response.reasoning_summary_*, response.reasoning_text.* | Reasoning is delivered as a single reasoning item at the end of the stream, not as deltas. |
response.output_text.annotation.added | Citations arrive on the finished output_text part, not as incremental events. On a stream they land on the terminal response.completed event. |
response.refusal.* | A refusal is delivered as a refusal content part on the non-streaming response. On a stream it currently arrives as response.incomplete with incomplete_details.reason: "content_filter" on the Claude family, and is not distinguishable at all on the GPT family. |
Messages
The standard Anthropic event sequence:
event: message_start → event: ping → event: content_block_start
→ event: content_block_delta … → event: content_block_stop
→ event: message_delta → event: message_stop
Differences from Anthropic's native stream:
pingis sent once, right aftermessage_start, not periodically. Don't use pings as a liveness signal.- On non-Claude models,
message_startreportsinput_tokens: 0. The real counts arrive in the finalmessage_delta. Token-tracking UIs will undercount mid-stream on non-Claude models. - A failure after the body opens arrives as an
errorevent, typed exactly as the same failure would have been before the body opened, and the SDKs raise it. A mid-generation failure can still surface as a normal-looking completion with truncated or empty content; if your application must detect failed generations, sanity-check the output as well. - Thinking blocks stream too, in Anthropic's required order, with their
signature, so a streaming client can replay them on its next request. See Reasoning.
Per-model differences
- Tool-call streaming varies by model family. OpenAI- and Claude-family models stream a tool call as an opening delta (with
id,type, and the function name) followed by argument fragments. Gemini models deliver each tool call as a single chunk carrying the complete arguments. - Web search on Fireworks-hosted models and Kimi is not streamed live. Those models search through a server-side loop; the finished answer is replayed to you as a stream once the loop completes, so the first token arrives late.
- Gemini models always send a terminal frame, even on failure, so the "no
finish_reasonmeans failure" rule above does not apply to them.
Errors you can hit
A request rejected before generation (400, 401, 402, 404, 429) is a plain JSON error in the lane's envelope, never an SSE stream. Mid-stream failures are lane-specific, as described above. The full catalog is in Errors.