DEV Community

Cover image for Claude Opus 5.5 Migration: Four Errors You'll See and Three You Won't
galian for Cursuri AI

Posted on

Claude Opus 5.5 Migration: Four Errors You'll See and Three You Won't

Anthropic released Claude Opus 5.5 on 22 September 2026. It costs less than Opus 5 ($4/$20 per million input/output tokens, down from $5/$25), cache reads dropped from $0.50 to $0.20, and in Anthropic's own benchmark table it beats Claude Fable 5.1 on agentic coding at 40% of Fable's list price. Most teams running Opus 5 will move, and most will start by changing one string:

model = "claude-opus-5-5"  # was "claude-opus-5"
Enter fullscreen mode Exit fullscreen mode

That line is the whole migration only if your integration never disabled thinking, never forced a tool, never used the older computer use tool, and never edits its own history. Four request shapes that Opus 5 accepted now come back as a 400. Those are the easy ones: they fail loudly, in staging, with a readable message. The three changes that don't throw are the ones that reach production — a default that moved, an agent UI that goes quiet, and an unattended agent that stops halfway.

Below: each exact error message (so you can grep your logs for it), the fix, and the three silent changes with the code that catches them. It is the model-upgrade drill we run in the production LLM integration track at Cursuri-AI.ro, cut down to what this release actually changes.

What changed, in one table

Claude Opus 5 Claude Opus 5.5
Model ID claude-opus-5 claude-opus-5-5
Input / output (per MTok) $5 / $25 $4 / $20
Cache read (per MTok) $0.50 $0.20 (0.05× input)
5-minute cache write (per MTok) $6.25 $5
Batch API (per MTok) $2.50 / $12.50 $2 / $10
Fast mode (Claude API only, research preview) $10 / $50 $8 / $40
Default effort high medium
thinking: {"type": "disabled"} Accepted at high or below 400 at every level
tool_choice of type any / tool Accepted 400
computer_20251124 on Claude API / Google Cloud Accepted with beta header 400 (toolset only)
Text between tool calls text blocks thinking blocks, empty by default

The context window (1M tokens) and max output (128K; up to 300K on the Batch API with the output-300k-2026-03-24 beta header) are unchanged. The knowledge cutoff is June 2026, and Anthropic commits to not retiring the model before 22 September 2027. The numbers come from the Opus 5.5 model page, what's new in Opus 5.5 and the fast mode docs.

The four errors you'll see

1. "thinking.type.disabled" is not supported for this model

"thinking.type.disabled" is not supported for this model. Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior.
Enter fullscreen mode Exit fullscreen mode

On Opus 5, thinking was on by default, but you could switch it off at high effort or below — and latency-sensitive integrations often did. On Opus 5.5 adaptive thinking is always on, and disabled returns a 400 at every effort level. (Manual {"type": "enabled", "budget_tokens": N} budgets were already rejected on Opus 5, with the same message naming "thinking.type.enabled".)

The replacement for "off" is a lower effort:

resp = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=16_000,
    output_config={"effort": "low"},   # was: thinking={"type": "disabled"}
    messages=[{"role": "user", "content": prompt}],
)
Enter fullscreen mode Exit fullscreen mode

The error is the easy part. The follow-on bug is in the code that reads the response. With thinking disabled, resp.content[0] was always your text. Now a response can begin with one or more thinking blocks, and at the default display: "omitted" their text is an empty string, so they don't even look like content when you print them. resp.content[0].text starts raising AttributeError on some turns and not others, depending on whether the model decided to think. Select blocks by type, always:

def text_of(resp) -> str:
    return "".join(b.text for b in resp.content if b.type == "text")
Enter fullscreen mode Exit fullscreen mode

Two more consequences of "always on". Thinking tokens are billed as output tokens even when their text isn't returned to you, so effort now moves your bill whether or not you ever look at the reasoning. And max_tokens covers thinking plus the reply: a limit sized for Opus 5 with thinking off can now end replies early with stop_reason: "max_tokens".

2. tool_choice: type "tool" and "any" are not supported for this model

tool_choice: type "tool" and "any" are not supported for this model.
Enter fullscreen mode Exit fullscreen mode

Forced tool use is gone. {"type": "any"} and {"type": "tool", "name": ...} are rejected; auto (the default) and none still work. The same validation runs on the token-counting endpoint, so if a cost estimator counts tokens for the same request shape, it breaks too — in a different service, usually owned by a different person.

Teams forced tools for two different jobs, and each job has its own replacement.

