Skip to content
kenari.

Streaming

Streaming returns an answer piece by piece while the model writes it, so your app can show text right away instead of waiting for the whole reply. Use it for chat interfaces, for long answers, and for anything that reasons for a while. All three chat endpoints stream: Chat completions, Messages and Responses.

Set "stream": true in the request body. The response has the content type text/event-stream, and each event follows the format of the endpoint you called.

EndpointFrameThe stream ends with
POST /v1/chat/completionsdata: line onlydata: [DONE]
POST /v1/messagesevent: line and data: linethe message_stop event
POST /v1/responsesevent: line and data: linethe response.completed or response.failed event

Every stream follows the same rules:

  • A frame is one event followed by a blank line. The data: line holds a JSON object, except the final data: [DONE] line of Chat completions, which is not JSON. Check for [DONE] before you decode.
  • A line that starts with : is a comment. kenari sends : keepalive when the model has been silent for a while, for example during a long prompt or while it reasons. Ignore these lines. The official SDKs do.
  • Errors that happen before the stream starts, such as a bad key, an invalid request, low balance or a rate limit, come back as a normal HTTP error with a JSON body. See Errors.
  • Once the first event is sent, the HTTP status is already 200. A failure after that arrives as an event in the stream, described for each format below.
Chat completionsMessagesResponses
Answer textdelta.contenttext_delta in content_block_deltaresponse.output_text.delta
Reasoningdelta.reasoning and delta.reasoning_content, before the texta thinking block with thinking_delta eventsa reasoning item with response.reasoning_summary_text.delta events
Function calldelta.tool_calls, arguments in piecesa tool_use block with input_json_delta eventsa function_call item with response.function_call_arguments.delta events
Token usageusage on the last chunk, when token counts are non-zeromessage_start and message_deltaresponse.completed
Finish reasonfinish_reasonstop_reason in message_deltastatus in response.completed
Failure after the starta chunk with an error object, then [DONE]an error eventa response.failed event

Reasoning is streamed in all three formats, ahead of the answer text. Only reasoning models produce it, and on some requests a model reasons very little or not at all. On Chat completions you can leave it out of the stream with "reasoning": {"exclude": true}. The tokens are still generated and billed. To control how much a model reasons, see Reasoning.

Each chunk is a chat.completion.chunk with the same id and a model that echoes the one you requested.

Terminal window
curl -N https://kenari.id/v1/chat/completions \
-H "Authorization: Bearer $KENARI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "step-3-7-flash:free",
"stream": true,
"messages": [{ "role": "user", "content": "Count from 1 to 3." }]
}'

The -N flag turns off curl’s buffering, so each event prints as it arrives. The stream looks like this, shortened:

data: {"id":"4f1c9a2e7b3d5a60","object":"chat.completion.chunk","created":1790000000,"model":"step-3-7-flash:free","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"4f1c9a2e7b3d5a60","object":"chat.completion.chunk","created":1790000000,"model":"step-3-7-flash:free","choices":[{"index":0,"delta":{"content":"1, 2"},"finish_reason":null}]}
data: {"id":"4f1c9a2e7b3d5a60","object":"chat.completion.chunk","created":1790000000,"model":"step-3-7-flash:free","choices":[{"index":0,"delta":{"content":", 3"},"finish_reason":null}]}
data: {"id":"4f1c9a2e7b3d5a60","object":"chat.completion.chunk","created":1790000000,"model":"step-3-7-flash:free","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":14,"completion_tokens":6,"total_tokens":20}}
data: [DONE]
  • The first chunk carries "role": "assistant". Append every delta.content after it to build the answer.
  • Reasoning arrives in delta.reasoning and delta.reasoning_content, with the same text under both names.
  • A function call opens with one chunk that has the call’s index, id, type and function.name, and an empty arguments. Later chunks with the same index carry pieces of function.arguments. Join the pieces, then parse the JSON. The call ends with finish_reason set to tool_calls. See Function calling.
  • The last chunk has an empty delta, the finish_reason, and usage only when the token counts are non-zero. usage.prompt_tokens_details is added only when cached tokens are above 0. kenari’s own usage chunk does not depend on stream_options, so you never need include_usage. There is no separate usage chunk with an empty choices array.
  • When a kenari server tool searched the web, one more chunk with delta.sources arrives just before [DONE].
  • A failure after the start arrives as a chunk with an error object and no choices, followed by [DONE]:
data: {"error":{"message":"the model's provider interrupted the response before it finished. send the request again as a new request","type":"upstream_error","param":null,"code":"stream_interrupted"}}
data: [DONE]

With the OpenAI Python SDK:

import os
from openai import OpenAI
client = OpenAI(base_url="https://kenari.id/v1", api_key=os.environ["KENARI_API_KEY"])
stream = client.chat.completions.create(
model="step-3-7-flash:free",
messages=[{"role": "user", "content": "Count from 1 to 3."}],
stream=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("\ntokens:", chunk.usage.total_tokens)

With the OpenAI JavaScript SDK:

import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://kenari.id/v1",
apiKey: process.env.KENARI_API_KEY,
});
const stream = await client.chat.completions.create({
model: "step-3-7-flash:free",
messages: [{ role: "user", content: "Count from 1 to 3." }],
stream: true,
});
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content;
if (text) process.stdout.write(text);
}

Each frame has an event: line that names the event and a data: line with the JSON. The base URL for the Anthropic SDKs is https://kenari.id, as described on Messages.

