OSS / Operations
Pricing & Cost Calculation
How Aurora turns tokens into dollars, the two-layer cost model, provenance fields, and how to recalculate and reconcile costs.
Browse docs
Aurora turns token counts into dollar amounts on every request, then stores the result with full provenance so you can reconcile against provider invoices later. This page covers the calculation, the data flow, the schema, and the operations you actually run day-to-day. For how to set pricing, see Model Pricing.
The two-layer model
Every usage record carries a cost computed by one of two paths, recorded explicitly as cost_source:
Real-time estimation (the default)
As each response arrives, Aurora extracts token counts and extended usage fields, then computes an estimated cost from the model pricing registry. The estimate is written to the usage store immediately so dashboards and rate-limit enforcement have near-real-time cost data.
Provider-reported reconciliation (override)
Some providers — currently OpenRouter and xAI — return the exact final cost in their API response. When detected, the provider-reported cost overrides the estimated cost at write time. The raw provider payload is always preserved in raw_data so you can reconcile against invoices later.
Both layers write to the same UsageEntry schema. The cost_source and pricing_source fields tell you which path produced the value, so a "the dashboard says $0.007 but my OpenRouter bill says $0.007059" question has a deterministic answer.
What a model costs — the ModelPricing schema
ModelPricing is the per-model cost record. Fields are independent — set only what applies. Every field is in USD per million tokens unless noted.
ModelPricing values are seeded from the upstream models.json registry. Operators can override or add pricing via config.yaml provider_overrides — see Model Pricing.
Where pricing came from — PricingProvenance
Every cost computation records provenance alongside the dollar amounts. The fields are stored on the UsageEntry:
When a cost came from the provider, the resolver's provenance is intentionally not carried through — the provider is the source of truth, and the registry version is irrelevant for that row.
How cost is computed — the dispatch order
CalculateUsageCost is the single entry point. It runs in this order, and the first match wins:
OpenRouter credits
If the provider is openrouter and raw_data["cost"] is a finite non-negative number, use it as the total. If raw_data["cost_details"] has matching upstream_inference_prompt_cost + upstream_inference_completions_cost (or their aliases), use those as the input/output split. Otherwise the total stays unsplit. cost_source = "openrouter_credits".
xAI ticks
If the provider is xai and raw_data["cost_in_usd_ticks"] is a finite non-negative number, divide by 10¹⁰ to get USD. No input/output split is available. cost_source = "xai_cost_in_usd_ticks".
Granular calculation
Call CalculateGranularCost with the model's pricing. This walks the per-provider rawData → pricing-field mapping. cost_source = "model_pricing".
The granular calculation in detail
CalculateGranularCost walks every rawData key the provider cares about and applies the right pricing field. The dispatch table is defined in internal/usage/cost.go and looks like this:
What this means in practice:
- A request to OpenAI with
cached_tokens=50andreasoning_tokens=10is priced as:50input atCachedInputPerMtok+input_tokens - 50input atInputPerMtok+output_tokens - 10output atOutputPerMtok+10output atReasoningOutputPerMtok. - A request to Anthropic with
cache_read_input_tokens=100,cache_creation_input_tokens=200, no reasoning, is priced as:100cached +200cache-write + remaining input at base rate. - A request to xAI with
image_tokens=2is priced as:2 × InputPerImageplus the text token costs.
The data flow
┌──────────────────┐
│ models.json │ ← fetched from upstream URL
│ version=2 │ or loaded from local file
│ updated_at=... │
└────────┬─────────┘
│
┌────────▼─────────┐
│ ModelRegistry │
│ ResolvePricing() │ ← returns *PricingResult
└────────┬─────────┘
│
┌───────────────────┼─────────────────────┐
▼ ▼ ▼
Chat / Responses SSE Stream Cached Response
ExtractFrom* StreamObserver ExtractFromCache
│ │ │
└───────────────────┼─────────────────────┘
▼
┌─────────────────────┐
│ applyUsageCosts() │
│ CalculateUsageCost │
│ (granular / OR / xAI)│
└──────────┬──────────┘
│
┌────────▼─────────┐
│ UsageEntry │
│ cost / caveat │
│ cost_source │
│ pricing_source │
│ pricing_version │
│ pricing_resolved│
│ raw_data │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Usage Store │
│ SQLite / PG / │
│ MongoDB │
└─────────────────┘
Stored schema — what you can query
Each UsageEntry row has 23 columns. The cost-related ones:
MongoDB stores the same fields as a single BSON document in the usage collection.
Querying the cost data
Per-request usage log
GET /admin/api/v1/usage/log?model=gpt-4o&limit=10{
"entries": [
{
"id": "abc-123",
"model": "gpt-4o",
"provider": "openai",
"input_tokens": 150,
"output_tokens": 42,
"total_tokens": 192,
"raw_data": {
"cached_tokens": 50,
"reasoning_tokens": 10
},
"input_cost": 0.002775,
"output_cost": 0.004284,
"total_cost": 0.007059,
"cost_source": "model_pricing",
"costs_calculation_caveat": "",
"pricing_source": "upstream_registry",
"pricing_version": "2",
"pricing_resolved_at": "2026-07-14T06:17:05Z"
}
]
}Aggregated summary
GET /admin/api/v1/usage/summary?start_date=2026-07-01&end_date=2026-07-31{
"total_requests": 5842,
"total_input_tokens": 12500000,
"total_output_tokens": 3200000,
"total_input_cost": 12.50,
"total_output_cost": 8.96,
"total_cost": 21.46
}Per-model breakdown
GET /admin/api/v1/usage/models?start_date=2026-07-01&end_date=2026-07-31[
{
"model": "gpt-4o",
"provider": "openai",
"total_requests": 1200,
"total_input_tokens": 5000000,
"total_output_tokens": 1500000,
"total_input_cost": 5.00,
"total_output_cost": 6.00,
"total_cost": 11.00
}
]Cache savings
GET /admin/api/v1/cache/overview{
"summary": {
"total_saved_cost": 3.45
},
"daily": []
}See the full endpoint reference in Admin API: Usage and Admin API: Cache.
Recalculating after pricing changes
When models.json updates — Anthropic lowers cache-write pricing, OpenAI introduces a new tier, you add a local model — previously stored usage records still show the old costs. Recalculation re-runs cost computation against the current pricing for historical rows, with full provenance.
Endpoint
POST /admin/api/v1/usage/recalculate-pricingRequest body
{
"confirmation": "recalculate",
"selector": "anthropic/claude-sonnet-4-20250514",
"start_date": "2026-06-01",
"end_date": "2026-06-30",
"user_path": "/dashboard",
"days": 90
}Response
{
"status": "ok",
"matched": 5842,
"recalculated": 5842,
"with_pricing": 5731,
"without_pricing": 111
}What recalc does
For each matched row:
- Reads stored
model,provider,provider_name,input_tokens,output_tokens,raw_data,endpoint. - Resolves current pricing via the registry. Uses
provider_nameif set, falls back toprovider. - Calls
CalculateUsageCostwith the current pricing. - Writes back:
input_cost,output_cost,total_cost,cost_source,costs_calculation_caveat,pricing_source,pricing_version,pricing_resolved_at.
Provenance during recalc
- For OpenRouter and xAI rows:
pricing_source = "provider_reported". The provider-reported cost is the final source, not the registry. The version and resolved-at are cleared. - For all other providers:
pricing_source = "upstream_registry",pricing_version= the currentmodels.jsonversion,pricing_resolved_at= the time recalc ran.
Gating
Recalculation is gated behind the env var USAGE_PRICING_RECALCULATION_ENABLED=true (default). Without it, the endpoint returns 503.
Concurrency
A single mutex (h.pricingMu) ensures only one recalc runs at a time. A 60-second request timeout applies; the SQLite implementation uses cursor-based pagination to handle large datasets.
Best practices
Always check pricing_source
When displaying cost data, show whether it is an estimate or provider-reported:
Cost: $0.007059 (upstream registry v2, resolved 2026-07-14)
Cost: $0.004200 (provider reported, OpenRouter exact cost)
This makes any "the dashboard and the invoice disagree" question a one-liner.
Recalculate after pricing changes
After any models.json update that changes prices, run:
curl -X POST http://localhost:8080/admin/api/v1/usage/recalculate-pricing \
-H "Authorization: Bearer $AURORA_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"confirmation": "recalculate"}'This updates all stored records to use the new pricing and bumps pricing_version accordingly.
Scoped recalc for targeted fixes
If only one model's pricing changed, recalc just that model:
{
"confirmation": "recalculate",
"selector": "anthropic/claude-sonnet-4-5"
}This is faster and avoids touching unrelated rows.
Provider-reported costs do not need recalc
OpenRouter and xAI costs are recorded at ingestion time from the provider's response. Recalculation skips them (pricing_source stays "provider_reported"). If you need to change these values, you must re-ingest from the original response — the registry's pricing is irrelevant for them.
Use raw_data for invoice reconciliation
The raw_data field preserves exactly what the provider returned. Export it alongside costs to reconcile against provider-side invoices:
{
"raw_data": {
"cost": 0.0042,
"cost_details": {
"upstream_inference_prompt_cost": 0.0012,
"upstream_inference_completions_cost": 0.0030
}
},
"cost_source": "openrouter_credits",
"total_cost": 0.0042
}The cost_details shape depends on the provider — OpenRouter, OpenAI, Anthropic, Gemini, and the OpenAI-compatible providers all use different key names. The full original payload is always in raw_data.
Monitor caveats
A non-empty costs_calculation_caveat means some part of the token usage could not be priced. The most common caveat is unmapped token field: <name> — a new provider-specific token counter that Aurora does not yet know about. If you see these for a provider you use, consider adding a provider_overrides config entry or opening an issue.
Configuration
config.yaml
cache:
model:
model_list:
local_path: "data/models.local.json" # vendored snapshot
url: "https://..." # upstream feed
refresh_interval: "1h"
usage:
enabled: true
enforce_returning_usage_data: true
buffer_size: 100
flush_interval: "5s"
# USAGE_PRICING_RECALCULATION_ENABLED — when set to true, the admin
# usage recalculation endpoint is enabled. Default: false.Environment variables
Troubleshooting
"No pricing available for model X"
The model is not in models.json and no provider_overrides config covers it. Add pricing via config:
provider_overrides:
- provider_type: "ollama"
models:
"my-custom-model":
pricing:
input_per_mtok: 0.50
output_per_mtok: 1.00Or set a pricing override through the admin API:
curl -X PUT "http://localhost:8080/admin/api/v1/model-pricing/ollama%2Fmy-custom-model" \
-H "Authorization: Bearer $AURORA_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"pricing": {"input_per_mtok": 0.50, "output_per_mtok": 1.00}}'Recalculation returns 0 matched rows
Check:
- The date range covers stored data (
start_date/end_dateare inclusive). - The
selectorformat isprovider/model, not justmodel. USAGE_PRICING_RECALCULATION_ENABLED=trueis set.- The usage store has data —
GET /admin/api/v1/usage/summaryshould be non-zero.
pricing_resolved_at shows current time, not the model list's updated_at
The models.json updated_at field must be parseable. Aurora tries these layouts in order:
If none match, time.Now().UTC() is used as a fallback. The provenance field is still set correctly — only the timestamp is now instead of the registry's updated_at.
Cost caveat says "unmapped token field: xxx"
This means raw_data contains a key that Aurora did not recognize as either a priced token type or an informational breakdown. This is usually harmless — the key is skipped for costing. But if it represents a billable token type, it indicates a gap in the mapping table. Open an issue with the provider name, the key, and an example response.
Cost is consistently lower than the invoice
Two common causes:
- Pricing version drift. Your
models.jsonis older than the provider's current rates. Run recalc after updatingmodels.json. - Granular calculation is missing a field. A new token type was added by the provider that Aurora does not yet map. Check
costs_calculation_caveaton recent rows.
Cost is consistently higher than the invoice
Check whether the upstream has a free tier, a discount program, or a committed-use rate that Aurora is not modeling. Set a per-model provider_overrides entry to match the real rate you pay.
Related
- Model Pricing — how to set, override, and import pricing
- Observability — usage analytics, audit logs, the dashboard
Usagepage - Admin API: Usage — every admin endpoint that touches usage and cost
- Storage — how the usage store is persisted across SQLite, PostgreSQL, and MongoDB
- Configuration — full env-var reference