Skip to content

Streaming

Chat completions as server-sent events, token by token.

"stream": true returns the reply as server-sent events in the OpenAI chat.completion.chunk format.

Stream a reply

Stream every request that can take longer than 100 s: a non-streamed request without a response after about 100 s fails with 524.

curl

curl -N https://api.albastov.com/v1/chat/completions \
  -H "Authorization: Bearer $ALBASTOV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3.8-27b",
    "stream": true,
    "max_tokens": 80,
    "messages": [{"role": "user", "content": "Explain prefix caching in two sentences."}],
    "chat_template_kwargs": {"enable_thinking": false}
  }'

Python

import os
from openai import OpenAI

client = OpenAI(base_url="https://api.albastov.com/v1", api_key=os.environ["ALBASTOV_API_KEY"])
stream = client.chat.completions.create(
    model="qwen3.8-27b",
    messages=[{"role": "user", "content": "Explain prefix caching in two sentences."}],
    stream=True,
    stream_options={"include_usage": True},
    max_tokens=80,
    extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
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("\n", chunk.usage)

JavaScript

const res = await fetch("https://api.albastov.com/v1/chat/completions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ALBASTOV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "qwen3.8-27b",
    stream: true,
    stream_options: { include_usage: true },
    max_tokens: 80,
    messages: [{ role: "user", content: "Explain prefix caching in two sentences." }],
    chat_template_kwargs: { enable_thinking: false },
  }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);

const decoder = new TextDecoder();
let buffer = "";
for await (const bytes of res.body) {
  buffer += decoder.decode(bytes, { stream: true });
  let end;
  while ((end = buffer.indexOf("\n\n")) >= 0) {
    const event = buffer.slice(0, end);
    buffer = buffer.slice(end + 2);
    for (const line of event.split("\n")) {
      if (!line.startsWith("data:")) continue; // ": loading ..." comments
      const data = line.slice(5).trim();
      if (data === "[DONE]") continue;
      const chunk = JSON.parse(data);
      if (chunk.error) throw new Error(`${chunk.error.type}: ${chunk.error.message}`);
      const text = chunk.choices?.[0]?.delta?.content;
      if (text) process.stdout.write(text);
      if (chunk.usage) console.log("\n", chunk.usage);
    }
  }
}

Call the API from a server. A key in browser code is visible to every visitor.

Read the stream

Each event is one data: line and a blank line. Answer text arrives in choices[0].delta.content, reasoning in choices[0].delta.reasoning, tool calls in choices[0].delta.tool_calls. The last choice chunk carries finish_reason; data: [DONE] ends the stream. Lines that start with : are comments: skip them.

Stream (abridged)
data: {"id":"chatcmpl-a1fb7d45","object":"chat.completion.chunk","created":1793305229,"model":"qwen3.8-27b","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-a1fb7d45","object":"chat.completion.chunk","created":1793305229,"model":"qwen3.8-27b","choices":[{"index":0,"delta":{"content":"One"},"finish_reason":null}]}

data: {"id":"chatcmpl-a1fb7d45","object":"chat.completion.chunk","created":1793305229,"model":"qwen3.8-27b","choices":[{"index":0,"delta":{"content":""},"finish_reason":"stop"}]}

data: {"id":"chatcmpl-a1fb7d45","object":"chat.completion.chunk","created":1793305229,"model":"qwen3.8-27b","choices":[],"usage":{"prompt_tokens":22,"completion_tokens":11,"total_tokens":33}}

data: [DONE]

Token usage

"stream_options": {"include_usage": true} adds a final chunk with usage and an empty choices list, before data: [DONE]. Without include_usage, the stream has no usage chunk.

Model loading

A streamed request for a model that is not loaded waits for the load, up to 1800 s:

  1. After 10 s without a reply, the response starts: 200, text/event-stream, and the comment : loading <model>, repeated every 15 s.
  2. After the load, the model's events follow on the same connection.
  3. If the load fails, the stream sends one data: {"error": {...}} event, then data: [DONE].

With X-LLM-Wait: 0, a model that is not loaded returns 503 model_loading or 503 queued before the stream starts. Load times and the queue: Language models.

Stream failures

A failure before the first event is an HTTP error with a JSON body. A failure after the stream has started arrives as an event; check every data: event for an error key.

EventNext
data: {"error": {"code": "model_loading", ...}}data: [DONE]
data: {"error": {"code": "gpu-busy", ...}}data: [DONE]
data: {"error": {"type": "engine_restarted", "code": 502, ...}}The connection closes

In an engine_restarted event, code is the number 502: read type. A model restarted after a stalled reply answers the next request after a reload.

Reference