Per-tenant usage metering, the usage-export API, and invoice generation —
rag_observability.billing. Mirrors the Step 5.6c cost-anomaly design: plain
dataclasses in rag-observability (the gateway wraps them in a Pydantic
response), so there's no rag-core type / dist/schemas churn.
| Piece | What |
|---|---|
UsageMeter |
Thread-safe per-tenant, per-dimension counters; fed O(1) from the request paths |
BillingDimension |
queries · docs_ingested · storage_gb · reranker_calls · llm_tokens |
UsageSnapshot |
One tenant's accrued usage (dimension → quantity) |
PricingModel / load_pricing |
Tiers + metered rates, loadable from marketplace/pricing.yaml |
generate_invoice |
Pure usage + pricing → Invoice; line-item quantities equal metered overage exactly |
BillingProvider |
Seam for reporting usage to Stripe / a marketplace metering API (degrade-open) |
UsageMeter is observe-only — it never rejects or mutates a request. It's fed
from record_request_usage (queries + LLM tokens) and serves the usage API:
curl 'localhost:8000/v1/billing/usage?tenant_id=acme'
# {"tenant_id": "acme", "usage": {"queries": 1234, "llm_tokens": 50000}}Empty ({}) when cfg.billing.enabled is false (the default). Enable it:
# rag.yaml
billing:
enabled: true
default_tier: pro
max_tenants: 10000Invoicing is offline (not a hot-path concern) and priced from config, so a
price change is a pricing.yaml edit, not code:
ragctl billing --tenant acme --tier pro
# meters synthetic usage → invoice line items + total + a reconciliation checkgenerate_invoice(snapshot, pricing, tier_id) is pure: a base-fee line plus one
line per dimension whose usage exceeds the tier's inclusion, billed at the metered
rate. Because each metered line item's quantity equals the metered overage, the
bill reconciles with usage exactly (well within the ±0.5% acceptance bar).
- No
dist/schemaschurn — the gateway'sBillingUsageResponseis a gateway-local Pydantic model wrapping the dataclass viadataclasses.asdict(same pattern asCostStatusResponse). Onlydist/openapi.*gains the endpoint. - No governed SPI call — metering is observe-only (like cost / drift / feedback), so the PolicyEngine coverage linter needs no new entry. Enforcement is the quota enforcer (Step 4.5); billing only meters.
- Fed independently of quotas — a deployment can meter usage without enabling quota enforcement.
- Report to an external biller. Implement
BillingProvider.report_usage(a Stripe usage-records adapter, or the AWS/Azure/GCP marketplace metering APIs) and call it degrade-open from the gateway. The defaultNoopBillingProviderrecords nothing (the usage API still serves). - More feed points.
docs_ingested(ingest path),reranker_calls(rerank stage), andstorage_gb(a gauge) are supported by the meter; wire additionalobserve/set_gaugecalls at those sites.
- marketplace/pricing.yaml — the canonical pricing the dimensions map to
- reference/cost.md — the cost-anomaly sibling (5.6c) this mirrors
- reference/quotas.md — the enforcement path (4.5)
- ADR-0051