docs · guides
Meter token usage#
Hekma counts tokens in one of two ways: the agent prints a KTESIO_USAGE line for each unit of work (self-reported), or Hekma sits between the agent and its model provider as a small local proxy and reads the usage from each response (engine-observed). Either way the counts land in the usage ledger, which drives every usage figure and budget. Usage is counted only while a hekma command or an embedding program holds the engine.
Choose a metering source#
Every adapter declares one metering source in its [metering] section. Pick the one that matches what your agent can do:
| Self-reported | Engine-observed | |
|---|---|---|
| Manifest value | source = "self-reported" | source = "engine-observed" |
| Who counts | The agent, which prints one KTESIO_USAGE line per unit of work | Hekma, by reading the usage object in each model response |
| What the agent needs | A way to print a line on stdout (or a wrapper script that does) | A setting for its model provider's base URL, such as OPENAI_BASE_URL |
| Provider formats | Any (the agent does the counting) | OpenAI-compatible and Anthropic responses |
Can run with --detach | Yes | No (the proxy lives in the starting command) |
| Duplicates | Removed: lines with a repeated sequence are counted once | Each forwarded call counts once; the agent's own retries count as separate calls |
Use self-reported when you control the agent's code or can wrap it, or when you want to run it detached. Use engine-observed for an agent you cannot change but whose provider URL you can point somewhere else.
The ACP kind supports both: it uses self-reported lines by default and switches to engine-observed when you set metering.upstream_base_url on the agent. See Run an ACP agent.
Self-reported usage#
The agent prints one line per unit of work, on stdout:
KTESIO_USAGE {"sequence": 0, "input_tokens": 1200, "output_tokens": 300}The line starts with KTESIO_USAGE and a space, followed by a JSON object with these fields:
| Field | Required | Meaning |
|---|---|---|
sequence | yes | A whole number that identifies the event within its run. Count up from 0 (any start works). Hekma keeps one event per sequence in a run, so a line printed twice is counted once. |
input_tokens | yes | Tokens sent to the model, including any cached tokens. |
output_tokens | yes | Tokens the model generated. |
cached_tokens | no | The part of input_tokens the provider served from its prompt cache. Leave it out if you do not know it; Hekma treats a missing value as 0. |
The rules are strict, and a line that breaks one is skipped without stopping the agent:
- All counts are whole numbers, 0 or more.
cached_tokenscannot be larger thaninput_tokens. Cached tokens are part of the input, never extra.- No other fields are allowed. A typo such as
"output_token"makes Hekma skip the line. - Lines that do not start with
KTESIO_USAGEare ordinary output and are ignored.
Hekma prints no message for a skipped line. If the numbers do not move, check hekma agent usage and the agent's logs.
The line stays in the agent's output, so you see it in hekma agent logs. That is the quickest way to check the agent is reporting.
ACP agents print the line on stderr. An ACP agent's stdout carries the protocol itself, so for the acp kind Hekma reads KTESIO_USAGE lines from stderr instead.
A shell agent can report usage with one echo, as the Quickstart stand-in does:
echo "KTESIO_USAGE {\"sequence\": $n, \"input_tokens\": 1200, \"output_tokens\": 300}"If you cannot change the agent, a small wrapper can read its own usage reports (an API log, a session file) and print the lines for it. Keep sequence tied to something stable in the agent's records, so a replayed batch keeps its old numbers and is not counted twice. If the provider does not report usage for a call, print nothing for it. A line with zeros claims the call used no tokens.
Engine-observed usage#
For an engine-observed agent, Hekma starts a small HTTP proxy on 127.0.0.1 when the agent starts. The agent sends its model requests to the proxy, the proxy forwards them to the real provider and passes the responses back unchanged, and Hekma reads the token counts from each response. Request and response bodies, headers and API keys are never logged or stored; only the counts are kept.Diagram source (Mermaid)
sequenceDiagram
participant A as Agent
participant P as Hekma proxy (127.0.0.1)
participant U as Provider (metering.upstream_base_url)
participant L as Usage ledger
A->>P: model request (to metering.base_url)
P->>U: same request, forwarded
U-->>P: response with usage
P-->>A: same response, unchanged
P->>L: input, output and cached token counts
Set it up#
You need three things:
The manifest declares the source and maps the proxy address. Hekma writes the proxy's address (
http://127.0.0.1:<port>, a new port each start) into the reserved config keymetering.base_url. Map that key to the environment variable, flag or file setting your agent reads its base URL from:[metering] source = "engine-observed" [config."metering.base_url"] env = "OPENAI_BASE_URL"Use whatever your agent reads:
OPENAI_BASE_URL,ANTHROPIC_BASE_URL, a--base-urlflag, or a key in its config file. You never setmetering.base_urlyourself; Hekma fills it in at start.You tell Hekma where the real provider is. Set
metering.upstream_base_urlon the agent:hekma agent config set my-agent metering.upstream_base_url https://api.openai.comThe proxy appends the exact path the agent calls to this URL. If your agent calls
<base>/v1/chat/completions, set the upstream tohttps://api.openai.com; if it calls<base>/chat/completions, set it tohttps://api.openai.com/v1. Bothhttp://andhttps://work; Hekma checks certificates against a built-in set of public root certificates, so a provider behind a private certificate authority will not connect.You start it attached, from a process that stays open.
start --detachis refused for engine-observed agents, because the proxy runs inside the command that starts the agent and would vanish with it:✗ Agent Instance 'obs' cannot be started with --detach: the instance meters via the engine-observed channel, whose loopback forward listener lives inside the starting command — detaching would leave the agent pointed at a listener that dies with the command. Start it without --detach (the in-command supervision keeps the listener alive), or switch the adapter's metering source to self-reported.. Nothing was changed.This exits with code 5 and changes nothing. A plain
hekma agent startworks but returns at once and stops the agent when it exits, so in practice an engine-observed agent needs a program that embeds the engine and keeps running. A long-running service host,hekma serve(Unreleased: after 0.11.0), also covers this; see Run Hekma as a service.
If metering.upstream_base_url is missing, the start fails before anything launches:
✗ Agent Instance 'obs' could not start engine-observed metering: no upstream provider URL configured; set `metering.upstream_base_url` to the agent's real OpenAI-compatible endpoint. Nothing was changed; fix the metering configuration and start it again.Formats Hekma reads#
The proxy reads the usage object from each response and maps it onto the ledger's three counts:
| Provider format | Input tokens | Output tokens | Cached tokens |
|---|---|---|---|
| OpenAI-compatible | prompt_tokens | completion_tokens | prompt_tokens_details.cached_tokens |
| Anthropic | input_tokens + cache_creation_input_tokens + cache_read_input_tokens | output_tokens | cache_read_input_tokens |
Anthropic reports cache reads and cache writes separately from input_tokens, so Hekma adds them back in. That keeps one rule across formats: input always includes the cached part. For example, a response with input_tokens: 100, cache_creation_input_tokens: 20, cache_read_input_tokens: 30 and output_tokens: 50 is recorded as 150 in, 50 out, 30 cached. Cache writes are counted as plain input; Anthropic's higher price for cache writes is not modelled.
A response with no usage object, or one Hekma cannot read, is passed to the agent unchanged and simply not counted. Metering never breaks the agent's call. If the provider cannot be reached, the agent gets an HTTP 502 error from the proxy, never a made-up response.
Streaming responses#
OpenAI-compatible providers send usage in a streamed response only when the request asks for it. When an agent sends a streaming request ("stream": true) to a path ending in /chat/completions, the proxy adds "stream_options": {"include_usage": true} to the request so the provider includes a final usage message. This is the only change the proxy makes to traffic. It skips the change when the agent already sets stream_options, and it never touches other endpoints. Streamed Anthropic usage is not read.
The proxy buffers each response before passing it on, so a streamed response reaches the agent in one piece when the stream ends, not token by token. Requests and responses are limited to 64 MB each.
What is not counted#
The ledger counts what it sees while it is watching. Some usage is never counted:
| Case | Why |
|---|---|
Usage a detached agent reports while no hekma command is running | Nobody is reading the agent's output. When the next command adopts the agent, it starts reading from the current end of the log, so lines written in the gap are skipped. Keep hekma agent logs NAME --follow running to close the gap; see Supervision and detached runs. |
| Usage held in memory when a supervising command exits or crashes | A small amount of usage read but not yet written can be lost with the process. Hekma says so when it adopts the agent again. |
| Requests in flight when an engine-observed agent stops | The proxy stops with the run, so a call that has not returned yet is not counted. |
| Lines that break the format rules | Skipped, as described above. |
| Provider retries the agent makes | Counted, once per call: each retry is a real call that used real tokens. |
Treat the ledger as a lower bound on real spend: Hekma does not invent usage, and anything it could not see is missing rather than guessed.
Read usage#
Three commands show usage, all from the same ledger:
hekma agent list # every agent, with a fleet total
hekma agent show my-agent # one agent in detail
hekma agent usage my-agent # cumulative and current-run totals
hekma agent usage # fleet-wide totals▸ Usage for spender (5 rows)
Field Value
────────────────────────────────────────────────────
Cumulative tokens in 4800 / out 1200 / cached 3200
Cumulative cost $0.02 (estimated)
Current-run tokens in 0 / out 0
Current-run cost $0.00 (estimated)
Metering source self-reportedCumulative covers every run of the agent. Current run covers the run in progress and drops to 0 when a new run starts. The cached count appears once any cached tokens have been recorded. Dollar figures appear only when you set a rate, and they are always labelled estimates; see Budgets and cost.
Add --json for exact integers a script can use; JSON output documents the fields. The ledger itself is the usage_events table in state.db in the state directory.
Read next#
- Budgets and cost: turn usage into token and dollar ceilings.
- Bring your own agent: pick a metering source while writing an adapter.
- Supervision and detached runs: keep usage counted while an agent runs.
- Metering agents you don't control: the design behind the proxy.