Text

Count tokens

Measure how large a request is before spending anything on it.

POST/v1/messages/count_tokens

Takes the same body as Messages and returns only the input token count. Useful for staying inside a context window, predicting cost, and deciding when to trim conversation history.

curl https://api.xkiro.com/v1/messages/count_tokens \
  -H "x-api-key: $XKIRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-opus-5",
    "system": "You are a concise assistant.",
    "messages": [{ "role": "user", "content": "Hello there" }]
  }'
200 OK
{ "input_tokens": 19 }

Request body#

FieldTypeDescription
modelrequiredstringFull model ID — tokenisation differs between model families.
messagesrequiredarrayThe conversation to measure.
systemstring | arraySystem prompt. Counted as part of the input.
toolsarrayTool definitions. These count too, on every turn of a tool-calling loop.

Tool definitions are not free

They are re-sent with every request in a tool-calling loop, so a large schema is a recurring cost. Counting a request with and without tools shows exactly what they add.

A practical use#

Trim history until the request fits, instead of discovering the limit through a failed call:

async function fitToWindow(messages: unknown[], budget = 150_000) {
  const trimmed = [...messages];

  // Drop from the oldest end until it fits. Keeping the newest turns matters
  // more than keeping the first ones — the model needs recent context.
  while (trimmed.length > 2) {
    const { input_tokens } = await fetch(
      "https://api.xkiro.com/v1/messages/count_tokens",
      {
        method: "POST",
        headers: {
          "x-api-key": process.env.XKIRO_API_KEY!,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          model: "anthropic/claude-opus-5",
          messages: trimmed,
        }),
      },
    ).then((r) => r.json());

    if (input_tokens <= budget) break;
    trimmed.shift();
  }

  return trimmed;
}

Leave headroom for the answer: the context window covers input and output, so a budget equal to the full window leaves the model no room to reply.