Dev

Opus 5.5 is cheaper, and the 400s are the easy part

A request that returns 400 has already told you what to fix. The changes that cost you on this upgrade come back as 200: a response that no longer starts with text, an effort level that dropped a notch because you never set it, and an alias that moved you to a new model without a deploy.

Claude Opus 5.5 shipped on September 22 at $4 input and $20 output per million tokens, down from Opus 5's $5 and $25. The same day, Claude Code v2.1.280 made it the default Opus model. Anthropic's own list of what breaks for code already running on Opus 5 has four items. All four end in a 400, three of them on the very first request.

A 400 is the good kind of break. The request is rejected up front, and the message names the fix. The changes worth an afternoon on this upgrade are the ones that come back as 200.

Which requests now return 400?

Four kinds, if you are coming from Opus 5: turning thinking off, forcing a tool call, the old computer use tool on the Claude API and Google Cloud, and replaying a thinking block after you edited what came before it. The first three come with error strings specific enough to grep your logs for. The computer use one goes on to list the tool types the model does accept:

text
"thinking.type.disabled" is not supported for this model. Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior.
tool_choice: type "tool" and "any" are not supported for this model.
'claude-opus-5-5' does not support tool types: computer_20251124.

Two of the fixes are deletions. Drop the thinking field (adaptive thinking is always on, and thinking: {"type": "adaptive"} is equivalent to omitting it) and control depth with output_config.effort. The forced-tool check also applies to the token counting endpoint, so a cost estimator that mirrors your production request fails too. Computer use is not a deletion. The request declares computer_toolset_20260801 with no beta header, and the agent loop has to change with it: the action now arrives as the tool_use block's name instead of input.action, several can come in one turn, and every result echoes toolset_name. On Amazon Bedrock the old computer_20251124 tool keeps working.

The replay case is easy to miss because it depends on your account's age. For accounts created on or after August 31, 2026, 00:00 UTC, replaying a thinking block after you edited the system prompt, the tools or an earlier message returns 400 by default. An append-only conversation never hits it. Code that rewrites history in place to save tokens can, unless you opt into dropping the stale blocks (thinking.block_binding.prefix_mismatch_behavior: "drop_block" behind the thinking-binding-controls-2026-08-01 beta header).

Coming from further back adds older rejections that 5.5 keeps: a manual budget_tokens thinking budget and non-default temperature, top_p or top_k (both rejected since Opus 4.7), and a prefilled assistant turn (since 4.6).

What replaces forced tool use, and is it the same guarantee?

Not quite. The migration guide says to use tool_choice: {"type": "auto"} with strict: true on the tool and to say in the prompt when the tool applies. Strict tool use guarantees that a call, when it happens, matches your schema. It does not guarantee the call happens. Forcing did. (Strict mode also takes only a subset of JSON Schema and needs additionalProperties: false on every object, so check each input_schema before you flip it on.)

So look at why you forced the call. If it was only to get JSON back, move the schema to structured outputs (output_config.format), which constrains the response itself; a refusal or a max_tokens cutoff can still come back as a 200 without valid JSON, so check stop_reason before you parse. If the model genuinely has to act, the request can no longer promise it. Your code has to notice when it didn't:

python
calls = [b for b in resp.content if b.type == "tool_use" and b.name == "get_weather"]
if not calls:
    raise RuntimeError(f"no get_weather call (stop_reason={resp.stop_reason})")

The prompt makes the call likely. The check doesn't make it happen; it makes a missing call an explicit failure instead of a quiet text reply, which is the weighing-versus-binding cut from a prompt is not an invariant, one level down.

What breaks without an error?

The response shape. Thinking runs on every Opus 5.5 request, so a response can begin with one or more thinking blocks before the first text block, and at the default display: "omitted" those blocks arrive with an empty thinking field. Anything that reads the reply by position gets a thinking block where it expected text.

I have shipped this pattern twice. In March I published the Anthropic branch of Beetroot's multi-provider integration with "Response shape: data.content[0].text" as one of its four differences from OpenAI. My agent-recall package does the same in its API backend: resp.content[0].text, max_tokens=4096, no thinking field, wrapped in a broad except that logs the failure and returns None. Neither hits this change as written. agent-recall's API alias pins Opus 4.6, which runs without thinking unless asked, and the Beetroot mapping predates 5.5 entirely. Point either one at claude-opus-5-5 and the request can succeed and get billed while the parse fails in your own code.

A synthetic response in the shape the migration guide describes reproduces it with the anthropic Python SDK 1.8.0, and the fix is one line:

python
from anthropic.types import Message
 
resp = Message.model_validate({
    "id": "msg_x", "type": "message", "role": "assistant",
    "model": "claude-opus-5-5", "stop_reason": "end_turn", "stop_sequence": None,
    "usage": {"input_tokens": 10, "output_tokens": 50},
    "content": [
        {"type": "thinking", "thinking": "", "signature": "sig"},
        {"type": "text", "text": "the answer"},
    ],
})
 
resp.content[0].text
# AttributeError: 'ThinkingBlock' object has no attribute 'text'
 
"".join(b.text for b in resp.content if b.type == "text")
# 'the answer'

Three more changes land without an error.

Effort dropped a notch. A request that omits effort now runs at medium, where Opus 5 ran at high. Nothing fails; the default just moved down a level, and at any given level the model tends to think more per turn than Opus 5 did. If you never set effort, you now run on a setting you did not choose. Set it explicitly and re-run whatever eval told you the old one was right.

