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.
Turn on streaming
Section titled “Turn on streaming”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.
| Endpoint | Frame | The stream ends with |
|---|---|---|
POST /v1/chat/completions | data: line only | data: [DONE] |
POST /v1/messages | event: line and data: line | the message_stop event |
POST /v1/responses | event: line and data: line | the 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 finaldata: [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: keepalivewhen 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.
Fields and events by API format
Section titled “Fields and events by API format”| Chat completions | Messages | Responses | |
|---|---|---|---|
| Answer text | delta.content | text_delta in content_block_delta | response.output_text.delta |
| Reasoning | delta.reasoning and delta.reasoning_content, before the text | a thinking block with thinking_delta events | a reasoning item with response.reasoning_summary_text.delta events |
| Function call | delta.tool_calls, arguments in pieces | a tool_use block with input_json_delta events | a function_call item with response.function_call_arguments.delta events |
| Token usage | usage on the last chunk, when token counts are non-zero | message_start and message_delta | response.completed |
| Finish reason | finish_reason | stop_reason in message_delta | status in response.completed |
| Failure after the start | a chunk with an error object, then [DONE] | an error event | a 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.
Chat completions stream
Section titled “Chat completions stream”Each chunk is a chat.completion.chunk with the same id and a model that echoes the one you requested.
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 everydelta.contentafter it to build the answer. - Reasoning arrives in
delta.reasoninganddelta.reasoning_content, with the same text under both names. - A function call opens with one chunk that has the call’s
index,id,typeandfunction.name, and an emptyarguments. Later chunks with the sameindexcarry pieces offunction.arguments. Join the pieces, then parse the JSON. The call ends withfinish_reasonset totool_calls. See Function calling. - The last chunk has an empty
delta, thefinish_reason, andusageonly when the token counts are non-zero.usage.prompt_tokens_detailsis added only when cached tokens are above 0. kenari’s own usage chunk does not depend onstream_options, so you never needinclude_usage. There is no separate usage chunk with an emptychoicesarray. - When a kenari server tool searched the web, one more chunk with
delta.sourcesarrives just before[DONE]. - A failure after the start arrives as a chunk with an
errorobject and nochoices, 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 osfrom 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);}Messages stream
Section titled “Messages stream”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.
| Event | Meaning |
|---|---|
message_start | Opens the message with its id, model and a first usage. |
content_block_start | Opens the block at index. The type is text, tool_use (with id and name) or thinking. |
content_block_delta | A piece of the block: text_delta, input_json_delta, thinking_delta or signature_delta. |
content_block_stop | Closes the block. |
message_delta | delta.stop_reason and the final usage, with output_tokens and the cache fields when present. |
message_stop | The end of the stream. |
ping | A keep-alive from the provider. Ignore it. |
error | The message failed after it started. No message_stop follows. |
A short stream:
event: message_startdata: {"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_startdata: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_deltadata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"1, 2, 3"}}
event: content_block_stopdata: {"type":"content_block_stop","index":0}
event: message_deltadata: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":14,"output_tokens":6}}
event: message_stopdata: {"type":"message_stop"}- When the model reasons, a
thinkingblock comes first, with its text inthinking_deltaevents followed by asignature_delta. The signature can be empty. Send the block back unchanged in the next turn. - A function call is a
tool_useblock whoseinputstarts as{}. The arguments arrive aspartial_jsonstrings ininput_json_deltaevents. Join them, then parse the JSON when the block closes. The message ends withstop_reasonset totool_use. - A failure after the start is an
errorevent. Itserror.typeisapi_errororinvalid_request_error. Readerror.messageand treat the stream as over.
event: errordata: {"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 osimport 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);Responses stream
Section titled “Responses stream”Each frame has an event: line and a data: line. Every event has a type and a sequence_number that counts up from 0.
| Event | Meaning |
|---|---|
response.created | The first event. Carries the response id, model and status of in_progress. |
response.output_item.added | An item starts at output_index: reasoning, message or function_call. |
response.reasoning_summary_part.added | The reasoning item opens its one summary part. |
response.reasoning_summary_text.delta | A piece of the reasoning text. |
response.reasoning_summary_text.done | The full reasoning text. |
response.reasoning_summary_part.done | The summary part is complete. |
response.output_text.delta | A piece of the answer text. |
response.function_call_arguments.delta | A piece of the function arguments. |
response.function_call_arguments.done | The full arguments string. |
response.output_item.done | The item is complete and carries its full content. |
response.completed | The last event. Carries the full output and usage. |
response.failed | The 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
reasoningitem before any message or function call. Its text streams through theresponse.reasoning_summary_text.deltaevents. - A function call is a
function_callitem. Itsargumentsstream throughresponse.function_call_arguments.delta, andresponse.function_call_arguments.donecarries the whole string. response.completedholds the finalusage, includinginput_tokens_details.cached_tokens.- A failure after the start is a
response.failedevent, withresponse.error.codeandresponse.error.message. Nothing follows it.
event: response.faileddata: {"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 osfrom 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);}Limits and pitfalls
Section titled “Limits and pitfalls”- 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
EventSourceonly sendsGETrequests. Usefetchwith a readable stream, or an SDK. - kenari sends
Cache-Control: no-cache, no-transformandX-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.