---
title: "Usage analytics"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:administration-analytics@0.35"
source: "Administration/Analytics.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# Usage analytics

The Analytics submodule turns the per-request data in
`tx_nrllm_service_usage` into an at-a-glance view of what your AI
spend and usage look like over time — cost and request trends,
breakdowns by provider, model, and service, and per-user consumption
against this month's budget.

![LLM Usage Analytics dashboard — KPI tiles, a cost and request trend chart, breakdowns by provider, model, and service, and a per-user table with monthly budget bars](../Images/backend-analytics.png)

## Opening the module

Open **AI > Operation > Analytics**. The submodule sits next
to the other LLM sections in the left-hand navigation and is
admin-only, like the rest of the module.

## Choosing a date range

A range switcher at the top of the page selects the reporting window.
The range is a plain `?range=` link, so changing it is an ordinary
page reload — there is no AJAX. Four presets are available:

| Preset | Window |
| --- | --- |
| `7d` | The last 7 days (today and the six preceding days). |
| `30d` | The last 30 days. This is the default — any unknown range value falls back to `30d`. |
| `90d` | The last 90 days. |
| `month` | From the first of the current calendar month to today. |

## KPI tiles

A row of tiles summarises the selected range:

-   **Total cost** — the summed estimated cost across the window.
-   **Total requests** — the number of AI requests recorded.
-   **Total tokens** — prompt plus completion tokens consumed.
-   **Providers** — how many distinct providers were used.
-   **Models** — how many distinct models were used.

These are totals for the chosen range, not all-time figures.

## Cost and requests trend

A line chart plots daily estimated cost and daily request count across
the range. Days with no usage are filled in as zero so the line is
continuous rather than skipping gaps.

## Breakdown charts

Four bar charts split the window's usage along different axes:

-   **By provider** — cost and requests per `service_provider`
    (OpenAI, Anthropic, Ollama, …).
-   **By model** — cost and requests per model. This dimension is new:
    it relies on the `model_uid` / `model_id` columns added to the
    usage table, so it only reflects usage recorded after that change.
-   **By service** — cost and requests per service type (chat, vision,
    translation, speech, image).
-   **By extension** — cost and requests per calling extension, see
    [Per-extension table](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-analytics-per-extension@0.35).

## Per-extension table

A table lists cost, requests and tokens per **calling extension**, ordered
by cost. It answers which piece of software spent what: a consumer names
itself with `AbstractOptions::withCallerSource()` ([ADR-177](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-177@0.35)), and that name is stored on the usage row alongside the money
([ADR-178](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-178@0.35)).

Where the entries come from:

-   The compatibility layer [nr-llm-compat](https://github.com/netresearch/t3x-nr-llm-compat) tags every call it
    reroutes, so an intercepted third-party extension (`ai_filemetadata`,
    `texter`, `ns_t3ai`, …) shows up under its own extension key.
-   Any other consumer that annotates its calls appears the same way.
-   Everything else — wizard tasks, scheduler runs, playground calls and any
    consumer that does not annotate — is listed as **Unattributed**. That is
    the normal state, not an error.

> [!NOTE]
> The name is what the caller claims. Attribution is an inventory of the
> installation, not an access control: a caller can name itself anything,
> and nothing verifies it. Rows written before the column existed are
> unattributed — no migration invents an origin for them.

## Per-user table

A table lists usage grouped by backend user, ordered by cost. Each row
shows the user's request count, token total, and estimated cost for the
selected range, plus a **monthly-budget bar** that visualises how much
of their per-user budget (see [Per-user AI budgets](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-user-budgets@0.35)) they
have consumed.

> [!NOTE]
> The budget bar always reflects the **current calendar month**,
> regardless of the date range selected above. The other columns
> follow the selected range; the budget bar does not, because a
> budget is a monthly ceiling.

Requests made without an authenticated backend user (CLI, scheduler,
`be_user = 0`) are grouped under a **system** row.

## Fallback rescues

A table lists the runs a **different** configuration answered after the
requested one failed — each line is one request the configuration you
configured did not serve. It shows what was requested and what answered,
each with its provider and model, how many configurations were tried, and
how long the whole run took.

Unlike the rest of this module the list is read from the telemetry log
(`tx_nrllm_telemetry`), not from the usage table, so it also covers
runs that produced no billable usage.

Two things it deliberately does not show:

-   **Runs nobody served.** A chain that was tried and exhausted names no
    serving configuration — it is a failure, not a rescue, and appears in
    the provider health scores instead.
-   **Runs recorded before this feature existed.** Rows written by an
    older version carry no serving configuration and are left out rather
    than guessed at.

At most the 200 newest rescues of the period are listed. The limit counts
rescues, not failed attempts, so a long outage — which writes one row per
request — cannot push the rescues out of the list.

A configuration appearing here repeatedly is the signal to look at: its
calls are being answered by a sibling, which may use a different provider,
model and price than the one you selected.

## Provider health and circuits

A table lists every provider that is either configured and active or named
by a run in the telemetry window, with its **health score**, the **number
of samples** the score is based on, the **window** those samples were taken
over, and the state of its **circuit breaker**.

Health and circuit state are both keyed by **adapter type**, not by
provider record — two provider records on the same adapter share one score
and one circuit, because it is the provider that is unhealthy, not the
record.

> [!NOTE]
> **Direct calls with a pinned provider are not in this table.**
> `chat()`, `complete()`, `embed()` and their siblings
> called with a `provider` option skip the default configuration
> entirely and run against a transient one carrying no model. Such a run
> records no provider in the telemetry log, and its circuit is kept
> under the call's own identifier (`ad-hoc:chat:openai`, one per
> operation) rather than under `openai`. An `openai` row here
> therefore reports the circuit of the configuration-backed calls only;
> a circuit opened for direct pinned calls is not shown anywhere on this
> page. Route traffic through a configuration if you need it covered.