Job one: "give me JSON." Forcing a single extract tool was the classic way to get schema-shaped output. That job now belongs to structured outputs, which are generally available and need no beta header:

import json

resp = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=4_096,
    output_config={
        "effort": "low",
        "format": {
            "type": "json_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "vendor": {"type": "string"},
                    "total_cents": {"type": "integer"},
                    "due_date": {"type": "string", "format": "date"},
                },
                "required": ["vendor", "total_cents", "due_date"],
                "additionalProperties": False,
            },
        },
    },
    messages=[{"role": "user", "content": f"Extract the invoice fields:\n\n{invoice_text}"}],
)
invoice = json.loads(text_of(resp))
Enter fullscreen mode Exit fullscreen mode

The JSON arrives in a text block, which is one more reason text_of() exists. Check the schema limitations before you port: numeric bounds (minimum, maximum), string lengths and recursive schemas aren't supported, so keep validating those in code.

Job two: "call this tool now." Where you forced a specific tool in an agent step, the migration guide's answer is to keep tool_choice on auto, add strict: true so every call matches the tool's schema, and say in the prompt when the tool applies:

resp = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=16_000,
    tools=[{**tool, "strict": True} for tool in TOOLS],
    tool_choice={"type": "auto"},
    messages=[{"role": "user", "content": "Look up order 4417 with get_order before you answer."}],
)
called_tool = any(b.type == "tool_use" for b in resp.content)
Enter fullscreen mode Exit fullscreen mode

What you lose is the guarantee. Log called_tool as a metric: a rising share of turns where the model answered in text instead of calling the tool is a prompt problem you want to see on a dashboard, not in a support ticket.

3. 'claude-opus-5-5' does not support tool types: computer_20251124.

'claude-opus-5-5' does not support tool types: computer_20251124.
Enter fullscreen mode Exit fullscreen mode

The message goes on to list the tool types the model does accept. On the Claude API and Google Cloud, Opus 5.5 takes computer use only as the computer_toolset_20260801 toolset. Amazon Bedrock still accepts the old tool, which is a trap of its own: the same code passes on Bedrock and fails on the Claude API.

The request change is small. Drop the beta header; the toolset entry takes no name and no display size:

# Before (Opus 5)
client.beta.messages.create(
    model="claude-opus-5",
    max_tokens=4096,
    betas=["computer-use-2025-11-24"],
    tools=[{"type": "computer_20251124", "name": "computer",
            "display_width_px": 1024, "display_height_px": 768}],
    messages=messages,
)

# After (Opus 5.5)
client.messages.create(
    model="claude-opus-5-5",
    max_tokens=4096,
    tools=[{"type": "computer_toolset_20260801"}],
    messages=messages,
)
Enter fullscreen mode Exit fullscreen mode

The loop change is bigger. With the toolset, the action is the tool_use block's name, not input.action; one turn can carry several of those blocks; and every result has to echo toolset_name. A dispatcher written as match block.input["action"] needs a rewrite, not a rename. The computer use migration notes list every change.

4. Invalid `signature` in `thinking` block

messages.{i}.content.{j}: 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".
Enter fullscreen mode Exit fullscreen mode

Opus 5.5 inherits Fable 5.1's binding: a thinking block stays valid only while the system prompt, the tools and every message before it are unchanged. For accounts created on or after 31 August 2026, editing any of those and replaying the block returns this 400; older accounts get the check only when they opt in. Rebuilding the system prompt per request, trimming old turns on the client, or injecting a reminder and deleting it on the next call all trigger it, and retrying the same request body won't clear it.

The fix is structural: keep the history append-only, change instructions with mid-conversation system messages instead of edits, and leave trimming to server-side compaction or context editing. It is the same discipline as keeping agent context append-only for caching reasons, except that on Opus 5.5 it is also a correctness rule.

What is new with Opus 5.5 is which models can read whose reasoning, and that part never throws:

Conversation moves… Reasoning carried over?
Opus 5 (or earlier Opus, Sonnet, Haiku) → Opus 5.5 Yes
Opus 5.5 → Fable 5.1 or Mythos 5.1 (Claude API) Yes
Opus 5.5 → any other model (Sonnet 5, Haiku 4.5, Opus 5, Opus 4.8…) No, blocks silently dropped
Fable or Mythos → Opus 5.5 No, blocks silently dropped

A block the target model can't read is removed before the model sees it: the request succeeds, and the dropped blocks aren't billed. With the thinking-binding-controls-2026-08-01 beta header, the drop is reported in a top-level input_transformations array. Turn that on in any service that routes between models, or you will never know it happened.