EventMeaning
message_startOpens the message with its id, model and a first usage.
content_block_startOpens the block at index. The type is text, tool_use (with id and name) or thinking.
content_block_deltaA piece of the block: text_delta, input_json_delta, thinking_delta or signature_delta.
content_block_stopCloses the block.
message_deltadelta.stop_reason and the final usage, with output_tokens and the cache fields when present.
message_stopThe end of the stream.
pingA keep-alive from the provider. Ignore it.
errorThe message failed after it started. No message_stop follows.

A short stream:

event: message_start
data: {"type":"message_start","message":{"id":"4f1c9a2e7b3d5a60","type":"message","role":"assistant","model":"step-3-7-flash:free","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":14,"output_tokens":0}}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"1, 2, 3"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":14,"output_tokens":6}}
event: message_stop
data: {"type":"message_stop"}
  • When the model reasons, a thinking block comes first, with its text in thinking_delta events followed by a signature_delta. The signature can be empty. Send the block back unchanged in the next turn.
  • A function call is a tool_use block whose input starts as {}. The arguments arrive as partial_json strings in input_json_delta events. Join them, then parse the JSON when the block closes. The message ends with stop_reason set to tool_use.
  • A failure after the start is an error event. Its error.type is api_error or invalid_request_error. Read error.message and treat the stream as over.
event: error
data: {"type":"error","error":{"type":"api_error","message":"the model's provider interrupted the response before it finished. send the request again as a new request"}}

With the Anthropic Python SDK:

import os
import anthropic
client = anthropic.Anthropic(base_url="https://kenari.id", api_key=os.environ["KENARI_API_KEY"])
with client.messages.stream(
model="step-3-7-flash:free",
max_tokens=512,
messages=[{"role": "user", "content": "Count from 1 to 3."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
message = stream.get_final_message()
print("\ntokens:", message.usage.output_tokens)

With the Anthropic TypeScript SDK:

import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
baseURL: "https://kenari.id",
apiKey: process.env.KENARI_API_KEY,
});
const stream = client.messages.stream({
model: "step-3-7-flash:free",
max_tokens: 512,
messages: [{ role: "user", content: "Count from 1 to 3." }],
});
stream.on("text", (text) => process.stdout.write(text));
const message = await stream.finalMessage();
console.log("\ntokens:", message.usage.output_tokens);

Each frame has an event: line and a data: line. Every event has a type and a sequence_number that counts up from 0.

EventMeaning
response.createdThe first event. Carries the response id, model and status of in_progress.
response.output_item.addedAn item starts at output_index: reasoning, message or function_call.
response.reasoning_summary_part.addedThe reasoning item opens its one summary part.
response.reasoning_summary_text.deltaA piece of the reasoning text.
response.reasoning_summary_text.doneThe full reasoning text.
response.reasoning_summary_part.doneThe summary part is complete.
response.output_text.deltaA piece of the answer text.
response.function_call_arguments.deltaA piece of the function arguments.
response.function_call_arguments.doneThe full arguments string.
response.output_item.doneThe item is complete and carries its full content.
response.completedThe last event. Carries the full output and usage.
response.failedThe response failed after it started.

kenari does not send response.in_progress, response.content_part.added, response.content_part.done, response.output_text.done or response.incomplete. A response cut short by the token limit ends with response.completed whose status is incomplete.

  • Reasoning comes as one reasoning item before any message or function call. Its text streams through the response.reasoning_summary_text.delta events.
  • A function call is a function_call item. Its arguments stream through response.function_call_arguments.delta, and response.function_call_arguments.done carries the whole string.
  • response.completed holds the final usage, including input_tokens_details.cached_tokens.
  • A failure after the start is a response.failed event, with response.error.code and response.error.message. Nothing follows it.
event: response.failed
data: {"type":"response.failed","response":{"id":"resp_4f1c9a2e7b3d5a60","status":"failed","model":"step-3-7-flash:free","error":{"code":"stream_interrupted","message":"the model's provider interrupted the response before it finished. send the request again as a new request"}},"sequence_number":4}

With the OpenAI Python SDK:

import os
from openai import OpenAI
client = OpenAI(base_url="https://kenari.id/v1", api_key=os.environ["KENARI_API_KEY"])
stream = client.responses.create(
model="step-3-7-flash:free",
input="Count from 1 to 3.",
stream=True,
)
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)

With the OpenAI JavaScript SDK:

import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://kenari.id/v1",
apiKey: process.env.KENARI_API_KEY,
});
const stream = await client.responses.create({
model: "step-3-7-flash:free",
input: "Count from 1 to 3.",
stream: true,
});
for await (const event of stream) {
if (event.type === "response.output_text.delta") process.stdout.write(event.delta);
}
  • A stream cannot be resumed. After an error, send the request again as a new request. The status was already 200 when the stream started, so retry logic that reacts to HTTP errors does not see the failure.
  • Do not split the stream on network chunks. One chunk can hold several events or half of one. Read it by frames, or let the SDK do it.
  • A function call is not valid JSON until its last piece arrives. Parse it after the block or item closes.
  • The browser EventSource only sends GET requests. Use fetch with a readable stream, or an SDK.
  • kenari sends Cache-Control: no-cache, no-transform and X-Accel-Buffering: no. If you put your own proxy in front, turn off response buffering there too, or events will arrive in bursts.
  • Use streaming for long answers. A request without it holds the connection open until the whole answer is ready, and a proxy or load balancer between you and kenari may close it first.