Zum Hauptinhalt springen

Cross-surface AI-spend monitoring, served on the admin Cost tab (/admin/ai/cost). One table merges token usage from every AI surface and prices it from a reference rate table — derived at read, never charged, never faked.

The dollars are a reference at standard pay-as-you-go rates. Our keys run free-tier, so the real charge is $0. Same honesty stance as the provider quota board and the image reader cost panel.


What it is

/admin/ai/cost is the 6th AI admin sub-tab: Overview · Models · Usage · Cost · nRAG · Tools. It answers one question — "how many tokens did each model burn across the whole app, and roughly what would that cost?" — without ever implying a real bill.

The tab holds three things:

Block Source Shows
Usage by Model (30 days) chat + image telemetry, merged per-(surface, model) token counts + reference cost
Image Metadata Reader health image telemetry analysis KPIs + a binary save/abandon funnel
Image Analyses per Day image telemetry 30-day volume bar chart (VolumeBarChart.svelte, shared with the Usage tab)

Relocation: the chatbot per-model token table once lived on the Models tab. It now lives here, widened to cover both surfaces. The Models tab is provider routing/config only.


The cross-surface merge

Two telemetry tables feed the usage table, and they stay independent:

Surface Table Counts a "call" as
chat ai.conversation_step a step
image image.ai_proposal an analysis

The merge is app-layer, never SQL. buildUnifiedModelUsage(chatRows, imageRows) in src/lib/server/ai/usage-summary.ts is the only place that knows both tables exist — no UNION, no DB view, no shared schema. Each surface keeps its own query module; the seam is one pure function.

Rows are keyed on (surface, modelId). The same model can appear twice — once per surface — with different token profiles (a chat step and a vision analysis spend very differently). Busiest rows sort first; rows with no reported tokens sort last.

Columns: Surface · Model · Provider · Input · Output · Thinking · Total · Calls · Ref. cost, plus a <tfoot> totals row. Calls carries its unit (steps vs analyses) and is therefore not summed in the footer — the two units don't add up.


Honesty model

Three rules make a misleading number structurally impossible.

Cost is reference, derived, never stored

Each row's cost comes from estimateCost(modelId, tokens) in pricing.ts, keyed by modelId (vision-token cost differs ~25× across models on one provider). The estimate is computed at read from the versioned price map — token counts are the only stored fact. CostEstimate.kind: 'reference' is the machine-readable honesty bit; the summary's costKind is always 'reference'.

Partial coverage shows "—", never a fake $0

Not every model is priced — the Groq llama-3.3-70b-versatile chat model and the unknown bucket have no entry. estimateCost returns null for them, and the row renders "—", never $0.

The summary carries costCoverage: 'full' | 'partial' | 'none' and pricedRowCount. The totals row sums cost over priced rows only, and a caption states the coverage: "Cost summed over N of M priced rows — unpriced models show '—', rates as of {asOf}." A grand total that silently drops unpriced models can't masquerade as complete.

Token Total = Input + Output only

"Thinking"/reasoning tokens are a subset of output, already inside Total. They get their own column for transparency but are never added on top — doing so double-counts reasoning-heavy models by ~40%. The footer's Thinking cell is intentionally "—".


Image Metadata Reader health

A heartbeat block for the image showcase, on the same tab.

KPI tiles: successful analyses (30d), analyses today, distinct images (30d), last analysis (relative time).

Binary conversion funnel — saved vs abandoned. This is conversion, not approval quality. The code only ever writes metadata status='approved' at save time; the rejected / draft / proposed enum values exist but are never written, so there is no reject path to measure. "Abandoned" = an image was analyzed but no metadata record was saved.

Failure visibility caveat. Failed analyses (provider error, timeout, unreadable image) are not recorded. Counts are therefore labelled "successful analyses" — there is no true success rate. The tab states this limitation inline; treat the numbers as a floor.


Overview-tab heartbeat

The Overview tab gains a small Image Metadata Reader card (analysis counts) linking here. It deliberately shows no dollar figure — money stays off the glance layer so a $ next to chat counts can't be misread as "total AI spend." Dollars live only on the Cost tab, always captioned as reference.


Module map

Piece Location Role
Merge seam src/lib/server/ai/usage-summary.ts buildUnifiedModelUsage() — app-layer union + honest summary
Price table src/lib/server/ai/pricing.ts MODEL_PRICES + estimateCost(); reference, server-only
Chat usage query src/lib/server/db/ai/admin-queries.ts getModelUsage() — adds providerId, nullable token sums
Image usage queries src/lib/server/db/ai/image-metadata-queries.ts getImageModelUsage / getImageVolumeByDay / getImageConversionFunnel / getImageUsageKpis
Client DTOs src/lib/schemas/admin/model-usage.ts UnifiedModelUsageRow, ModelUsageSummary, AiSurface
Volume chart src/lib/components/admin/ai/VolumeBarChart.svelte Shared by Usage + Cost tabs
Page src/routes/[[locale=locale]]/admin/ai/cost/+page.svelte The tab

A DB index image_ai_proposal_created_model_idx (created_at DESC, model_id) backs the per-model and per-day image rollups.


← Back to Blueprint

Geht dieses Pattern noch besser? Sag uns, wie.

Feedback geben