Task 06 — Usage recorder
Depends on: 01, 05
Unblocks: 07, 08, 09, 10
Goal
Single-call API that writes one usage_events row and atomically updates the
usage_daily aggregate.
Scope
New file src/lib/usage-recorder.ts:
export interface RecordUsageInput {
account: Account
modelId: string
endpoint: 'chat.completions' | 'messages' | 'embeddings'
upstreamFormat: 'openai' | 'anthropic'
isStreaming: boolean
usage: NormalizedUsage
durationMs: number
status: 'ok' | 'error' | 'aborted'
requestId?: string
isInternal?: boolean // gate from x-internal-pricing-sync
}
export function recordUsage(input: RecordUsageInput): void
Behavior:
- If
isInternal === true, return immediately (no insert).
- Look up current
model_pricing row by modelId. Missing → all six
*_price_snapshot columns are NULL. premiumMultiplier likewise → 0.
- Compute
premium_request_count = 1 * (premiumMultiplier ?? 0).
- In one transaction:
INSERT INTO usage_events (...).
INSERT INTO usage_daily (...) ON CONFLICT(day, account_name, model_id, endpoint) DO UPDATE SET req_count = req_count + 1, input_tokens = input_tokens + excluded.input_tokens, ....
- Wrap the entire body in
try/catch; recorder errors must NOT propagate.
Log via consola.error.
Day computation
date(?ts/1000, 'unixepoch', 'localtime')
Use SQLite's expression so the boundary matches the user's local timezone
without dragging Node's Intl into the hot path.
Definition of Done
Task 06 — Usage recorder
Depends on: 01, 05
Unblocks: 07, 08, 09, 10
Goal
Single-call API that writes one
usage_eventsrow and atomically updates theusage_dailyaggregate.Scope
New file
src/lib/usage-recorder.ts:Behavior:
isInternal === true, return immediately (no insert).model_pricingrow bymodelId. Missing → all six*_price_snapshotcolumns are NULL.premiumMultiplierlikewise → 0.premium_request_count = 1 * (premiumMultiplier ?? 0).INSERT INTO usage_events (...).INSERT INTO usage_daily (...) ON CONFLICT(day, account_name, model_id, endpoint) DO UPDATE SET req_count = req_count + 1, input_tokens = input_tokens + excluded.input_tokens, ....try/catch; recorder errors must NOT propagate.Log via
consola.error.Day computation
Use SQLite's expression so the boundary matches the user's local timezone
without dragging Node's
Intlinto the hot path.Definition of Done
1 daily row with the right counts.
(day, account, model, endpoint)increments the daily row.model_pricingrow → snapshots are NULL, no throw.isInternal: true→ 0 rows inserted.docs/tasks/06-usage-recorder.mddocs/design/