---
title: "ADR-057: Speech and image services carry attribution in options"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-057@0.35"
source: "Adr/Adr057SpecializedServiceAttribution.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-057: Speech and image services carry attribution in options

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

## Context

[ADR-052](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-052@0.35) made the caller-supplied `beUserUid` win
over the ambient `backend.user` aspect for usage attribution, but
deferred the four specialized speech/image services: their option
shapes (`TranscriptionOptions`, `SpeechSynthesisOptions`,
`ImageGenerationOptions`, and `FalImageService`'s plain array)
carried no budget fields, so every transcription, synthesis and image
generation landed in the ambient bucket — `be_user = 0` for
frontend, CLI and worker callers — with no way for a consumer to
attribute the spend.

Unlike chat/completion/embedding/vision, these services do not run
through the middleware pipeline (they dispatch HTTP directly via
`AbstractSpecializedService`), so neither `BudgetMiddleware` nor
`UsageMiddleware` can supply the uid; the services call
`trackUsage()` themselves.

## Decision

Extend the options-based attribution of ADR-052 to the specialized
services — attribution only, no enforcement:

-   `TranscriptionOptions`, `SpeechSynthesisOptions` and
    `ImageGenerationOptions` implement `BudgetAwareOptionsInterface`
    via `BudgetFieldsTrait`: optional trailing `beUserUid` /
    `plannedCost` constructor parameters, `fromArray()` keys of the
    same names, validation rejecting negative values. The fields stay
    out of `toArray()` — the services build their wire payload from
    `toArray()`, so a missed exclusion would leak the uid to the
    remote API.
-   `WhisperTranscriptionService`, `TextToSpeechService` and
    `DallEImageService` forward `getBeUserUid()` to their
    `trackUsage()` calls.
-   `FalImageService` keeps its plain options array (no DTO exists)
    and reads a documented `beUserUid` key — the same array pattern
    `TranslationService` uses for translators. The payload builder is
    an explicit allowlist, so the key never reaches the FAL API.
-   `DallEImageService::createVariations()` and `edit()` take no
    options object; they gain an optional trailing `?int $beUserUid`
    scalar parameter instead of growing a new options type for two
    DALL-E-2-only endpoints.

## Consequences

-   Consumers can attribute speech and image spend per backend user from
    any context; without the uid the ambient fallback keeps the previous
    behaviour.
-   Attribution and enforcement remain decoupled: the specialized
    services still bypass the middleware pipeline, so per-user budget
    ceilings are NOT enforced on speech/image calls. `plannedCost` is
    carried but unused there. Routing these services through a budget
    pre-flight is a separate decision with its own trade-offs (no
    token-based cost model for FAL, multipart request flows) and gets
    its own ADR if a consumer needs it.
-   The three option DTOs and the two DALL-E signatures are public
    surface; the additions are optional trailing parameters
    (semver-minor in the 0.x line, same policy as the ADR-052
    `trackUsage()` change).
-   `BudgetAwareOptionsInterface`'s docblock now names the
    attribution-only consumers so the "reaches BudgetMiddleware"
    assumption is not silently wrong.