The narration went quiet. The short notes the model writes between tool calls now come back as thinking blocks, empty at the default display. A product that streamed them to users as progress updates stops updating mid-task. Set thinking: {"type": "adaptive", "display": "updates"} with the thinking-display-updates-2026-08-18 beta header to get the progress notes back while the reasoning stays hidden, or "summarized" to get both mixed together, then render the non-empty blocks ahead of the tool call they precede.

Thinking now shares your output budget. On Opus 4.8 and earlier, a request without a thinking field ran without thinking. On 5.5 it thinks, the thinking counts against the same max_tokens your answer used to have to itself, and it is billed as output even when you never see the text. The per-token price went down. For a workload that used to run without thinking, the per-request cost can go the other way. Measure per request.

Why does the default flip matter more than the price?

Because an alias moves you without a deploy. Claude Code v2.1.280 made Opus 5.5 the default Opus, so anything that shells out to claude -p --model opus gets the new model the day that Claude Code updates, unless you overrode the alias, with no change on your side.

agent-recall shows both halves in one package. Its API backend resolves the alias opus to a pinned claude-opus-4-6, so it is safe from the response-shape change and also blind to it. Its CLI backend runs claude -p --model opus, so it follows the alias whenever Claude Code updates. Claude Code parses the response shape itself and hands back plain text, so that path doesn't break on the parse. What it doesn't absorb is the change in the model: the effort, the length, the disposition. Same config key, two models.

I argued earlier this month that a model is a dependency that won't hold still: pin the version so you own the moment it changes. The flip is the other side of that. A pin protects the API path from a break and also hides the break from you until the day you move it. An alias gives you the new model on the vendor's schedule and no 400 to tell you it happened.

How do you catch this before a default flip reaches you?

Keep a canary: one real-shaped request per model you depend on, including the next one before you switch, that reads the response by block type and fails loudly when the answer isn't there.

python
import sys
import anthropic
 
client = anthropic.Anthropic()
MODELS = ["claude-opus-5", "claude-opus-5-5"]
failures = []
 
for model in MODELS:
    try:
        resp = client.messages.create(
            model=model,
            max_tokens=2048,
            output_config={"effort": "medium"},
            messages=[{"role": "user", "content": "Reply with the word ok."}],
        )
    except anthropic.BadRequestError as e:
        failures.append(f"{model}: 400 {e.message}")
        continue
    kinds = [b.type for b in resp.content]
    text = "".join(b.text for b in resp.content if b.type == "text")
    print(f"{model}: blocks={kinds} stop={resp.stop_reason} out={resp.usage.output_tokens}")
    if resp.stop_reason != "end_turn" or "ok" not in text.lower():
        failures.append(f"{model}: stop={resp.stop_reason} text={text!r}")
 
if failures:
    sys.exit("\n".join(failures))

That is the skeleton. Put your real request in it: your tools, your tool_choice, your max_tokens, and your production parser in place of the join. If you run tool loops, add a second turn with a tool result, because the replay rules only show up there. The point is to send the new model exactly what production sends and watch it break while it is still a script.

Run it when a changelog lands; Anthropic's release notes and the Claude Code releases page both list model changes on the day. For anything invoked through an alias, run it against the model the alias is about to point at, not the one it points at now. The migration guide also ships an automated path, /claude-api migrate in Claude Code, which rewrites the parameters and hands you a checklist to verify by hand. That fixes the requests. Whether the responses still parse is the canary's job.

What should you change this week?

In order of how quietly each one fails:

  1. Read content blocks by type everywhere, and pass thinking blocks back unmodified in tool loops. The API returns 200 here and the failure surfaces in your own code, if at all.
  2. Set effort explicitly on every request, so a default change can't pick it for you.
  3. Check for stop_reason: "max_tokens" on anything that used to run without thinking, then raise the limit or lower effort, and re-baseline cost per request.
  4. Replace forced tool use: structured outputs where you wanted JSON, auto plus a missing-call check where you wanted an action, and strict on the tools whose schemas support it.
  5. Check thinking.display if users watch the agent work.
  6. Remove thinking: disabled and manual budgets, and move computer use to the toolset along with its agent loop (Claude API and Google Cloud). The 400s will remind you anyway.

The price cut is real, and it arrives the way the 400s do, announced. Everything else on this list comes back as a 200 and waits for you to notice.

Discussion

No comment section here — all discussions happen on X.

Max Nardit

Max Nardit

@mnardit

More articles

A price you cannot compute

The bill went up by $96 a year, which is nothing. What changed underneath it is that the number is no longer derivable: the included allowance has no published size, the free credit allotment has no published size, and the annual credit costs more than the monthly one. A price you cannot compute from published figures is an estimate, and an estimate is a different kind of dependency.

Claude Code now reads AGENTS.md, and the default is a fallback

Anthropic solved loading again and left precedence where it was. The default decides which project file loads by what happens to exist on disk, the file it loads stays out of the inventories you would audit it with, and the old one-line import is still the setup that behaves the same across providers and versions.

The tool you install has your reach

An installed tool holds whatever authority you already have, and handing it over is the one security decision that gets no second look: taken once, in the least deliberate act of the whole install, and never revisited. You can contain what it can reach or read what its code actually touches; trusting the name does neither.