# Apply token budgets to LLM traffic

A token budget caps how many LLM tokens your project can consume in an hour, a day, or a month. In `enforce` mode Catalyst rejects LLM calls once the allowance is gone; in `alert` mode it only counts, so you can size an allowance before you commit to it.

For what a budget is and how enforcement behaves, see [Token budgets](https://docs.diagrid.io/concepts/token-budgets). This page covers creating and running them.

## Create a budget in the console

1. Open your project in the [Catalyst console](https://catalyst.diagrid.io) and go to **Monitor › Token Budgets**.
2. Select **Create**.
3. Give the budget a **name**, a **token limit**, and a **window** (Hourly, Daily, or Monthly).
4. Choose a **mode**: **Enforce** to reject calls once the allowance is gone, or **Alert only** to count without rejecting.
5. Optionally set a **scope**: a **Workload**, an **LLM component**, a **Model**, or a combination. Leave it empty to cover every LLM call in the project.
6. Select **Create**.

The budget appears in the list with its scope, mode, window, current usage, and when the window resets.

![Token Budgets list in the Catalyst console showing two hourly budgets on the same scope, one in Enforce mode at 446 of 1k tokens and one in Alert only mode at 444 of 500](https://docs.diagrid.io/img/catalyst/token-budgets-list.png)

The two budgets above are a useful pairing. Both cover the same workload and LLM component, but the `alert`-mode one has the lower limit, so it runs out first and warns you while the `enforce` cap still has room. It acts as a tripwire under the ceiling.

## Create a budget with the CLI

Use `diagrid tokenbudget create`:

```bash
# 50M tokens a month across the whole project, enforced
diagrid tokenbudget create monthly-project-cap \
  --limit 50000000 --window month --mode enforce \
  --project my-project

# 1M tokens a day for one agent, counted but never rejected
diagrid tokenbudget create research-agent-daily \
  --limit 1000000 --window day --mode alert \
  --scope-id research-agent \
  --project my-project
```

Scope selectors are `--scope-id` (a [workload identity](https://docs.diagrid.io/concepts/identities)), `--scope-component` (a conversation component), and `--scope-model`. Set several and they are combined with AND. Omit them all and the budget covers the project.

List and inspect budgets:

```bash
diagrid tokenbudget list --project my-project
diagrid tokenbudget get monthly-project-cap -o yaml --project my-project
```

For every flag, see the [Diagrid CLI reference](https://docs.diagrid.io/references/catalyst/catalyst-cli-intro).

## Declare a budget in YAML

`TokenBudget` is a declarative resource, so budgets can live in Git alongside your other project configuration. See [Declarative management](https://docs.diagrid.io/operate/project-operations/declarative-management) for the wider workflow.

```yaml
# research-agent-daily.yaml
apiVersion: cra.diagrid.io/v1beta1
kind: TokenBudget
metadata:
  name: research-agent-daily
spec:
  limit: 1000000
  window: day        # hour | day | month
  mode: enforce      # enforce | alert
  scope:             # optional; all selectors are AND-combined
    appId: research-agent
    # component: openai-prod
    # model: gpt-4o
```

Apply it, and re-apply to change it:

```bash
diagrid apply -f research-agent-daily.yaml --project my-project
```

## Cover provider SDK calls with the LLM proxy

Calls made with the [Conversation API](https://docs.diagrid.io/concepts/dapr-apis) are already counted. An application that talks to a provider with the provider's own SDK is counted too, once you send it through Catalyst instead of straight to the provider.

Two things change on the client. The base URL points at Catalyst instead of the provider, and a `dapr-api-token` header carries the workload's API token. Everything else, including your provider key, stays as it is:

```python
import os
from openai import OpenAI

client = OpenAI(
    # The same OpenAI API, reached through Catalyst
    base_url=f"{os.environ['DAPR_HTTP_ENDPOINT']}/v1.0/diagrid/llm/api.openai.com/v1",
    api_key=os.environ["OPENAI_API_KEY"],
    default_headers={"dapr-api-token": os.environ["DAPR_API_TOKEN"]},
)

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Summarise this incident report."}],
)
```

### The authentication header

`dapr-api-token` is how Catalyst authenticates the call. Without it the request is rejected before it reaches a provider, so this is the one change you cannot skip.

The token is the same one your workload already uses to call any other Catalyst API, published to it as `DAPR_API_TOKEN`. See [Connect to Catalyst](https://docs.diagrid.io/develop/connect) for where to find it and your project's HTTP endpoint.

It does more than let the call in. The token identifies **which** workload is calling, which is what lets a budget scoped to a workload identity recognise the call as yours. Without it Catalyst would have no way to tell one caller's proxied traffic from another's.

Catalyst removes the header once it has checked it, so your Catalyst token is never forwarded to the provider. Your provider key is untouched and the SDK keeps sending it exactly as it would if it were talking to the provider directly.

### Other providers and SDKs

Every provider SDK works the same way, in any language. Keep the client as it is, point its base URL at `<your project's HTTP endpoint>/v1.0/diagrid/llm/<provider host>`, and add the header. For the Anthropic SDK the base URL is `.../v1.0/diagrid/llm/api.anthropic.com`, since it appends `/v1/messages` itself.

Streaming responses work as usual, and are counted when the stream finishes.

:::note Permitted providers
The proxy reaches `api.openai.com` and `api.anthropic.com`. A call to any other host is rejected with `403`, which stops the proxy being used as general-purpose egress. Ask Diagrid if you need another provider added.
:::

Budgets apply to proxied calls the same way, with one difference: a proxied call involves no Catalyst component, so a budget scoped with `--scope-component` never matches it. Scope by workload identity or model, or leave the budget project-wide, to cover both routes.

## Check what a budget has consumed

The **Usage** column in the console shows consumption against the limit, and **Resets** shows when the window rolls over. The list refreshes on its own, a few seconds behind live traffic.

From the CLI, `get` returns the same reading under `status.utilization`:

```bash
diagrid tokenbudget get monthly-project-cap -o yaml --project my-project
```

```yaml
status:
  utilization:
    consumed: 41283910
    remaining: 8716090
    limit: 50000000
    exhausted: false
    windowEndsAt: "2026-10-01T00:00:00Z"
    observedAt: "2026-09-10T14:32:04Z"
    accuracy: exact
```

`accuracy` tells you how much to trust the number. `exact` means it is the same figure enforcement is working from. `partial` means Catalyst could only estimate, and the estimate can read higher than what you have really spent, so treat it as a rough guide and don't alert on it.

## Get notified when a budget runs out

You should not have to poll a budget to discover it has been rejecting traffic for an hour. Catalyst raises an `ALERT_TYPE_TOKEN_BUDGET_DEPLETED` alert when a charge takes a budget to or past its limit, within about half a minute of the crossing. Alerts are listed in the console under **Monitor › Alerts**:

![Alerts page in the Catalyst console showing an ALERT\_TYPE\_TOKEN\_BUDGET\_DEPLETED entry with its message and the budget and appid labels](https://docs.diagrid.io/img/catalyst/token-budget-depleted-alert.png)

The `budget` and `appid` labels name the budget that ran out and the workload that spent the last of the allowance. Routes match on them.

The alert fires **once per depletion**, not once per rejected request. Everything blocked afterwards is silent, and the window reset re-arms it, so a daily budget that runs dry every afternoon pages you once a day instead of once a call.

Both modes raise it. An `alert`-mode budget tells you it would have started rejecting traffic, without rejecting any, which is what makes alert mode useful for sizing an allowance.

Deliver it by pairing a destination with a route:

```bash
# Where alerts go
diagrid notification destination create ops-webhook \
  --webhook-url https://hooks.example.com/catalyst \
  --project my-project

# Which alerts go there
diagrid notification route create budget-depleted \
  --type ALERT_TYPE_TOKEN_BUDGET_DEPLETED \
  --destination ops-webhook \
  --project my-project
```

Routes narrow further on the alert's labels, so one team can take only its own agents:

```bash
diagrid notification route create research-budget-depleted \
  --type ALERT_TYPE_TOKEN_BUDGET_DEPLETED \
  --filter appid=~research-.* \
  --destination research-oncall \
  --project my-project
```

A budget whose usage reads `accuracy: partial` still enforces, but it does not raise this alert.

## What callers see when a budget is exhausted

The call is rejected before it reaches the provider, so it costs nothing. What the caller gets back depends on how it reached Catalyst.

**Through the LLM proxy**, the rejection comes back as `429 Too Many Requests`, which most provider SDKs already understand as a rate limit and back off from.

**Through the Conversation API**, the rejection arrives as a component failure: HTTP `500` with the error code `ERR_CONVERSATION_INVOKE`, or gRPC `Internal`. That is the same status code any conversation failure uses, so the message is what identifies the cause:

```
failed conversing with component "llm-provider": LLM token budget "monthly-project-cap"
exhausted (50000000 tokens/month); resets at window end
```

If your application handles budget exhaustion specially, match on that message rather than on the status code, which cannot tell an exhausted budget from an unreachable provider.

An `alert`-mode budget never rejects, however much it has consumed.

## Reset, edit, and delete

**Reset** starts the current window's count from zero. The limit, window, mode, and scope are untouched. Use it to restore service after a runaway agent has eaten an allowance you would rather not wait out, or to clear a window you filled up while testing.

In the console, open the budget's row menu and choose **Reset**, between Edit and Delete. It asks for confirmation first, since resetting an exhausted `enforce` budget lifts the block for the rest of the window.

From the CLI:

```bash
diagrid tokenbudget reset monthly-project-cap --project my-project
```

That prompts too. Pass `--approve` to skip the prompt in a script, and `--wait` to return only once the reset has taken effect:

```bash
diagrid tokenbudget reset monthly-project-cap --approve --wait --project my-project
```

Either way the count restarts within a few seconds rather than instantly. Resetting needs the same permission as editing.

**Edit** a budget from its row menu in the console, or re-apply its YAML. Changes take effect within seconds, and the usage reading catches up without waiting for more traffic, so you can lower a limit and see straight away where it leaves you.

A new limit governs what you spend from then on and does not rewrite what the window has already used, so setting one below that point exhausts the budget immediately. That does not raise a depletion alert: nothing was spent to cross the line. Changing the window starts the count over. Editing never clears the count on its own. Use **Reset** for that. A budget's name is fixed: to rename one, create a replacement and delete the original.

**Delete** removes the budget and everything it has counted:

```bash
diagrid tokenbudget delete monthly-project-cap --project my-project
```

## Constraints

- A project can hold up to **20** token budgets.
- Names must start with a lowercase letter and contain only lowercase letters, numbers, and dashes, up to 63 characters. A name is unique within the project.
- Limits are whole numbers greater than zero, counted as prompt + completion tokens as reported by the provider.

## See also

- [Token budgets](https://docs.diagrid.io/concepts/token-budgets): how enforcement works and why a budget can overshoot by one request.
- [API logs](https://docs.diagrid.io/operate/project-operations/observability/api-logs): per-call token counts for every LLM call, budgeted or not.
- [Declarative management](https://docs.diagrid.io/operate/project-operations/declarative-management): keeping budgets in Git.
- [`diagrid notification`](https://docs.diagrid.io/references/catalyst/cli-reference/notification): destinations and routes for depletion alerts.
- [Operate AI agents](https://docs.diagrid.io/operate/project-operations/agents): finding the agent behind an exhausted budget.
