Your tool definitions are re-sent on every single request, before the model does anything you asked for. Tonight we price a real setup on screen, then watch one added tool re-price an entire conversation, because tools render at position zero. Nothing errors. The only symptom is the bill.
Ask an engineer how big their context window is and you get an instant answer. Ask what their agent spends before the user has typed anything and you get a pause, then a guess.
Window size. Output tokens. Maybe a monthly bill, looked at when it jumps.
The tool schemas, the system prompt and the history, re-sent in full on every turn. It is not a one-off. It is a per-turn standing charge that grows every time somebody wires in another server.
Treat it as a size and the only question is "will it fit". Treat it as a per-turn budget and three better questions appear: what am I re-paying for, at what rate, and what do I throw away when it fills. Those are tonight.
| Tier | Changes how often? | What it costs to touch |
|---|---|---|
| tools | rarely, and never on purpose mid-conversation | everything after it, which is everything |
| system | per session, if you interpolate into it | the whole conversation history |
| messages | every turn, by design | only what comes after the change |
$ npm run budget filesystem 6 tools 3106 bytes ~ 1195 tokens ticketing 4 tools 2874 bytes ~ 1105 tokens docs-search 2 tools 1121 bytes ~ 431 tokens system prompt 1958 bytes ~ 490 tokens FIXED PREFIX ~3221 tokens ............................ 1.6% of a 200K window, spent before the user has said anything.
It is appended to the end of the tools array. It is unrelated to the conversation in flight. It is never called. Your prompt caching is configured correctly and has been reading happily for twenty turns.
Nothing. It's one small tool at the end.
The tools cache is lost. The rest holds.
Everything is lost.
$ npm run cache RUN 2 - ONE TOOL IS ADDED DEAD tools tool definitions 60faf1235eff -> 85026d8c1e2f DEAD system system prompt 5c7b254effd7 -> af197e6f0398 DEAD messages turn 2: user 8cbad35e1568 -> 52bf2cc96f26
Tools render at position zero. A tool appended to the end of your tools array still lands at the front of what the model sees, ahead of the system prompt and ahead of every message in the conversation.
The cache key at each breakpoint is the bytes of everything before it. Change byte 40 and every key downstream changes with it.
flowchart TB T["tools
position zero"]:::hot --> S["system prompt"]:::warm S --> M["every message
in the history"]:::warm X["one tool appended
to the end of the array"]:::bad --> T T -.->|"re-keys everything
downstream"| M classDef hot fill:#FEE4E2,stroke:#0D1B33,color:#0D1B33; classDef warm fill:#EEE6FF,stroke:#0D1B33,color:#0D1B33; classDef bad fill:#0D1B33,stroke:#0D1B33,color:#ffffff;
| Turns | Uncached | Cached | Difference |
|---|---|---|---|
| 1 | $0.0161 | $0.0201 | −$0.0040 |
| 10 | $0.1611 | $0.0346 | $0.1264 |
| 50 | $0.8053 | $0.0990 | $0.7062 |
| 200 | $3.2210 | $0.3406 | $2.8804 |
A breakpoint looks back at most twenty content blocks to find a prior cache entry. Exceed that in a single turn and the next request silently misses. No error. No log line.
blocks appended in one turn reaches the last entry? 14 yes 20 yes 21 NO - silent miss
Everything so far is about the rate you pay. The rest is about capacity: a long trajectory outgrows the window, and you have three ways to make room. They free almost identical amounts, so that is never the question.
Delete old tool results outright. Cheapest. Leaves nothing behind.
Summarise the old turns into one block. Keeps a trace of everything.
Keep nothing, re-fetch on demand. Loses nothing permanently, pays in latency.
$ npm run evict 39 blocks, ~37535 tokens, 6 of them load-bearing CLEAR TOOL RESULTS freed ~32400 lost: turn 4, the runbook COMPACTION freed ~32550 lost: turn 4, the runbook RETRIEVAL freed ~36800 lost: turn 4, the runbook survived all three: KI-233, the legacy-pipeline finding, the customer's question
count_tokens on the request body you already send, with and without your tools array. The difference is your standing charge. This is the step that changes the conversation, because right now nobody in your team has the number.system, adds a tool, or interpolates a timestamp into the prefix. Then look at cache_read_input_tokens across repeated requests: if it is zero, you have a silent invalidator.Public, MIT, zero runtime dependencies, no API key, no network call. Three commands: budget, cache, evict. Eighteen tests, including one for every claim I made out loud tonight.
github.com/ehsangazar/lightning-lesson-context-engineering-starter
npm install && npm run cache # run 2 is the interesting one
~. Only your provider's tokenizer knows the real number.Not how many it uses. How many are defined in the request. Count the servers you have wired in and multiply.
| Tools defined | ~tokens, at this fixture's rate | Of a 200K window |
|---|---|---|
| 12 | 3,221 | 1.6% |
| 30 | 7,330 | 3.7% |
| 60 | 14,170 | 7.1% |
| 100 | 23,290 | 11.6% |