Build with the AI
Streaming
Stream tokens as they are generated with Server-Sent Events.
By default chat completions return one JSON object once the whole
answer is ready. Set "stream": true and Krallo instead streams the response back over
Server-Sent Events (SSE) as a sequence of OpenAI chat.completion.chunk frames,
so you can render tokens the moment they arrive. The wire format is byte-for-byte compatible with the OpenAI
Chat Completions streaming API, so the official OpenAI SDKs and editor integrations work unchanged.
/api/v1/chat/completions
Streaming uses the same endpoint, the same bearer API key, and the
same ai:invoke scope as a buffered completion — it is one request
flag, not a separate route.
Enable streaming
Add "stream": true to an otherwise ordinary chat request. Omitting it or sending
false falls back to a single buffered JSON response (the string "true" is also
accepted, but send a real boolean).
curl -N https://exe.krallo.com/api/v1/chat/completions \
-H "Authorization: Bearer krallo_live_..." \
-H "Content-Type: application/json" \
-d '{
"model": "krallo-code",
"messages": [{"role": "user", "content": "Count to three."}],
"stream": true
}'
The -N (--no-buffer) flag tells curl to print bytes as they arrive instead of waiting for the connection to close — without it you will not see the incremental effect on the command line.
The event stream
A streamed response is sent with HTTP 200 and the headers below. The body is chunk-transferred:
each event is a line beginning with data:, followed by a blank line.
| Header | Value |
|---|---|
Content-Type | text/event-stream; charset=utf-8 |
Cache-Control | no-cache |
Connection | keep-alive |
X-Accel-Buffering | no — disables proxy buffering so frames flush immediately |
Frame sequence
For a text-only turn the frames always arrive in this order:
- One role frame —
deltais{"role":"assistant"}. - Zero or more content frames — each
deltais{"content":"…"}carrying the next piece of text. - One terminal frame — an empty
delta({}) withfinish_reasonset to"stop". - The literal terminator line
data: [DONE], then the connection closes.
Content is emitted in whatever granularity the upstream model produces — usually a few characters to a word
per frame, not one frame per token. Concatenate every delta.content to reconstruct the full
message text.
Chunk object
Every non-terminator frame carries one chat.completion.chunk object with this shape:
| Field | Type | Description |
|---|---|---|
id | string | A chatcmpl-<hex> id. Identical across every frame of a single stream. |
object | string | Always chat.completion.chunk. |
created | integer | Unix timestamp in seconds. Identical across every frame of a single stream. |
model | string | The public model id you requested, e.g. krallo-code (defaults to krallo-code if you omit model). |
choices | array | Always exactly one element. |
choices[].index | integer | Always 0. |
choices[].delta | object | The incremental payload: {"role":"assistant"}, {"content":"…"}, {"tool_calls":[…]}, or {}. |
choices[].finish_reason | string · null | Present on every frame. It is null until the terminal frame, which sets it to "stop" or "tool_calls". |
Streamed frames never include a usage object. Krallo does not emit a trailing usage chunk and does not honor stream_options. To read token counts, send the request without stream (the buffered response includes usage), or query the Usage endpoint. Streaming is still fully metered — see Token usage and billing.
Example stream
The raw bytes for "Count to three." look like this (blank lines between frames are significant):
data: {"id":"chatcmpl-9f2c1a...","object":"chat.completion.chunk","created":1752566400,"model":"krallo-code","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-9f2c1a...","object":"chat.completion.chunk","created":1752566400,"model":"krallo-code","choices":[{"index":0,"delta":{"content":"One"},"finish_reason":null}]}
data: {"id":"chatcmpl-9f2c1a...","object":"chat.completion.chunk","created":1752566400,"model":"krallo-code","choices":[{"index":0,"delta":{"content":", two"},"finish_reason":null}]}
data: {"id":"chatcmpl-9f2c1a...","object":"chat.completion.chunk","created":1752566400,"model":"krallo-code","choices":[{"index":0,"delta":{"content":", three."},"finish_reason":null}]}
data: {"id":"chatcmpl-9f2c1a...","object":"chat.completion.chunk","created":1752566400,"model":"krallo-code","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
Consuming the stream
With an OpenAI SDK
Because the wire format is identical, the official OpenAI SDKs stream from Krallo with no changes beyond the base URL and key. This is the recommended path.
Node.jsimport OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.KRALLO_API_KEY,
baseURL: "https://exe.krallo.com/api/v1",
});
const stream = await client.chat.completions.create({
model: "krallo-code",
messages: [{ role: "user", content: "Write a haiku about streams." }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
Parsing SSE by hand
If you are not using an SDK, split the body on blank lines, strip the data: prefix, stop on the
literal [DONE] sentinel, and JSON.parse everything else. Buffer partial frames —
a single read() may split a frame across chunk boundaries.
const res = await fetch("https://exe.krallo.com/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KRALLO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "krallo-code",
messages: [{ role: "user", content: "Count to five." }],
stream: true,
}),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let nl;
while ((nl = buffer.indexOf("\n\n")) !== -1) {
const frame = buffer.slice(0, nl).trim();
buffer = buffer.slice(nl + 2);
if (!frame.startsWith("data:")) continue;
const payload = frame.slice(5).trim();
if (payload === "[DONE]") break;
const chunk = JSON.parse(payload);
const delta = chunk.choices?.[0]?.delta?.content;
if (delta) process.stdout.write(delta);
}
}
Python
import json, httpx
payload = {
"model": "krallo-code",
"messages": [{"role": "user", "content": "Count to five."}],
"stream": True,
}
headers = {"Authorization": "Bearer krallo_live_...", "Content-Type": "application/json"}
with httpx.stream("POST", "https://exe.krallo.com/api/v1/chat/completions",
json=payload, headers=headers, timeout=None) as r:
for line in r.iter_lines():
if not line.startswith("data:"):
continue
data = line[5:].strip()
if data == "[DONE]":
break
delta = json.loads(data)["choices"][0]["delta"]
print(delta.get("content", ""), end="", flush=True)
Streaming with tools
When your request includes a tools array, the turn cannot be streamed incrementally — the model
gateway has no combined streaming-plus-tools path. Krallo runs the completion to completion first,
then replays the finished result as a valid SSE stream. Your client still receives well-formed
chat.completion.chunk frames (including any tool_calls); they just arrive together at
the end rather than token-by-token.
The replayed sequence is:
- The role frame —
{"role":"assistant"}. - If the model produced text, one content frame carrying the entire content string at once (not split).
- If the model requested tools, one
tool_callsframe carrying every call — each with a completefunction.nameand fully-formedfunction.argumentsstring, plus anindex. - The terminal frame with an empty
delta. Itsfinish_reasonis"tool_calls"when tools were requested, otherwise"stop". data: [DONE].
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1752566400,"model":"krallo-code","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1752566400,"model":"krallo-code","choices":[{"index":0,"delta":{"tool_calls":[{"id":"call_abc123","type":"function","function":{"name":"get_weather","arguments":"{\"city\":\"Paris\"}"},"index":0}]},"finish_reason":null}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1752566400,"model":"krallo-code","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}
data: [DONE]
Unlike OpenAI, Krallo does not stream partial function.arguments deltas across frames — each tool call is delivered whole in a single frame. Do not accumulate arguments by index; parse them once. Krallo relays tool requests only and never executes them; your client runs the tool and continues the loop. See Tools & function calling.
Errors and interruptions
There are two distinct failure regimes, split by whether the 200 response headers have been sent.
Before the stream starts
Authentication, scope, rate-limit, request validation, and the pre-flight credit reservation all run before any bytes are written. A failure here is a normal HTTP error with the correct status code and a JSON error body — the response is not an event stream at all. These are the same errors as a buffered completion:
| Status | type | When |
|---|---|---|
| 400 | invalid_request_error | Body is not JSON, or messages is missing or empty, or a requested persona isn't one you can use (your own, or a shared system/global one). |
| 400 | model_not_found | Unknown model id. The message lists the valid ids. |
| 401 | — | Missing or invalid bearer key. See Authentication. |
| 403 | — | Key lacks the ai:invoke scope. |
| 402 | insufficient_credits | Not enough balance to reserve the call. The body includes required_units and balance. See Billing & quotas. |
| 429 | — | Per-key rate limit exceeded. |
| 405 | method_not_allowed | Any method other than POST (or the OPTIONS pre-flight). |
After the stream starts
Once headers are flushed the HTTP status is committed at 200 and cannot change. If the upstream
model stream then fails mid-flight, Krallo emits a single in-band error frame and closes the
connection without a [DONE] terminator:
data: {"error":{"message":"The model provider stream failed.","type":"upstream_error"}}
Your parser must treat a data: frame that contains an error object as a failure, and
must treat a stream that ends without [DONE] as incomplete. When this happens the credit
reservation is rolled back — you are not charged for the failed call.
Do not rely on the HTTP status to detect a mid-stream failure — it is always 200. Detect success by observing the terminal frame (finish_reason set) followed by [DONE]. If the client disconnects, that cancels the upstream generation and also rolls back the reservation, so an abandoned stream is not billed.
Token usage and billing
Streaming is billed exactly like a buffered completion, against your owner wallet. Credits are reserved
before inference and settled after the stream finishes. Because most providers omit a usage block
on streamed turns, Krallo settles from a character-based token estimate of the prompt and generated text when
no provider count is available — so the meter stays proportional to real token volume rather than collapsing to
a flat per-call charge. A failed or client-cancelled stream is rolled back and not charged.
Since no usage is delivered in the stream itself, read authoritative counts from the
Usage endpoint, or issue a non-streaming request when you need the
usage object inline. Full pricing and quota mechanics are in
Billing & quotas.
Next steps
- Chat completions — the full request and response reference.
- Tools & function calling — drive an agent loop from your client.
- Use with opencode — a streaming editor integration with zero config.
- Errors — the platform-wide error model.