Moving to Claude Sonnet 5.5: the three things that break
Sonnet 5.5 is 30% faster than Sonnet 5 at the same per-token price. Swapping the model name is easy; three habits in your code are not.
Every model upgrade starts with the same small lie we tell ourselves: it’s just a string. You open the config, replace one model name with another, deploy, and go make coffee. With Claude Sonnet 5.5 that lie holds for maybe ten minutes, which is exactly how long it takes for the first request to come back in a shape your code did not expect, or not come back at all.
The upgrade is worth it. According to Anthropic, Sonnet 5.5 is smarter and more efficient than Sonnet 5 and 30% faster, and the per-token price is unchanged. Because it usually needs fewer tokens for the same job, the announcement puts the saving at up to 30% for most work. So the question is not whether to move, but what to fix on the way. I wrote a short sheet about this on Consonance; this is the longer version, with the code.
The idea in one sentence
Change the model name, then fix the three places where Sonnet 5.5 stops tolerating old habits: reading the first block, turning thinking off, and forcing a tool.
model="claude-sonnet-5-5"
1. Your code reads content[0].text
This is the quiet one, because it does not raise an API error. Sonnet 5.5 thinks by default: a request with no thinking field runs with adaptive thinking. That means the response can begin with a thinking block, and the line that grabs the text of the first block either crashes or hands you the wrong thing.
The fix is to stop assuming position and read each block by its type. This is the request Anthropic gives as working on Sonnet 5.5 as written:
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
messages=[
{
"role": "user",
"content": "Analyze the trade-offs between microservices and monolithic architectures",
}
],
output_config={"effort": "medium"},
)
for block in response.content:
if block.type == "text":
print(block.text)
The whole lesson is in that last loop, and specifically in one line:
if block.type == "text":
Two related habits from the migration guide are worth adopting at the same time. In tool-use loops, pass thinking blocks back unchanged, empty ones included. And revisit max_tokens: it covers thinking plus text, and thinking tokens are billed as output.
2. You sent thinking={"type": "disabled"}
If you ran Sonnet 5 with thinking off, that exact setting now returns a 400 invalid_request_error. The replacement is a new value, between_tools. With it, the model does not think before responding; thinking only happens between tool calls, and Anthropic says total response time is the same or faster. Here is the before and after from the announcement:
# Before: Claude Sonnet 5
client.messages.create(
model="claude-sonnet-5",
max_tokens=16000,
thinking={"type": "disabled"},
output_config={"effort": "xhigh"},
messages=[{"role": "user", "content": "..."}],
)
# After: Claude Sonnet 5.5
client.messages.create(
model="claude-sonnet-5-5",
max_tokens=16000,
thinking={"type": "between_tools"},
output_config={"effort": "high"},
messages=[{"role": "user", "content": "..."}],
)
Look closely: the effort also drops from xhigh to high. That is not cosmetic. between_tools comes with a few rules:
- It works at
low,mediumandhigheffort. Atxhighormaxit returns a 400 error; to run there, use adaptive thinking. - It takes no other field. Sending
display,budget_tokensorblock_bindingalongside it returns a 400 error. - Effort cannot change mid-conversation. If you want a different level per turn, use adaptive thinking.
- The short progress updates the model writes between tool calls still arrive as
thinkingblocks. Which brings us straight back to section 1. - It needs no beta header, but if your SDK version does not define
between_tools, update it.
One last nuance: for requests without tools that still need a few steps of working out, the announcement recommends adaptive thinking rather than between_tools.
3. You forced a tool with tool_choice
A tool_choice of type any or tool is rejected with a 400 error, including on the token counting endpoint. The message is explicit:
tool_choice: type "tool" and "any" are not supported for this model.
The replacement has three parts. Send tool_choice={"type": "auto"}. Mark the tool "strict": True so its input matches the schema. And because auto means the model is free to answer without calling the tool, say in the prompt when to use it:
weather_tool = {
"name": "get_weather",
"description": "Get the current weather in a given location",
"input_schema": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
"additionalProperties": False,
},
"strict": True,
}
client.messages.create(
model="claude-sonnet-5-5",
max_tokens=1024,
tools=[weather_tool],
tool_choice={"type": "auto"}, # was {"type": "tool", "name": "get_weather"}
messages=[
{"role": "user", "content": "What's the weather in Paris? Use the get_weather tool."}
],
)
Strict tool use needs additionalProperties: false on every object in the schema, not only at the top level. That is the detail that tends to bite on nested inputs.
Let Claude Code do the boring part
On a real project, these patterns are scattered across many files. Claude Code can run the migration for you through the bundled Claude API skill, which applies the model ID swap and the breaking parameter changes across the code base:
/claude-api migrate this project to claude-sonnet-5-5
Even then, a few things are judgment calls rather than search-and-replace:
- Effort levels are recalibrated. A level does not produce the same amount of thinking as on Sonnet 5, so your old setting does not carry over. Re-run your effort tests. The default is
highon the Claude API andmediumin Claude Code. - Large images cost more. A 2000×1500 image costs about 2.5 times as many tokens as on Sonnet 4.6. If you do not need the detail, downscale before sending.
- The full list is longer. The guide counts five breaking changes and one change to the response shape. Beyond the ones above, check append-only conversations, computer use and the advisor tool if you use them.
Wrapping up
The model name is the easy part. The real migration is three small changes in how your code talks to the model: read blocks by type, replace disabled with between_tools, and swap a forced tool for auto plus a strict schema and a clear prompt. Do those, re-run your effort tests, and you get a faster model at the same per-token price without a surprise 400 in production.