Files
2026-07-13 13:39:12 +08:00

262 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Cost & Spend Tracking"
version: 3.8.40
lastUpdated: 2026-06-28
---
# Cost & Spend Tracking
How OmniRoute estimates, records, and reports the cost of every request — and why the
dashboard number is a **savings tracker**, not a bill.
See also: [User Guide](./USER_GUIDE.md) · [Features Gallery](./FEATURES.md)
---
## What it is (and what it is not)
OmniRoute attributes a per-request USD cost to every completion by multiplying token
counts by a model's pricing rates. These numbers power the **Costs** dashboard, the
`omniroute cost` / `omniroute usage` CLI, CSV/JSON exports, and per-API-key budgets.
> **The dashboard "cost" is a savings tracker, not a bill.** OmniRoute never charges you
> — it routes your requests to providers you have already connected (your own
> subscriptions, free tiers, and API keys). A "$290 total cost" accrued entirely on free
> models means roughly **$290 you did _not_ pay** a paid API. The figure is an _estimate_
> of what the same traffic would have cost at standard list prices, so you can see where
> your usage is concentrated and how much routing to cheaper/free providers is saving you.
This framing is stated directly in the project [README](../../README.md) ("the dashboard
'cost' is a savings tracker, not a bill").
Because the number is an estimate:
- It depends on the pricing table OmniRoute has for each model. A model with no pricing
entry contributes `0` cost (it shows as a "Legacy / Free" row in the explorer).
- Free-tier and subscription traffic still accrues an _estimated_ cost — that is the
amount you are saving, not an amount owed.
---
## How costs are estimated
### The pricing source
Costs come from a pricing table resolved in this precedence order
([`src/lib/pricingSync.ts`](../../src/lib/pricingSync.ts)):
1. **User overrides** — prices you set in the dashboard / via `PATCH /api/pricing`.
2. **Synced external pricing** — fetched from LiteLLM's public
`model_prices_and_context_window.json` when sync is enabled (stored in a separate
`pricing_synced` namespace so it never clobbers your overrides).
3. **Hardcoded defaults** — shipped with OmniRoute.
External pricing sync is **opt-in**, disabled by default. Relevant env vars
(see [`.env.example`](../../.env.example)):
| Env var | Default | Purpose |
| ----------------------- | --------- | ---------------------------------------------------------------- |
| `PRICING_SYNC_ENABLED` | `false` | Enable the background LiteLLM pricing sync at startup. |
| `PRICING_SYNC_INTERVAL` | `86400` | Sync interval in **seconds** (default daily). |
| `PRICING_SYNC_SOURCES` | `litellm` | Comma-separated source list (only `litellm` is supported today). |
### The cost formula
Cost is computed per request from token counts and per-million-token rates in
[`src/lib/usage/costCalculator.ts`](../../src/lib/usage/costCalculator.ts)
(`computeCostFromPricing` / `calculateCost`):
- **Input tokens** (minus cache reads and cache-creation tokens) × `input` rate.
- **Cache-read tokens** × `cached` rate (falls back to the input rate).
- **Cache-creation tokens** × `cache_creation` rate (falls back to the input rate).
- **Output tokens** × `output` rate.
- **Reasoning tokens** × `reasoning` rate (falls back to the output rate).
All rates are interpreted as USD per 1,000,000 tokens. A Codex "fast"/"priority" or
"flex" service tier applies a cost multiplier (`getCodexFastCostMultiplier`) — e.g. flex
is billed at a 50% token discount, surfaced as **flex savings** in the dashboard.
Model names are normalized first (provider-path prefixes such as `openai/` or
`accounts/fireworks/models/` are stripped) so historical rows still match a price.
### How spend is recorded
- The per-request cost is computed after the response and recorded fire-and-forget so it
never adds latency to the client. Shared-quota consumption is scheduled on the next
event-loop tick via [`src/lib/quota/spendRecorder.ts`](../../src/lib/quota/spendRecorder.ts).
- API-key spend is buffered and flushed in batches by the
[`SpendBatchWriter`](../../src/lib/spend/batchWriter.ts) (default 60s flush interval,
1,000-entry buffer). Tunable via:
| Env var | Default | Purpose |
| ----------------------------------- | ------- | ---------------------------------- |
| `OMNIROUTE_SPEND_FLUSH_INTERVAL_MS` | `60000` | Flush interval in milliseconds. |
| `OMNIROUTE_SPEND_MAX_BUFFER_SIZE` | `1000` | Max buffered entries before flush. |
The dashboard's cost figures are **not** read from a stored per-row dollar amount — they
are recomputed on the fly from token counts and the current pricing table each time the
analytics endpoint runs. That means correcting a wrong price (and re-syncing) updates
historical cost estimates retroactively.
---
## Dashboard: the Costs page
The **Costs** page lives at `/dashboard/costs`
(`src/app/(dashboard)/dashboard/costs/`).
Its main view is the **Cost Overview** tab
(`src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx`),
which loads everything from `GET /api/usage/analytics`.
What it shows:
- **Spend tiles** — estimated spend for _Today (1d)_, _7d_, _30d_, and the selected
window. Range selector: `7d`, `30d`, `90d`, `all`.
- **Headline metrics** — requests in window, active providers, active models, average
cost per request.
- **Cost Explorer** — a sortable/filterable table grouped by **provider**, **model**,
**API key**, **account**, or **service tier**, with cost, requests, tokens, avg
cost/request, and share-of-total %.
- **Token usage** — total / input / output tokens and input:output ratio.
- **Routing efficiency** — fallback count, fallback rate, and requested-model coverage.
- **Monthly forecast** — projects month-end spend from the recent daily average.
- **Period comparison** — % change between the first and second half of the window.
- **Charts** — daily cost trend, provider share (pie), top providers, top models, cost
by API key, cost by account, weekly usage pattern, and an activity heatmap.
- **Export** — download the current window as **CSV** or **JSON** (the buttons appear
once there is non-zero cost data).
When there is no priced traffic, rows render as a "Legacy / Free" label instead of `$0`,
reflecting the savings-tracker model.
### Related Costs sub-pages
The Costs area also hosts (all under `/dashboard/costs/`):
- **Pricing** (`/dashboard/costs/pricing`) — view and override per-model prices (renders
the shared Pricing tab).
- **Budget** (`/dashboard/costs/budget`) — set per-scope spend limits (renders the shared
Budget tab).
- **Quota Share** (`/dashboard/costs/quota-share`) — shared-quota pools and burn-rate
views.
---
## API endpoints
All of these require management auth (loopback/JWT, via `requireManagementAuth`) unless
noted.
### Usage & cost analytics
| Method | Endpoint | Purpose |
| ------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/api/usage/analytics` | Full cost/usage analytics: summary, daily trend, by provider/model/API key/account/tier. Query: `range`, `startDate`, `endDate`, `apiKeyIds`, `presets`. |
| `GET` | `/api/usage/utilization` | Per-provider quota utilization over time. Query: `range` (`1h`/`24h`/`7d`/`30d`), `provider`. |
| `GET` | `/api/usage/history` | Raw usage history rows. |
| `GET` | `/api/usage/call-logs` | Per-request call logs (model, tokens, cost, latency, status). |
| `GET` | `/api/usage/quota` | Provider quota status. |
| `GET` | `/api/usage/proxy-logs` | Proxy request logs. |
### Budgets
| Method | Endpoint | Purpose |
| ------ | ------------------------ | ------------------------------------------------------------------------------ |
| `GET` | `/api/usage/budget` | Cost summary + budget check for one API key (`apiKeyId` query param required). |
| `POST` | `/api/usage/budget` | Set daily/weekly/monthly USD limits + warning threshold for an API key. |
| `GET` | `/api/usage/budget/bulk` | Bulk budget summaries across API keys. |
> The budget API is scoped per **API key** (`apiKeyId`). Limits returned by
> `GET /api/usage/budget` include `dailyLimitUsd`, `weeklyLimitUsd`, `monthlyLimitUsd`,
> a `warningThreshold`, and the running totals (`totalCostToday`, `totalCostMonth`, …).
### Pricing
| Method | Endpoint | Purpose |
| -------- | ----------------------- | ----------------------------------------------------------------------------------------------- |
| `GET` | `/api/pricing` | Current merged pricing (user + synced + defaults). `?includeSources=1` to see source per entry. |
| `PATCH` | `/api/pricing` | Override pricing for `{ provider: { model: { input, output, cached, … } } }`. |
| `DELETE` | `/api/pricing` | Reset pricing to defaults (optionally scoped by `?provider=&model=`). |
| `GET` | `/api/pricing/defaults` | Show default per-1M fallback rates. |
| `GET` | `/api/pricing/models` | Pricing keyed by model. |
| `POST` | `/api/pricing/sync` | Trigger a manual sync from external sources (LiteLLM). |
| `GET` | `/api/pricing/sync` | Current sync status. |
| `DELETE` | `/api/pricing/sync` | Clear all synced pricing data. |
### Other cost-relevant endpoints
| Method | Endpoint | Purpose |
| ------ | ----------------------------- | ----------------------------------------------------------------------- |
| `GET` | `/api/free-tier/summary` | Free-model token totals, used-this-month, and remaining free allowance. |
| `GET` | `/api/quota/pools/[id]/usage` | Usage for a shared-quota pool. |
---
## CLI
OmniRoute's CLI exposes cost, usage, and pricing commands (registered in
[`bin/cli/commands/registry.mjs`](../../bin/cli/commands/registry.mjs)).
### `omniroute cost`
A cost report aggregated from `/api/usage/analytics`.
```bash
omniroute cost # last 30d, grouped by provider
omniroute cost --period 7d # last 7 days
omniroute cost --group-by model # group by provider | model | combo | api-key | day
omniroute cost --since 2026-06-01 --until 2026-06-13
omniroute cost --api-key <key> --limit 50
```
Columns: group, requests, tokens in/out, cost (USD), and % of total. A grand total line
is printed at the end (suppressed with `--quiet` or `--output json`).
### `omniroute usage`
```bash
omniroute usage analytics --period 30d [--provider <id>] # per-provider cost summary
omniroute usage logs [--limit 100] [--follow] [--api-key <k>] [--search <q>]
omniroute usage quota [--provider <id>] [--check]
omniroute usage utilization [--api-key <k>]
omniroute usage history [--limit 100]
omniroute usage proxy-logs [--limit 100]
# Budgets
omniroute usage budget list
omniroute usage budget get [scope]
omniroute usage budget set <amount> [--scope global] [--period monthly]
omniroute usage budget reset [scope]
```
### `omniroute pricing`
```bash
omniroute pricing list [--provider <p>] [--model <m>] [--limit 200]
omniroute pricing get <model>
omniroute pricing sync [--provider <p>] [--force] # POST /api/pricing/sync
omniroute pricing diff [--model <m>]
omniroute pricing defaults show
omniroute pricing defaults set [--input <p>] [--output <p>] [--cache-read <p>] [--cache-write <p>]
```
> `pricing defaults show` reads `GET /api/pricing/defaults`. To edit individual model
> prices instead, use the **Pricing** dashboard page or `PATCH /api/pricing`.
---
## Troubleshooting
- **All costs show $0 / "Legacy / Free".** The models in use have no pricing entry.
Enable external sync (`PRICING_SYNC_ENABLED=true`) and run `omniroute pricing sync`, or
set prices manually via the Pricing page / `PATCH /api/pricing`.
- **A historical model is mispriced.** Fix the price (override or re-sync) — cost is
recomputed from token counts on every analytics read, so estimates update retroactively.
- **Spend lags behind real time.** Per-key spend is batched; lower
`OMNIROUTE_SPEND_FLUSH_INTERVAL_MS` if you need fresher numbers.
---
For where this fits in the broader dashboard, see the [User Guide](./USER_GUIDE.md) and
the [Features Gallery](./FEATURES.md).