ADR-029: Usage Analytics Dashboard
- Status
-
Accepted (amended; current contract below)
- Date
-
2026-06-01
- Amended
- Authors
-
Netresearch DTT GmbH
Note
Current contract (reviewed 2026-11-07): The V1 exclusion of specialized services is historical. Specialized usage and pricing have a separate lifecycle; see Usage analytics.
Streaming now records estimated usage and telemetry, including partial output under the settlement rules. It does not supply provider token counts.
The decision text below records its original context; the linked successor explains the changed contract.
Context
tx_ has recorded request counts and token totals
per service type and provider since day one, and the per-request cost
column (estimated_) existed from the start. The plumbing to fill
it never did: Usage always passed a null cost,
Model:: had zero callers, and so every row carried
estimated_. The downstream effect was visible — the
AI cost this month dashboard widget (see ADR-024: Dashboard Widgets) summed a column
that was structurally always zero and showed $0 regardless of real
spend.
The table also had no model dimension. Usage could be sliced by provider
and service type, but not by the specific model that produced it, so a
gpt- call and a gpt- call against the same provider were
indistinguishable in the data — even though their pricing differs by an
order of magnitude.
Reporting itself was thin. The only at-a-glance surfaces were the two global dashboard widgets from ADR-024: Dashboard Widgets; there was no dedicated view that combined cost trends, model-level breakdowns, and per-user consumption. With usage now flowing through the middleware pipeline (ADR-026: Provider Middleware Pipeline), there is a single, well-defined place to compute cost as a side effect of every productive provider call.
Decision
Ship a read-only usage analytics module backed by a richer usage table and real cost computation:
- Schema. Add
model_,uid model_,id prompt_, andtokens completion_totokens tx_. Daily granularity is kept — rows still aggregate per day — andnrllm_ service_ usage model_joins the aggregation key (alongsideuid service_,type service_, andprovider request_) so model-level usage rolls up without a second write per request.date - Cost computation.
Usagenow derivesMiddleware estimated_from the configuration'scost Modelpricing viaModel::, using the prompt/completion token split recorded on the usage object. Pricing is stored as cents-per-1M tokens; the estimate is the per-side token count times its rate. When a caller already supplies a cost it is preserved; otherwise the model-derived value is recorded. This fixes the long-standing always-zero-cost defect.estimate Cost () - Read layer. Add
Usage, a read-only reporting service over the usage table. It exposes KPI totals (Analytics Service get), a daily cost/requests trend with filled gaps (Kpi Totals get), breakdowns by provider, model, and service (Daily Trend get/Breakdown By Provider get/Breakdown By Model get), and per-user usage with this-month budget consumption (Breakdown By Service get). A smallPer User Usage Analyticsvalue object normalizes the date-range presetsPeriod 7d/30d/90d/monthand defaults unknown values to30d. - Backend submodule. Register
nrllm_as an admin-only child of the main LLM module (Admin Tools > LLM > Analytics), driven byanalytics Analyticsand a Fluid template: KPI tiles, a cost-plus-requests trend line, provider / model / service breakdown bar charts, and a per-user table with monthly-budget bars. The active range is a plainController ?range=GET parameter — the page is a full reload with no AJAX. Charts render with Chart.js (vendored underResources/).Public/ Java Script/ Vendor/ - Demo data. Ship a dev-only
ddev seed-generator that populates roughly 90 days of realistic historic usage so the module and widgets have something to show during local development.usage
Consequences
Positive:
- ●● Real cost reporting.
estimated_reflects actual model pricing, so the AI cost this month widget (ADR-024: Dashboard Widgets) and the new module both show real figures instead ofcost $0. - ● Model-level breakdowns. The added
model_/uid model_columns let usage and cost be sliced per model, not just per provider.id - ◐ A single dedicated reporting surface combines trend, breakdowns, and per-user consumption that previously had no home.
Negative:
- ◑ One extra write column-set per request (
model_,uid model_,id prompt_,tokens completion_). Negligible — the row was already being written; this widens it, it does not add a second write.tokens - ✕ Specialized-service cost and streaming usage are out of scope for v1
and documented as such. DALL·E / TTS / Whisper / DeepL still record
requests and units but their cost stays
0(no token-based pricing model yet), and streaming responses are skipped by the usage middleware because chunked output has no single terminal token count to price. - ◑ No backfill of pre-migration rows. Rows written before the schema
change keep
model_anduid = 0 estimated_; analytics only reflect cost from the migration forward.cost = 0
Net Score: +3 (Positive)
Alternatives considered
- Per-request (non-aggregated) rows to enable arbitrary slicing.
Rejected — daily aggregation keyed on
service_keeps the table small and the existing widget queries fast; the model dimension is the only slice that was actually missing.type / service_ provider / request_ date / model_ uid - Compute cost lazily in the read layer from stored token counts and
current model pricing. Rejected — pricing drifts over time, so cost must
be captured at call time against the pricing in effect then. Storing
estimated_at write time is the durable record.cost - A third dashboard widget instead of a dedicated module. Rejected — the dashboard widget shapes (ADR-024: Dashboard Widgets) cannot host a trend line, multiple breakdown charts, and a per-user table together; those belong in a full module view.