What breaks when you switch to Claude Opus 5.5: four 400s, three silent changes, and what your fallback loses
Four Opus 5 request shapes now return 400 on Claude Opus 5.5, three changes return 200, and a fallback to Opus 5 loses the reasoning. Error text, fixes, replay.

Changing claude-opus-5 to claude-opus-5-5 is one line, and four request shapes that
worked on Opus 5 now return a 400 invalid_request_error: thinking set to disabled or
to a manual budget, a forced tool_choice of any or a named tool, a thinking block
replayed after you edited the history in front of it, and the computer_20251124 tool on
the Claude API and Google Cloud. Three of those 400s show up on your first request; the
history one may not show up on your own key at all. Three more changes return 200: notes
between tool calls arrive as empty thinking blocks, and a request without an effort
setting runs at medium instead of high. The third is the expensive one: a fallback from
Opus 5.5 to Opus 5 succeeds without any of Opus 5.5’s reasoning, runs an unset effort at
high again, and pays 25% more per token.
No measurements of mine here. This is Anthropic’s Opus 5.5 documentation read field by field as it stood on 23 September 2026, error strings copied verbatim, plus one router’s published migration guide as a second surface. We build a gateway, so for each change I ask which layer should absorb it. A migration order is at the end.
| Change | On Claude Opus 5 | On Claude Opus 5.5 |
|---|---|---|
thinking disabled | Accepted at effort high or below | 400 at every effort level |
tool_choice of any or tool | Accepted | 400, on count_tokens too |
Thinking block replayed after system, tools or earlier messages changed | Accepted; no check | 400 on accounts created from 31 Aug 2026, 00:00 UTC; in Batch, dropped silently |
computer_20251124 tool | Accepted with its beta header | 400 on the Claude API and Google Cloud; Bedrock still accepts it |
| Notes between tool calls | text blocks | 200; thinking blocks, empty at the default display |
| Request with no effort set | Runs at high | 200; runs at medium |
| Conversation moved from 5.5 down to Opus 5 | n/a | 200; Opus 5.5 thinking blocks dropped, not billed |
Correction (2026-09-28): an earlier version of this table listed a manual
budget_tokensalongside disabled thinking as new on Opus 5.5. Manual extended-thinking budgets have returned 400 since Claude Opus 4.7, so code already running on Opus 5 does not send them; the one new thinking 400 isdisabled(migration guide).
Sources for every row: What’s new in Claude Opus 5.5 and Migrating to Claude Opus 5.5. The first three 400s also apply to Claude Fable 5.1, the tier above Opus, so if a workload already runs there, most of this is familiar.
Claude Opus 5.5 rejects thinking.type.disabled: thinking is always on
On Opus 5 you could switch thinking off at effort high or below. On Opus 5.5 you cannot,
and the manual budget form is gone too. Either shape returns a 400 with one of these:
"thinking.type.disabled" is not supported for this model. Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior."thinking.type.enabled" is not supported for this model. Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior.Delete the thinking field, or send {"type": "adaptive"}, which the documentation calls
equivalent. Cost control moves to effort, which lives at output_config.effort:
{ "model": "claude-opus-5-5", "max_tokens": 16000, "output_config": { "effort": "low" } }The Effort page says the
disabled form returns “a 400 error at every effort level,” so low is the floor, not an
off switch.
Teams that ran with thinking off hit two more things. Every response can now start with one
or more thinking blocks, so content[0].text breaks. Select by type:
text = "".join(b.text for b in response.content if b.type == "text")And thinking counts toward max_tokens even when its text is hidden. The
Prompting Claude Opus 5.5
page: “a limit sized for Claude Opus 5 with thinking off can cut replies off.” It reports
128,000, the model’s maximum, “has worked well in Anthropic’s testing” for long agentic
turns. Raising it is not free everywhere: a gateway that reserves quota by max_tokens
reserves more, and a non-streaming call with a long ceiling can outlive your HTTP timeout.
The thinking that fills it bills as output at $20 per million tokens.
Forced tool_choice returns 400 on Claude Opus 5.5, count_tokens included
tool_choice: type "tool" and "any" are not supported for this model.auto and none still work, and What’s new adds one clause that matters later: “the same
validation applies to the token counting endpoint.” If you forced a tool to get
schema-valid JSON, keep auto and set strict: true
(Strict tool use)
or move to structured outputs. If you forced it to get a call at all, say so in the prompt,
and check the response for a tool_use block, because auto does not promise one. A retry
resends the whole input: at least $0.20 on an uncached 50,000-token context, about a cent on
a warm cache. In the
Batch API,
validation is asynchronous, so these come back as errored items when the whole batch ends.
Errored items are not billed.
Editing history before a thinking block returns 400 on accounts created from 31 August 2026
The text of a thinking block is not the reasoning itself; the block
carries an encrypted signature from which the API rebuilds the model’s earlier reasoning
when you send the block back. That is why tool-use loops must return thinking blocks
unmodified. On Opus 5.5 the signature is also bound to the conversation it came from.
Preserved thinking
calls that conversation the block’s prefix: the top-level system prompt, the tools
list, and every message before the block. Change any of them later, replay the block, and:
messages.1.content.0: Invalid `signature` in `thinking` block. The block is bound to a different conversation. Remove the block, or set `thinking.block_binding.prefix_mismatch_behavior` to "drop_block".Accounts created on or after 31 August 2026, 00:00 UTC get this 400 by default, on the
Claude API and cloud platforms. Older accounts get it only on requests that set
thinking.block_binding.prefix_mismatch_behavior. The docs spell out the trap for anyone
shipping a tool that runs on other people’s keys: “a run with no errors on your own key
doesn’t show whether your code is affected.” The same trap applies to a key pool that mixes
accounts from before and after that date: one history can return 200 on one key and 400 on
the next. Set prefix_mismatch_behavior explicitly on every request (it needs the
thinking-binding-controls-2026-08-01 beta header) and every account behaves the same.
Batch is the exception to the 400. On an enforced account, a batch item that leaves the field unset “doesn’t fail”; the API drops the failing blocks instead. That puts Batch in the silent column.
What counts as an edit is broader than it sounds. effort, max_tokens, output_config,
tool_choice and metadata are outside the check. Rebuilding the system prompt with
today’s date, an MCP server that connects after the first turn, clearing old tool results,
summarizing old turns while keeping recent thinking, and adding a tool mid-session are all
edits. Removing thinking blocks from the start, the end, or all of them is allowed;
removing one from the middle invalidates every block after it.
With "drop_block", the request succeeds, the failing blocks are dropped and not billed,
and each is listed in input_transformations with reason: "prefix_binding_mismatch".
Anthropic’s own caveat: it keeps requests succeeding “but doesn’t fix the edit.” Alert on
those entries.
computer_20251124 is rejected on the Claude API and Google Cloud, accepted on Bedrock
The conclusion first: as I read the docs on 23 September 2026, no single computer-use
request for Opus 5.5 works on both the Claude API and Bedrock. On the Claude API and Google
Cloud, Opus 5.5 accepts only the computer_toolset_20260801 toolset, and the earlier tool
gets a 400 whose message begins:
'claude-opus-5-5' does not support tool types: computer_20251124.On Bedrock, What’s new says the earlier tool “continues to work on Claude Opus 5.5 as it does on Claude Opus 5.” The Computer use tool compatibility section says platforms other than the Claude API and Google Cloud “currently offer only the earlier beta tool versions.” Read together, the two pages imply that a failover between the Claude API and Bedrock has to swap the tool declaration and the agent loop, not just the endpoint. For Claude Platform on AWS and Microsoft Foundry, neither page lists either form for Opus 5.5.
The tools entry, before and after:
{ "tools": [{ "type": "computer_20251124", "name": "computer", "display_width_px": 1024, "display_height_px": 768 }] }
{ "tools": [{ "type": "computer_toolset_20260801" }] }Drop the computer-use-2025-11-24 header too. The loop change is bigger: read the action
from the member tool_use block’s name, not input.action; expect several per turn; echo
toolset_name on every result. Two defaults also change
(Migrate from computer_20251124):
zoom is on by default, and the toolset “rejects a screenshot or zoom image that exceeds the
model’s image limits instead of downscaling it.”
The three changes that return 200: quiet progress notes, a lower default effort, and a fallback that forgets
Status codes catch none of these.
Notes between tool calls arrive as empty thinking blocks
On Opus 5, the short note the model writes between tool calls, what it just found and what
it checks next, is a text block, and many agent UIs stream it as a status line. On Opus
5.5 it is a progress-update thinking block, at most one per tool call
(Thinking, Progress updates between tool calls),
empty at the default display: "omitted". The migration guide’s phrase is the bug report
your users will file: the application “goes quiet between tool calls.”

