Skip to content
kenari.

Chat completions

Send a conversation and get the model’s next message back. The endpoint is compatible with the OpenAI Chat Completions API, so the official OpenAI SDKs work once you point the base URL at https://kenari.id/v1.

POST /v1/chat/completions

Authenticate with Authorization: Bearer kn-.... See Authentication and API keys.

FieldTypeRequiredDescription
modelstringYesWhat to run. See Choose a model.
messagesarrayYesThe conversation so far. See Messages.
streambooleanNoSet true to receive Server-Sent Events. Default false. See Streaming.
stream_optionsobjectNoPassed to providers served in the OpenAI format. It does not change kenari’s own usage chunk. See Streaming.
max_tokensintegerNoUpper limit on generated tokens, reasoning included. Default 65,536.
max_completion_tokensintegerNoAlias of max_tokens. max_tokens wins if you send both.
temperaturenumberNoSampling temperature.
top_pnumberNoNucleus sampling.
stopstring or arrayNoText that ends generation.
nintegerNoOnly 1 is supported. A larger value returns 400.
userstringNoAn id for your end user. It is passed on as request metadata where the provider accepts one.
toolsarrayNoFunction tools, and kenari server tools such as {"type": "kenari:web_search"}. See Function calling and Server tools.
tool_choicestring or objectNoauto, none, required, or {"type": "function", "function": {"name": "..."}}. Any other shape is treated as auto, except {"type": "function"} without function.name, which returns 400.
response_formatobjectNoAsk for JSON output. See Structured output.
reasoning_effortstringNonone, minimal, low, medium, high, xhigh or max. See Reasoning.
reasoningobjectNo{"effort", "enabled", "max_tokens", "exclude"}. Wins over reasoning_effort when both are set. {"enabled": true} with no effort or max_tokens means medium, and {"enabled": false} turns reasoning off. See Reasoning.
pluginsarrayNoOptional. file-parser picks how attached documents are read: auto, pdf-text, native or ocr. See Read documents.
prompt_cache_keystringNoA stable id for one conversation, so follow-up requests reuse its cached prefix. Up to 512 bytes. Longer or blank values are ignored. See Prompt caching.

kenari does not interpret other OpenAI fields such as frequency_penalty, presence_penalty or seed. It passes them to the provider unchanged when the model is served in the OpenAI format and drops them otherwise, so do not depend on them. The same goes for response_format when you also declare a kenari server tool.

model valueExampleWhat runs
Catalog idclaude-sonnet-5-5The model, billed per token. List ids with Models and pricing.
:free idstep-3-7-flash:freeThe free version of a model. See Free models.
Route namekenari-freeA route you created, or the built-in kenari-free. See Routing and routes.
BYOK idbyok/deepseek/deepseek-v4-flashThe model at one of your own providers, in the form byok/<provider>/<model>. kenari does not charge tokens for it. See BYOK.

The <model> part of a BYOK id is the provider’s own id, copied exactly. It is case sensitive and can contain slashes.

messages is an array of {"role", "content"} objects.

RoleNotes
system, developerInstructions. Leading ones are merged into one system prompt. One that appears after a user message stays where it is.
usercontent is a string or an array of parts.
assistantEarlier model replies. May carry tool_calls, and reasoning_content or reasoning to hand earlier reasoning back to the model.
toolA function result. Set tool_call_id to the id of the call it answers.

The legacy function role returns 400.

A user message can mix these content parts. Unknown part types are ignored.

PartShapeSee
text{"type": "text", "text": "..."}
image_url{"type": "image_url", "image_url": {"url": "https://example.com/a.png"}}Images and files as input
file{"type": "file", "file": {"filename": "a.pdf", "file_data": "data:application/pdf;base64,..."}}Images and files as input

url is an https URL or a data URL such as data:image/png;base64,....

A file part carries a document (PDF, text or image) and works on any model. kenari reads it before routing, and the model receives its text and page images, billed as normal input tokens, so a plan that covers the model covers the document. A request can carry several documents. The optional file-parser plugin changes how documents are read, for example "plugins": [{"id": "file-parser", "pdf": {"engine": "ocr"}}] for paid OCR, which takes one document per request and is billed per page. See Read documents.

