---
title: "ADR-232: An editor's budget status is public API"
manual: "LLM Foundation"
version: "0.42"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-232@0.42"
source: "Adr/Adr232AnEditorsBudgetStatusIsPublicApi.rst"
rendered: "2026-11-01T11:38:21+00:00"
---

# ADR-232: An editor's budget status is public API {#adr-232}

-   *Status:* Accepted
-   *Date:* 2026-10-28
-   *Authors:* Netresearch DTT GmbH

## Context {#adr-232-context}

The AI Tasks module shows an editor their own usage today and this month
against the limits of their budget record. One read model produces it:
`UserBudgetStatusService::forUser()` returns a `UserBudgetStatus`
of two `BudgetWindowStatus` objects with three `BudgetMeter` each,
formatted by `LocalizedNumberFormatter` in the reader's locale. All five
classes were `@internal`.

Other extensions show the same status to the same editor. The cowriter
(`netresearch/t3-cowriter`) does it in its dialog and calls
`forUser()` and `LocalizedNumberFormatter::forBackendUser()`
directly. Under `@internal` these may change in any release, including a
patch release, and `forUser()` already changed its signature between
0.41.8 and 0.41.9. A consumer that accepts every patch release of a minor
line would then break without a version conflict.

## Decision {#adr-232-decision}

The five classes are `@api` ([ADR-127](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-127@0.42)):

-   `Netresearch\NrLlm\Service\Budget\UserBudgetStatusService`, the
    entry point. Consumers obtain it from the DI container; its constructor is
    frozen in the snapshot but out of contract, and its parameter types
    (the repository and the usage-window reader) stay `@internal`, as
    [API stability](https://docs.typo3.org/permalink/netresearch/nr-llm:api-stability@0.42) states for every service.
-   `Netresearch\NrLlm\Domain\ValueObject\UserBudgetStatus`,
    `BudgetWindowStatus` and `BudgetMeter`, the value objects
    `forUser()` returns, including their `toArray()` shapes.
-   `Netresearch\NrLlm\Service\Formatting\LocalizedNumberFormatter`,
    which `forUser()` takes and `BudgetMeter` carries.

This is the signature-transitive closure of `forUser()`: no further
`Netresearch\NrLlm` type appears in a method or property signature of the
five. The snapshot test asserts that.

Nothing changes in behaviour or signatures. The change is additive: five
classes are added to `Tests/Unit/Api/api-surface.txt`.

## Consequences {#adr-232-consequences}

-   Within a minor line the read model keeps its signatures and its
    `toArray()` shapes; during 0.x a break needs a minor release and a
    BREAKING changelog entry.
-   `UserBudgetStatusService` has no interface. A consumer that wants to
    replace it in a test builds a `UserBudgetStatus` itself, which the
    value-object constructors allow.
-   `BudgetMeter::$formatter` and `BudgetWindowStatus::fromUsage()`
    are part of the surface. `fromUsage()` takes the usage array in the shape
    the internal usage-window reader returns; that shape is now frozen with it.

## Alternatives considered {#adr-232-alternatives}

-   **A new facade that returns plain arrays.** Consumers would get a second
    read path next to the one the AI Tasks module uses, and the two could
    drift. Promoting the existing model keeps one.
-   **Keeping the classes internal and asking consumers to pin a patch
    release.** That moves the cost to every consumer and still gives no
    promise.
