ADR 057 · Accepted
Speech and Image Services Carry Attribution in Options
Context
ADR-052 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.