Get 2,500 events tracked for freeSign up now

All posts
Syed Raza5 min readBudgets, Attribution

The identity a budget is keyed to decides what it stops

An LLM gateway can only scope a limit to a credential or a tag the caller attached. Neither is the agent, and an unlabelled call is an unlimited one.

Every spend limit is keyed to an identity, and it can only stop calls that arrive carrying that identity. At the gateway layer there are two identities available: the credential the request authenticated with, and whatever labels the caller attached to the request body or headers. Neither of them is the agent that made the call, and a multi-agent run emits plenty of billed calls that carry the wrong credential, a stale label, or no label at all — each of which is a call the limit does not apply to.

What an LLM gateway can see

An LLM gateway is a proxy that sits in front of the provider APIs: your application points at it instead of at the provider directly, and because every request passes through it, it can meter, route and block. What it can key those decisions on is whatever the request carries over the wire — the API key it authenticated with, the team or organisation that key belongs to, and any tags or metadata fields the caller chose to include. The call stack inside your process is not in the request, so it is not available to the gateway.

That leaves the credential as the default scope, and a credential is a deployment artefact rather than a description of your system. One key usually serves every agent in a process. A pull request opened this week against LiteLLM, proposing tag-scoped token, request, dollar and concurrency limits, states the current position without hedging:

Existing tpm/rpm limits are process-global and only scoped to key/user/team, not to a tag value

To be clear about what the tool already does: LiteLLM is MIT-licensed and free, covers 140+ providers, and enforces hard budgets per key, team, organisation and model. That is more enforcement than most of this stack offers. The question is not whether a limit binds. It is what it binds to.

A tag is an identity someone has to remember to attach

The usual fix for a credential being too coarse is to let the caller label the request. LiteLLM does this with tags, supplied per request through metadata.tags or the x-litellm-tags header, with budgets attached to tag values (tag budget docs). Tags are flat rather than hierarchical: the calls in a request carry the tags that request was given, and there is no parent-child relation that makes a planner's budget cover the calls of a researcher it spawned.

Two consequences follow. The first is threading: the label has to be attached at every call site, including sites inside functions you did not write, and it has to stay attached as the code changes. Our July 2026 scan of public agent repositories found a median of 2 agents per repository and 8 at the 90th percentile (method and limitations) — so at the top of that range, eight identities to apply by hand and keep correct. The second is inheritance: a sub-agent reports as whatever its caller remembered to pass down, so per-agent budgets below the top level exist only as a naming convention.

The paths that go unlabelled are the expensive ones

The same pull request names a gap that matters more than the threading work:

Fallback hops never get their own tag-scoped enforcement

A fallback hop is what happens when the first deployment errors or is rate limited and the request is retried against another one, frequently a larger model. That is precisely the moment spend leaves the plan — under failure, often in a loop — and it is a call your code did not issue. A separate LiteLLM pull request on tag routing, opened days earlier, reports a related failure in the matching logic itself:

Set-based trust logic could be tricked into discarding an inherited tag constraint

Fallbacks are not the only unlabelled path. Client-library retries, framework internals, memory and retrieval embeddings, and guardrail or moderation scans all produce billed requests that no line of your code originated, which is the subject of agent cost includes the calls you never wrote. Every one of them reaches the provider carrying the credential and nothing else.

Decide what an unidentified call is allowed to do

Most enforcement layers treat a call that matches no rule as permitted, which is the right default for availability and the wrong default for a spend ceiling you believed was total. The decision itself is unavoidable — what is avoidable is leaving it implicit and uncounted. If unmatched calls proceed, the number of them is a metric, and it should be visible next to the budget that did not cover them. Fail-open versus fail-closed is the same trade one layer down.

That same pull request also proposes rolling windows in place of scheduled resets, which is a different axis of the same problem: a limit's window decides how long a breach runs before anything notices, and the unit it counts decides whether it bounds money at all. Run quotas and concurrency caps do not bound spend works through that half.

Four questions to ask of any spend limit

  1. What identity is it keyed to, and who assigns that identity? A key is minted by whoever provisions credentials; a tag is assigned by whoever wrote the call site. Neither is usually the person who owns the budget.
  2. Does a nested call inherit it? If the answer is "the caller passes it down", then depth is where attribution and enforcement both stop.
  3. Which paths can issue a billed call without it? Retries, fallback hops, framework-internal calls, embeddings written to memory.
  4. What happens to a call carrying no identity, and can you count those? An uncounted remainder is the part of the bill no limit is watching.

How this works in Capsera

Capsera runs in-process rather than as a gateway, which is what makes the third identity — the agent — available at the moment of the call. capsera.init() patches the provider clients, so every Anthropic, OpenAI and Google Gemini request in the process is captured whether or not you wrote the line that made it, sync or async. A @capsera.agent decorator on a function you already have says which agent to attribute the calls inside it to. Because the attribution stack is a ContextVar rather than thread-local state, nested agents inherit automatically and asyncio.gather does not interleave identities — nothing is threaded through call sites, and there are no per-request tags to forget.

Budgets are scoped per agent, per team or globally, and are checked before the provider call, so a blocking budget raises BudgetExceededError and no tokens are spent. The honest limits of that: a call matching no budget proceeds, and network or backend errors fail open, because an outage in a spend tool must not take down your application. Capsera stores hashes and token counts, never prompt or response content. If what you need is multi-provider routing or a gateway in the request path, LiteLLM is the tool for that job; the layering is set out in gateway vs observability vs governance.

Give every agent an identity, a budget, and hard limits.

One line of code. Anthropic, OpenAI, and Google Gemini.

See pricing