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.
Request
Section titled “Request”| Field | Type | Required | Description |
|---|---|---|---|
model | string | Yes | What to run. See Choose a model. |
messages | array | Yes | The conversation so far. See Messages. |
stream | boolean | No | Set true to receive Server-Sent Events. Default false. See Streaming. |
stream_options | object | No | Passed to providers served in the OpenAI format. It does not change kenari’s own usage chunk. See Streaming. |
max_tokens | integer | No | Upper limit on generated tokens, reasoning included. Default 65,536. |
max_completion_tokens | integer | No | Alias of max_tokens. max_tokens wins if you send both. |
temperature | number | No | Sampling temperature. |
top_p | number | No | Nucleus sampling. |
stop | string or array | No | Text that ends generation. |
n | integer | No | Only 1 is supported. A larger value returns 400. |
user | string | No | An id for your end user. It is passed on as request metadata where the provider accepts one. |
tools | array | No | Function tools, and kenari server tools such as {"type": "kenari:web_search"}. See Function calling and Server tools. |
tool_choice | string or object | No | auto, none, required, or {"type": "function", "function": {"name": "..."}}. Any other shape is treated as auto, except {"type": "function"} without function.name, which returns 400. |
response_format | object | No | Ask for JSON output. See Structured output. |
reasoning_effort | string | No | none, minimal, low, medium, high, xhigh or max. See Reasoning. |
reasoning | object | No | {"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. |
plugins | array | No | Optional. file-parser picks how attached documents are read: auto, pdf-text, native or ocr. See Read documents. |
prompt_cache_key | string | No | A 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.
Choose a model
Section titled “Choose a model”model value | Example | What runs |
|---|---|---|
| Catalog id | claude-sonnet-5-5 | The model, billed per token. List ids with Models and pricing. |
:free id | step-3-7-flash:free | The free version of a model. See Free models. |
| Route name | kenari-free | A route you created, or the built-in kenari-free. See Routing and routes. |
| BYOK id | byok/deepseek/deepseek-v4-flash | The 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
Section titled “Messages”messages is an array of {"role", "content"} objects.
| Role | Notes |
|---|---|
system, developer | Instructions. Leading ones are merged into one system prompt. One that appears after a user message stays where it is. |
user | content is a string or an array of parts. |
assistant | Earlier model replies. May carry tool_calls, and reasoning_content or reasoning to hand earlier reasoning back to the model. |
tool | A 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.
| Part | Shape | See |
|---|---|---|
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.
Response
Section titled “Response”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 } }}| Field | Description |
|---|---|
choices[0].finish_reason | stop, length (hit max_tokens), tool_calls, or content_filter. |
choices[0].message.tool_calls | Present when the model calls functions. arguments is a JSON string. |
choices[0].message.reasoning and reasoning_content | The 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.sources | Present when a kenari server tool searched the web. Lists {url, title} for each source. |
choices[0].message.annotations | url_citation entries for those sources, and one file entry for a document read with the ocr engine. See Read documents. |
usage.prompt_tokens | Input tokens, cached ones included. |
usage.completion_tokens | Output tokens. Reasoning tokens are billed as output. |
usage.prompt_tokens_details.cached_tokens | Input 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.
Examples
Section titled “Examples”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!" } ] }'Python
Section titled “Python”import osfrom 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)JavaScript
Section titled “JavaScript”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);Billing
Section titled “Billing”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
Section titled “Errors”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.
| Status | Code | When |
|---|---|---|
| 400 | bad_request | The 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. |
| 400 | model_not_found | The model is not in the catalog and is not one of your routes. |
| 400 | bad_request | The model is not a chat model, such as an image or speech model. The message names the endpoint to use instead. |
| 400 | context_length_exceeded | The request is longer than the model’s context window, shown as context_length in Models and pricing. |
| 400 | byok_malformed_model_id | A byok/ id is not in the form byok/<provider>/<model>. |
| 400 | byok_provider_not_found | You saved no key for the provider named in a byok/ id. |
| 403 | subscribers_only | The model is only for accounts with an active subscription. See Subscriptions. |
| 403 | byok_credential_unavailable | You have a key for that provider, but none can serve right now. |
| 403 | byok_spend_capped_key | The API key has a spend cap, which BYOK requests cannot use. |