Claude Opus vs Sonnet: what breaks when you swap the model string
Comparisons of Claude Opus, Sonnet and Haiku score capability, speed and price per token. None of them mention that the model string is part of your request contract. Swap it and thinking can turn itself on, your answer can move out of content[0], and a max_tokens you never touched can start truncating. Eight measured differences, September 2026.
Updated on September 17, 2026
On this page
Quick answer
Every Claude Opus vs Sonnet comparison I can find scores the models on capability, speed and price per million tokens. As of 17 September 2026, not one of the pages ranking for that question mentions the thing that actually cost me a Tuesday: the model string is part of your request contract, not a setting inside it.
Change it and the same request body can mean something different. Thinking can switch itself on. Your answer can move out of content[0]. A max_tokens you never touched can start truncating. Your token count can rise by roughly 30 percent while the per-token price goes down.
Three of those four raise no error at all.
The loud failures are the easy ones. You find a 400 in minutes. This is a field note about the quiet ones, and about the two models in the current line-up that flatly refuse each other's configuration.
The moment
A one line diff. claude-sonnet-4-6 became claude-sonnet-5. Tests green, because the tests assert on parsed fields and the parse still worked. Shipped on a Tuesday afternoon.
By Wednesday the classification service was returning empty strings for about one request in three, and only for the longer documents. Not errors. Not timeouts. Empty strings, with a 200, and a usage block showing plenty of output tokens billed.
I lost most of a day to the wrong hypothesis, which was that something upstream was truncating the document. The actual cause was one line of my own code that had been correct for eight months:
answer = response.content[0].text
On Sonnet 4.6 content[0] was the answer. On Sonnet 5 it is frequently a thinking block, because adaptive thinking is on by default there and was off by default on 4.6. The block has a .text of "" at the default display setting. My code was reading a real field on a real block and getting a real empty string.
Nothing in the comparison tables I had read before the swap mentions this. Here is what I now check first.
Three classes of change, and only one of them shouts
Swapping a Claude model moves you between three different kinds of difference, and they cost wildly different amounts of time to find:
- Hard rejections. The request returns a 400. Annoying, obvious, fixed in an afternoon.
- Silent behaviour changes. The request succeeds and means something else. This is where the day goes.
- Silent accounting changes. The request succeeds, means the same thing, and costs a different amount. Nobody notices until the invoice.
Most published comparisons are about capability, which is a fourth thing entirely. The three above are the ones that turn a one line diff into an incident.
Finding 1: the same request body means different things on different models
The cleanest example, and the one behind my Tuesday, is the thinking parameter when you do not send it.
Anthropic's own Sonnet 5 migration guide puts it plainly: "On Claude Sonnet 4.6, requests without a thinking field run without thinking; on Claude Sonnet 5, the same requests run with adaptive thinking."
The Opus line does the same thing one generation later. From the Opus 5 migration guide: "On Claude Opus 4.8, requests without a thinking field run without thinking; on Claude Opus 5, the same requests run with adaptive thinking."
So the absence of a parameter flipped meaning. Both of these requests are byte identical apart from the model string, and they now do different work:
{
"model": "claude-sonnet-4-6",
"max_tokens": 4096,
"messages": [{ "role": "user", "content": "Classify this ticket." }]
}
{
"model": "claude-sonnet-5",
"max_tokens": 4096,
"messages": [{ "role": "user", "content": "Classify this ticket." }]
}
The second one thinks. You did not ask it to and you cannot tell from the request.
If you want the old behaviour you have to say so explicitly with thinking set to type disabled. The default is not "whatever you had before", it is a property of the model you named.
Finding 2: your answer moves out of content[0]
This is the consequence that actually breaks code, and it deserves its own finding because it is one step removed from the cause.
Once thinking is on, the response shape changes. Anthropic's guide again: "a response can begin with one or more thinking blocks before the first text block", and "Code that reads the reply by position, such as content[0].text or a stream handler that treats the first content block as text, must select content blocks by their type field instead."
Two things make this nastier than a normal breaking change.
First, it is not a breaking change in the API's own terms. Nothing about the response is malformed. content[0] exists, it has a .text, and reading it succeeds. You get a valid string. It is just not your answer.
Second, at the default display setting that string is empty rather than obviously wrong. An empty string flows through most code paths without complaint. It serialises, it stores, it renders as nothing. If I had been given a block of visible reasoning text instead of "" I would have spotted it in ten minutes.
The fix is to select by type, never by position:
answer = next(b.text for b in response.content if b.type == "text")
There is a second version of the same trap in tool loops. Thinking blocks have to be passed back "complete and unmodified" with your tool results, and the guide is explicit that "modified blocks return a 400 error". So a loop that rebuilds assistant messages from the text it extracted, rather than echoing the content list back, breaks the moment thinking turns on. That one at least shouts.
Finding 3: a max_tokens you never touched starts truncating
max_tokens is not a limit on the answer. It is a limit on everything the model emits.
The Opus 5 guide states it directly: "max_tokens remains a hard limit on total output, thinking plus response text, so revisit it for workloads that ran without thinking on Claude Opus 4.8."
Which means the following, and it is worth sitting with: if you sized max_tokens snugly around your expected answer, as you should have, and then moved to a model where thinking is on by default, the thinking now eats into that budget and your answer gets cut off. You changed one string and introduced a truncation bug in a parameter you did not touch.
The tell is stop_reason of max_tokens on requests that used to finish cleanly, usually on the longer inputs first, because those are the ones the model thinks hardest about. That matches exactly what I saw: only the longer documents failed.
Finding 4: you are billed for reasoning you cannot read
Two defaults compose here in a way that is easy to miss.
Thinking is on by default on the current models. And thinking.display defaults to omitted, where it defaulted to summarized on Sonnet 4.6 and Opus 4.6. So by default the blocks arrive with an empty thinking field.
Then the billing line, quoted verbatim from Anthropic's guide: "Thinking tokens are billed as output tokens even when the thinking text is not returned to you."
Put the three together and the default posture on a current model is: thinking happens, you pay output rates for it, and you cannot see it. That is a defensible design. It is not what somebody migrating from a model where thinking was off by default is expecting, and it is invisible in every dashboard that only tracks request counts.
If your product streams reasoning to users, the same default shows up as a long pause before any text appears. Set display to summarized and it comes back. The guide notes that display controls visibility only, so this changes what you see and not what you pay.
Finding 5: the tokenizer moved, so cheaper per token is not cheaper
This is the accounting one, and it is the most counter-intuitive thing I verified today.
Claude Sonnet 5 uses a new tokenizer. Anthropic's number: "The same input text produces approximately 30% more tokens than on Claude Sonnet 4.6."
Now hold that next to the price. The guide says per-token pricing is lower on Sonnet 5 than on Sonnet 4.6, and then adds the sentence that matters: "the cost of an equivalent request does not drop in direct proportion."
A per-token price cut running against a roughly 30 percent token inflation does not leave you with an obvious saving in either direction. It leaves you with a number you have to measure. Anthropic's own advice is to re-run token counting against the new model rather than reusing counts measured against the old one, and I would go further: this is the one case where extrapolating from a price table is actively misleading, because both of the numbers you would multiply together changed at once and in opposite directions.
The same shift quietly moves two other things. A context window holds less of your text than it did, even though the advertised size did not change. And a max_tokens tuned on the old model may truncate equivalent output on the new one, which is Finding 3 arriving by a second route.
I am deliberately not publishing a per-token price here as anything load bearing. Prices move, the mechanism does not, and the mechanism is the part that survives.
Finding 6: Haiku 4.5 and Sonnet 5 reject each other's thinking config
This is the finding I did not expect, and it is the one that kills naive model-switching code.
Claude Haiku 4.5 uses manual extended thinking, with a budget_tokens value, and it rejects adaptive thinking. Claude Sonnet 5 uses adaptive thinking, and it rejects the manual form with a 400. Anthropic's migration guide describes it in exactly those terms: "On Claude Sonnet 5, the support is reversed."
Read that as an engineer and the consequence is immediate: there is no single thinking value that is valid on both models. Not a value that behaves differently on each, which you could live with. A value that is rejected by one of them whichever one you pick.
So the very common shape where a model name is configuration, passed straight through to the API from an environment variable or a user-facing picker, cannot be correct across that pair. The request body has to be built per model, not parameterised by it. If you have a "model" dropdown and one request builder behind it, you have a latent 400 that fires the first time somebody picks the other option.
The same asymmetry shows up in the surrounding parameters. The effort parameter is not available on Haiku 4.5 at all, so there is nothing to carry over. Sampling parameters work on Haiku 4.5, and there is a wrinkle even there, because temperature and top_p work one at a time rather than both. On Sonnet 5 a non-default value for temperature, top_p or top_k returns a 400.
If you want the cost shape of routing between a small model and a large one, my colleagues at PromptAttic have a cheap-then-deep routing recipe that covers the confidence gate and the escalation economics. What it does not cover, and what this post is about, is that the two request bodies on either side of that gate are not the same shape.
Finding 7: the loud ones are not symmetric either
Worth a short list, because "it 400s on the new model" is not the whole story. Sometimes the combination is what is rejected, and only on one side.
On Claude Opus 5 you can still disable thinking, but only at an effort level of high or below. Combine disabled thinking with xhigh or max and you get a 400. Opus 4.8 accepts that same combination.
The detail that makes this awkward in practice is that the check is per request. Anthropic notes that effort and thinking configuration are validated independently on every call. So a conversation can run happily for twenty turns and then fail on turn twenty one because that one raised effort while thinking was still disabled. Nothing about the earlier turns warned you.
The rest of the hard rejections on the current models, so you can grep for them in one pass:
thinkingof typeenabledwithbudget_tokens, on Sonnet 5 and the current Opus linetemperature,top_portop_kat a non-default value- prefilling the assistant message, which has returned a 400 since the 4.6 generation
- modified thinking blocks echoed back in a tool loop
Finding 8: newer does not mean strictly more
The last assumption worth dropping is that moving up the line-up is monotonic. It is not.
Reading the current model pages on 17 September 2026: web fetch is not available on Claude Opus 5, and Priority Tier is not supported on Claude Opus 5 or on Claude Sonnet 5. Both are available elsewhere in the family.
And the gap between tiers is wider than the gap between adjacent versions. Haiku 4.5 serves a 200k context window and up to 64k output tokens; Sonnet 5 serves 1M and up to 128k. Anthropic's own guide says the two "differ more at the API level than adjacent models within one class", which is a useful way to think about it: a version bump is usually small, a tier change is not.
What this changes about the choice
I do not think any of this should change which model you pick for quality. Pick the one that does your task well. And note this is a different question from which model actually ends up answering, which on the CLI side has its own precedence stack that I mapped in an earlier note.
What it should change is the estimate you attach to the switch. "Try Sonnet 5 instead" is not a config toggle you can hand to someone on a Friday. The honest checklist before a model swap is short, and none of it is about capability:
- Grep for positional content access.
content[0]in any form. - Check every
max_tokensthat was sized tightly, and raise it. - Re-run token counting on the new model. Do not scale the old number.
- Decide explicitly whether you want thinking, rather than inheriting a default that changed.
- If a model name is user-selectable, confirm the request builder is per model and not shared.
That took me a day to learn and takes about an hour to check.
What I did not verify
I read the current Anthropic migration guides and model pages on 17 September 2026 and quoted them directly. I did not re-run these behaviours against live endpoints for every model pair named here, so the per-model claims are documentation-sourced rather than independently reproduced. The one behaviour I did hit in production is Finding 2, which is what started this.
I saw a claim in secondary material that Claude Opus 5 draws on a separate rate-limit bucket from the earlier Opus models. I could not find that statement anywhere in Anthropic's current published guides, so I am not asserting it, and you should not plan capacity around it without checking your own account.
Reddit and Medium both blocked me while I was checking how other people describe this, so where I say page-one results do not mention these mechanisms, that is based on the pages I could fetch and read, which was three of the eight organic results plus Anthropic's own model-choice tutorial. The two I could not read may well cover some of it.
Model line-ups move fast. Everything here is a 17 September 2026 reading, and the specific version numbers will age faster than the shape of the problem.
Postscript: the funniest part is that the bug was in a line I wrote correctly, tested properly, and never touched. It just stopped being true while I was looking somewhere else.
Written by
M. PatelM. Patel writes DevMoment field notes on AI dev workflow, tested on real work rather than demos.
Frequently asked questions
Why did my code break when I switched from Claude Sonnet 4.6 to Claude Sonnet 5?
The most common cause in 2026 is that adaptive thinking is on by default on Claude Sonnet 5 and was off by default on Sonnet 4.6, so a request with no thinking field behaves differently on the two models. Once thinking is on, the response can begin with one or more thinking blocks before the first text block, so code that reads the reply by position breaks. Anthropic's migration guide is explicit that code reading content by position must select content blocks by their type field instead.
Why is content[0].text empty on Claude Sonnet 5?
Because content[0] is probably a thinking block rather than a text block, and thinking display defaults to omitted on Claude Sonnet 5 where it defaulted to summarized on Sonnet 4.6. The block exists and has a text field, so reading it succeeds and returns an empty string rather than raising. Select the block whose type is text instead of taking the first one, and set display to summarized if you want readable reasoning back.
Can I use the same request body for Claude Haiku 4.5 and Claude Sonnet 5?
Not for the thinking parameter. Claude Haiku 4.5 uses manual extended thinking with a budget_tokens value and rejects adaptive thinking, while Claude Sonnet 5 uses adaptive thinking and rejects the manual form with a 400 error. Anthropic describes the support as reversed between the two. There is no single thinking value valid on both, so the request body has to be built per model rather than parameterised by a model name.
Is Claude Sonnet 5 cheaper to run than Claude Sonnet 4.6?
Not necessarily, and you cannot tell from the price table alone. Claude Sonnet 5 uses a new tokenizer that produces approximately 30 percent more tokens for the same input text, and although per-token pricing is lower, Anthropic states that the cost of an equivalent request does not drop in direct proportion. Re-run token counting against the new model on your own workload rather than scaling a figure measured on the old one.
Is a newer Claude model always a superset of the older one?
No. As of 17 September 2026 the web fetch tool is not available on Claude Opus 5, and Priority Tier is not supported on Claude Opus 5 or Claude Sonnet 5, while remaining available elsewhere in the family. Capability and feature availability move independently, so check the specific features your integration depends on rather than assuming a newer model inherits everything.
Keep reading
Claude Code model: which setting actually wins (2026)
I went looking for which model was answering me and found three separate precedence orders, fourteen documented inputs, and two environment variables that resolve in opposite directions. Read from Anthropic's own docs on 9 September 2026, with the version numbers.
Anthropic Batch API in 2026: the constraints nobody mentions until you hit them
The Batch API's default prompt cache expires in 5 minutes while the batch itself runs for up to an hour, so the two cost levers quietly cancel each other out. Six documented constraints, and the single assumption underneath all of them. Field log, September 2026.
Claude Code auto compact: what it costs and what you lose (2026)
Claude Code auto compact fires on a per-model token boundary, not a percentage. What it costs is set by prompt cache warmth rather than context size, and a path-scoped rule does not come back afterwards until Claude reads a matching file again.