ADR-178: Caller source on the cost path
- Status
-
Accepted
- Date
-
2026-08-19
- Authors
-
Netresearch DTT GmbH
- Amends
-
ADR-177 (its consequence that analytics aggregation by source is out of scope, and its confinement of the caller identity to the telemetry table)
Context
ADR-177 gave a caller a way to name itself and persisted that name on the telemetry row. It deliberately stopped there: "analytics aggregation by source is not part of this record; it can follow as UI work once rows carry the columns."
The question the attribution exists to answer turns out to be a cost
question: which extension spends what. That number does not live in
telemetry. Telemetry is the per-call observability log (ADR-063 retention window, purged); the money is aggregated daily in
tx_nrllm_service_usage by UsageTrackerService, and every analytics
figure — the KPI totals, the provider and model breakdowns, the per-user
report — reads that table alone. A source column on telemetry therefore
cannot be joined to a cost, and telemetry retention would truncate the
answer anyway.
The follow-up work is not "UI work" as ADR-177 assumed. It is a second persistence decision.
Decision
The caller identity travels the cost path as well: source_extension
becomes a column on tx_nrllm_service_usage and part of that table's
daily aggregation key, so two extensions calling the same model on the same
day produce two rows instead of one.
UsageMiddleware reads the identity from the same request metadata
TelemetryMiddleware reads, so both write-sites are fed from one source
and cannot drift apart. UsageTrackerServiceInterface::trackUsage()
grows one optional trailing parameter — additive on the @api surface,
defaulting to '' so every existing caller keeps compiling and keeps
writing the unattributed row it writes today.
Only the extension key is carried, not the operation. The operation stays a telemetry detail: it multiplies the aggregation key without answering the cost question, and a per-operation cost breakdown is a report, not a dashboard axis.
The Analytics module gains a "By extension" breakdown next to provider and model — same table, same query shape, same chart. Unattributed usage (everything not annotated, which is the normal case for a wizard task or a scheduler run) groups under an explicit label rather than being hidden or silently folded into another bucket.
Alternatives
- Join telemetry to usage — no shared key exists (usage is a daily aggregate, telemetry is per call), and telemetry is purged on its own retention schedule, so the join would lose history.
- Cost estimated from telemetry token counts — recomputing money
outside
UsageMiddlewareduplicates the pricing logic and would drift from the figure the budget enforcement uses. - Carrying the operation too — a finer aggregation key on the cost table for a breakdown nobody asked for; can be added later, cannot be taken back cheaply once rows exist.
Consequences
- Per-extension cost, requests and tokens are answerable from the same table and the same range the rest of the dashboard uses.
- Aggregation granularity on
tx_nrllm_service_usagegrows: an install where several extensions use one model sees more rows per day. Bounded by the number of distinct callers, which is the number of installed integrations. - Rows written before this change carry
''and appear as unattributed — no migration invents an origin for them. - Attribution stays honest but unauthenticated (ADR-177): a caller can claim any name, so the breakdown is an inventory, not an accounting control.
api-surface.txtgrows additively (one optional parameter).