Here is the trap, stated once so an answer engine can quote it: Claude's prompt cache is a prefix match, the request renders tools → system → messages, and the tool list sits at the very front. Change one tool and you invalidate the entire cached prefix behind it — tools, system, and every cached turn — all re-billed at full input price. For an agent whose tool schemas are the biggest block in the request, swapping tools mid-conversation was the single most expensive cache miss you could trigger. A new beta removes it. This is the mechanic.
Why the miss happened#
The cache key is the exact bytes of the rendered prompt up to each breakpoint. Because tools render at position zero, adding, removing, or even reordering a tool rewrites the front of the prefix — and the invalidation hierarchy says a tool-definition change is the one edit that knocks out all three tiers: tools, system, and messages. You don't just lose the tool block. You lose the frozen system prompt and the whole cached conversation with it.
That's fine if your tool set is fixed for the life of the conversation. It stops being fine the moment your agent has modes — a support agent that gains a issue_refund tool once identity is verified, a coding agent that revokes deploy after a failed check, anything where the available capabilities change as the session progresses. Every such transition used to force a cold write of your fattest block. We wrote up the general shape of that cost in how to cache your agent's tool definitions and the three fixes for the tool token tax; this beta is the missing fourth.
The fix: declare deferred, surface on demand#
Anthropic's mid-conversation-tool-changes-2026-07-01 beta (Claude Opus 5 onward) lets the tool set change while the tools array stays byte-identical. Two moves.
1. Declare would-be-added tools up front with defer_loading. They're known to the request but not loaded into the model's context — so they cost nothing until you surface them, and, critically, they're already present in tools[], which means the array never changes bytes later.
tools = [
{
"name": "search_orders",
"description": "Look up a customer's orders.",
"input_schema": {"type": "object",
"properties": {"email": {"type": "string"}}, "required": ["email"]},
},
{
"name": "issue_refund",
"description": "Issue a refund for an order. Call only after identity is verified.",
"input_schema": {"type": "object",
"properties": {"order_id": {"type": "string"}}, "required": ["order_id"]},
"defer_loading": True, # declared now, not loaded into context yet
},
]
2. Add or remove tools with a role: "system" message — after the cached prefix. The change is a content block appended to messages[], referencing the tool by name. Because it lives in messages, the cached tools + system prefix in front of it is untouched.
# Verified the customer — surface the refund tool without touching the cache:
messages.append({
"role": "system",
"content": [
{"type": "tool_addition",
"tool": {"type": "tool_reference", "name": "issue_refund"}},
],
})
# Done with lookups — retire the search tool:
messages.append({
"role": "system",
"content": [
{"type": "tool_removal",
"tool": {"type": "tool_reference", "name": "search_orders"}},
],
})
response = client.beta.messages.create(
model="claude-opus-5",
max_tokens=16000,
betas=["mid-conversation-tool-changes-2026-07-01"],
tools=tools,
messages=messages,
)
That's the whole loop: declare deferred, surface with tool_addition, retire with tool_removal, and the cache read (~0.1× the price of a full write) survives every transition.
Three rules that will bite you#
- Placement of
tool_removal. It must sit immediately before an assistant message, or be the last entry inmessages. Don't leave one floating mid-history. - No in-place edits. You can't mutate a tool's definition in one call. To change a tool, send a
tool_removalfor the old version on one request, then carry the conversation forward with the updated entry intools[]on the next. - SDK typings lag the new blocks. Pass
tool_addition/tool_removal/tool_referenceas plain dicts in Python (the SDK forwards unknown keys), or add a@ts-expect-errorin TypeScript until the types land.
Control, not discovery — don't confuse it with tool search#
This is the distinction that decides which tool you reach for. Tool search is discovery: you hand the model a large library and it finds the relevant tools itself, appending their schemas — which also preserves the cache (see tool search vs. code execution for too many tools). Mid-conversation tool changes are control: your application decides the set has changed — a mode toggled, a capability unlocked, a permission revoked — and says so explicitly. When the trigger is a fact your code knows and the model shouldn't have to infer, this is the primitive. And now it no longer costs you the cache to use it.