The design consequence for routers: escalate up, don't hand down. Start a hard task on Opus 5.5 and escalate to Fable 5.1 when it stalls, and the escalated model keeps everything Opus 5.5 reasoned so far. Route the other way — Fable plans, Opus executes in the same conversation — and every hand-off starts cold. The same applies to refusal fallbacks and to mid-conversation downgrades that save money. If your system is built on routing and escalation between agents, this table belongs in its design doc.

The three changes you won't see

1. The default effort dropped a level, and the benchmarks didn't

A request that omits effort ran at high on Opus 5. On Opus 5.5 it runs at medium. No error, no warning, different behavior.

Anthropic's position is that this is fine: in its testing, Opus 5.5 at medium matches or beats Opus 5 at high on coding and knowledge-work evaluations, and on several coding evaluations low comes close at much lower cost. That may well hold for your workload. But read the announcement's fine print before you rely on it:

  • The benchmark table is run at max effort: "unless otherwise noted, all Claude Opus 5.5 results use adaptive thinking at max effort."
  • The cost claim, 40% less than Opus 5 on typical workloads, is "at default settings", which means medium.

Both statements are true. They describe different configurations. The model that scores 66.4% on Terminal-Bench 4.0 and the model that costs 40% less are two settings of the same weights, and you choose one per request.

Two more effort facts change the arithmetic. At any given level, Opus 5.5 tends to think more per turn than Opus 5, most of all at xhigh and max, so carrying your Opus 5 setting over means longer turns and more output tokens, not the same bill. And level names don't map across models: Opus 5.5's medium is not Opus 5's medium.

So don't carry settings over. Re-run the sweep. Each case is a prompt plus a check function, the same shape as any eval:

import time
from anthropic import Anthropic

client = Anthropic()
USD_IN, USD_OUT = 4 / 1_000_000, 20 / 1_000_000   # claude-opus-5-5 list prices

def run_case(case: dict, effort: str) -> dict:
    started = time.monotonic()
    with client.messages.stream(               # stream long turns
        model="claude-opus-5-5",
        max_tokens=64_000,
        output_config={"effort": effort},
        messages=[{"role": "user", "content": case["prompt"]}],
    ) as stream:
        resp = stream.get_final_message()
    return {
        "passed": case["check"](text_of(resp)),
        "seconds": time.monotonic() - started,
        "usd": resp.usage.input_tokens * USD_IN + resp.usage.output_tokens * USD_OUT,
    }

def sweep(cases: list[dict], levels=("low", "medium", "high", "xhigh")) -> None:
    for effort in levels:
        rows = [run_case(case, effort) for case in cases]
        n = len(rows)
        print(f"{effort:>6}  pass {sum(r['passed'] for r in rows) / n:5.0%}  "
              f"avg {sum(r['seconds'] for r in rows) / n:6.1f}s  "
              f"avg ${sum(r['usd'] for r in rows) / n:.4f}")
Enter fullscreen mode Exit fullscreen mode

Pick the lowest level that holds your pass rate, set it explicitly in code (never rely on a default that has changed once and can change again), and leave max_tokens room for thinking; Anthropic reports that 128,000, the model's maximum, worked well for long agentic coding turns. If one conversation needs different levels at different points, use a per-message effort change (beta header mid-conversation-output-config-2026-07-01), because changing the top-level value between requests invalidates the prompt cache. The sweep above is deliberately tiny. The version that earns its keep is a real eval harness with your own traffic as the dataset.

2. Your agent's progress messages went quiet

On Opus 5, the short notes the model writes between tool calls ("Found the failing test, checking the auth module next") came back as text blocks. Plenty of agent UIs stream exactly those as a status line.

On Opus 5.5, as on Fable 5.1, those notes arrive as progress-update thinking blocks, at most one before each tool call. At the default display: "omitted" their text is an empty string. Nothing fails. Your UI just shows a spinner for three minutes where it used to narrate.

To get the notes back, request display: "updates" (beta), which keeps reasoning hidden and returns progress updates as text, and render any non-empty thinking delta:

with client.beta.messages.stream(
    model="claude-opus-5-5",
    max_tokens=64_000,
    thinking={"type": "adaptive", "display": "updates"},
    betas=["thinking-display-updates-2026-08-18"],
    tools=TOOLS,
    messages=messages,
) as stream:
    for event in stream:
        if (event.type == "content_block_delta"
                and event.delta.type == "thinking_delta"
                and event.delta.thinking):         # non-empty text = a progress update
            ui.status(event.delta.thinking)
    final = stream.get_final_message()

