Spaces:
Runtime error
Runtime error
File size: 12,799 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 | ---
title: "Usage, Quota & Spend Tracking"
version: 3.8.16
lastUpdated: 2026-06-08
---
# Usage, Quota & Spend Tracking
> **TL;DR**: OmniRoute tracks every request's token usage, computes cost, enforces per-API-key quota, and surfaces analytics in the dashboard. This guide explains how it all works.
**Sources:**
- `open-sse/services/usage.ts` (~70KB) β main usage tracking
- `src/lib/usageAnalytics.ts` (~10KB) β aggregation for dashboard
- `src/lib/db/quotaSnapshots.ts` β historical quota data
- `src/lib/db/usage*.ts` β multiple usage-related DB modules
---
## Overview
Every request that flows through OmniRoute generates a **usage record** that captures:
- **Identity**: which API key, provider, model, combo
- **Tokens**: prompt tokens, completion tokens, cached tokens, total
- **Cost**: USD amount (computed from pricing data)
- **Timing**: latency, start/end timestamps
- **Status**: success, error, rate-limited, etc.
These records are aggregated into **analytics**, persisted as **quota snapshots**, and used to enforce **per-key budget limits**.
```
Request βββΆ chatCore βββΆ usage.record() βββΆ SQLite
β
βββββββββΌββββββββ
βΌ βΌ βΌ
analytics quota billing
(dashboard) (enforce) (export)
```
---
## What Gets Recorded
The `usage.ts` service captures a **usage event** for every request:
| Field | Type | Source |
|-------|------|--------|
| `id` | string | UUID generated on record |
| `apiKeyId` | string | The API key that initiated the request |
| `provider` | string | Provider ID (openai, anthropic, etc.) |
| `model` | string | Model ID (gpt-5, claude-opus-4-6, etc.) |
| `comboId` | string? | Combo ID if routed through a combo |
| `promptTokens` | number | From upstream response |
| `completionTokens` | number | From upstream response |
| `cachedTokens` | number | Cache hit tokens (Anthropic prompt caching, etc.) |
| `totalTokens` | number | prompt + completion |
| `costUsd` | number | Computed from pricing data |
| `latencyMs` | number | End-to-end request duration |
| `status` | enum | `success`, `error`, `rate_limited`, `timeout`, `cancelled` |
| `errorClass` | string? | Error class if status != success |
| `timestamp` | string | ISO 8601 UTC |
| `metadata` | object | Custom plugin-injected data |
### Where Tokens Come From
Tokens are extracted from the upstream provider's response in the **response handler**:
```ts
// From open-sse/handlers/chatCore.ts
const response = await providerExecutor.execute(provider, request);
const usage = response.usage || {
prompt_tokens: 0,
completion_tokens: 0,
cached_tokens: 0,
};
```
For providers that don't return usage (some web-cookie providers), OmniRoute **estimates** tokens using a `~4 chars per token` heuristic (see `open-sse/services/autoCombo/pipelineRouter.ts`).
### Cached Tokens
OmniRoute tracks `cached_tokens` separately from `prompt_tokens` because:
- Anthropic prompt caching charges a reduced rate for cached tokens (10% of normal)
- Some providers return `cache_read_input_tokens` that should be priced differently
- Analytics can show the **cache hit rate** = `cached_tokens / prompt_tokens`
---
## Cost Calculation
Costs are computed from **pricing data** synced from LiteLLM (`src/lib/pricingSync.ts`):
| Model | Input $/1M | Output $/1M | Cached $/1M |
|-------|-----------|-------------|-------------|
| gpt-5 | $2.50 | $10.00 | β |
| claude-opus-4-6 | $15.00 | $75.00 | $1.50 |
| claude-sonnet-4-5 | $3.00 | $15.00 | $0.30 |
| gemini-2.5-pro | $1.25 | $10.00 | β |
The cost formula (`src/lib/usage/costCalculator.ts`):
```ts
cost = (prompt_tokens - cached_tokens) * input_price
+ cached_tokens * cached_price
+ completion_tokens * output_price
```
> **Why subtract cached from prompt?** The cached portion is priced separately; charging input price on the whole prompt would over-count.
### Pricing Sync
Pricing data is auto-synced from LiteLLM via the `/api/pricing/sync` endpoint (triggered by the built-in cron task, not a user-facing env var):
```bash
# Manual trigger
curl -X POST http://localhost:20128/api/pricing/sync
```
For models with no pricing data, OmniRoute falls back to **estimating cost** using internal average rates (sourced from LiteLLM's pricing data).
---
## Date Range Aggregation
The `usageAnalytics.ts` module computes dashboard widgets from raw usage data. It supports 7 time ranges:
| Range | Window | Use case |
|-------|--------|----------|
| `1d` | Last 24 hours | Hourly cost spike detection |
| `7d` | Last 7 days | Weekly review |
| `30d` | Last 30 days | Monthly billing |
| `90d` | Last 90 days | Quarterly analysis |
| `ytd` | Since Jan 1 of current year | Annual budget tracking |
| `all` | All time | Lifetime stats |
| `custom` | User-defined start/end | Audits, ad-hoc queries |
### Dashboard Widgets Computed
For any date range, the analytics layer computes:
| Widget | Description |
|--------|-------------|
| **Summary cards** | Total requests, total cost, total tokens, success rate |
| **Daily trend chart** | Cost + tokens per day, stacked by model |
| **Activity heatmap** | Hour-of-day Γ day-of-week grid, color = request count |
| **Model breakdown** | Pie chart of cost by model |
| **Provider breakdown** | Bar chart of requests by provider |
| **Top API keys** | Table of top 10 keys by cost |
| **Error analysis** | Error rate over time, top error classes |
### Programmatic Access
```ts
import { computeAnalytics } from "@/lib/usageAnalytics";
const analytics = await computeAnalytics(
history, // usage history records
"7d", // time range: "1d" | "7d" | "30d" | "90d" | "ytd" | "all" | "custom"
connectionMap, // provider connection map (connectionId β account name)
{
startDate: "2025-01-01", // optional: for "custom" range
endDate: "2025-06-01", // optional: for "custom" range
}
);
console.log(analytics.summary.totalCost); // 12.34 (cents)
console.log(analytics.byModel[0]); // { model, cost, requests, promptTokens, completionTokens }
---
## Quota Enforcement
Per-API-key quota is enforced in two places:
1. **Soft limit** (`quotaWarnAt`): dashboard warning when usage exceeds threshold
2. **Hard limit** (`quotaLimit`): request rejected with HTTP 429 when exceeded
### Configuration
```ts
// Per API key
await updateApiKey(keyId, {
quotaWarnAt: 5_00, // $5.00 β show warning
quotaLimit: 10_00, // $10.00 β hard stop
quotaWindow: "month", // "day" | "week" | "month" | "all"
});
```
### Enforcement Flow
```
Request βββΆ quotaCheck()
β
βββ Within limit? βββΆ allow
β
βββ Over limit? βββΆ 429 Too Many Requests
with Retry-After header
```
### Quota Snapshots
`quotaSnapshots` table stores **historical quota state** for trend analysis:
| Field | Description |
|-------|-------------|
| `apiKeyId` | The key being tracked |
| `window` | "day" | "week" | "month" |
| `used` | Cost used in this window (cents) |
| `limit` | The limit (cents) |
| `resetAt` | When the window resets |
| `createdAt` | When the snapshot was taken |
Snapshots are taken **on every request** that uses > 0 cost, and used to:
- Render the quota progress bar in the dashboard
- Show 30-day quota trend charts
- Trigger alerts when usage approaches the limit
---
## REST API
### List Usage Records
```bash
GET /api/usage?range=7d&limit=100
GET /api/usage?apiKeyId=key-123&range=30d
GET /api/usage?provider=openai&range=1d
```
Response:
```json
{
"records": [
{
"id": "uuid",
"apiKeyId": "key-123",
"provider": "openai",
"model": "gpt-5",
"promptTokens": 1234,
"completionTokens": 567,
"totalTokens": 1801,
"costUsd": 0.0050,
"latencyMs": 1234,
"status": "success",
"timestamp": "2026-06-08T12:00:00Z"
}
],
"total": 1234,
"nextCursor": "..."
}
```
### Get Analytics Summary
```bash
GET /api/usage/analytics?range=7d&groupBy=model
```
Response:
```json
{
"summary": {
"totalCost": 12.34,
"totalRequests": 5678,
"totalTokens": 12345678,
"successRate": 0.987,
"avgLatencyMs": 1234
},
"models": [
{ "model": "gpt-5", "cost": 8.50, "requests": 1234, "tokens": 4567890 },
{ "model": "claude-opus-4-6", "cost": 3.84, "requests": 234, "tokens": 234567 }
],
"daily": [
{ "date": "2026-06-01", "cost": 1.50, "requests": 800 },
{ "date": "2026-06-02", "cost": 2.00, "requests": 1000 }
]
}
```
### Query Usage Analytics
Usage data is accessed via the dashboard or MCP tools, not direct REST export endpoints. Available analytics:
- **`/api/usage/analytics`** β aggregated usage metrics (group by model, provider, key)
- **`/api/usage/quota`** β current quota status per API key
- **`/api/usage/history`** β request history logs
---
## MCP Tools
Two MCP tools expose usage data to agents (see `open-sse/mcp-server/tools/`):
| Tool | Description |
|------|-------------|
| `omniroute_cost_report` | Generates a per-key cost report for a given period |
| `omniroute_check_quota` | Returns current quota status for an API key |
Example agent invocation:
```json
{
"tool": "omniroute_cost_report",
"args": { "period": "week" }
}
```
---
## Retention and Cleanup
Usage data grows ~1-10KB per request. At scale, this can be significant.
### Retention Settings
Usage history retention is configured via the Database Settings in the UI or via `/api/settings/database`.
By default, usage history is retained for **90 days**.
### Cleanup
Old records are cleaned up by `src/lib/db/cleanup.ts`:
- Triggered by the background cron process
- Deletes records from `usage_history` older than the configured `usageHistory` retention setting
### Storage Estimation
| Request rate | 30-day storage | 90-day storage |
|--------------|----------------|----------------|
| 100 req/day | ~3MB | ~9MB |
| 1,000 req/day | ~30MB | ~90MB |
| 10,000 req/day | ~300MB | ~900MB |
| 100,000 req/day | ~3GB | ~9GB |
For very high traffic, consider:
- Reducing the retention period via Database Settings
- Using `aggregated_metrics` instead of raw records (only for analytics)
---
## Cost Optimization Tips
### 1. Use the Right Model
```bash
# Quick answer β use cheap + fast
curl -d '{"model":"auto/fast","messages":[...]}'
# Complex task β use quality
curl -d '{"model":"auto/smart","messages":[...]}'
```
### 2. Enable Caching
Anthropic prompt caching saves **90% on repeated context**:
```ts
// The caching is automatic β just include the same large system prompt
const response = await openai.chat({
model: "claude-sonnet-4-5",
system: longSystemPrompt, // Will be cached automatically
messages: [{ role: "user", content: "..." }]
});
```
### 3. Use Compression
RTK + Caveman compression saves **15-95% on tool-heavy sessions**:
```ts
const config = {
compression: {
engine: "rtk",
intensity: "aggressive"
}
};
```
### 4. Set Per-Key Quotas
Always set `quotaLimit` to prevent runaway costs:
```ts
await updateApiKey(keyId, { quotaLimit: 10_00 }); // $10/month cap
```
### 5. Audit Top Consumers
Use the dashboard or **`/api/usage/analytics`** to group by API key and sort by cost:
```bash
GET /api/usage/analytics?groupBy=apiKey
```
---
## Troubleshooting
### "Cost is higher than expected"
1. Check **`/api/usage/analytics?groupBy=model`** β find the expensive model
2. Check **`/api/usage/analytics?groupBy=apiKey`** β find the heavy consumer
3. Verify pricing data is up to date: `POST /api/pricing/sync`
### "Records missing"
- Check DB retention settings under Dashboard β Database β Cleanup β old records are deleted by the periodic cleanup task (`src/lib/db/cleanup.ts`)
- Check for errors in `src/lib/db/usage*.ts` β DB write failures are logged but not surfaced
- Verify the request actually reached `chatCore` β check combo routing
### "Quota not enforcing"
- Check the key's `quotaLimit` setting
- Verify `quotaWindow` is set correctly
- Look for `quotaSnapshots` records β they should be created on every request
---
## See Also
- [DATABASE_GUIDE.md](../ops/DATABASE_GUIDE.md) β Schema for usage tables
- [ENVIRONMENT.md](../reference/ENVIRONMENT.md#18-pricing-sync) β pricing sync env vars
- [AUTO-COMBO.md](../routing/AUTO-COMBO.md) β How `auto/fast`, `auto/cheap` reduce cost
- [API_REFERENCE.md](../reference/API_REFERENCE.md) β Full `/api/usage/*` reference
- Source: `open-sse/services/usage.ts`, `src/lib/usageAnalytics.ts`, `src/lib/db/usage*.ts`
|