ADR-052: Usage attribution honours the caller-supplied beUserUid
- Status
-
Accepted
- Date
-
2026-07-12
- Authors
-
Netresearch DTT GmbH
Context
Every option object carries with
(Budget), and the manager forwards that uid as
pipeline metadata (Budget), where
Budget uses it for per-user budget enforcement. Usage
attribution, however, ignored it: Usage always
read the ambient backend. context aspect to fill the
be_ column.
For backend-module calls the two sources agree. For every caller
outside a backend-user request — frontend plugins, Messenger/CLI
workers, scheduler tasks — they do not: the aspect resolves to 0,
so usage lands in the anonymous bucket even when the caller passed an
explicit uid. Downstream extensions worked around this by
impersonating a technical backend user for the duration of a call —
swapping the backend. aspect (and restoring it in a
finally) purely so the usage row gets the right be_.
nr_'s Backend is such a
workaround, wrapped around every RAG chat call. Enforcement and
attribution also disagreed with each other: the budget gate charged
the option-supplied user while the usage row credited the ambient one.
Decision
The caller-supplied uid wins; the ambient aspect stays the fallback.
Usagegains an optional trailingTracker Service Interface:: track Usage () ?int $beparameter.User Uid = null nullpreserves the previous behaviour (ambientbackend.aspect,user 0when unauthenticated).UsagereadsMiddleware Budgetfrom the pipeline context — the same key the budget gate reads — and passes it through, so enforcement and attribution can no longer disagree.Middleware:: METADATA_ BE_ USER_ UID
Consequences
- A consumer that already sets
withgets correct attribution in frontend/CLI contexts with no further wiring; the aspect-swap workaround becomes unnecessary for usage tracking.Be User Uid () - Backend-module calls are unaffected: they set no option uid, and the ambient fallback resolves the same user as before.
Usageimplementers must add the new parameter (semver-minor breaking in the 0.x line, same policy asTracker Service Interface Toolin 0.15.0). In-repo,Interface:: get Group () Usageis the only implementation.Tracker Service - The specialized translator path forwards the uid even though it
bypasses the middleware pipeline:
Translationre-attaches the resolved uid to the options array it hands toService Translatorimplementations (theInterface bekey — budget fields are deliberately excluded fromUser Uid Translation), andOptions:: to Array () Deep/LTranslator Llmpass it on toTranslator track. The key is attribution metadata only; translators never send it to the remote API.Usage () Llmadditionally threads the uid into theTranslator Chatof its underlying chat calls (translation and language detection), so the pipeline-recorded chat row — which carries the tokens and cost — lands under the sameOptions be_as the translation-level row, anduser Budgetenforces the caller's budget on those calls. A directMiddleware Translatorcall has no options parameter and stays ambient.Interface:: detect Language () - The speech and image services were initially deferred and are now
covered by ADR-057:
Transcription,Options SpeechandSynthesis Options ImageimplementGeneration Options Budget,Aware Options Interface Falreads the documentedImage Service bearray key, and all four services forward the uid to theirUser Uid trackcalls. Attribution only — those services bypass the middleware pipeline, so no budget enforcement happens there.Usage ()