Get 2,500 events tracked for freeSign up now

All posts
Talha Tayyab6 min readCost per run, Attribution

A tool catalog is billed on every turn

Tool and MCP schemas are input tokens on every call of an agent loop, and the catalog's size is set in configuration. How to measure what it costs per run.

Every tool you hand an agent is text in the request. The JSON schema for each one — name, description, parameter types, enum values — is serialised into the prompt on every call the loop makes, not once per run. So a tool catalog is a fixed input floor multiplied by the number of turns, and its size is usually set in a configuration file rather than in the code anyone reviews.

This is not tool-result bloat, which is about what the tools return once they run. This is about what the tools are, on every turn, including the turns where none of them is called.

The catalog arrives whole

A pull request opened on 2026-09-08, adding a schema discovery proxy mode to LiteLLM's MCP support, states the problem in one line:

Large MCP catalogs are eagerly loaded into every client session

and describes the behaviour it wants to change concretely:

Before: a client connecting to the aggregate MCP endpoint receives the full configured catalog

The proposal is to replace the eager load with a lookup — it "Adds /mcp/proxy with search_tools, get_tool_schema, and call_tool". It is an open proposal in a public repository, not shipped behaviour, and the reason to read it is the shape of the problem rather than the fix.

Two properties of this cost follow from the phrase "the full configured catalog". It is aggregate: one endpoint fronting several MCP servers hands over every server's tools, so the catalog grows by addition and never by use. And it is a property of configuration rather than of the task, which means adding a ninth server to a config file makes every call of every agent behind that endpoint slightly more expensive — including agents that will never call anything in it. Those are tokens you never wrote, in the same sense that a guardrail scan is a call you never wrote.

The multiplier is decided at runtime

A Langfuse discussion opened on 2026-09-10, asking for prompt and model linking on agent observations, describes the shape of an agent run precisely enough to price it:

One agent run is one agent observation with N generation children, where N is the tool-call loop depth and varies per input.

Same shape in the OpenAI Agents SDK, LangGraph, and anything else that round-trips tools.

N is not a number you configured. The catalog floor is paid N times, and N is settled at runtime by how hard the input turned out to be — which is the same reason a run is the only honest denominator for any of this.

For the range N reaches, a Hacker News comment on 2026-09-10, in a thread about a new flash model's cache pricing, prices out "a real long-running coding task, medium codebase, 447 turns". And a GitHub discussion on 2026-09-08 in the AutoGen repository describes what else is riding along by then: "as tasks expand across multiple files and turns, the message history accumulates terminal outputs, file contents, and assistant reasoning", so that "Every subsequent turn re-sends the entire history". The growing history and the fixed catalog are additive, both are charged per turn, and only one of them is visible in your source code.

The arithmetic, over assumed numbers

None of the following is a measurement — it is arithmetic over inputs you would have to supply yourself.

Take 40 tools whose schemas average 200 tokens. That is a floor of 8k input tokens on every request. A 30-turn run pays 240k tokens of catalog before a single line of conversation is counted; the same run against a 10-tool subset pays 60k. The 180k-token difference was decided by a configuration file rather than by the task, and it lands on the input side, where it is least visible.

Two quantities produce that number: the token cost of each schema, and the turns per run. Both are knowable. Neither is on a provider invoice.

Trimming the catalog works against the cache

Dynamic discovery is the obvious fix — send a small catalog, look schemas up when the agent needs them — and it has two costs worth pricing before adopting it.

The first is that discovery is itself requests. A search call and a schema fetch are round trips that exist only because the catalog was trimmed.

The second is the cache. The tool block is part of the repeated preamble a provider cache is matched against, and prefix caching matches exactly from the first byte. A catalog assembled per turn is a preamble that changes per turn, so everything after the change loses the match too. A fixed catalog is larger and cacheable; a dynamic one is smaller and may be paying full price on every turn. Which is cheaper is arithmetic over your own numbers rather than a default, and the two options fail in opposite directions.

What to record

  1. The input tokens on the smallest call each agent makes. A trivial turn that still sends 8k input tokens has told you the floor without any instrumentation of the catalog itself.
  2. Turns per completed run, as a distribution. A mean hides the runs that went on for hundreds of turns, and those are the runs the floor is multiplied through.
  3. Which tools were enabled, and from when. Catalog membership is a versioned configuration fact that belongs next to cost, or a step change in input tokens has no cause attached to it.
  4. Discovery calls as their own line. If you trim the catalog dynamically, the lookups are spend that the trimming created and they should be netted off the saving rather than absorbed.
  5. Cost per run, not per call. Shrinking the catalog lowers the price of every call and can raise the number of calls; the run is the only unit where both land on the same line.

How this works in Capsera

Capsera never transmits tool definitions, tool arguments or tool results, and never prompt or completion content — it stores hashes and token counts. So the catalog is not itemised anywhere in the product, and that limitation is worth stating plainly: Capsera cannot tell you that 8k of a call's input tokens were schemas.

What it does give you is the floor and the multiplier, per agent rather than per key. capsera.init() patches the provider clients — Anthropic messages, OpenAI chat completions, OpenAI embeddings and Google Gemini, sync and async — so every call is captured with its input, output and cache token counts. @capsera.agent(name="researcher") adds who, not whether, and because the agent stack is a ContextVar rather than thread-local state, sub-agents inherit the identity and calls made underneath report as themselves under the run that caused them. The smallest call's input tokens per agent is then a readable number, and so is the call count per run. Optional prompt analysis adds structural metadata — token estimates, message counts, context-window utilisation — without storing anything it describes.

Because N is not knowable before the run, a ceiling is the part that does not depend on estimating it. Budgets are checked before the provider call and scoped per agent, per team or globally, so a loop that keeps re-paying the floor is refused at its envelope rather than reconciled afterwards.

Two places another tool is the right answer. If the catalog is served by a gateway, the aggregate endpoint's configuration is where it gets shrunk, and that is the gateway's job rather than ours: LiteLLM is MIT-licensed, free, covers 140+ providers and enforces hard budgets per key, team, organisation and model. And if the question is what was actually in the tool block on a specific call — the content of the request rather than its size — Langfuse is open source, free, and built to store and inspect exactly that. Capsera is not, by design.

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

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

See pricing