Streaming
Receive responses incrementally with server-sent events, and read usage and errors from the stream.
This page explains how to stream responses from the Engy API, what the stream contains, and how errors and billing work for streamed requests. It is intended for developers who display output as the model generates it.
Enable streaming
Set stream to true in the request body. Engy returns the response as server-sent events (text/event-stream).
curl https://api.engy.ai/v1/chat/completions \
-H "Authorization: Bearer $ENGY_API_KEY" \
-H "Content-Type: application/json" \
-N \
-d '{
"model": "glm-5.2",
"stream": true,
"messages": [{"role": "user", "content": "Write a haiku about the sea."}]
}'Streaming is available on /v1/chat/completions, /v1/messages, and /v1/responses. The /v1/completions endpoint does not stream. A request to it with stream set to true fails with HTTP status 400.
Chat completions stream format
Each event is a data: line that contains a JSON chunk. The stream proceeds as follows:
- The first chunk contains
{"role": "assistant"}indelta. - Content chunks contain the generated text in
delta.content. For reasoning models, they also containdelta.reasoning_content. For details, see Reasoning. - The final chunk contains
finish_reason, theusageobject, and thex_engyobject. - The stream ends with
data: [DONE].
Engy can merge several tokens into one chunk. Do not assume that each chunk contains exactly one token.
Usage in the stream
The final chunk always contains usage information, even if you do not request it:
{
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
"usage": {
"prompt_tokens": 14,
"completion_tokens": 18,
"total_tokens": 32,
"prompt_tokens_details": {"cached_tokens": 0},
"completion_tokens_details": {"reasoning_tokens": 0},
"cost": 0.000037
},
"x_engy": {"request_id": "...", "charged_micro": 37}
}| Field | Description |
|---|---|
usage.cost | The amount charged, in USD. |
x_engy.request_id | The unique identifier of the request. |
x_engy.charged_micro | The amount charged, in micro-USD. |
If you set stream_options.include_usage to true, Engy also sends the standard OpenAI usage chunk, with an empty choices array, before [DONE].
Keepalive comments
If the model produces no output for 15 seconds, for example while it processes a long prompt, Engy sends an SSE comment line (: keepalive). Comment lines keep the connection open. Standard SSE clients ignore them.
Other stream formats
| Endpoint | Event sequence | Usage |
|---|---|---|
/v1/messages | message_start, content_block_start, content_block_delta, content_block_stop, message_delta, message_stop. Engy sends a ping event every 4 seconds. | Exact counts are in message_delta. The input count in message_start is an estimate. |
/v1/responses | response.created, output item and content part events, response.output_text.delta, response.function_call_arguments.delta, and response.completed | In response.completed |
On /v1/messages, reasoning is streamed as thinking content blocks unless thinking is disabled. On /v1/responses, reasoning is not returned.
Errors during a stream
If an error occurs after Engy has started the stream, the HTTP status is already 200. Engy reports the error inside the stream instead:
| Endpoint | Error event |
|---|---|
/v1/chat/completions | A data: line with an error object, followed by the end of the stream without [DONE] |
/v1/messages | An error event |
/v1/responses | A response.failed event |
Errors that occur before generation starts, such as an insufficient balance or no available capacity, can also arrive this way when stream is true.
To detect a failed stream on /v1/chat/completions, check each chunk for an error field, and treat a stream that ends without [DONE] as incomplete. The error object has the same format as other errors. For details, see Errors and limits.
import json
import os
import httpx
with httpx.stream(
"POST",
"https://api.engy.ai/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['ENGY_API_KEY']}"},
json={
"model": "glm-5.2",
"stream": True,
"messages": [{"role": "user", "content": "Hello"}],
},
timeout=600,
) as response:
completed = False
for line in response.iter_lines():
if not line.startswith("data: "):
continue
data = line.removeprefix("data: ")
if data == "[DONE]":
completed = True
break
chunk = json.loads(data)
if "error" in chunk:
raise RuntimeError(chunk["error"]["message"])
for choice in chunk.get("choices", []):
print(choice["delta"].get("content") or "", end="", flush=True)
if not completed:
raise RuntimeError("The stream ended before it completed.")Billing for streamed requests
- When the stream completes, Engy charges for the tokens that the model generated.
- If the request fails, Engy does not charge for it.
- If you close the connection before the stream completes, Engy stops generation and does not charge for the request.
Limitations
- Streamed chunks always use choice index
0. Do not usengreater than1with streaming.