messages.append({"role": "assistant", "content": final.content})  # unchanged, see error 4
Enter fullscreen mode Exit fullscreen mode

Three details. Send the blocks back unchanged with the rest of the assistant turn. Expect fewer updates at higher effort and in long tool chains, and a pause of several seconds before each one. And when a response stops on max_tokens right after a tool call, the last progress block reads This part of the response was interrupted before it finished.: show it rather than swallow it. display: "summarized" also returns the updates, but mixed with reasoning summaries you can't tell apart.

3. Your unattended agent stops halfway

On long, multi-part tasks Opus 5.5 reports progress readily, and some of those reports end the turn with text instead of a tool call: stop_reason: "end_turn", a tidy summary, and "next, I'll migrate the remaining two endpoints." A loop that treats end_turn as "task complete" exits right there. In an interactive product that is a feature. In a nightly job it is a half-done migration that looks finished in the logs.

Anthropic's prompting guide for Opus 5.5 says to treat a text-only end of turn as a report, not as proof of completion: keep the task's parts in a checklist (a to-do tool, a file), and if items remain open with no blocker stated, nudge — two or three times at most:

MAX_NUDGES = 3

def run_unattended(task: str, checklist) -> list:
    messages = [{"role": "user", "content": task}]
    nudges = 0
    while True:
        with client.messages.stream(
            model="claude-opus-5-5",
            max_tokens=128_000,
            output_config={"effort": "medium"},
            tools=TOOLS,
            messages=messages,
        ) as stream:
            resp = stream.get_final_message()
        messages.append({"role": "assistant", "content": resp.content})

        if resp.stop_reason == "tool_use":
            messages.append({"role": "user", "content": run_tools(resp.content)})
            continue

        open_items = checklist.open_items()        # your state, not the model's summary
        if resp.stop_reason == "end_turn" and open_items and nudges < MAX_NUDGES:
            nudges += 1
            messages.append({"role": "user", "content": (
                f"Your task list still has open items: {'; '.join(open_items)}. "
                "Continue with them. If one is blocked, say what is blocking it."
            )})
            continue

        return messages   # finished, refused, out of tokens, or stuck: a human looks next
Enter fullscreen mode Exit fullscreen mode

The checklist is the important part: completion comes from your state, not from the tone of the model's last message. And cap the nudges. An agent that is genuinely stuck should stop and be reviewed, not loop politely forever.

One more non-error: refusals have new categories

Opus 5.5 runs a biology classifier in addition to the cybersecurity one, plus a new reasoning_extraction category. A decline is an HTTP 200 with stop_reason: "refusal" and a stop_details.category, so code that only checks the status code logs it as a success with an empty answer. Two practical notes:

  • Prompts that tell the model to write out its reasoning in the response, a common workaround from the thinking-disabled days, can be declined as reasoning_extraction. Remove those instructions, and read display: "summarized" thinking if you need the reasoning.
  • Server-side fallback (fallbacks: "default", beta header server-side-fallback-2026-07-01) retries a declined request on a recommended model, except for reasoning_extraction, which comes straight back to you. And a fallback to another model runs without Opus 5.5's reasoning; see the table under error 4.

A refusal that arrives before any output isn't billed, but it still counts against your rate limits.

The money, honestly

At identical token counts the list-price drop is real. One agent turn with a 60K-token cached prefix, 3K fresh input tokens and 2K output tokens:

Opus 5 Opus 5.5
60K cache read $0.030 $0.012
3K input $0.015 $0.012
2K output $0.050 $0.040
Per turn $0.095 $0.064 (−33%)

Token counts won't stay identical, though, and they move in opposite directions: more thinking per turn at a given effort pushes output up, while a lower default effort and (according to Anthropic and its early testers) fewer steps per task push it down. The honest answer to "will my bill drop?" is "re-baseline and see", which the sweep above already does. Two other levers: the Batch API halves everything ($2/$10) for work that can wait, and fast mode ($8/$40, up to 2.5× more output tokens per second, Claude API only, research preview) is for when latency is worth paying double. Requests at different speeds don't share cached prefixes, so don't flip fast mode on and off within a conversation.

Opus 5.5 or Fable 5.1?

Vendor-reported results, at max effort, from Anthropic's announcement:

