# Streaming
> Server-sent events, the final usage chunk, keep-alives and mid-stream errors.
Set `"stream": true` to receive the response as [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) while the model generates it. The response has `Content-Type: text/event-stream`.
## With the OpenAI SDKs
```python
stream = client.chat.completions.create(
model="google/gemini-3.8-flash",
messages=[{"role": "user", "content": "Explain TCP slow start."}],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
if chunk.usage:
print("\ncost:", chunk.usage.cost)
```
```js
const stream = await client.chat.completions.create({
model: "google/gemini-3.8-flash",
messages: [{ role: "user", content: "Explain TCP slow start." }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
if (chunk.usage) console.log("\ncost:", chunk.usage.cost);
}
```
Check that `choices` isn’t empty before reading it: the final usage chunk has no choices.
## Event format
Each event is a line starting with `data:` followed by a JSON chat completion chunk, then a blank line. The stream ends with `data: [DONE]`.
```text
data: {"id":"gen-01M67FGKKVQ8ZK2M3N5P7R9S1V","object":"chat.completion.chunk","created":1793400000,"model":"google/gemini-3.8-flash","provider":"Google AI Studio","choices":[{"index":0,"delta":{"role":"assistant","content":"TCP"},"finish_reason":null}]}
data: {"id":"gen-01M67FGKKVQ8ZK2M3N5P7R9S1V","object":"chat.completion.chunk","created":1793400000,"model":"google/gemini-3.8-flash","provider":"Google AI Studio","choices":[{"index":0,"delta":{"content":" slow start"},"finish_reason":null}]}
data: {"id":"gen-01M67FGKKVQ8ZK2M3N5P7R9S1V","object":"chat.completion.chunk","created":1793400000,"model":"google/gemini-3.8-flash","provider":"Google AI Studio","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"gen-01M67FGKKVQ8ZK2M3N5P7R9S1V","object":"chat.completion.chunk","created":1793400000,"model":"google/gemini-3.8-flash","provider":"Google AI Studio","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":240,"total_tokens":252,"prompt_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"completion_tokens_details":{"reasoning_tokens":0},"cost":0.000909,"is_byok":false}}
data: [DONE]
```
Every chunk carries the same `id` (the generation id, also sent in the `x-clonvo-generation-id` response header), the canonical `model` id that is answering, and **`provider`**, the display name of the provider serving the request.
### Deltas
| Field | Description |
| --- | --- |
| `delta.role` | `assistant`, in the first chunk. |
| `delta.content` | The next piece of text. |
| `delta.reasoning` | **Extension.** The next piece of reasoning text, for models that stream it. |
| `delta.tool_calls` | Tool call fragments. Concatenate `function.arguments` by `index`. See [Tool calling](https://clonvo.com/docs/tool-calling.md#streaming-tool-calls). |
| `finish_reason` | `null` until the last content chunk. |
### The usage chunk
Every stream ends with one chunk that has an empty `choices` array and a `usage` object, sent just before `data: [DONE]`. It has the same fields as `usage` in a [non-streaming response](https://clonvo.com/docs/chat-completions.md#response), including `cost` (US dollars at list price) and `is_byok`. It’s always sent, whether or not you set `stream_options.include_usage`, and also after an error.
## Keep-alive comments
While a model is thinking or a provider is slow, Clonvo sends an SSE comment whenever the stream has been quiet for about 15 seconds:
```text
: keep-alive
```
Lines starting with `:` are comments in the SSE format. The OpenAI SDKs and standard SSE parsers ignore them; if you parse the stream yourself, skip them.
Clonvo holds the response for up to 3 seconds while it waits for the provider’s first content, so a failing provider can still be replaced by a [fallback](https://clonvo.com/docs/fallbacks.md) before anything is sent. The response headers can arrive up to 3 seconds later than they would without this check.
## Errors
If something goes wrong **before** the stream starts (invalid request, no credits, unknown model, the provider rejects the request), you get a normal JSON error response with an HTTP error status. See [Errors](https://clonvo.com/docs/errors.md).
If the provider fails **after** the stream has started, the HTTP status is already `200`. Clonvo then sends a chunk with an `error` object and `finish_reason: "error"`, then the usage chunk, then `data: [DONE]`:
```text
data: {"id":"gen-01M67FGKKVQ8ZK2M3N5P7R9S1V","object":"chat.completion.chunk","created":1793400000,"model":"google/gemini-3.8-flash","provider":"Google AI Studio","error":{"message":"The provider’s stream ended unexpectedly. Try again.","type":"server_error","code":"upstream_stream_error","status":502},"choices":[{"index":0,"delta":{"content":""},"finish_reason":"error"}]}
data: {"id":"gen-01M67FGKKVQ8ZK2M3N5P7R9S1V","object":"chat.completion.chunk","created":1793400000,"model":"google/gemini-3.8-flash","provider":"Google AI Studio","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":96,"total_tokens":108,"prompt_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"completion_tokens_details":{"reasoning_tokens":0},"cost":0.000369,"is_byok":false}}
data: [DONE]
```
| `error` field | Description |
| --- | --- |
| `message` | What went wrong. Provider errors start with the provider’s name. |
| `type` | Error category, such as `server_error`, `rate_limit_error` or `timeout_error`. |
| `code` | A specific code, such as `upstream_stream_error`, `upstream_error`, `upstream_overloaded` or `upstream_timeout`. |
| `status` | The HTTP status the error would have had before streaming started. |
Treat `finish_reason: "error"` as a failed generation. You’re charged for the tokens produced before the failure, as reported in the usage chunk. If the provider reports no output tokens for a failed stream, or fewer than were streamed, Clonvo estimates them from the streamed text (about four characters per token).
## Fallbacks and streaming
[Fallback models](https://clonvo.com/docs/fallbacks.md) are only tried before the first chunk is sent. Once streaming has started, Clonvo can’t switch models without sending you a second response.
## Cancelling
Close the connection to stop generation (for example `stream.close()` in Python or `controller.abort()` in Node.js). Clonvo cancels the request to the provider and records the generation as cancelled, with the tokens counted up to that point, estimated the same way as for a failed stream. You’re charged for those tokens.