Moving a working Claude integration to Sonnet 5.5 or Haiku 5.5 and getting a wall of invalid_request_error responses is normal. Both models reject a handful of settings that older models accepted, and the error text doesn't always tell you what to change. This is the symptom, cause and fix list, plus a small script that checks a request payload before you send it.
Sources, all read on 2026-10-31: the Sonnet 5.5 migration guide, the Haiku 5.5 migration guide, the API release notes (Sep 28 and Oct 7 entries) and Anthropic's models overview. The linter at the end is my own code, run offline with no API call. Nothing here was tested against the live API, because there is no key in this environment.
If you're still choosing a model, start with the Claude model picker. This post is for after you've chosen.
The quick table
| Symptom | Models | Cause | Fix |
|---|---|---|---|
400 on thinking: {"type": "enabled", "budget_tokens": N} | Sonnet 5.5, Haiku 5.5 | Manual thinking budgets are gone | thinking: {"type": "adaptive"} plus output_config.effort |
400 on thinking: {"type": "disabled"} | Sonnet 5.5 | Not accepted | thinking: {"type": "between_tools"} at high effort or below |
400 on temperature, top_p, top_k | Both | Sampling parameters rejected | Remove them; steer with prompts |
| 400 when the last message is from the assistant | Both | Prefill rejected | End with a user turn |
400 on tool_choice of any or tool | Sonnet 5.5 | Forced tool use rejected | auto with strict: true tools |
400 on computer_20250124 | Both | Old computer tool | computer_toolset_20260801 (Claude API) |
400 on computer_20251124 | Sonnet 5.5 (Claude API) | Old computer tool | computer_toolset_20260801 |
| 400 on a replayed thinking block | Both | Prefix changed since the block was signed | Keep conversations append-only |
content[0].text fails or is empty | Both | Responses can start with thinking blocks | Select blocks by type |
max_tokens stop before any text | Both | Thinking tokens count toward max_tokens | Raise it or lower effort |
| Advisor tool 400 | Sonnet 5.5 as executor | Unsupported advisor model | Use a supported advisor |
| Replayed history loses reasoning, no error | Both | Thinking blocks are account-bound | Replay through the producing account |
Each row is explained below with the fix.
Thinking is on by default
On Sonnet 5.5, a request with no thinking field runs with adaptive thinking. The Sonnet 5.5 guide says Sonnet 4.6 and earlier, and Haiku 4.5, ran without thinking in that case. The Haiku 5.5 guide says the same for Haiku: adaptive thinking is on by default, so a response can start with thinking blocks even if you never set thinking.
Three consequences:
- Read blocks by
type. Code that readscontent[0].textbreaks when the first block is athinkingblock. - Revisit
max_tokens. Thinking tokens count toward it, and are billed as output tokens. A smallmax_tokenscan stop withstop_reason: "max_tokens"after a thinking block and before any text. - Thinking text is omitted by default. Blocks arrive with an empty
thinkingfield and asignature. Setthinking: {"type": "adaptive", "display": "summarized"}if you show or log the reasoning.
A safe text extractor works on every model:
def answer_text(response):
return "".join(b.text for b in response.content if b.type == "text")
Fix: manual thinking budgets
Symptom: a 400 on a request that sends thinking: {"type": "enabled", "budget_tokens": 8000}. Sonnet 5.5's guide quotes the message as: "thinking.type.enabled" is not supported for this model. Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior.
Fix: delete the budget and set an effort level.
{
"model": "claude-haiku-5-5",
"max_tokens": 16000,
"thinking": { "type": "adaptive" },
"output_config": { "effort": "medium" },
"messages": [{ "role": "user", "content": "..." }]
}
Anthropic says there is no fixed mapping from a token budget to an effort level, so run your evals at two or three levels. The defaults differ by model: the models overview lists high for Sonnet 5.5 and medium for Haiku 5.5. Where Haiku 4.5 ran with no thinking or a small budget to save tokens, Anthropic suggests a lower effort. The effort parameter post explains the cost side.
Fix: turning thinking off on Sonnet 5.5
Symptom: a 400 on thinking: {"type": "disabled"}. This is the one that breaks code migrated from Sonnet 5. The guide quotes the error text as: To turn thinking off on this model, send "thinking": {"type": "between_tools"} instead of {"type": "disabled"}.
Fix: send {"type": "between_tools"}. The rules from the guide:
- It's accepted at
low,mediumandhigheffort. Atxhighormaxit returns a 400; use adaptive thinking there. - It takes no other field.
display,budget_tokensorblock_bindingsent with it return a 400. - Short progress updates between tool calls still come back as
thinkingblocks. With no tools, the response is text only. - Effort can't change mid-conversation with it: a per-message
output_config.effortthat differs from the level in effect returns a 400.
On Haiku 5.5, the guide I read does not say how to turn thinking off, so I'm not going to guess. Anthropic's Oct 5 and Oct 15 release notes added capabilities.thinking.types.disabled and capabilities.thinking.types.between_tools to the Models API. Ask the API instead of trusting a blog post (including this one):
curl https://api.anthropic.com/v1/models/claude-haiku-5-5 \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01"
Look at capabilities.thinking.types in the response. This call is untested here. The overview page says that for Sonnet 5.5 the API reports between_tools.supported as true and disabled.supported as false, and that a supported: true value can still be rejected for another reason, such as an effort level the model doesn't allow.
Fix: sampling parameters
Symptom: a 400 on temperature, top_p or top_k. Both 5.5 models reject them. The Haiku guide says to omit them and use prompting to guide behaviour.
There's a second reason to remove them. Under API version 2026-10-21, these three parameters return a 400 on every model. If you're also doing that upgrade, see the version 2026-10-21 and SDK 2.0 migration checklist. In the new Python SDK the keyword doesn't exist at all, so you get a TypeError before the request is sent.
Fix: assistant prefill
Symptom: a 400 when messages ends with an assistant turn. Haiku 5.5 rejects prefill even with thinking off. Sonnet 5.5's guide lists prefill among the five settings that return a 400. Replace each use by purpose, following Anthropic's Haiku guide:
- Forcing an output format (for example starting with an opening brace): use structured outputs, or a tool with an enum field for classification.
- Skipping preambles: ask for a direct answer in the system prompt.
- Continuing a cut-off answer: move it into the user message, with the earlier text quoted and an instruction to continue.
- Reminders of context: put them in the user turn.
Fix: forced tool use on Sonnet 5.5
Symptom: a 400 with the message tool_choice: type "tool" and "any" are not supported for this model. It also applies to the token counting endpoint.
Fix: send tool_choice: {"type": "auto"} and mark the tool strict: true so its input matches your schema. Strict tools need additionalProperties: false on every object. Because the model can now answer without calling the tool, say in the prompt when it must call it.
{
"model": "claude-sonnet-5-5",
"max_tokens": 2048,
"tool_choice": { "type": "auto" },
"tools": [{
"name": "get_order",
"description": "Look up an order by its numeric ID.",
"strict": true,
"input_schema": {
"type": "object",
"properties": { "order_id": { "type": "integer" } },
"required": ["order_id"],
"additionalProperties": false
}
}],
"messages": [{ "role": "user", "content": "Where is order 4417? Use get_order." }]
}
This is a request shape taken from the migration pattern, untested. For schema pitfalls, see writing tool descriptions for LLM agents.
Haiku 5.5 behaves differently. It accepts a forced tool_choice, but the reply starts with the tool call and has no thinking block. If you want the model to think before calling a tool, use auto there too.
Fix: computer use
The release notes (Sep 28 and Oct 7) say computer_20250124 returns a 400 on Haiku 5.5 and is not accepted on Sonnet 5.5 on any platform. On the Claude API and Google Cloud, Sonnet 5.5 supports computer use only through computer_toolset_20260801, and computer_20251124 also returns a 400. On Amazon Bedrock the guides point to computer_20251124 with the matching beta header.
Steps from the guide: drop the old computer-use-* beta header, replace the tools entry with {"type": "computer_toolset_20260801"}, and change the agent loop to dispatch on each tool_use block's name and toolset_name. If you send the fine-grained-tool-streaming-2025-05-14 beta header alongside the toolset, that returns a 400; set eager_input_streaming: true on the tool instead. Zoom is on by default; disable it with "configs": {"zoom": {"enabled": false}} if your environment doesn't implement it. Whether to run computer use at all is a safety question first; see AI coding agent safety and sandboxing.
Thinking blocks are bound to the account and the conversation
Two rules that cause silent or late failures:
- Account-bound. A thinking block produced by Sonnet 5.5 (or Haiku 5.5) works only in the account that produced it, or a linked one. If another account sends it, the API drops the block before the model sees it and the request still succeeds. That bites services that keep one conversation store for several customers' accounts.
- Prefix-bound. A block is signed over the conversation before it. If you edit
system,toolsor earlier messages and send the block back, andthinking.block_binding.prefix_mismatch_behavioris unset, you get a 400 on accounts created on or after August 31, 2026. Keep conversations append-only, and change instructions or tools with mid-conversation system messages.
Also from the guides: Sonnet 5.5 and Haiku 5.5 don't read each other's blocks in every direction. On the Claude API, Sonnet 5.5 reads Haiku 5.5's blocks, but Haiku 5.5 doesn't read Sonnet 5.5's, and the API drops them without an error. If you route between models mid-conversation, expect reasoning to vanish on the downgrade.
Other changes worth a ticket
- Tokens. Haiku 5.5 uses the newer tokenizer, so the same text produces roughly 30% more tokens than Haiku 4.5 per the guide. Recount before you budget.
- Haiku 5.5 pricing is tiered by prompt length. $0.10 / $0.50 per million input / output for prompts up to 100k tokens, $0.50 / $2.50 above that. The count includes cache reads and writes. Pricing page, read 2026-10-31.
- Priority Tier is not supported on Haiku 5.5 per its guide.
- Refusals. Handle
stop_reason: "refusal". Sonnet 5.5 can decline in more categories than Sonnet 5, and Haiku 5.5 has no server-side fallback. - Advisor tool. A Sonnet 5.5 executor needs one of Opus 5, Opus 5.5, Sonnet 5.5, Fable 5, Fable 5.1, Mythos 5 or Mythos 5.1 as advisor. Opus 4.8, Opus 4.7, Sonnet 5 and Haiku 5.5 as advisors return a 400.
- Prompt cache minimum on Sonnet 5.5 is 512 tokens, down from 1,024 on Sonnet 5, per the guide.
An offline payload linter
Rather than learn each 400 from production, check payloads before they leave your process. This script encodes the rules above. I wrote it for this post and ran it on 2026-10-31 with Python 3. It does not call the API, and it only checks request shape, so a clean result doesn't guarantee success.
MODELS = {"claude-sonnet-5-5": "sonnet", "claude-haiku-5-5": "haiku"}
SAMPLING = ("temperature", "top_p", "top_k")
def lint(payload, api_version="2023-06-01", betas=()):
kind = MODELS.get(payload.get("model"))
if kind is None:
return [("SKIP", "model", "not a 5.5 Sonnet or Haiku model")]
out = []
add = lambda level, where, msg: out.append((level, where, msg))
for p in SAMPLING:
if p in payload:
add("ERROR", p, "returns 400; remove it")
th = payload.get("thinking") or {}
ttype = th.get("type")
effort = (payload.get("output_config") or {}).get("effort")
if ttype == "enabled" or "budget_tokens" in th:
add("ERROR", "thinking", "manual budget_tokens returns 400; use adaptive + effort")
if ttype == "disabled" and kind == "sonnet":
add("ERROR", "thinking", "Sonnet 5.5 rejects disabled; use between_tools")
if ttype == "disabled" and kind == "haiku":
add("WARN", "thinking", "undocumented in the Haiku guide; check the Models API")
if ttype == "between_tools":
if effort in ("xhigh", "max"):
add("ERROR", "thinking", "between_tools returns 400 at xhigh or max effort")
for extra in ("display", "budget_tokens", "block_binding"):
if extra in th:
add("ERROR", f"thinking.{extra}", "between_tools takes no other field")
msgs = payload.get("messages") or []
if msgs and msgs[-1].get("role") == "assistant":
add("ERROR", "messages[-1]", "assistant prefill returns 400")
tc = (payload.get("tool_choice") or {}).get("type")
if tc in ("any", "tool") and kind == "sonnet":
add("ERROR", "tool_choice", "Sonnet 5.5 rejects any/tool; use auto + strict tools")
for t in payload.get("tools") or []:
ty = t.get("type", "")
if ty == "computer_20250124":
add("ERROR", "tools", "use computer_toolset_20260801 (Claude API)")
if ty == "computer_20251124" and kind == "sonnet":
add("ERROR", "tools", "use computer_toolset_20260801 on the Claude API")
return out
I left out a few rules (advisor models, beta-header combinations, the 2026-10-21 document and citation checks) to keep the listing short. They're in the file I ran. Here is the real output of running the full script on four sample payloads:
== haiku-4.5 style request, retargeted to Haiku 5.5
ERROR temperature: returns 400; remove it (at API version 2026-10-21 it is removed on every model)
ERROR thinking: manual budget_tokens returns 400; use {"type":"adaptive"} plus output_config.effort
ERROR messages[-1]: assistant prefill returns 400; end with a user turn (use structured outputs or a system prompt)
== sonnet 5 style request, retargeted to Sonnet 5.5
ERROR thinking: Sonnet 5.5 rejects "disabled"; send {"type":"between_tools"} at high effort or below
ERROR tool_choice: Sonnet 5.5 rejects any/tool; use {"type":"auto"} with strict:true tools
== between_tools at max effort
ERROR thinking: between_tools returns 400 at xhigh or max effort
ERROR thinking.display: between_tools takes no other field; returns 400
== clean request
no findings
Where to run it: in a unit test that builds your real request objects, or in a logging middleware in staging that lints each outgoing payload and logs findings. Because rules change, treat it as a tripwire and keep the vendor guides as the source of truth. Add a rule when you learn a new 400, not before.
Rollout order I'd use
- Run the linter over a sample of logged production payloads (strip user content first).
- Fix sampling parameters, thinking settings, prefill and
tool_choicein code. - Change text extraction to select by
type, then raisemax_tokens. - Switch models behind a flag for a small share of traffic.
- Recount tokens and re-baseline cost, using the cost-per-1,000-requests worked examples as a template.
- Re-run your eval at two or three effort levels before you declare it done.
What I couldn't verify
- No request in this post was sent to the live API. The error messages for thinking budgets,
disabledandtool_choiceare quoted from Anthropic's Sonnet 5.5 guide; the Haiku behaviour is from its guide. - The Haiku 5.5 guide does not cover turning thinking off. I flagged it instead of inferring.
- I didn't open the Sonnet 5.5 "what's new" page or the prompting pages; they may contain behaviour notes this post leaves out.



