Skip to main content

Usage

Usage meters record the token cost of every LLM generation, so spend can be attributed to a project, agent, and generation.

Overview

Whenever an agent completes a generation, SOAT records one usage-meter row from the token counts the provider reports for that call: input, output, cached, and reasoning tokens. Rows are written at the single point every agent completion flows through, so adding a provider cannot silently skip metering.

Meter rows are append-only and immutable — there is no update or delete path and no updated_at — so historical usage never changes after the fact. Writes are idempotent on the generation's public ID: a replayed completion upserts into a no-op instead of double counting.

Every meter row links back to the resources it attributes spend to: the generation that produced the call and its agent, the trace it belongs to, the AI provider instance billed, the project it rolls up to, and — when applicable — the trigger or orchestration run that initiated it.

See the Permissions Reference for the IAM action strings for this module.

Data Model

UsageMeter

FieldTypeDescription
idstringPublic identifier for the meter row (um_ prefix)
project_idstringProject the usage is attributed to
run_idstring | nullOrchestration run that initiated the call, when the generation ran inside a run
node_idstring | nullOrchestration node within the run, when applicable
agent_idstring | nullAgent that ran the generation
generation_idstring | nullGeneration this usage was recorded for
trace_idstring | nullTrace this usage belongs to (reconcile against the trace tree)
ai_provider_idstring | nullAI provider instance billed; correlates the meter to the price book
trigger_idstring | nullTrigger that initiated the generation (agent-target triggers); null otherwise
action_idstring | nullCaller-supplied logical action label, for rolling spend up per action
providerstringDenormalized as-billed provider slug (e.g. openai), retained if the provider is deleted
modelstringModel identifier the provider billed
input_tokensnumberInput (prompt) tokens reported by the provider
output_tokensnumberOutput (completion) tokens reported by the provider
cached_tokensnumberCached input tokens read, when the provider reports them (else 0)
reasoning_tokensnumberReasoning tokens the provider reports separately (else 0)
cost_usdnumber | nullCost in USD computed at write time from the price book; null when no price row covers the model
price_idstring | nullPrice-book row that produced cost_usd (the price-table version); null when unpriced
created_atstringISO 8601 creation timestamp

PriceBook

Versioned unit prices used to compute cost_usd, in three scopes within one table, resolved most-specific first. A per-provider override (ai_provider_id set) prices one specific AI provider instance — e.g. an enterprise-negotiated rate or a gateway with markup. A project + provider-slug price (project_id set, ai_provider_id null) covers every one of a project's instances of a slug. A global default (both null) is keyed by (provider, model, effective_from). Cost lookup prefers instance → project+slug → global; within each scope the latest effective_from <= now() applies.

FieldTypeDescription
idstringPublic identifier for the price row (price_ prefix)
ai_provider_idstring | nullSet for a per-provider override; null otherwise
project_idstring | nullSet for a project + provider-slug price; null otherwise
providerstringAI provider slug (e.g. openai)
modelstringModel identifier
input_price_per_mnumberUSD per one million input (prompt) tokens
output_price_per_mnumberUSD per one million output (completion) tokens
cached_price_per_mnumber | nullUSD per one million cached input tokens; null falls back to input
effective_fromstringISO 8601; the latest row <= now() prices a call
created_atstringISO 8601 creation timestamp

Key Concepts

Token breakdown

Each row separates the provider's token report into four counts. cached_tokens and reasoning_tokens are recorded only when the provider reports them (e.g. OpenAI's prompt_tokens_details.cached_tokens and completion_tokens_details.reasoning_tokens); a provider that omits a breakdown records 0, never null, so the counts stay summable.

Coverage

Usage is metered for agent generations — including conversations and orchestration agent nodes, which run through the same agent-completion path. run_id and node_id are reserved for orchestration attribution and are null for standalone generations.

Trigger and action attribution

action_id is a caller-supplied label passed on the generate request (action_id), persisted on the generation and copied onto its meter so spend can be rolled up per logical action independent of the agent or generation. trigger_id is set automatically when an agent-target trigger initiates the generation. Filter the raw meter list by either (?trigger_id= / ?action_id=) to roll usage up by trigger or action. (Trigger attribution for generations produced inside an orchestration run is tracked with the run-scoping work and is null until then.)

Pricing

cost_usd is computed at write time from the effective price row for the meter's (provider, model), resolved most-specific first: the AI provider instance → the project's rate for that slug → the global default. Cost is frozen onto the meter, so later price changes never alter it — swapping a model changes new-run cost while historical receipts stay put. Cached input tokens are billed at cached_price_per_m (falling back to the input rate); reasoning tokens are part of the output count. cost_usd is null only when no price row covers the model — the tokens are still captured, it does not mean the call was free.

Each meter records price_id — the exact price-book row that produced its cost_usd. Because cost is frozen and the price row is versioned, a receipt is auditable to the precise price applied even after prices change.

SOAT ships global default prices for common provider/model pairs (seeded at startup) so cost is computed out of the box. Prices are managed where their scope lives:

Past-effective prices are immutable — corrections ship as new future-dated rows.

Receipts and reconciliation

GET /api/v1/usage/receipt?generation_id=… returns a billing receipt for a completed generation: per-model line items (tokens in/out, the price_id version that priced them, and cost) plus totals. Because every line carries the exact price-book version and the cost is frozen at write time, receipts stay reproducible and are meant to reconcile against the provider's invoice within a small tolerance (target ±2%); investigate any project whose summed receipts drift beyond it. Per-run receipts (summing a run's generations) follow once run-scoping lands.

Examples

soat list-usage-meters --generation-id gen_V1StGXR8Z5jdHi6B

Get a generation's receipt:

soat get-usage-receipt --generation-id gen_V1StGXR8Z5jdHi6B

Read the price book:

soat get-price-book