---
title: "ADR-052: Usage attribution honours the caller-supplied beUserUid"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-052@0.35"
source: "Adr/Adr052UsageAttributionViaOptions.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-052: Usage attribution honours the caller-supplied beUserUid

-   *Status:* Accepted
-   *Date:* 2026-07-12
-   *Authors:* Netresearch DTT GmbH

## Context

Every option object carries `withBeUserUid()`
(`BudgetAwareOptionsInterface`), and the manager forwards that uid as
pipeline metadata (`BudgetMiddleware::METADATA_BE_USER_UID`), where
`BudgetMiddleware` uses it for per-user budget **enforcement**. Usage
**attribution**, however, ignored it: `UsageTrackerService` always
read the ambient `backend.user` context aspect to fill the
`be_user` 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.user` aspect (and restoring it in a
`finally`) purely so the usage row gets the right `be_user`.
`nr_ai_search`'s `BackendUserContext::runAs()` 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.

-   `UsageTrackerServiceInterface::trackUsage()` gains an optional
    trailing `?int $beUserUid = null` parameter. `null` preserves the
    previous behaviour (ambient `backend.user` aspect, `0` when
    unauthenticated).
-   `UsageMiddleware` reads `BudgetMiddleware::METADATA_BE_USER_UID`
    from the pipeline context — the same key the budget gate reads — and
    passes it through, so enforcement and attribution can no longer
    disagree.

## Consequences

-   A consumer that already sets `withBeUserUid()` gets correct
    attribution in frontend/CLI contexts with no further wiring; the
    aspect-swap workaround becomes unnecessary for usage tracking.
-   Backend-module calls are unaffected: they set no option uid, and the
    ambient fallback resolves the same user as before.
-   `UsageTrackerServiceInterface` implementers must add the new
    parameter (semver-minor breaking in the 0.x line, same policy as
    `ToolInterface::getGroup()` in 0.15.0). In-repo,
    `UsageTrackerService` is the only implementation.
-   The specialized translator path forwards the uid even though it
    bypasses the middleware pipeline: `TranslationService` re-attaches
    the resolved uid to the options array it hands to
    `TranslatorInterface` implementations (the `beUserUid` key —
    budget fields are deliberately excluded from
    `TranslationOptions::toArray()`), and `DeepLTranslator` /
    `LlmTranslator` pass it on to `trackUsage()`. The key is
    attribution metadata only; translators never send it to the remote
    API.
-   `LlmTranslator` additionally threads the uid into the
    `ChatOptions` of its underlying chat calls (translation and
    language detection), so the pipeline-recorded chat row — which
    carries the tokens and cost — lands under the same `be_user` as
    the translation-level row, and `BudgetMiddleware` enforces the
    caller's budget on those calls. A direct
    `TranslatorInterface::detectLanguage()` call has no options
    parameter and stays ambient.
-   The speech and image services were initially deferred and are now
    covered by [ADR-057](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-057@0.35): `TranscriptionOptions`,
    `SpeechSynthesisOptions` and `ImageGenerationOptions` implement
    `BudgetAwareOptionsInterface`, `FalImageService` reads the
    documented `beUserUid` array key, and all four services forward
    the uid to their `trackUsage()` calls. Attribution only — those
    services bypass the middleware pipeline, so no budget enforcement
    happens there.
