Skip to content
kenari.

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.

FieldTypeRequiredDescription
modelstringYesA catalog id, a :free id, a route name or a byok/ id. The same values as in Chat completions.
messagesarrayYesThe conversation. See Messages array.
max_tokensintegerNoUpper limit on generated tokens. Optional here, unlike the Anthropic API. Default 65,536.
systemstring or arrayNoSystem 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.
streambooleanNoSet true to receive Server-Sent Events. See Streaming.
temperaturenumberNoSampling temperature.
top_pnumberNoNucleus sampling.
top_kintegerNoSample from the top K tokens.
stop_sequencesarrayNoStrings that end generation.
toolsarrayNoFunction tools with name, description and input_schema, and kenari server tools such as {"type": "kenari:web_search"}. See Function calling and Server tools.
tool_choiceobjectNo{"type": "auto"}, {"type": "any"}, {"type": "none"} or {"type": "tool", "name": "..."}.
thinkingobjectNo{"type": "enabled", "budget_tokens": 4096} or {"type": "disabled"}. See Reasoning.
output_configobjectNo{"effort": "high"} sets a reasoning level without a token budget. See Reasoning.
metadataobjectNoAccepted.

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.

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.

BlockUse
textPlain text.
imageAn image from a base64 or url source. See Images and files as input.
documentA 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_useA function call the model made earlier.
tool_resultThe 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_thinkingEarlier reasoning, sent back unchanged.

Unknown block types are ignored. A block of a known type that is malformed returns 400.

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.

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 }
}
FieldDescription
contentAn array of blocks: text, tool_use, and thinking when the model reasoned. See Reasoning.
stop_reasonend_turn, max_tokens, stop_sequence, tool_use or refusal.
stop_sequenceAlways null. When a stop sequence ends the message, stop_reason is stop_sequence.
usage.input_tokensInput tokens that were not served from or written to the cache.
usage.output_tokensOutput tokens. Reasoning tokens are billed as output.
usage.cache_read_input_tokensInput tokens read from cache. Present when the provider reports it.
usage.cache_creation_input_tokensInput tokens written to cache. Present when the provider reports it.
sourcesPresent when a kenari server tool searched the web. Lists {url, title} for each source.

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.

Terminal window
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!" }]
}'
Terminal window
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!" }]
}'
import os
import 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"))
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);
}

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 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.

Statuserror.typeWhen
400invalid_request_errorThe 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.
400not_found_errorThe 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.
403permission_errorThe 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.
405noneThe base URL ends in /v1, so the SDK called /v1/v1/messages. Use https://kenari.id.