The fix is display: "updates", which returns progress notes and keeps reasoning hidden. It
is in beta:
{ "headers": { "anthropic-beta": "thinking-display-updates-2026-08-18" },
"body": { "thinking": { "type": "adaptive", "display": "updates" } } }Without that header, the same page says, the value “is rejected with the same 400
invalid_request_error as an unknown display value,” so a proxy that strips beta headers
it does not recognize turns the fix into a fifth 400. "summarized" returns notes and
reasoning summaries mixed. Either way, a note “counts toward usage.output_tokens at its
full length,” and the model writes fewer of them at higher effort.
A request with no effort set now runs at medium
Effort: “Most Claude models default to high effort… Claude Opus 5.5 defaults to medium.”
Anthropic’s testing says 5.5 at medium “matches or exceeds Claude Opus 5 at high” on
coding and knowledge work, and that 5.5 tends to think more per turn at any level. The
prompting guide’s line is the one to keep: “Effort level names don’t correspond to the same
amount of thinking across models.” I made that argument across vendors in
reasoning_effort is one name with five physics;
it now holds inside one model family. On one model, re-run your effort sweep. Behind a
router, read on.
A fallback from Opus 5.5 to Opus 5 succeeds without the reasoning
Opus 5.5 reads thinking blocks from Opus 5 and earlier Opus, Sonnet and Haiku models. On the Claude API, Claude Fable 5.1 and Claude Mythos 5.1 (the restricted-access version of the same model) read Opus 5.5’s blocks, and no other model does. Upgrading a conversation from Opus 5 to 5.5 keeps its reasoning. The migration guide on the other direction: “A router or fallback that moves a conversation from Claude Opus 5.5 to any other model runs those turns without them.”
The API drops the unreadable blocks, the request succeeds, and the drops are not billed.
Without the thinking-binding-controls-2026-08-01 header, “the drop is silent”; with it,
each drop appears in input_transformations as model_binding_mismatch. The API never
edits your messages, so when the next turn returns to 5.5 the blocks are readable again,
unless your own client stripped them.
On that fallback turn, if the request did not set effort:
- Opus 5 sees none of Opus 5.5’s reasoning.
- It runs at
high, its own default: a different effort than the one you tested. - Tokens cost 25% more: $5 input and $25 output per million, against $4 and $20.
Every hop is a 200, so the error-rate panel stays flat.
Refusals make this concrete. A classifier refusal on Opus 5.5 is itself a 200 with
stop_reason: "refusal" (One model, two safety tiers
covers the shape), and 5.5 adds classifiers: bio and reasoning_extraction join cyber.
Anthropic’s announcement (Introducing Claude Opus 5.5)
says “most cybersecurity tasks will be re-routed to Opus 4.8,” a model outside the short list
that reads 5.5’s blocks. Expect your refusal rate to move and watch it during the canary.
One category never retries: server-side fallback “doesn’t retry requests declined with
"reasoning_extraction"; that refusal is returned to you.”
Where a router should absorb each change, and where it must not
A router handles every one of these requests before the model does, so each change has a natural place to be caught:
| Change | Layer | What absorbing it honestly looks like |
|---|---|---|
thinking disabled | Before routing | Reject for a target that does not accept it, or route only to one that does |
Forced tool_choice | Before routing | Same; validate the body against every target in the fallback set |
| History edit before a 5.5 thinking block | History | Forward history byte for byte; pass anthropic-beta and block_binding through; return input_transformations |
computer_20251124 | Endpoint selection | Treat Claude API and Bedrock as different tool contracts |
| Progress notes | Response translation | Keep thinking blocks with text apart from reasoning when converting to another API shape |
| Default effort | Every route | Write effort on every request, primary and fallback |
| Dropped reasoning on fallback | Routing policy | Within the Claude API, prefer targets that read the blocks; across clouds, accept the loss and log model_binding_mismatch counts |
The general rule: a fallback set must be validated against the intersection of what every
target accepts. Anthropic applies a version of it to its own fallback list: a request “must
be valid as a direct request to every model named”
(Refusals and fallback).
Its fallback entries can override thinking, max_tokens, output_config and speed per
attempt; tool_choice is not on that list, so a forced tool has to be valid everywhere.
For the history layer, the Preserved thinking page has a section titled “Libraries, proxies,
and gateways.” Its four rules: forward the caller’s anthropic-beta values and
thinking.block_binding unchanged and return input_transformations; leave a role: "system"
message where the caller put it; to turn tools off, “send tool_choice: {"type": "none"}.
Don’t remove tools”; and “Don’t hide the 400.”
OpenRouter’s
Claude Opus 5.5 migration guide
shows a router making different choices and documenting them. Disabling reasoning returns a
400 at the router. Thinking budgets “are accepted by OpenRouter but the request goes upstream
as adaptive thinking with no budget… Remove them and set an effort instead.” Preserved
thinking: “Requests through OpenRouter are not subject to this enforcement.” And it sends
summarized thinking by default: “Set thinking.display to "omitted", or
reasoning.exclude: true on Chat Completions, if you do not want it.” If you run a direct
key and a router side by side, a budgeted request or an edited history can be a 400 on one
path and a 200 on the other, and the progress-note UI differs. Test your failover against
the strictest path.
For our own gateway, the trade-off we chose is a 400 over a rewrite whenever a caller’s field would change their bill, because the caller who set a budget believes something about their costs.
These specific behaviors are design, not shipped: our gateway is designed to reject a
disabled or budgeted thinking field and a forced tool_choice for a target that does not
accept them, returning Anthropic’s documented error text; to forward history, beta headers
and block_binding untouched and return input_transformations; and to write effort on
every fallback hop.
Find the requests that will 400 before you switch: pass 0, count_tokens and max_tokens: 0
Your Opus 5 logs are the test set. Use a separate workspace and key for replays, and check that logged bodies with user data are allowed to leave their retention policy.
Pass 0, local. Three of the four 400s are visible in the body: thinking.type of
disabled or enabled, tool_choice.type of any or tool, and a computer_20251124 tool.
Pass 1, free: count_tokens. The
Token counting page
says it “is free to use,” with its own rate limit. It is documented to apply the
tool_choice check, not the others, so a 200 means “not rejected yet.” It accepts only eight
top-level fields
(Count tokens in a Message, API reference),
and it rejects server tools, the MCP connector, and url or file sources that Messages
accepts. Those are false positives.
Pass 2, input tokens only: max_tokens: 0. Anthropic’s cache pre-warming shape
(Prompt caching, Pre-warming the cache)
reads the prompt and returns with “zero output tokens” billed, through the real Messages
validation. Its purpose here is to catch what the docs do not list. It has its own reject
list: stream: true, thinking.type: "enabled", output_config.format and forced
tool_choice, so the script strips the first and third and leaves the others to pass 0.
Cost of pass 2: requests × average input tokens × $4 per million, plus uncached prefixes
with a cache_control breakpoint at $5 per million. Sample 200 requests first. The script
targets the Claude API, so Bedrock traffic that uses computer_20251124 will be flagged here
and is fine where it runs.
The script needs Python 3.9+ and a JSONL file exported from your request logs, one line per
request with the body and its anthropic-beta value.
# replay.py: which logged Opus 5 requests would Opus 5.5 reject?
# Input: JSONL, one logged request per line: {"beta": "<anthropic-beta header or empty>", "body": {...}}
# Without ANTHROPIC_API_KEY only pass 0 runs (local, free). PASS2=1 enables the input-billed pass.
import collections, json, os, sys, time, urllib.error, urllib.request
from concurrent.futures import ThreadPoolExecutor
API = os.environ.get("REPLAY_API", "https://api.anthropic.com/v1/messages")
KEY = os.environ.get("ANTHROPIC_API_KEY") # use a separate workspace key for replays
COUNT_FIELDS = {"model", "messages", "system", "tools", "tool_choice",
"thinking", "output_config", "cache_control"}
KNOWN = ("thinking.type.", "tool_choice: type", "does not support tool types")
def pass0(body): # the three documented 400s, read straight off the body
thinking = (body.get("thinking") or {}).get("type")
if thinking in ("disabled", "enabled"):
return f"pass0: thinking.type.{thinking}"
if (body.get("tool_choice") or {}).get("type") in ("any", "tool"):
return "pass0: forced tool_choice"
if any(t.get("type") == "computer_20251124" for t in body.get("tools") or []):
return "pass0: computer_20251124 (rejected on Claude API and Google Cloud only)"
return None
def post(url, body, beta, tries=4):
headers = {"x-api-key": KEY, "anthropic-version": "2023-06-01",
"content-type": "application/json"}
if beta:
headers["anthropic-beta"] = beta
for attempt in range(tries):
req = urllib.request.Request(url, json.dumps(body).encode(), headers)
try:
with urllib.request.urlopen(req, timeout=60) as r:
return r.status, ""
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", "replace")
if e.code in (429, 529) and attempt < tries - 1:
time.sleep(2 ** attempt)
continue
try:
return e.code, json.loads(raw)["error"]["message"]
except (ValueError, KeyError, TypeError):
return e.code, raw[:200]
except (urllib.error.URLError, TimeoutError) as e:
return 0, f"network: {e}"
def check(entry):
body = entry.get("body", entry) | {"model": "claude-opus-5-5"}
beta = entry.get("beta", "")
found = pass0(body)
if found or not KEY:
return found or "pass0: clean (API passes skipped, no key)"
status, msg = post(API + "/count_tokens",
{k: v for k, v in body.items() if k in COUNT_FIELDS}, beta)
if status in (429, 529, 0):
return f"unchecked: {status} {msg[:60]}" # never fall through to the billed pass
if any(k in msg for k in KNOWN) or os.environ.get("PASS2") != "1":
return f"pass1: {msg[:90]}" if msg else "pass1: no known rejection"
body.pop("stream", None)
(body.get("output_config") or {}).pop("format", None) # max_tokens: 0 rejects it on its own
status, msg = post(API, body | {"max_tokens": 0}, beta)
return f"pass2: {status} {msg[:90]}" if msg else "pass2: accepted"
entries = [json.loads(line) for line in open(sys.argv[1]) if line.strip()]
with ThreadPoolExecutor(max_workers=int(os.environ.get("CONCURRENCY", "4"))) as pool:
results = list(pool.map(check, entries))
for msg, count in collections.Counter(results).most_common():
print(f"{count:7d} {msg}")Offline diff for history edits. Replay cannot trigger the prefix check: it applies to
Opus 5.5 thinking blocks, and Opus 5 logs have none. But the edits that would trip it are in
your logs. For each conversation, compare consecutive requests: system and tools must be
identical, and the earlier request’s messages must be an exact prefix of the later one’s.
Any conversation that fails it would 400 on a new account once it runs on Opus 5.5 and
replays thinking blocks past the edit.
What only live traffic shows. Edits your logs do not capture, such as a proxy rewriting
bodies after logging. On the first Opus 5.5 sessions, send the binding header with
"drop_block" and alert on prefix_binding_mismatch. On an older account you can leave the
field unset and read thinking_mismatch_allowed entries instead: what a new account would
reject, without a 400.
A migration order for Claude Opus 5.5: inventory, replay, change, canary, roll back
- Inventory. Run pass 0 and the offline diff over a week of logs. That sizes the work for free.
- Replay. Pass 1 on everything, pass 2 on a 200-request sample (replay).
- Change code. Read blocks by type and raise
max_tokens(thinking); replace forced tools (tool_choice); make history append-only (prefix); move to the computer toolset where you call the Claude API or Google Cloud; adddisplay: "updates"with its header; write effort on every hop. - Canary. New conversations only, with
drop_blockand the binding header. Watch the share ofstop_reason: "max_tokens", output tokens per request, refusal rate,input_transformationsentries, and p95 latency against the Opus 5 baseline. - Roll back at conversation boundaries. Send new conversations to Opus 5 and let open
ones finish where they are. Do not strip thinking blocks on the way back: if a
conversation returns to 5.5, its reasoning is still readable. Write effort explicitly on
the rollback hop.
highmatches the quality you had on Opus 5 and costs what Opus 5 cost;mediumholds the token budget you tuned on 5.5 and accepts lower quality on Opus 5.
Start with pass 0 today. It costs nothing, and it tells you how big step 3 is.