The response is an OpenAI chat.completion object with one choice.

{
"id": "4f1c9a2e7b3d5a60",
"object": "chat.completion",
"created": 1790000000,
"model": "step-3-7-flash:free",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Hello! How can I help?" },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 12,
"completion_tokens": 9,
"total_tokens": 21,
"prompt_tokens_details": { "cached_tokens": 0 }
}
}
FieldDescription
choices[0].finish_reasonstop, length (hit max_tokens), tool_calls, or content_filter.
choices[0].message.tool_callsPresent when the model calls functions. arguments is a JSON string.
choices[0].message.reasoning and reasoning_contentThe model’s reasoning text, under both names with the same content. Present only when the model reasoned and you did not set reasoning.exclude.
choices[0].message.sourcesPresent when a kenari server tool searched the web. Lists {url, title} for each source.
choices[0].message.annotationsurl_citation entries for those sources, and one file entry for a document read with the ocr engine. See Read documents.
usage.prompt_tokensInput tokens, cached ones included.
usage.completion_tokensOutput tokens. Reasoning tokens are billed as output.
usage.prompt_tokens_details.cached_tokensInput tokens served from cache. In a non-streaming response it is always present, 0 when none. In a stream, prompt_tokens_details appears on the final chunk only when cached tokens are above 0. See Prompt caching.

The id is the same value as the x-request-id response header. Quote it when you contact support.

Terminal window
curl https://kenari.id/v1/chat/completions \
-H "Authorization: Bearer $KENARI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "step-3-7-flash:free",
"messages": [
{ "role": "system", "content": "You answer briefly." },
{ "role": "user", "content": "Hello!" }
]
}'
import os
from openai import OpenAI
client = OpenAI(base_url="https://kenari.id/v1", api_key=os.environ["KENARI_API_KEY"])
response = client.chat.completions.create(
model="step-3-7-flash:free",
messages=[
{"role": "system", "content": "You answer briefly."},
{"role": "user", "content": "Hello!"},
],
)
print(response.choices[0].message.content)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://kenari.id/v1",
apiKey: process.env.KENARI_API_KEY,
});
const response = await client.chat.completions.create({
model: "step-3-7-flash:free",
messages: [
{ role: "system", content: "You answer briefly." },
{ role: "user", content: "Hello!" },
],
});
console.log(response.choices[0].message.content);

Each request is billed per token at the model’s Input, Output, Cache read and Cache write prices. :free models cost nothing. Before a paid request runs, kenari checks your balance against the worst case for max_tokens, so set a smaller limit when you call an expensive model on a low balance. See How billing works.

Errors use the OpenAI envelope: {"error": {"message", "type", "param", "code"}}. Only the errors specific to this endpoint are listed here. The rest are in Errors.

StatusCodeWhen
400bad_requestThe body is not valid JSON or lacks model or messages. n is above 1. A role is unknown. A file part has no file object. A document has an unsupported type, is too large, is damaged or has too many pages. A PDF has more than 20 pages without a text layer. A document makes the request longer than the model’s context. The file-parser engine is not auto, pdf-text, native or ocr. With native, the model does not read that kind of file, or the part carries an image. With ocr, the request carries more than one file part.
400model_not_foundThe model is not in the catalog and is not one of your routes.
400bad_requestThe model is not a chat model, such as an image or speech model. The message names the endpoint to use instead.
400context_length_exceededThe request is longer than the model’s context window, shown as context_length in Models and pricing.
400byok_malformed_model_idA byok/ id is not in the form byok/<provider>/<model>.
400byok_provider_not_foundYou saved no key for the provider named in a byok/ id.
403subscribers_onlyThe model is only for accounts with an active subscription. See Subscriptions.
403byok_credential_unavailableYou have a key for that provider, but none can serve right now.
403byok_spend_capped_keyThe API key has a spend cap, which BYOK requests cannot use.