| Column | Meaning |
| --- | --- |
| Score | A single 0.00–1.00 number combining success rate and mean latency, success rate weighted four times as heavily (see [ADR-063: Provider Resilience — Circuit Breaker, Health, Idempotency](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-063@0.35)). Higher is healthier. |
| Samples in window | How many runs the score was computed from. Read it before the score: 0.90 over two calls and 0.90 over two thousand are different statements. |
| Success rate | Share of runs the provider served **itself**. A run a fallback rescued counts as a failure of the requested provider. |
| Avg latency | Mean end-to-end time of the self-served runs. A provider whose runs in the window were **all** rescued by a fallback has no self-served run to measure: the cell says the latency was not measured instead of showing `0 ms`. Its score and success rate are real — the first attempt did lose. |
| Circuit | `closed` (normal), `open` (failing fast for the cooldown) or `half-open` (cooldown elapsed, one probe due), plus the current consecutive-failure streak. |

Unlike the rest of this module the table ignores the date range selected
above. Scores come from a rolling telemetry window (15 minutes by
default, named on the page) and circuit state is live cache state — neither
can be re-cut to a 90-day report period.

> [!IMPORTANT]
> **A score only changes something when you switched it on.**
> **Health-Aware Fallback Reorder** (`health.reorderFallback`
> in the extension configuration) is **off by default**. While it is
> off, the scores are diagnostic only: the fallback chain keeps the
> order you configured. The page states which of the two positions the
> switch is in, above the table. The circuit breaker
> (`circuitBreaker.enabled`) is on by default, and the page says so
> when it is not.

A provider with no telemetry in the window shows **no data** — not a score
of zero. It was not called; that is not the same as failing.

## A note on cost

All cost figures are **estimated**. They are computed from the model
pricing you configured (cents per 1M tokens, applied to the recorded
prompt/completion token split), not billed back from the provider.
Treat them as a planning and trend signal, not as an invoice. Costs are
captured at call time, so they reflect the pricing in effect when each
request ran. See [ADR-029: Usage Analytics Dashboard](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-029@0.35) for the design rationale.

Specialized services (DALL·E, text-to-speech, Whisper, DeepL) still
record their requests and units, but their cost is currently shown as
`0` — token-based pricing does not apply to them yet. Streaming
responses are not recorded at all, because chunked output has no single
terminal token count to price.

## Usage columns in the list views

The Providers, Models, Configurations, and Tasks list views each carry
three extra columns — **Cost (30d)**, **Requests (30d)**
and **Tokens (30d)** — summarising the last 30 days of usage for
that row, so you can spot the heavy hitters without leaving the list.

![Models list with Cost / Requests / Tokens (30d) columns showing per-model usage and estimated cost](../Images/backend-models-usage.png)

Two attribution notes:

-   The Providers column aggregates by **adapter type** (the value stored
    on each usage row), not by individual provider record — two providers
    that share an adapter therefore show the same figures.
-   The Tasks column relies on per-task tracking: each task execution
    records its `task_uid` so usage rolls up to the task that triggered
    it. Calls made outside a task (direct API/service use) are not
    attributed to any task row.

## Demo data for local development

To populate the module with something to look at during local
development, run the dev-only DDEV command:

```bash
ddev seed-demo
```

It generates roughly 90 days of realistic historic usage across
providers, models, services, and users so the trend line, breakdown
charts, and per-user table all have content. This command is for local
DDEV environments only — do not run it against production data.
