Cost Tracking
Attribute token spend to runs, steps, and models.
Tokens and cost are recorded on three tables. LLM calls land in ai_traces with
per-call token counts and cost. Agent steps can carry their own tokens and
cost_usd. Agent runs hold rollups in total_tokens and total_cost_usd.
The important rule: a run's totals prefer its linked traces. You almost never compute them yourself.
Cost is priced server-side
When a trace arrives at POST /v1/ingest/ai with both input_tokens and
output_tokens, the API prices it from a per-model rate catalog compiled into
the binary and overwrites whatever cost_usd the client sent. A client-supplied
cost_usd is kept only when the model has no known rate.
Model lookup is case-insensitive and trims whitespace. If the exact ID is not in
the catalog, a trailing dated snapshot suffix — -YYYY-MM-DD or @YYYYMMDD —
is stripped and the base model's rate is tried, so gpt-4o-2099-01-01 falls back
to gpt-4o. Rates are stored as USD per 1,000,000 tokens:
cost = input_tokens / 1e6 * input_per_mtok
+ output_tokens / 1e6 * output_per_mtoktotal_tokens is filled in the same request: if you omit it but send both token
counts, the API stores their sum.
This is why a fresh self-hosted install reports real costs with no configuration. You only need to send token counts.
How run totals derive from traces
A trace links to a run through agent_run_id. Once linked, the run's totals are
kept in sync with its traces in two independent places.
At write time. A trigger on ai_traces recomputes the parent run's
total_tokens and total_cost_usd after each insert. Tokens are derived
null-safely, preferring the trace's own total_tokens and falling back to
input_tokens + output_tokens:
total_tokens = coalesce((
SELECT sum(coalesce(t.total_tokens, t.input_tokens + t.output_tokens))
FROM ai_traces t
WHERE t.agent_run_id = r.id AND t.project_id = r.project_id
), r.total_tokens)The outer coalesce is what makes manual reporting still work: when a run has no
linked traces the subquery is NULL and the stored rollup is left alone.
This trigger exists because traces usually arrive after the run finishes.
A rollup computed at finish time would miss them, and the continuous aggregate
that feeds cost alerting reads the raw columns and cannot join ai_traces.
At read time. The query layer applies the same prefer-traces logic when serving runs, so the value you read is correct even before the trigger has caught up.
Traces win over the rollup. If you report total_cost_usd on finish and
emit linked traces for the same run, the trace sum replaces your value rather
than adding to it. Pick one strategy per run.
Steps also accept tokens and cost_usd, and the SDKs accumulate those into the
totals they send at finish. Step-level values are not summed into run totals by
the server — they are there for per-step attribution in the timeline.
Reporting tokens and cost
Automatically, via adapters
The provider adapters read usage off each response and send it for you, linking to the active run when one is open. Nothing else is required.
import anthropic
from keelwave import Keelwave
client = Keelwave(api_key=os.environ["KEELWAVE_API_KEY"])
llm = client.wrap_anthropic(anthropic.Anthropic())
with client.run("research-agent") as run:
# input_tokens / output_tokens read from resp.usage, cost priced server-side
resp = llm.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": "summarise this"}],
)wrap_openai() does the same for the OpenAI client.
Manually, per call
Send a trace yourself when you are not using an adapter. Pass agent_run_id to
attribute it to a run:
client.ingest_ai(
model="gpt-4o",
status="success",
provider="openai",
input_tokens=1200,
output_tokens=350,
latency_ms=820,
agent_run_id=run.id,
)status must be one of success, error, or timeout.
Per step
For non-LLM work you want costed, attach the numbers to the step:
run.step("think", content="planning", tokens=420)
run.tool_call("web_search", input={"q": q}, output=res, tokens=90, cost_usd=0.0004)The SDK adds these into the run's running totals, which are sent at finish.
On finish
If you track spend entirely yourself, report it once when the run closes. The context managers do this from their accumulated totals; the raw call is:
client.ingest_agent_run_finish(
run_id,
timestamp=timestamp,
status="completed",
termination_reason="clean",
total_steps=12,
total_tokens=48_000,
total_cost_usd=0.42,
duration_ms=8_400,
)timestamp must be the value returned when the run was started — it is part of
the hypertable primary key.
Where totals surface
GET /v1/projects/{projectID}/agent/runs and
GET /v1/projects/{projectID}/agent/runs/{runID} return total_tokens and
total_cost_usd with the prefer-traces derivation already applied.
total_cost_usd is omitted when neither traces nor a rollup reported a cost.
Aggregates:
GET /v1/projects/{projectID}/agent/summary—avg_cost_usdandavg_tokensfor the window and the one before it, so you can render a delta.avg_cost_usdis null when no run in the window reported a cost.GET /v1/projects/{projectID}/agent/health— the same two averages broken down peragent_name.
Alert rules accept a cost_burn signal, evaluated against the raw run columns
the trigger keeps in sync.