rated_price_usd plus an audit token rate_rule. This is the layer that lets a biller like ShipVoice keep Stripe, invoices, and credits while VoiceGateway produces the rated usage those invoices are built from.
This page covers the rating subsystem: the rate card, how a rule is resolved, where rating runs, and how the rated numbers feed the billing API and the voicegw prices commands.
The rate card
A rate card is a globaldefault_markup fallback plus an ordered list of rules. Each rule is scoped on two axes and carries exactly one kind of arithmetic. It is configured under rate_card: in voicegw.yaml (see the config reference).
Scope fields (all optional, default “any”): modality, provider, model, tenant, plan. A rule with no scope fields matches every request.
Rule kind is one of two:
Each rule also names which side of the ledger it sets via sets: price (the default, what the tenant is charged) or cost (what the operator pays, replacing the catalogue figure on the recorded row). The two resolve independently, so a model-specific cost and a global markup both apply rather than the more specific one winning outright. A cost rule must be fixed, because cost_plus multiplies a recorded cost and so cannot produce one. See rate_card.
A cost rule is what makes cost_plus honest. Without one, the number being marked up is always the voice-prices list price, so anyone on a negotiated contract has a margin computed against a figure they do not pay.
- cost_plus (
markup): the billable price is the recorded provider cost multiplied bymarkup. Because it multiplies the recorded cost, a cost-plus rule auto-follows voice-prices base movement: when the base price changes, the rated price tracks it with no edit. - fixed (
fixed+unit): the billable price is an advertised$/unitmultiplied by the request’s billable quantity in that unit. A fixed rule is decoupled from the base cost, so it holds a stable advertised price even as the base moves. Single-sided units:minute,second(stt),char,1k_char(tts),request(any). - fixed, per leg (
input_price_usd+output_price_usd+ a tokenunit): the LLM form. Token units (token,1k_token,1m_token) carry a rate per leg, because input and output bill at different rates on every provider in the catalogue and one blended rate cannot express either.cached_input_price_usdis optional and defaults to the input rate. Cached prompt tokens are a subset of the prompt, so the uncached leg isinput - cached. A fixed rule describes a flat contract and cannot express context-window tiers; usecost_plusto track a tiered list price.
How a rule is resolved
Rating a request resolves the single most specific matching rule. Specificity ranks tenant over plan over global, and model over provider over modality-only:tenant > plan > global, and within that model > provider > modality-only.
Among rules that tie on specificity, the one that appears later in the list wins. That ordering is deliberate: a DB override layered after the YAML seed takes precedence over the seed rule it shadows. When no rule matches, the request falls back to a cost-plus pass at the card’s default_markup.
Write-time, immutable rating
Rating happens at write time, once, on the same path that records cost. Every request row stores two fields:
The
rate_rule token is human-readable and self-documenting: cost_plus:1.3 (recorded cost times 1.3), fixed:0.006/minute (an advertised fixed rate), fixed:in=2.5,cached=1.25,out=10/1m_token (an LLM rate, every leg named), or default:1 (no rule matched, default markup applied). Because the price and its rule are stamped at write time, they are immutable: editing the card later never rewrites historical rows. Yesterday’s usage stays billed at yesterday’s card, which is what makes the rated numbers safe to invoice against.
Where rating runs
Rating runs server-side, in the gateway’s cost-tracking middleware, when you runvoicegw serve. The active card is the rate_card: seed plus any DB overrides, merged at startup and on every config refresh, and applied as each request row is written. If rating ever fails, it falls back to a cost pass-through so the row is still recorded with a billable price.
The agent-side attach() path stays a cost pass-through by design. Margins are a server-side concern: an agent process records raw cost, and the server (or a hosted cloud that rates on ingest) applies the card. The rating logic lives in the pure voicegateway.billing module, so a hosted cloud can import it and rate on ingest without pulling in the rest of the gateway.
A self-hosted collector re-rates fleet rows on ingest. Rows pushed to
POST /v1/ingest arrive unrated (agents rate at pass-through), so the collector rates each one against its own card, using the tenant resolved from the verified vk_ key, and overwrites any agent-supplied rated price (agents are not trusted with margins). Both the SQLite and ClickHouse sinks store rated_price_usd / rate_rule, and GET /v1/billing/usage reads from whichever store the collector uses (ClickHouse when configured, SQL otherwise). The voicegw prices reconcile CLI still reads the SQL store directly, so on a ClickHouse-backed collector use the HTTP endpoint for accurate margins.How rated usage is consumed
Once every row carriesrated_price_usd, two surfaces read it:
- Billing API.
GET /v1/billing/usagerolls rated revenue, recorded cost, and margin up per tenant for a window; passingtenantadds per-(modality, model) line items for invoice detail.GET /v1/billing/rate-cardreturns the card in effect. See the HTTP API reference. voicegw pricescommands.voicegw prices lsprints the effective card,voicegw prices reconcileflags tenants with thin or negative margins over a window,voicegw prices syncchecks fixed rules against the current base cost, andvoicegw prices set/rmedit the DB overrides. See voicegw prices.
The rate card is one store with four write surfaces: the
rate_card: seed in voicegw.yaml plus DB overrides edited from the CLI (voicegw prices set / rm), over HTTP (POST / DELETE /v1/billing/rate-card/rules), over MCP (set_rate_card_override / delete_rate_card_override), or from the dashboard (Configure -> Rate card). Each override is one rule per scope (keyed by tenant|plan|modality|provider|model), and a DB override wins a specificity tie against the seed rule it shadows.