Blog

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.

Leo Kaka16 min readUpdated Sep 28, 2026
Table of five changes from Claude Opus 5 to Claude Opus 5.5: thinking off, forced tools, edited history, computer use and progress notes, with what Opus 5 accepted, what Opus 5.5 returns (four HTTP 400s and one HTTP 200 with a quiet UI), and how you find out for each

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.

ChangeOn Claude Opus 5On Claude Opus 5.5
thinking disabledAccepted at effort high or below✗400 at every effort level
tool_choice of any or toolAccepted✗400, on count_tokens too
Thinking block replayed after system, tools or earlier messages changedAccepted; no check✗400 on accounts created from 31 Aug 2026, 00:00 UTC; in Batch, dropped silently
computer_20251124 toolAccepted with its beta header✗400 on the Claude API and Google Cloud; Bedrock still accepts it
Notes between tool callstext blocks⚠200; thinking blocks, empty at the default display
Request with no effort setRuns at high⚠200; runs at medium
Conversation moved from 5.5 down to Opus 5n/a⚠200; Opus 5.5 thinking blocks dropped, not billed

Correction (2026-09-28): an earlier version of this table listed a manual budget_tokens alongside 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 is disabled (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.”

Flowchart of a streamed response with thinking. After the thinking block opens, the path splits: with display summarized, thinking_delta events stream the thinking summary; with display omitted, one empty thinking_delta arrives and no thinking text streams. Both paths then get a single signature_delta and content_block_stop, after which the text block streams.
Anthropic's diagram of a streamed thinking block. The right-hand branch, display omitted, is the Opus 5.5 default: one empty delta and a signature, no text for your UI.Source: Thinking, Streaming thinking (Claude Platform Docs)

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:

  1. Opus 5 sees none of Opus 5.5’s reasoning.
  2. It runs at high, its own default: a different effort than the one you tested.
  3. 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:

ChangeLayerWhat absorbing it honestly looks like
thinking disabledBefore routingReject for a target that does not accept it, or route only to one that does
Forced tool_choiceBefore routingSame; validate the body against every target in the fallback set
History edit before a 5.5 thinking blockHistoryForward history byte for byte; pass anthropic-beta and block_binding through; return input_transformations
computer_20251124Endpoint selectionTreat Claude API and Bedrock as different tool contracts
Progress notesResponse translationKeep thinking blocks with text apart from reasoning when converting to another API shape
Default effortEvery routeWrite effort on every request, primary and fallback
Dropped reasoning on fallbackRouting policyWithin 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

  1. Inventory. Run pass 0 and the offline diff over a week of logs. That sizes the work for free.
  2. Replay. Pass 1 on everything, pass 2 on a 200-request sample (replay).
  3. 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; add display: "updates" with its header; write effort on every hop.
  4. Canary. New conversations only, with drop_block and the binding header. Watch the share of stop_reason: "max_tokens", output tokens per request, refusal rate, input_transformations entries, and p95 latency against the Opus 5 baseline.
  5. 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. high matches the quality you had on Opus 5 and costs what Opus 5 cost; medium holds 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.