Messages
Create a message with the Anthropic Messages format, and estimate how many input tokens a request will use. The endpoints are compatible with the Anthropic API, so the official Anthropic SDKs and Claude Code work once you change the base URL.
POST /v1/messages
POST /v1/messages/count_tokens
Use https://kenari.id as the base URL, without /v1. The Anthropic SDKs append /v1/messages themselves, and a base URL of https://kenari.id/v1 produces /v1/v1/messages, which returns 405. For Claude Code setup, see Claude Code.
Authenticate with x-api-key: kn-... or Authorization: Bearer kn-.... See Authentication and API keys.
Request
Section titled “Request”| Field | Type | Required | Description |
|---|---|---|---|
model | string | Yes | A catalog id, a :free id, a route name or a byok/ id. The same values as in Chat completions. |
messages | array | Yes | The conversation. See Messages array. |
max_tokens | integer | No | Upper limit on generated tokens. Optional here, unlike the Anthropic API. Default 65,536. |
system | string or array | No | System prompt, as a string or an array of {"type": "text", "text": "..."} blocks. Text blocks are joined with a blank line, and other block types are ignored. |
stream | boolean | No | Set true to receive Server-Sent Events. See Streaming. |
temperature | number | No | Sampling temperature. |
top_p | number | No | Nucleus sampling. |
top_k | integer | No | Sample from the top K tokens. |
stop_sequences | array | No | Strings that end generation. |
tools | array | No | Function tools with name, description and input_schema, and kenari server tools such as {"type": "kenari:web_search"}. See Function calling and Server tools. |
tool_choice | object | No | {"type": "auto"}, {"type": "any"}, {"type": "none"} or {"type": "tool", "name": "..."}. |
thinking | object | No | {"type": "enabled", "budget_tokens": 4096} or {"type": "disabled"}. See Reasoning. |
output_config | object | No | {"effort": "high"} sets a reasoning level without a token budget. See Reasoning. |
metadata | object | No | Accepted. |
kenari does not interpret other fields. It passes them to the provider unchanged when the model is served in the Anthropic format and drops them otherwise, so do not depend on them.
Messages array
Section titled “Messages array”Each message has a role and a content field. role is user or assistant. A system message is also accepted in the array, which newer Claude Code versions send. content is a string or an array of blocks.
| Block | Use |
|---|---|
text | Plain text. |
image | An image from a base64 or url source. See Images and files as input. |
document | A PDF from a base64 source is read on any model, the same way as a chat file part, and billed as input tokens. A url source reaches only models served through an Anthropic-style API and is dropped without an error for other models. A text source is plain text. See Read documents. |
tool_use | A function call the model made earlier. |
tool_result | The result of a call. content is a string or an array of text and image blocks. Set is_error to report a failure. |
thinking, redacted_thinking | Earlier reasoning, sent back unchanged. |
Unknown block types are ignored. A block of a known type that is malformed returns 400.
Prompt caching
Section titled “Prompt caching”Send cache_control markers as you would to Anthropic. kenari keeps them when the model is served in the Anthropic format. It also adds its own cache breakpoints when it rebuilds the request, so caching does not depend on your markers. See Prompt caching.
Response
Section titled “Response”The response is an Anthropic message object.
{ "id": "4f1c9a2e7b3d5a60", "type": "message", "role": "assistant", "model": "step-3-7-flash:free", "content": [ { "type": "text", "text": "Hello! How can I help?" } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 9, "output_tokens": 12 }}| Field | Description |
|---|---|
content | An array of blocks: text, tool_use, and thinking when the model reasoned. See Reasoning. |
stop_reason | end_turn, max_tokens, stop_sequence, tool_use or refusal. |
stop_sequence | Always null. When a stop sequence ends the message, stop_reason is stop_sequence. |
usage.input_tokens | Input tokens that were not served from or written to the cache. |
usage.output_tokens | Output tokens. Reasoning tokens are billed as output. |
usage.cache_read_input_tokens | Input tokens read from cache. Present when the provider reports it. |
usage.cache_creation_input_tokens | Input tokens written to cache. Present when the provider reports it. |
sources | Present when a kenari server tool searched the web. Lists {url, title} for each source. |
Count tokens
Section titled “Count tokens”POST /v1/messages/count_tokens takes the same body as /v1/messages and returns an estimate of the input tokens.
{ "input_tokens": 41 }The number is estimated from the size of the request, not counted with a tokenizer, so treat it as a rough guide for fitting a context window. The call needs a valid key but does not check the model, does not run it and costs nothing. A body that cannot be parsed returns 400.
curl https://kenari.id/v1/messages/count_tokens \ -H "x-api-key: $KENARI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "step-3-7-flash:free", "messages": [{ "role": "user", "content": "Hello!" }] }'Examples
Section titled “Examples”curl https://kenari.id/v1/messages \ -H "x-api-key: $KENARI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "step-3-7-flash:free", "max_tokens": 512, "system": "You answer briefly.", "messages": [{ "role": "user", "content": "Hello!" }] }'Python
Section titled “Python”import osimport anthropic
client = anthropic.Anthropic( base_url="https://kenari.id", api_key=os.environ["KENARI_API_KEY"],)
message = client.messages.create( model="step-3-7-flash:free", max_tokens=512, system="You answer briefly.", messages=[{"role": "user", "content": "Hello!"}],)print("".join(block.text for block in message.content if block.type == "text"))TypeScript
Section titled “TypeScript”import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://kenari.id", apiKey: process.env.KENARI_API_KEY,});
const message = await client.messages.create({ model: "step-3-7-flash:free", max_tokens: 512, system: "You answer briefly.", messages: [{ role: "user", content: "Hello!" }],});for (const block of message.content) { if (block.type === "text") console.log(block.text);}Billing
Section titled “Billing”Each message is billed per token at the model’s Input, Output, Cache read and Cache write prices. :free models cost nothing. count_tokens is free. See How billing works.
Errors
Section titled “Errors”Errors use the Anthropic envelope: an object with "type": "error" and an error object that holds type and message. Only the errors specific to this endpoint are listed here. The rest are in Errors.
| Status | error.type | When |
|---|---|---|
| 400 | invalid_request_error | The body is not valid JSON or lacks model or messages. A role, a tool_choice type or a known block type is malformed. A byok/ id is malformed. The model is not a chat model, such as an image or speech model. The request is longer than the model’s context window. |
| 400 | not_found_error | The model is not in the catalog and is not one of your routes, or you saved no key for the provider in a byok/ id. |
| 403 | permission_error | The model is only for accounts with an active subscription. A byok/ id names a provider whose keys cannot serve right now. The API key has a spend cap that BYOK requests cannot use. |
| 405 | none | The base URL ends in /v1, so the SDK called /v1/v1/messages. Use https://kenari.id. |