Streaming

Server-Sent Events protocol for chat completions, including chunk format, [DONE] handling, and stream_options.include_usage.

Overview

AllRoutes streams chat completions over Server-Sent Events (SSE). When you set stream: true on a chat completion request, the gateway flushes incremental tokens to your client as they arrive from the upstream provider, with a small (~5-15ms) gateway-side overhead for cache and guardrail checks.

The format is byte-compatible with OpenAI's streaming protocol, so any OpenAI-compatible client works out of the box.

Enabling Streaming

curl https://api.allroutes.ai/v1/chat/completions \
  -H "Authorization: Bearer allroutes_sk_..." \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{
    "model": "gpt-4o",
    "messages": [{"role": "user", "content": "Count to 5."}],
    "stream": true,
    "stream_options": {"include_usage": true}
  }'

The response uses Content-Type: text/event-stream, Cache-Control: no-cache, and Connection: keep-alive.

Chunk Format

Each chunk is a single SSE event prefixed with data: and terminated by a blank line. The payload is a JSON object with the same envelope as a non-streamed response, but with a delta instead of message:

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1714000000,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1714000000,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"One"},"finish_reason":null}]}

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1714000000,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":", two"},"finish_reason":null}]}

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1714000000,"model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1714000000,"model":"gpt-4o","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":4,"total_tokens":16}}

data: [DONE]

Chunk Fields

FieldTypeDescription
idstringStable across all chunks for one request
objectstringAlways chat.completion.chunk
modelstringResolved model identifier
choices[].delta.rolestringSent only on the first content chunk
choices[].delta.contentstringToken fragment to append to the assistant message
choices[].delta.tool_callsarrayIncremental tool-call construction
choices[].delta.reasoningstringReasoning/thinking fragment (Anthropic, DeepSeek)
choices[].finish_reasonstringnull until the final chunk; then stop, length, tool_calls, content_filter
usageobjectFinal token counts (only when stream_options.include_usage: true)

The [DONE] Sentinel

The literal string data: [DONE] is sent after the last JSON chunk. Treat it as the end-of-stream signal -- do not attempt to JSON-parse it. Most OpenAI-compatible parsers handle this automatically.

Stream Options

Pass stream_options to control extra metadata:

FieldTypeDefaultDescription
include_usagebooleanfalseEmit a final chunk with usage token counts before [DONE]

When include_usage: true, the final pre-[DONE] chunk has an empty choices: [] and a populated usage object. This is the only reliable way to get token counts from a streamed response.

Tool Call Streaming

Tool calls arrive incrementally. Each chunk's delta.tool_calls[] carries an index (the tool call number) and a partial function.arguments string. Concatenate by index:

delta.tool_calls = [{"index": 0, "id": "call_abc", "function": {"name": "get_weather", "arguments": ""}}]
delta.tool_calls = [{"index": 0, "function": {"arguments": "{\"loc"}}]
delta.tool_calls = [{"index": 0, "function": {"arguments": "ation\":"}}]
delta.tool_calls = [{"index": 0, "function": {"arguments": " \"NYC\"}"}}]

The full arguments string is {"location": "NYC"}. Only parse JSON once finish_reason: "tool_calls" is observed.

SDK Examples

Python

stream = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Tell a story."}],
    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)
    if chunk.usage:
        print(f"\n\nTotal tokens: {chunk.usage.total_tokens}")

Node.js

const stream = await client.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "Tell a story." }],
  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);
  if (chunk.usage) console.log(`\nTokens: ${chunk.usage.total_tokens}`);
}

Raw fetch (browser/Edge)

const res = await fetch("https://api.allroutes.ai/v1/chat/completions", {
  method: "POST",
  headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
  body: JSON.stringify({ model: "gpt-4o", messages, stream: true }),
});

const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });

  const lines = buffer.split("\n");
  buffer = lines.pop() ?? "";

  for (const line of lines) {
    if (!line.startsWith("data: ")) continue;
    const data = line.slice(6).trim();
    if (data === "[DONE]") return;
    const chunk = JSON.parse(data);
    const delta = chunk.choices?.[0]?.delta?.content ?? "";
    if (delta) console.log(delta);
  }
}

Heartbeats and Reconnection

For long-running streams, AllRoutes emits SSE comment frames (: keepalive) every 15 seconds to prevent intermediate proxies (Cloudflare, AWS ALB) from closing the connection. Comment frames are silently ignored by SSE parsers.

If your client disconnects mid-stream, the upstream provider call is not automatically resumed -- you must issue a new request. Use idempotency keys via the metadata.idempotency_key field if duplicate-call risk matters.

Troubleshooting

SymptomLikely CauseFix
Stream returns one big chunkBuffering proxy in front of your clientSet Accept: text/event-stream and disable proxy buffering (e.g., nginx proxy_buffering off)
usage always missingstream_options.include_usage not setPass {"include_usage": true}
Random 499 mid-streamClient disconnected (timeout, abort)Increase client timeout or add heartbeats
Stream hangs after first chunkProvider rate-limitCheck the X-AllRoutes-Provider header and switch via provider.order

See Also