Skip to content

Streaming

Beta. Updated 2026-10-29.

"stream": true returns the answer as server-sent events in the OpenAI chat.completion.chunk format. Stream any request that can take longer than 100 s.

Example

curl

curl -N https://citadel.albastov.com/v1/chat/completions \
  -H "Authorization: Bearer $CITADEL_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://citadel.albastov.com/v1", api_key=os.environ["CITADEL_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://citadel.albastov.com/v1/chat/completions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CITADEL_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 and blank lines
      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);
    }
  }
}

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

Events

200, Content-Type: text/event-stream, Cache-Control: no-cache. Each event is one data: line followed by a blank line.

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]
EventField
First chunkdelta.role
Reasoningdelta.reasoning
Answerdelta.content
Tool callsdelta.tool_calls
Last choice chunkfinish_reason
Usage chunkusage, empty choices
Enddata: [DONE]
Commentline starting with :

The usage chunk requires "stream_options": {"include_usage": true}. Ignore comment lines.

Model loading

A streamed request without X-LLM-Wait waits up to 15 minutes for a loading model.

  1. After 10 s, 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].

Check every data: event for an error key. With X-LLM-Wait: 0, a loading model returns 503 model_loading before the stream starts.

Errors

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

In an engine_restarted event, code is the number 502. Read type. Errors before the first event are HTTP errors with a JSON body (Chat completions errors).