ADR-079: Provider cost tables + per-customer margin (COGS)
Status: Accepted (2026-07-05; amended 2026-10-14, 2026-10-22; units amended by ADR-130 §3, #1381: a rate is unit_cost_cents, US cents per token, and the API sends costs as provider_cost_cents and cost_cents, decimal strings of cents. The stored stamp provider_cost_micros is unchanged.)
Context: docs/dev/design/cost-tables.md (locked design — 4-platform
quote-verified sweep + 5-lens adversarial panel; this ADR records the
decisions, the design doc holds the evidence).
Summary
Section titled “Summary”Hardcount records what the operator pays LLM providers per token and reports usage margin per customer at GET /v1/customers/{id}/margin. Each usage event gets its cost stamped once, at ingest, in provider_cost_micros, so a later rate edit never changes past events and nothing recomputes history. Since 2026-08-06, LiteLLM usage carries LiteLLM’s own cost for that event’s tokens when it reports one; otherwise the cost comes from the operator’s rate table. Revenue is split by model only where a price names the model, and the rest goes to unattributed_revenue_cents rather than a guessed split. Status: active, amended 2026-10-14: cache reads are now taken out of LiteLLM’s input cost, so they are not counted twice. Amended 2026-10-22: rates are in USD only.
Decision
Section titled “Decision”Model what the OPERATOR pays LLM providers as a first-class, separate ledger, and report per-customer margin in-app:
provider_cost_rates(migration 0137): per-tenant, per-mode rows keyed(provider, model, token_type)withcost_per_token NUMERIC(20,12)— decimal, never float (verified rates reach 1.5e-06/token). Current-rate semantics: one row per key, edited in place; NO effective_from (no verified peer effective-dates cost rates; trigger to add: first pre-staged price-change ask). Explicit RLS ENABLE+FORCE + mode-aware policy + set_livemode trigger.- COGS attaches at INGEST — the universal verified pattern (snapshot
semantics).
usage_events.provider_cost_micros BIGINT(micro-dollars, ROUND half-up) +provider_cost_source(‘table’ | ‘observed’ | ‘not_applicable’), stamped by a scalar subquery inside the single- funnelstore.IngestINSERT — every path (live, batch, backfill, LiteLLM) covered by construction, zero extra round trips. Rate edits are non-retroactive; NO recompute/backfill tooling. - Resolution key order:
model_rawexact →modelfamily → NULL (the LiteLLM mapper mixes family tokens and raw ids). NULL-safe ordering (IS NOT DISTINCT FROM). - Rate-table inference ONLY in phase 1. LiteLLM’s observed cost is
NOT stamped: it is a whole-call figure fanned across up to 3 per-half
events (stamping it would multi-count COGS 3×) — and the panel found
it was never persisted anyway (the “metadata column” was doc-code
drift, now corrected in payload.go). Named fast-follow rule:
per-half CostBreakdown only; cache_read and ResponseCost-only
payloads fall to table inference. [Amended 2026-10-14] LiteLLM’s
input_costincludes cache reads, so the input event taking all of it while cache_read was costed from the table counted cache reads twice. Now the cache_read event takescache_read_costand the input event takesinput_cost − cache_read_cost; with cache reads and nocache_read_cost, both fall to the table. SeeobservedCostMicros. - Honest margin, one surface.
GET /v1/customers/{id}/margin: headline = total rated USAGE revenue vs total stamped cost (labeled a unit-economics view — base fees/credits/taxes excluded); per-model revenue + margin ONLY where a pricing rule pinsmodelin dimension_match; everything else in an explicitunattributed_revenue_centsbucket — never heuristic allocation. Operator-auth only; COGS never renders on the customer-facing cost dashboard.unresolved_eventscounts costable-but-unmatched events only (‘not_applicable’ rows are excluded so non-token meters can’t drown the signal); cache-write exclusion disclosed. [Amended 2026-10-24, #1378] An event whose cost does not fit BIGINT micros is stored and billed with no cost, and counts as unresolved too. - Sourcing: operator CRUD only (
/v1/provider-costs+ dashboard page). No import in phase 1 (trigger: first >10-model operator; file upload only, never live-URL fetch). Deleting a rate never rewrites stamped history. [Amended 2026-10-22] Rates are in USD only. The stamp is micro-dollars and LiteLLM reports its cost in dollars, so a rate saved in EUR was counted as dollars and the margin was off by the exchange rate.PUT /v1/provider-costsnow answers 422 for any currency other than USD. LLM providers bill in USD, so no peer need for another currency is known; adding one means converting at stamp time. - Exports: the operator usage-events CSV carries
provider_cost_micros/provider_cost_source(the verified warehouse-join fallback).
Consequences
Section titled “Consequences”- Cost is deterministic per event and immune to later rate edits; the price of that is honest NULLs for pre-rate history (MANUAL_TEST flow orders rates before ingest; UI copy explains).
- Margin % can read near-100% for cheap models — correct: it is usage unit economics, not GAAP margin.
- Deferred (triggers named): observed-cost stamping, effective_from, import, regex model match + context-size tiers, cost-plus pricing (2-2 verified split — not table stakes), per-customer negotiated cost rates.