Skip to main content
VoiceGateway records what a request cost (see Cost Tracking). The rating layer turns that recorded cost into a billable price: it resolves a rate card, applies either a cost-plus markup or a fixed advertised rate, and stamps the result onto the request row as 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 global default_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:
  • cost_plus (markup): the billable price is the recorded provider cost multiplied by markup. 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 $/unit multiplied 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. Valid units: minute, second, char, 1k_char, token, 1k_token, 1m_token, request.

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:
FieldTypeMeaning
rated_price_usdfloatThe billable price for this request.
rate_rulestrAn audit token naming the rule that produced the price.
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), 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 run voicegw serve. The active card is loaded from rate_card: 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.
Today, only requests generated by the gateway’s own cost-tracking middleware are rated. Rows pushed to a self-hosted collector’s POST /v1/ingest (from fleet agents) are persisted with the agent’s pass-through value (rated_price_usd == cost_usd), so fleet-collected usage shows zero margin until the collector re-rates it. Re-rating ingested rows against the collector’s own card is a planned follow-up. Point voicegw prices reconcile and GET /v1/billing/usage at a single-node gateway for accurate margins in the meantime.

How rated usage is consumed

Once every row carries rated_price_usd, two surfaces read it:
  • Billing API. GET /v1/billing/usage rolls rated revenue, recorded cost, and margin up per tenant for a window; passing tenant adds per-(modality, model) line items for invoice detail. GET /v1/billing/rate-card returns the card in effect. See the HTTP API reference.
  • voicegw prices commands. voicegw prices ls prints the card, voicegw prices reconcile flags tenants with thin or negative margins over a window, and voicegw prices sync checks fixed rules against the current base cost. See voicegw prices.
Rate-card editing at runtime (a PUT on /v1/billing/rate-card, and voicegw prices set) is a planned follow-up tied to a DB override store that has not shipped yet. Today the card is the YAML seed; edit voicegw.yaml and reload to change rates.

Where to find each piece

ComponentPath
Rate card (data + resolution)src/voicegateway/billing/rate_card.py
Rating arithmeticsrc/voicegateway/billing/rating.py
Margin reconcile + price syncsrc/voicegateway/billing/reconcile.py
Billing APIsrc/voicegateway/server/api/billing.py
voicegw prices command groupsrc/voicegateway/cli/prices_cli.py
Rate card config schemasrc/voicegateway/schemas/config_schema.py