Benchmark Opus 5.5 Fable 5.1 Opus 5
Terminal-Bench 4.0 66.4% 55.8% 52.3%
CursorBench 4.0 57.8% 51.8% 46.6%
GDPval-AA v2.1 (Elo) 1846 1735 1708
OSWorld 2.0 (partial) 81.8% 80.7% 74.0%

The same table puts GPT-6 Astra ahead of Opus 5.5 on AutomationBench (41.4% vs 40.0%) and Terminal-Bench-Science (64.6% vs 58.7%), a useful reminder that "the leading model" depends on which row you read.

At $4/$20 against Fable 5.1's $10/$50, the default move for most workloads is Opus 5.5, with Fable 5.1 kept as the escalation tier rather than retired. Thanks to the compatibility table above, that direction — Opus 5.5 first, Fable 5.1 on escalation — is also the one that doesn't throw away reasoning. One caveat for cache-heavy traffic: Fable 5.1's cache read is $0.25 per MTok, close to Opus 5.5's $0.20, so for workloads dominated by cached context the saving comes mostly from output and fresh input, and it is smaller than the list prices suggest.

The migration checklist

  • [ ] Model ID → claude-opus-5-5 (on Bedrock: anthropic.claude-opus-5-5)
  • [ ] Remove thinking: {"type": "disabled"}; set output_config.effort explicitly
  • [ ] Read content by block type, never content[0].text
  • [ ] Raise max_tokens to leave room for thinking
  • [ ] Replace forced tool_choice: structured outputs for JSON jobs, auto + strict: true + a prompt instruction for tool jobs, including in token-counting calls
  • [ ] Computer use on the Claude API or Google Cloud → computer_toolset_20260801, and rewrite the dispatcher
  • [ ] No edits to system, tools or earlier turns mid-conversation; report input_transformations wherever you route between models
  • [ ] display: "updates" if your UI shows progress between tool calls
  • [ ] Unattended loops: completion comes from a checklist, with capped nudges on a text-only end_turn
  • [ ] Handle stop_reason: "refusal"; remove "write out your reasoning" instructions
  • [ ] Re-run the effort sweep and re-baseline cost and latency before moving production traffic

If your team works in Claude Code, the bundled Claude API skill automates the mechanical half: /claude-api migrate this project to claude-opus-5-5 swaps model IDs, fixes the breaking parameters, calibrates effort and hands back a list of things to verify by hand. It can't know that your UI depends on text between tool calls, or that your nightly job trusts end_turn; those are the items only your tests will catch. Structured Claude Code workflows — plan, migrate, verify against a test suite — are what turn that checklist into a one-afternoon job.

FAQ

Is Claude Opus 5.5 cheaper than Opus 5?
On list price, yes: $4/$20 against $5/$25 per million tokens, and cache reads fall from $0.50 to $0.20. Anthropic says typical workloads cost about 40% less at default settings, but the default effort is also lower, so compare the two at the effort level you will actually run.

Can I still turn thinking off on Opus 5.5?
No. Thinking is always on, and disabled returns a 400. Use effort: "low" for latency-sensitive paths, and keep display: "omitted" (the default) if you only want thinking text out of your responses.

Will conversations started on Opus 5 continue on Opus 5.5?
Yes. Opus 5.5 reads thinking blocks from Opus 5 and earlier Opus, Sonnet and Haiku models, so in-flight conversations keep their reasoning. The reverse doesn't hold: hand an Opus 5.5 conversation back to Opus 5 and its reasoning is dropped.

Does anything differ on Bedrock, Google Cloud or Microsoft Foundry?
The thinking, tool-choice and history changes apply on every platform. Computer use is the exception: the old computer_20251124 tool is rejected on the Claude API and Google Cloud but keeps working on Amazon Bedrock; for Foundry, check the compatibility table in the computer use docs. Fast mode exists only on the Claude API.

How do I get guaranteed JSON now that I can't force a tool?
Use structured outputs: output_config.format with a JSON schema. They are generally available and return the JSON in a text block.

The bottom line

On paper, Opus 5.5 is an easy upgrade: cheaper, faster and, by Anthropic's numbers, stronger on agentic work. The four 400s make sure you fix the obvious parts, so spend your testing time on the other three. Set effort explicitly and sweep it again, render progress from thinking blocks, and take task completion from your own state rather than from the model's last sentence. None of those three will ever show up as an error in your logs.


I build and teach production AI systems at Cursuri-AI.ro, Eastern Europe's AI education platform — hands-on courses on LLM integration, agent architecture, evaluation, and shipping model upgrades without surprises.

Top comments (0)