Claude Opus 5.5 (claude-opus-5-5), which Anthropic released on 22 September 2026, answers any request that forces a tool call with an HTTP 400 and the message tool_choice: type "tool" and "any" are not supported for this model., according to Anthropic's API errors reference. Many of the callers who hit it never wrote that tool_choice. An issue in langchain-aws, opened the next day, reports that LangChain's with_structured_output() sends a forced tool by default and gets exactly this message from Opus 5.5 through Amazon Bedrock. Other libraries are reported doing the same with temperature.

Forced tool choice is one of several request patterns that the newest reasoning models refuse or quietly neutralise. The others are sampling settings such as temperature, switching reasoning off, prefilling the start of the reply and editing earlier turns of a conversation. Anthropic, OpenAI and Google have moved control of sampling, of how the model's turn begins and of replayed reasoning away from the request, to different degrees and with different failure modes: Claude Opus 5.5, Sonnet 5.5, Haiku 5.5 and Fable 5.1 mostly return 400s, OpenAI's GPT-6 documentation mostly says which fields to remove, and Google documents some settings on Gemini 3.8 Flash as ignored and others as errors.

This guide is organised by the job each old pattern was doing. For each job it gives what Claude Opus 5.5, Sonnet 5.5, Haiku 5.5 and Fable 5.1, GPT-6 Astra, GPT-6 Sol and Luna, GPT-6.1 Sol, and Gemini 3.8 Flash do with the pattern, as documented on 27 September 2026 and rechecked on 13 October, what replaces it, and whether the vendor gives a reason (only three reasons are published). Near the end, a reference table puts all of it on one page, followed by a short script that checks a request body offline before you deploy. Everything here comes from vendor documentation and public issue reports; no request was sent to any API.

Updated 13 October 2026: adds Claude Sonnet 5.5 (released 28 September), Claude Haiku 5.5 (7 October) and GPT-6.1 Sol (29 September), the routing of gemini-3.7-flash to Gemini 3.8 Flash since 8 October, a correction on top_p, and a section on text between tool calls, which Claude Opus 5.5 and Sonnet 5.5 return in thinking blocks that are empty by default.

The error messages, and which layer sent the parameter

Upgrading to Claude Opus 5.5, Sonnet 5.5, Haiku 5.5 or Fable 5.1 produces documented error messages for thinking settings, forced tool choice and prefill, but the temperature message developers search for appears only in public issue reports from Amazon Bedrock, not in Anthropic's documentation. These are the strings, each with its provenance:

  • "thinking.type.disabled" is not supported for this model. Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior. Documented in Anthropic's errors reference, which gives the same message with "thinking.type.enabled", the type used to set a budget_tokens budget.
  • To turn thinking off on this model, send "thinking": {"type": "between_tools"} instead of {"type": "disabled"}. The model does not think before responding. The short updates it writes between tool calls come back as thinking blocks. Documented in the same reference, for Claude Sonnet 5.5 only.
  • output_config.effort 'xhigh' is not supported when thinking is disabled on this model. Use effort 'high' or below, or enable thinking. Documented in the same reference, for between_tools on Claude Sonnet 5.5 and disabled on Claude Haiku 5.5 at effort xhigh (and likewise max). Sending between_tools to any other model returns "thinking.type.between_tools" is not supported for this model.
  • tool_choice: type "tool" and "any" are not supported for this model. Documented in the same reference. The langchain-aws issue reports identical text through Bedrock's Converse and InvokeModel APIs, after Bedrock's own prefix The model returned the following errors:.
  • This model does not support assistant message prefill. The conversation must end with a user message. Documented in the same reference.
  • messages.{i}.content.{j}: Invalid `signature` in `thinking` block. The block is bound to a different conversation. Documented; this is the error that appears on a later turn when earlier history was edited. The documented text is how the message begins; it usually ends with a sentence naming what changed. The section on edited history below gives both. If the message stops after Invalid `signature` in `thinking` block, the signature itself did not verify, a different problem covered in the same section.
  • `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified Documented on Anthropic's thinking troubleshooting page as text contained in a different 400, returned to a request that sends tool results back. The page says it happens "most often because your code filters content blocks by type and drops redacted_thinking blocks, or rebuilds the assistant message instead of echoing it", and gives the fix: "Echo the assistant turn back verbatim, thinking blocks included."
  • `temperature` is deprecated for this model. Reported, not documented. It appears in a Bedrock InvokeModel log in trulens issue 2807 as An error occurred (ValidationException) when calling the InvokeModel operation: `temperature` is deprecated for this model., and in mem0 issue 7417 and dify-official-plugins issue 3925. Anthropic's Messages API reference says non-default values are "rejected with a 400 error" but quotes no message, so whether the Claude API returns this exact text is not documented.
  • This model doesn't support the temperature field. Remove temperature and try again. Reported for GPT-6 through Bedrock Converse in onyx issue 15078. This is Bedrock's message. OpenAI's own documentation quotes no error text for any GPT-6 case.

Some failures never reach the API. Anthropic's Opus 5.5 migration guide says the Python SDK from v1.0 (released 20 August 2026) "does not define" temperature, top_p or top_k, "and passing them raises a TypeError".

The issues also show who sends these parameters. mem0 defaults temperature to 0.1; trulens sends temperature: 0 and top_p: 1; LangChain's with_structured_output() forces a tool; and in the onyx report, LiteLLM maps an effort setting to Anthropic's thinking field and gets Unknown parameter: 'thinking'. back from GPT-6 on Bedrock.

Weigh those reports for what they are. All five issues cited in this guide were opened by one account, on 23 and 24 September 2026. Three (langchain-aws, trulens and dify-official-plugins) were closed after maintainers merged fixes that the reporter wrote; mem0 and onyx were still open on 13 October. The mem0 issue says "AI found and wrote this, and I have not reproduced it myself." The merges show that the projects accepted the fixes, not that anyone else reproduced the errors, so the strings are one reporter's record of what Bedrock returned, not five independent confirmations.

The practical step, which is our advice rather than a vendor's: log the request body your library actually sends and check that, not your call site.

Rejected, ignored or dropped: three outcomes to plan for

On Claude Opus 5.5, Sonnet 5.5, Haiku 5.5 and Fable 5.1, GPT-6 and Gemini 3.8 Flash, an old request pattern is either rejected with an error, accepted and ignored, or removed from what the model sees without an error, and only the first shows up in error logs:

  • Rejected: the vendor documents an error, usually HTTP 400.
  • Ignored: accepted with no effect. Google Cloud documents this for sampling fields on Gemini 3.8 Flash.
  • Dropped: content silently removed from what the model sees. Anthropic does this to thinking blocks a new model cannot read, or that Sonnet 5.5 or Haiku 5.5 produced in another account, and OpenAI to reasoning from another model family.

Two more labels, which the preflight script at the end of this guide also uses, describe the state of the documentation rather than a fourth outcome. Unsupported means the vendor says to remove a setting but not what the API does if you send it; OpenAI's rule for sampling fields on GPT-6 is the main example. Account-dependent is Anthropic's check on edited history, which returns a 400, drops blocks or lets the request through, depending on when the account was created and how the request is set.

The silent outcomes are the ones to plan for, because a test suite that only watches for errors passes while the model works with less than you sent. Google's Gemini 3.5 migration page gives a second silent case: on the generateContent API, mismatched function responses do not yet produce an error, and instead "cause the model to return empty responses with finish_reason: STOP in most cases". The Interactions API already returns an error for them. A third silent change, on Claude Opus 5.5 and Sonnet 5.5, is in the response rather than the request: text between tool calls now arrives in thinking blocks that are empty by default.

Only Anthropic documents a signal for silent outcomes, and it needs the thinking-binding-controls-2026-08-01 beta header; without it, Anthropic's preserved thinking page says, "the drop is silent." With the header, the response field input_transformations lists each thinking block the API dropped, with the reason model_binding_mismatch (after a model switch), prefix_binding_mismatch (after an edit) or organization_binding_mismatch (a Sonnet 5.5 or Haiku 5.5 block from another account, reported on the Claude API and Google Cloud only). On accounts created before 31 August 2026 it also lists each block that failed the edit check but reached the model anyway, as thinking_mismatch_allowed. No equivalent signal is documented on the OpenAI and Google pages read for this guide.

Temperature, top_p and top_k: lower randomness has no direct replacement

Claude Opus 5.5, Sonnet 5.5, Haiku 5.5 and Fable 5.1 return a 400 error for any temperature other than 1, any top_p other than 0.99, any top_k at all and any request that sends both temperature and top_p, and neither Anthropic nor OpenAI names a replacement for what temperature: 0 was used for.

Anthropic's Messages API reference marks temperature as deprecated: "Models released after Claude Opus 4.6 do not support setting temperature. A value of 1.0 will be accepted for backwards compatibility, all other values will be rejected with a 400 error." The default is 1.0. For top_p, "A value >= 0.99 will be accepted for backwards compatibility"; for top_k, "any value will be rejected with a 400 error". The migration guide puts it as "Omit temperature, top_p, and top_k, or leave them at their defaults: any other value is rejected. Use prompting to guide the model's behavior." That needs one qualification, explained in the correction below: temperature and top_p must not both be sent, even at their defaults. It also warns: "If you were using temperature = 0 for determinism, note that it never guaranteed identical outputs on prior models." Our explainer on why temperature 0 does not give the same answer twice shows where that variation comes from.

Correction, 13 October 2026: an earlier version of this guide said any top_p of at least 0.99 was accepted, following the Messages API reference, which still says so. Anthropic's thinking page now says that on these models "the top_p default is 0.99, so a top_p of 1 returns a 400 error, and so does a request that includes both temperature and top_p, even at their defaults." The Haiku 5.5 migration guide says the same. Where the two pages disagree, send neither field.

A tempting explanation is that sampling is locked because thinking is now always on. Anthropic's own documentation rules that out for Claude. Its thinking page lists the models where non-default sampling values "return a 400 error on every request, regardless of whether thinking is used", and the list includes Claude Opus 4.7, Opus 4.8 and Sonnet 5, on which thinking can still be off according to Anthropic's thinking troubleshooting table. On older models, the same page says, the restriction applies only while thinking is on.

OpenAI's rule depends on the model and the reasoning effort. GPT-6 Astra, released on 3 September 2026, "does not support custom temperature or top_p values or log probabilities (logprobs)", per OpenAI's changelog, and has no effort level at which that changes. For GPT-6 Sol and Luna, released on 22 September 2026, the Using GPT-6 guide says: "When reasoning effort is not none, remove temperature, top_p, and top_logprobs. For Chat Completions, also remove logprobs. For Responses, remove message.output_text.logprobs from include." GPT-6 Sol's model page lists medium as the default effort, so a request that sets temperature and no effort falls under the rule. OpenAI does not say whether such a request is rejected or the fields ignored, and quotes no error. GPT-6.1 Sol, released on 29 September 2026, does not support effort none, so under the same rule the fields always have to go.

Google's two sets of documentation describe Gemini 3.8 Flash differently. The Google Cloud developer guide (last updated 12 October 2026) says temperature, top_k and top_p "are ignored by the backend", and that "Passing frequency_penalty, presence_penalty or candidate_count will throw an active API error." The Gemini API's latest-model page says only to "Remove deprecated sampling parameters (temperature, top_p, top_k), candidate_count (unsupported in Gemini 3 and later), and prefilled model turns", without saying what happens if you do not. Treat "ignored" as Google Cloud's statement, not a documented fact about the Gemini API.

What to send instead:

  • Remove the fields. On GPT-6 Sol and Luna only, if you genuinely need them, run at effort none, where OpenAI's rule no longer requires removing them.
  • For a fixed output shape, use a schema rather than low randomness: structured outputs on Anthropic, and response_schema or json_schema on Gemini, which Google Cloud names alongside thinking_level as the way to "control determinism". A schema fixes the shape, not the values: in our test of six cheap models on JSON extraction, every strict-schema response matched its schema and some still had wrong fields.
  • Google's Gemini API documentation also recommends "defining a system instruction with explicit rules for your specific use case".
  • Our advice: tests that compared exact strings at temperature: 0 were relying on something Anthropic says was never guaranteed. Test the structure and the facts in the output instead.

Anthropic and OpenAI publish no reason for the sampling restriction. Google's Gemini 3.5 page gives one for all Gemini 3.x models: "Gemini 3's reasoning capabilities are optimized for the default settings." Its Gemini 3 page adds that setting temperature below 1.0 "may lead to unexpected behavior, such as looping or degraded performance, particularly in complex mathematical or reasoning tasks."

Turning thinking off or capping it: effort is the control that remains

Claude Opus 5.5 and Claude Fable 5.1 reject thinking: {"type": "disabled"} and thinking: {"type": "enabled"} with budget_tokens, because adaptive thinking is always on; the documented control is output_config.effort, whose lowest level is low. Claude Sonnet 5.5 (with between_tools) and Haiku 5.5 (with disabled) can still turn up-front thinking off, but only at effort high or below.

The What's new in Claude Fable 5.1 page states the rule and the fix in three sentences: "Adaptive thinking is always on. thinking: {"type": "enabled"} with budget_tokens and thinking: {"type": "disabled"} both return a 400 error. Omit thinking or send {"type": "adaptive"}." Anthropic's release notes date the loss of disabled thinking to Claude Fable 5 and Mythos 5 on 9 June 2026 and Claude Opus 5.5 on 22 September 2026. For which thinking.type values each Claude model rejects, Anthropic's thinking troubleshooting page keeps a per-model table.

Claude Sonnet 5.5 and Claude Haiku 5.5 still have an off switch, under different names and with the same effort limit. On Sonnet 5.5, released on 28 September 2026, thinking: {"type": "disabled"} returns a 400 whose message points to the replacement, {"type": "between_tools"}. The What's new in Claude Sonnet 5.5 page calls it "the lowest thinking setting on this model": the model does not think before it responds, but the short progress updates it writes between tool calls still come back as thinking blocks, with their summary text. "If your requests don't use tools, the response contains only text, as with disabled on Claude Sonnet 5." Haiku 5.5, released on 7 October 2026, still accepts disabled.

Both off switches carry the same limits, according to Anthropic's errors reference and the Sonnet 5.5 page:

  • Effort high or below only. At xhigh or max, the request returns output_config.effort 'xhigh' is not supported when thinking is disabled on this model. Use effort 'high' or below, or enable thinking. (with the level you sent). "Enable thinking" here means adaptive thinking.
  • No effort changes mid-conversation. A per-message output_config.effort that differs from the level in effect returns a 400. To vary effort per turn, use adaptive thinking.
  • No other thinking fields. On Sonnet 5.5, "display, budget_tokens, or block_binding sent with it returns a 400 error." On both models block_binding works only with adaptive thinking, so with thinking off the history has to stay append-only (the section on edited history below explains why).

Both models reject enabled with budget_tokens. Anthropic's thinking page adds a caveat for Haiku 5.5: "With thinking off, the model can skip a tool call it needs when you also request JSON output."

If you turned thinking off to keep reasoning text out of responses, there is nothing more to do on these models. Anthropic's thinking page lists Claude Opus 5.5 and Claude Fable 5.1 among the models where thinking is already on, and says "display defaults to "omitted" on these models, so the thinking text is hidden until you opt in."

"Effort" has a different name on each API: output_config.effort on Anthropic, reasoning.effort (Responses) or reasoning_effort (Chat Completions) on OpenAI, and thinking_level on Gemini. The lowest setting each target accepts, as of 13 October 2026:

  • Claude Opus 5.5 and Fable 5.1 accept max, xhigh, high, medium and low, with low the lowest, per Anthropic's effort page. The default is medium on Opus 5.5 and high on Fable 5.1, and a request with thinking disabled returns a 400 whatever the effort level, since Anthropic's thinking troubleshooting table lists thinking as "Always on" for both.
  • Claude Sonnet 5.5 accepts all five levels, with high the default; between_tools works at low, medium and high.
  • Claude Haiku 5.5 accepts all five levels, with medium the default; disabled works at high or below.
  • GPT-6 Astra accepts low to max. OpenAI's reasoning guide says setting effort to none "returns HTTP 400", and the Using GPT-6 guide says to "use low instead".
  • GPT-6.1 Sol accepts low, medium (the default), high, xhigh and max. Its model page says "The none and minimal reasoning efforts are not supported."
  • GPT-6 Sol and Luna accept none, where OpenAI's rule no longer requires removing the sampling fields and function calling works on Chat Completions.
  • Gemini 3.8 Flash accepts low, medium (the default) and high. Google Cloud says setting MINIMAL "will return an API validation error", and the Gemini 3.5 page says using thinking_level and the legacy thinking_budget in one request "will return a 400 error". There is no off switch.

Choosing among the remaining levels, and what each costs, is the subject of our guide to what the reasoning effort setting controls. No vendor documents why thinking can no longer be switched off.

Forced tool choice: schema-valid JSON with tool_choice auto

Claude Opus 5.5, Sonnet 5.5, Fable 5.1 and Mythos 5.1 reject tool_choice of type any or tool with a 400, including on token counting, while auto and none still work; Claude Haiku 5.5, OpenAI's GPT-6 models and Google's Gemini 3.8 Flash still accept forced tool calls. On Anthropic's side the change arrived with Fable 5.1 on 1 September 2026, Opus 5.5 on 22 September and Sonnet 5.5 on 28 September.

The mechanism explains why forced tools and prefill end up in the same place. Anthropic's tool definition docs say that "when you have tool_choice as any or tool, the API prefills the assistant message to force a tool to be used." Forcing a tool was a prefill done for you, which is why Anthropic's thinking page discusses the two together. That is a shared mechanism, not a stated motive: Anthropic has not said it is why either was removed.

Anthropic gives a reason, but only for two models. Of Claude Fable 5.1 and Mythos 5.1, the Fable 5.1 page says: "Thinking is always on for these models, and a forced tool call would skip it. The model would write its working-out into the tool arguments instead, which lowers argument quality." The Opus 5.5 pages say only that "Claude Opus 5.5 doesn't support forced tool use.", and the Sonnet 5.5 page says the same of Sonnet 5.5. Applying the Fable 5.1 reason to Opus 5.5 or Sonnet 5.5 would be our inference, not something Anthropic has written, and it is Anthropic's claim about its own models, not a general property of forced tool calls.

What replaces it depends on why you forced the tool:

  • You forced a tool only to get JSON. Move the schema to structured outputs (output_config.format). No tool call is needed.
  • Your loop needs a tool call. Keep tool_choice: {"type": "auto"}, set strict: true on the tool and, in Anthropic's words, "say in the prompt when the tool applies." Anthropic says Fable 5.1 "follows explicit tool instructions reliably"; that is the vendor's claim.

Strict mode does not force the call. Anthropic's strict tool use page guarantees that "Tool input strictly follows the input_schema" and that the tool name is valid. The tool docs describe the old way to get both guarantees at once, "combine tool_choice: {"type": "any"} with strict tool use", and that combination is exactly what these models now reject. With auto, whether a call happens depends on the prompt. Our advice: handle a text reply where you expected a tool call, rather than assuming the call.

Claude Haiku 5.5 is the exception among the new Claude models, with a catch. Its migration guide says it "accepts a forced tool_choice (any or a named tool), but the response starts with the tool call and has no thinking block." The forced call works, but without the thinking that Anthropic gives as the reason for rejecting it on Fable 5.1. To let Haiku 5.5 think first, Anthropic's advice is the same as for the other models: tool_choice: {"type": "auto"} and a prompt that says when to use the tool.

What strict mode needs from an existing schema

Adding strict: true to a tool whose schema was written for forced tool use is the next place a structured-output wrapper can break. Anthropic's structured outputs page lists what strict mode accepts and its limits:

  • Every object must set additionalProperties to false.
  • Numerical constraints (minimum, maximum, multipleOf) and string constraints (minLength, maxLength) are not supported. "If you use an unsupported feature, you'll receive a 400 error with details." Anthropic says "Most SDK helpers transform schemas that use unsupported features", moving the constraints into field descriptions, so a wrapper built on those helpers may never see that error while one sending raw JSON will.
  • Per request, the explicit limits are 20 strict tools, 24 optional parameters and 16 parameters with union types; the page quotes no error message for exceeding them. Separately, a set of schemas can exceed internal complexity limits even when each explicit limit is met, and that returns a 400 with Schema is too complex for compilation. The same internal limits include a compilation timeout of 180 seconds.
  • Prefilling is listed as "Incompatible with JSON outputs".

An illustrative refund tool shows the change. This schema fails strict mode on three counts: no additionalProperties: false, a minimum and a maxLength.

{
  "type": "object",
  "properties": {
    "amount": {"type": "number", "minimum": 0},
    "reason": {"type": "string", "maxLength": 200}
  },
  "required": ["amount", "reason"]
}

This one passes. The lower bound now lives only in the description, which the strict grammar does not enforce, so check it in your own code.

{
  "type": "object",
  "properties": {
    "amount": {"type": "number", "description": "Amount in EUR, at least 0"},
    "reason": {"type": "string"}
  },
  "required": ["amount", "reason"],
  "additionalProperties": false
}

OpenAI and Google have kept forced tool calls. OpenAI's function calling guide still documents tool_choice options including "Required" and a forced named function, and no GPT-6 restriction on them was found on the pages read. Its strict mode has similar requirements: "additionalProperties must be set to false for each object in the parameters", "All fields in properties must be marked as required", and a non-compliant schema "will be rejected with details about the missing constraints." Where tools can be used differs by model: GPT-6 Astra's tool calling requires the Responses API, and Sol and Luna "support function calling in Chat Completions only with reasoning_effort: "none"", per the Using GPT-6 guide. Google's function calling page keeps mode any ("Model is constrained to always predict a function call"), with the caveat that "the API may reject very large or deeply nested schemas" in that mode.

On Amazon Bedrock, structured outputs are not available for Claude Opus 5.5, Sonnet 5.5 or Haiku 5.5, so both Anthropic replacements are missing there; the Bedrock section below has the detail.

Prefill: where format, preamble and continuation instructions go now

Claude 4.6 and later models, including Claude Opus 5.5, Sonnet 5.5, Haiku 5.5 and Fable 5.1, reject a request whose messages end with an assistant turn, and Google Cloud documents that Gemini 3.8 Flash rejects a history that ends with a model turn; earlier assistant turns in the conversation are unaffected.

Anthropic's errors reference says "Claude 4.6 and later models and Claude Mythos Preview do not support prefilling assistant messages", and its release notes recorded the change for Opus 4.6 on 5 February 2026. Haiku is the newest family to lose it: the Haiku 5.5 migration guide says Claude Haiku 4.5 accepts a prefill when thinking is off, while "Claude Haiku 5.5 rejects it with a 400 error, even with thinking turned off." The restriction is narrow. "Prefill" here means only a trailing assistant turn: Anthropic's prompting best practices say "adding assistant messages elsewhere in the conversation is not affected."

The same page maps each use of prefill to a replacement:

  • Forcing a format, such as starting the reply with {: structured outputs, or instructions.
  • Skipping a preamble: an instruction in the system prompt.
  • Continuing a reply that was cut off: move the partial text into a new user turn. Anthropic's template is "Your previous response was interrupted and ended with [previous_response]. Continue from where you left off." For a delay notice cut off after Dear customer, your order, the request ends with a user message built from that template, with the fragment in place of the placeholder.
  • What Anthropic calls context hydration: the user turn, tools or compaction.

Gemini 3.8 Flash has the same rule with a different role name. The Google Cloud guide says "History payloads cannot end with a "model" role turn" and "Prefilled model turns are unsupported and must be removed when upgrading". Google Cloud's guide for Gemini 3.6 Flash describes the same case as triggering "a strict validation error"; neither guide quotes the error text. The Gemini API's latest-model page points multi-turn callers to server-side state with previous_interaction_id, which avoids resending history at all.

OpenAI documents no assistant prefill feature of this kind, and the GPT-6 pages read for this guide document no rule about a trailing assistant message.

No vendor documents why prefill is rejected. The nearest Anthropic sentence says prefill is rarely needed, not why it is refused: "Model intelligence and instruction following have advanced such that most use cases of prefill no longer require it."

Edited history: a thinking block is bound to everything before it

On Claude Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5, each replayed thinking block is checked against the top-level system prompt, the set of tools and every message before it, and any change there invalidates that block and every later one. Anthropic's preserved thinking page calls this the block's prefix: "The top-level system prompt, tools, and messages before the block are its prefix. If the prefix differs from what you sent when the block was produced, that block and every later thinking block are invalid".

Some things sit outside the prefix. "Request parameters outside those three fields, such as effort, max_tokens, output_config, tool_choice, and metadata, aren't part of the prefix check, and neither are cache_control markers." One more rule concerns the blocks themselves: "the thinking blocks you keep must be an unbroken run of the original sequence, so removing one from the middle invalidates the thinking blocks after it." Claude Mythos 5.1 and models released before Fable 5.1 do not run the check.

Anthropic gives a reason in the first sentence of the same page: "Preserved thinking is a property of newer Claude models that guards against distillation." Distillation here means training another model on a model's outputs; this guide does not go further into the threat.

Which edits break which blocks, in one conversation

Take an illustrative support conversation on Claude Fable 5.1. The system prompt includes today's date, there is one tool, and three assistant turns each carry a thinking block:

system       You are a support assistant for Acme Cloud. Today is 2026-09-26.
tools        lookup_order
0 user       Where is order A-1001?
1 assistant  [thinking block 1] calls lookup_order
2 user       tool result: shipped 2026-09-24, DHL, plus 2,000 characters of logs
3 assistant  [thinking block 2] It shipped on 24 September with DHL.
4 user       Can I change the delivery address?
5 assistant  [thinking block 3] Not after dispatch, but DHL can redirect it.
6 user       How do I ask DHL?

Thinking block 2's prefix is everything above it: the system prompt, the tools and messages 0 to 2. Now apply five edits an integration might make before sending the next request:

  1. The integration re-renders today's date in system, so 2026-09-26 becomes 2026-09-27. All three blocks are invalid, because system is in every block's prefix.
  2. It trims the old tool result in message 2 to save tokens. Block 1 is still valid, since message 2 comes after it; blocks 2 and 3 are invalid.
  3. It removes thinking block 2 but keeps block 3. Block 3 is invalid, because the kept blocks are no longer an unbroken run.
  4. It removes thinking block 1, the oldest. Nothing is invalid: removing blocks from the start of the run is allowed.
  5. It only appends: new turns at the end and a mid-conversation system message, together with a change of top-level effort from high to medium and a new cache_control marker. Nothing is invalid.

The pattern generalises. An edit invalidates every thinking block below it and none above it; system and tools sit above everything, and appending sits below everything. So rewording message 4 alone would cost only block 3.

A support conversation drawn as rows, from the system prompt and tools at the top down through three assistant turns that each carry a thinking block, ending with a row where new turns are appended. A bracket marks the system prompt, tools and messages 0 to 2 as the prefix of thinking block 2. Five columns each show one edit. Re-rendering the date in the system prompt invalidates all three thinking blocks. Trimming the earlier tool result invalidates blocks 2 and 3 but not block 1. Removing block 2 while keeping block 3 invalidates block 3. Removing block 1 invalidates nothing. Appending at the bottom invalidates nothing. Footer: changing system, tools or a message invalidates every block below it.

Illustrative conversation. The append-only column also changes effort from high to medium, adds a cache_control marker and appends a system message. Validity follows Anthropic's preserved thinking documentation for Claude Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5 as of 13 October 2026, computed with our prefix check; whether an invalid block is rejected, dropped or passed through depends on the account's creation date and the request settings.

Anthropic notes that the same discipline pays twice: "The same discipline keeps the prefix stable for prompt caching: the edits that invalidate thinking are the edits that restart the cache." How prompt caching works explains the cache side.

Edits that a serialiser or gateway makes on its own

On Claude Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5, a thinking-block signature error on a later turn can come from a serialiser or gateway that rewrites history in transit rather than from a deliberate edit, and Anthropic's preserved thinking page names the causes to check. Send assistant turns back with "every block type, in the order received, including thinking blocks whose thinking field is empty", it says, because "A serializer that drops unknown block types, drops empty fields, or reorders blocks edits the prefix for every later turn."

Empty thinking blocks are the easiest to lose. The page says: "On Claude Fable 5.1, Claude Opus 5.5, Claude Sonnet 5.5, and Claude Haiku 5.5, the thinking field is empty by default and the signature carries the reasoning, so a serializer that skips empty blocks removes thinking. If it removes all of them, nothing fails and the model loses its earlier reasoning on every turn." When this guide was first published the page named only Fable 5.1; it now names all four.

Anthropic documents three more causes:

  • An empty signature. "A block sent back with an empty signature fails." If the error stops after "Invalid signature in thinking block", the troubleshooting page says the signature itself did not verify: "it was truncated, altered, or sent back empty, and prefix_mismatch_behavior doesn't apply."
  • A gateway that moves system messages. "Leave a role: "system" message where the caller put it. Moving it into the top-level system field changes system on that request and invalidates every thinking block in the conversation." This turns the documented replacement for mid-session instructions, below, into an edit.
  • A saved session rebuilt from templates. For resumed sessions, Anthropic says "JSON formatting and key order don't matter; the values do", so replay what was sent rather than rendering it again.

Anthropic also sets out what libraries, proxies and gateways owe their callers. They should "Forward the caller's anthropic-beta values and thinking.block_binding unchanged, and return input_transformations to them", because "An options schema that rejects unknown keys stops your users from choosing "drop_block"." And: "Don't hide the 400. If your code catches it, strips thinking, and retries on the caller's behalf, log that it did: their history is still edited, and the model loses its earlier reasoning on every later request."

What the API does when the prefix changes

On Claude Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5, whether an invalid thinking block produces an error depends on the account. According to Anthropic's preserved thinking page, "The API enforces the prefix check by default for accounts created on or after August 31, 2026, 00:00 UTC." On those accounts the default is a 400 invalid_request_error that names the first failing block. On older accounts the API enforces the check only when the request sets thinking.block_binding.prefix_mismatch_behavior; otherwise the failing blocks reach the model unchanged and are billed, and nothing fails.

That setting takes two values:

  • "error", the default where the check is enforced: the request fails with a 400.
  • "drop_block": the API drops each failing block and every thinking block after it, and the request succeeds. Dropped blocks are not billed, and they are listed in input_transformations with reason: "prefix_binding_mismatch".

Setting it requires the thinking-binding-controls-2026-08-01 beta header. Without the header, a request that includes thinking.block_binding fails with a 400 whose message ends in block_binding: Extra inputs are not permitted. In the Message Batches API an item that leaves the field unset does not fail; where enforcement is on by default, the failing blocks are dropped instead.

The documented message begins:

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".

Without the beta header, it continues: That setting requires the `thinking-binding-controls-2026-08-01` value in the `anthropic-beta` header. The message usually ends with a sentence naming what changed, which the troubleshooting page says can be "the system prompt, the tools list, the first message or block that differs, content that is missing or new, or an earlier thinking block that is missing or out of order." That sentence is the quickest way to find the edit in your logs, but Anthropic adds: "That sentence is for people and logs. Its wording can change, so don't match on it in code."

Anthropic's instructions for handling this error in code start with "Don't resend the same body: it fails the same way every time." Then:

  1. Retry once with the beta header and prefix_mismatch_behavior: "drop_block".
  2. In Anthropic's words, "store that choice with the session so every later request sends it too, including after a restart." The edited prefix is still in the history on the next turn, so a retry that is not remembered only moves the failure one request later.
  3. If you can't send the header, "remove every thinking and redacted_thinking block from the history once, leave them out, and continue."
  4. "Then fix the edit that caused the mismatch."

The last step matters because, in the page's words, ""drop_block" keeps requests succeeding, but it doesn't fix the edit that caused the mismatch, and it has a cost." The prompt cache still restarts at the edit, and "Dropping blocks can significantly increase the output tokens generated, especially at higher effort levels or when blocks are dropped on every turn." The reason is that Claude "re-creates that reasoning" when it can no longer read it.

Sonnet 5.5 and Haiku 5.5 add a limit. Anthropic's errors reference says "On Claude Sonnet 5.5 and Claude Haiku 5.5, block_binding works only with thinking: {"type": "adaptive"}." With between_tools on Sonnet 5.5 or disabled on Haiku 5.5 there is no drop_block to fall back on, so the documented options are to "keep the history append-only, or strip the thinking blocks from the edited turn on."

Accounts created before 31 August 2026 need a different test. Anthropic warns that on them "a run with no errors on your own key doesn't show whether your code is affected. If people run your tool with their own API keys, those on newer accounts get the 400 error before you do." It documents two ways to see what those users see. Send the beta header without setting the field: behaviour does not change, and each response lists the failing blocks in input_transformations as thinking_mismatch_allowed, entries the page calls "a lower bound on what enforcement would remove". Or set prefix_mismatch_behavior: "error" on a test request, since "Setting the field opts a request in, so you can see what a new account sees without creating one." A request that sets the field never gets thinking_mismatch_allowed entries.

Doing the old jobs without editing the prefix

Each common reason to edit history has a documented append-only replacement on Anthropic's API. Several need a beta header, and the header names below are as of 13 October 2026; they are the part of this guide most likely to change.

  • Giving a new instruction mid-session: append a message with role: "system" to messages. Anthropic's mid-conversation system messages page says this needs no header and is available on the Claude API, Claude in Amazon Bedrock and Google Cloud. It is not supported on Claude Sonnet 5, and it works only if no layer in between moves the message into top-level system. On Claude Sonnet 5.5 and Haiku 5.5, user input typed while a tool loop runs does not belong in a system message: those models "can treat a system message placed right after a tool result as untrusted text and ignore it", so Anthropic says to append the user's words "as a text block after the last tool_result in the same user message".
  • A reminder that applies for one turn: a turn-scoped system message with clear_at, under the mid-conversation-system-clear-at-2026-08-21 header.
  • Adding or removing tools: tool_addition and tool_removal blocks in a system message, under inline-tools-2026-09-15 on the Claude API or mid-conversation-tool-changes-2026-07-01. To switch tools off for one request, Anthropic says "send tool_choice: {"type": "none"}. Don't remove tools."
  • Changing effort mid-conversation: a system message carrying output_config.effort, under mid-conversation-output-config-2026-07-01. Changing the top-level effort also leaves thinking blocks valid, because effort is outside the prefix, but it does restart the prompt cache: Anthropic's troubleshooting page counts the effort level as part of the cached prefix.
  • Trimming a long history: server-side context editing (clear_tool_uses_20250919, under context-management-2025-06-27) or on-demand compaction (compact-2026-09-04, launched on 14 September 2026). After compaction, "the checked prefix starts at the most recent compaction block".
  • Images or documents whose content changes: the check covers the fetched bytes, not the URL string, so a "latest screenshot" endpoint or a document someone edits between turns counts as an edit. "A rotating signed URL for the same file doesn't." Send such content by Files API file_id or as base64 instead. The Files API is not available on Claude in Amazon Bedrock.

OpenAI and Gemini: replay reasoning untouched

OpenAI and Google both tell callers to send reasoning back unchanged, but neither documents a check like Anthropic's. OpenAI's reasoning guide asks you to "ensure all items between the last user message and your function call output are passed into the next response untouched", and says "Our systems will smartly ignore any reasoning items that aren't relevant to your functions, and only retain those in context that are relevant." No validation or error for edited items is documented. OpenAI's append-only way to change effort is a configuration_update item, {"type": "configuration_update", "reasoning": {"effort": "high"}}, added before the next user message; the guide says "the API rejects adjacent updates" and not to combine them with automatic compaction or truncation.

Google is more insistent. Its thinking page, read on 13 October 2026, says, for stateless use of the Interactions API, "You MUST always resend all thought blocks exactly as they were received from the model", and "You should NOT remove or modify thought blocks from the history, as they contain the signatures required for the model to continue its reasoning." What happens if you do is not documented for Gemini 3.8 Flash. The Google Cloud guide for Gemini 3.6 Flash says "Invalid thought signatures will now produce errors", which applies to that model on Google Cloud. Server-side state with previous_interaction_id avoids replaying thoughts at all.

Switching models mid-conversation drops reasoning without an error

When a conversation moves from one model to another, Anthropic and OpenAI both discard reasoning the new model cannot read without returning an error, while Google tells callers to resend the old model's thought blocks and says its backend manages compatibility.

Anthropic's rules are one-directional: reasoning carries up the range, not down it. Its preserved thinking page says "Claude Opus 5.5 reads thinking blocks from Claude Opus 5, from earlier Opus, Sonnet, and Haiku models, and, on the Claude API and Google Cloud, from Claude Sonnet 5.5 and Claude Haiku 5.5, but not from Claude Fable or Claude Mythos models. On the Claude API, Claude Fable 5.1 and Claude Mythos 5.1 read thinking blocks from Claude Opus 5.5; no other model does." It also says "No other model reads thinking blocks from Claude Fable 5.1 or Claude Mythos 5.1."

The two new models extend the same ladder downwards. On the Claude API and Google Cloud, Sonnet 5.5 reads Haiku 5.5's blocks and Opus 5.5 reads both, but Sonnet 5.5 does not read Opus 5 or Opus 5.5, and Haiku 5.5 reads neither Opus 5, Opus 5.5 nor Sonnet 5.5. A move from Sonnet 5.5 down to Sonnet 5, which the page gives as a server-side fallback example, also loses the reasoning. Unreadable blocks are dropped and not billed, silently unless the request carries the binding header described above, which reports each one as model_binding_mismatch.

Sonnet 5.5 and Haiku 5.5 also bind their blocks to an account: "Thinking blocks that Claude Sonnet 5.5 or Claude Haiku 5.5 produces work only in the account that produced them, or in an account linked to it." A block sent from another account is dropped and the request succeeds; with the binding header, the Claude API and Google Cloud list it as organization_binding_mismatch. This matters for tools that share or replay conversations across customers' keys.

OpenAI's reasoning guide says "Persisted reasoning can be reused only within the same model family", and "When you switch model families, the API omits incompatible reasoning from the model's context, even when reasoning.context is all_turns." OpenAI's example is within GPT-5.6: "gpt-5.6-sol, gpt-5.6-terra, and gpt-5.6-luna can reuse each other's reasoning, but reasoning does not carry between the GPT-5.6 and GPT-5.5 families." The Using GPT-6 guide speaks of "the GPT-6 model family" and covers GPT-6.1 Sol on the same page, but no page read for this guide says whether GPT-6.1 Sol and GPT-6 Sol count as one family for this rule. Test a router that moves between them before relying on either answer. GPT-6.1 Sol is also the only GPT-6 model the reasoning guide lists as supporting reasoning.context: "all_turns".

Google's thinking page says: "When switching models within a session, you should still resend the previous model's thought blocks. The backend manages compatibility." Google has also started switching models for callers. Its changelog for 8 October 2026 says "API requests to gemini-3.7-flash are automatically routed to gemini-3.8-flash", so code that still names Gemini 3.7 Flash now runs on Gemini 3.8 Flash, under that model's rules on sampling parameters and trailing model turns, without a code change on the caller's side. The changelog entry is for the Gemini API; the Google Cloud guide read for this refresh does not mention the routing.

The practical consequence is for routers and fallbacks. A fallback from Claude Fable 5.1 to Claude Opus 5.5 continues the conversation without Fable's earlier reasoning and without an error. Sending the binding header from any router that switches Claude models is the only documented way to see that happen.

Text between tool calls now arrives in thinking blocks

On Claude Opus 5.5, the short notes the model writes between tool calls come back as thinking blocks instead of text blocks, and at the default display: "omitted" those blocks are empty, so an interface that showed the notes to users goes quiet without any error. Claude Sonnet 5.5 does the same for notes longer than a sentence or two. Claude Fable 5.1 has behaved this way since its release on 1 September 2026, but Claude Opus 5 and Sonnet 5 returned all such text as text blocks, so code moving up from them is the code that goes quiet.

Anthropic's Opus 5.5 migration guide describes the effect plainly: "No request fails, but an application that streams that text to its users as progress updates goes quiet between tool calls." The Sonnet 5.5 migration guide draws its line by length: "notes longer than a sentence or two that the model writes between tool calls come back as progress-update thinking blocks, empty at the default display. Shorter remarks stay text." Like a dropped thinking block, it produces no error, so it shows up in the interface rather than in error logs.

Anthropic's thinking page calls these notes progress updates, "written for the person watching the agent rather than as reasoning". It lists Claude Fable 5.1, Mythos 5.1, Opus 5.5, Sonnet 5.5 and Fable 5 as the models that write them; Claude Haiku 5.5 is not on the list. Each update is its own thinking block with its own signature, placed immediately before the tool_use block it introduces. At most one precedes each tool call, and the model can skip any of them.

What a progress-update block contains depends on display:

  • "omitted", the default on these models: empty, like every reasoning block.
  • "updates" (beta): reasoning blocks stay empty and progress updates carry summary text. It needs the thinking-display-updates-2026-08-18 header; without it, "the value is rejected with the same 400 invalid_request_error as an unknown display value."
  • "summarized": both carry text, and the page says an update is "not distinguishable from a reasoning block".

Under "updates", the start of an illustrative response after a tool_result looks like this, with a reasoning block that stays empty followed by a progress update that does not:

[
  { "type": "thinking", "thinking": "", "signature": "EuYB..." },
  { "type": "thinking", "thinking": "Order A-1001 shipped with DHL on 24 September. Checking whether DHL can redirect it.", "signature": "EpcC..." },
  { "type": "tool_use", "id": "toolu_01", "name": "lookup_carrier", "input": { "carrier": "DHL" } }
]

To restore the updates an interface used to show:

  1. On Opus 5.5, Fable 5.1 or Sonnet 5.5 with adaptive thinking, send thinking: {"type": "adaptive", "display": "updates"} with the beta header. On Sonnet 5.5 with between_tools, the updates already come back with their text, and display is not accepted with that type.
  2. Read the notes from thinking blocks as well as text blocks. Under "updates", any thinking block with non-empty text is a progress update: render it before the tool_use block that follows it, and render nothing for an empty block.
  3. Pass every block back unchanged with the rest of the assistant turn. Dropping the empty ones is the serialiser edit described in the history section above.

Three details change what users see. "The text you receive is a summary of the progress update, normally a sentence or two", while usage.output_tokens counts the update at its full length. "These models write fewer progress updates at higher effort and in long tool chains", so an interface that waits for one at every step will sometimes wait for nothing. And when a response stops on max_tokens, model_context_window_exceeded or stop_sequence soon after a tool call or tool result, the last block can be a stand-in for the unfinished work; under "updates" or "summarized" its text is exactly This part of the response was interrupted before it finished., and under "omitted" it is empty.

Anthropic does not say why the notes moved into thinking blocks. The pages read for this guide document no comparable change for OpenAI's GPT-6 models or Gemini 3.8 Flash.

Which restrictions come with a stated reason

As of 13 October 2026, vendors document a reason for three of these restrictions: Anthropic for forced tool use on Claude Fable 5.1 and Mythos 5.1 and for thinking-block binding, and Google for sampling settings on Gemini 3.x; for the rest, no reason is published.

Restriction Vendor and models Stated reason Where
Forced tool use rejected Anthropic: Claude Fable 5.1, Mythos 5.1 "Thinking is always on for these models, and a forced tool call would skip it. The model would write its working-out into the tool arguments instead, which lowers argument quality." What's new in Claude Fable 5.1
Forced tool use rejected Anthropic: Claude Opus 5.5, Sonnet 5.5 None published What's new in Claude Opus 5.5, What's new in Claude Sonnet 5.5
Thinking blocks bound to their prefix and readable only by some models Anthropic: Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5 and other newer models "Preserved thinking is a property of newer Claude models that guards against distillation." Preserved thinking
Sampling settings removed Google: all Gemini 3.x models "Gemini 3's reasoning capabilities are optimized for the default settings." What's new in Gemini 3.5
Non-default sampling rejected Anthropic: models after Opus 4.6 None published Messages API reference
Sampling fields to be removed (on Sol and Luna, above effort none) OpenAI: GPT-6 Astra, Sol, Luna; GPT-6.1 Sol None published Using GPT-6
Thinking cannot be switched off Anthropic: Opus 5.5, Fable 5.1 (Sonnet 5.5 and Haiku 5.5 can, at effort high or below); OpenAI: GPT-6 Astra, GPT-6.1 Sol; Google: Gemini 3.8 Flash None published Thinking and model pages cited above
Text between tool calls returned in empty thinking blocks Anthropic: Opus 5.5, Sonnet 5.5, Fable 5.1 None published Opus 5.5 migration guide
Trailing assistant or model turn rejected Anthropic: Claude 4.6 and later; Google: Gemini 3.8 Flash None published. Anthropic says only that "most use cases of prefill no longer require it" Prompting best practices

No single cause explains all of this. The obvious candidate, that thinking is always on, fails twice: Anthropic's Claude Opus 4.7, Opus 4.8 and Sonnet 5 reject non-default sampling even with thinking off, and OpenAI's GPT-6 Sol and Luna do not require removing sampling fields at effort none and, on the Responses API, keep forced tool calls with no documented restriction.

Explanations circulating in secondary coverage are not in any vendor document. The idea that sampling is locked to protect a reasoning model's calibration appears, for example, in a September 2025 post by Shion Honda, which offers it as a likely explanation for GPT-5 and OpenAI's o-series models, not as a vendor statement. If a vendor publishes a reason, the "None published" cells are the ones that change.

The cross-vendor reference table

For Claude Opus 5.5, Sonnet 5.5, Haiku 5.5 and Fable 5.1, GPT-6 and GPT-6.1 Sol, and Gemini 3.8 Flash, the table below gives, per job and vendor, the models affected and when the change began, what happens to the old pattern, the exact error text and where it comes from, the documented reason, and what to send instead, as of 13 October 2026, when every source was reread. It is generated from the same rules file as the checker behind the preflight script below. The "What happens" and "Replacement" cells are our summaries of the sources cited in the sections above; the error strings and the reasons in quotation marks are exact.

Job (old pattern) Vendor and models (change began) What happens Error text (provenance) Documented reason Replacement
Repeatable or less random output (temperature, top_p, top_k) Anthropic: Claude Opus 5.5, Sonnet 5.5, Haiku 5.5, Fable 5.1. Since models released after Opus 4.6 400 for temperature other than 1, top_p other than 0.99, any top_k, or temperature and top_p sent together None documented. Reported on Bedrock: `temperature` is deprecated for this model. (trulens #2807, mem0 #7417, dify-official-plugins #3925) Not documented Omit them and steer with the prompt; structured outputs for a fixed shape
Repeatable or less random output (temperature, top_p, top_k) OpenAI: GPT-6 Astra, Sol, Luna; GPT-6.1 Sol. Since GPT-6: Astra released 3 Sep 2026, Sol and Luna 22 Sep 2026; GPT-6.1 Sol 29 Sep 2026 Must be removed unless effort is none (GPT-6 Sol and Luna only); API response not documented None documented. Reported on Bedrock: This model doesn't support the temperature field. Remove temperature and try again. (onyx #15078) Not documented Remove them, or run GPT-6 Sol or Luna at effort none
Repeatable or less random output (temperature, top_p, top_k) Google: Gemini 3.8 Flash. Since all Gemini 3.x models Sampling ignored without error (Google Cloud guide); penalties and candidate_count return an error None for sampling (ignored); penalties and candidate_count: error documented, text not documented "reasoning capabilities are optimized for the default settings" (Google, all Gemini 3.x) Remove them; a system instruction with explicit rules, thinking_level, a response schema
Less or no reasoning (thinking off, token budgets, effort none, minimal) Anthropic: Claude Opus 5.5, Fable 5.1. Since budget_tokens rejected from Claude 4.7; thinking always on from Fable 5, 9 Jun 2026, and on Opus 5.5, 22 Sep 2026 400 for thinking disabled, enabled with budget_tokens, or between_tools "thinking.type.disabled" is not supported for this model. … / "thinking.type.enabled" is not supported for this model. … / "thinking.type.between_tools" is not supported for this model. (documented: Claude API errors) Not documented Omit thinking; lower output_config.effort (low is the lowest; default medium on Opus 5.5, high on Fable 5.1)
Less or no reasoning (thinking off, token budgets, effort none, minimal) Anthropic: Claude Sonnet 5.5, Haiku 5.5. Since Sonnet 5.5, 28 Sep 2026; Haiku 5.5, 7 Oct 2026 Sonnet: disabled returns 400; between_tools turns off up-front thinking at effort high or below. Haiku: disabled still accepted at effort high or below. Both: enabled with budget_tokens returns 400, and xhigh or max with thinking off returns 400 To turn thinking off on this model, send "thinking": {"type": "between_tools"} instead of {"type": "disabled"}. … / output_config.effort 'xhigh' is not supported when thinking is disabled on this model. … (documented: Claude API errors) Not documented Sonnet: between_tools with no other thinking field, effort low to high. Haiku: disabled at effort high or below. Either: omit thinking and lower output_config.effort (default high on Sonnet, medium on Haiku)
Less or no reasoning (thinking off, token budgets, effort none, minimal) OpenAI: GPT-6 Astra; GPT-6.1 Sol; GPT-6 Sol and Luna. Since GPT-6 Astra, 3 Sep 2026 Astra: effort none returns HTTP 400. GPT-6.1 Sol: none and minimal not supported. GPT-6 Sol and Luna: none accepted (default medium) None documented. Reported on Bedrock: Unsupported value: 'none' is not supported with the 'us.openai.gpt-6-astra' model (onyx #15078) Not documented Astra and GPT-6.1 Sol: effort low. GPT-6 Sol and Luna: none still works
Less or no reasoning (thinking off, token budgets, effort none, minimal) Google: Gemini 3.8 Flash. Since Gemini 3.8 Flash for minimal No off switch: minimal returns an error; thinking_budget with thinking_level returns a 400 Error documented; text not documented Not documented thinking_level low (levels: low, medium default, high), without thinking_budget
Force a tool call or JSON (tool_choice any, tool, required, named) Anthropic: Claude Opus 5.5, Sonnet 5.5, Fable 5.1, Mythos 5.1; Haiku 5.5 accepts it. Since Fable 5.1, 1 Sep 2026; Opus 5.5, 22 Sep 2026; Sonnet 5.5, 28 Sep 2026 400 for tool_choice any or tool, also on token counting; auto and none accepted. Haiku 5.5 accepts a forced tool but then skips thinking tool_choice: type "tool" and "any" are not supported for this model. (documented: Claude API errors); same text reported on Bedrock in langchain-aws #1310 "a forced tool call would skip it" (thinking), lowering argument quality; stated for Fable 5.1 and Mythos 5.1 only tool_choice auto with strict: true, or structured outputs, plus a prompt saying when to call the tool. On Claude in Amazon Bedrock structured outputs are not available for these models: auto alone, validated in code
Force a tool call or JSON (tool_choice any, tool, required, named) OpenAI: GPT-6 Astra, Sol, Luna; GPT-6.1 Sol Still supported on Responses (required, named function). Chat Completions: no function calling on Astra or GPT-6.1 Sol; GPT-6 Sol and Luna only at effort none None documented Not documented for the Chat Completions limits Keep forced tool_choice; send tools through the Responses API
Force a tool call or JSON (tool_choice any, tool, required, named) Google: Gemini 3.8 Flash Still supported (mode any); very large or deeply nested schemas may be rejected None documented Not applicable (no restriction documented) No change needed
Start the reply yourself (trailing assistant or model turn) Anthropic: Claude Opus 5.5, Sonnet 5.5, Haiku 5.5, Fable 5.1. Since Claude 4.6: Opus 4.6, 5 Feb 2026 400 if messages end with an assistant turn, with or without thinking; earlier assistant turns are fine This model does not support assistant message prefill. The conversation must end with a user message. (documented: Claude API errors) Not documented; Anthropic says only that most uses "no longer require it" End with a user turn: structured outputs or instructions for format, system prompt for preambles, the partial text in the user turn for continuations
Start the reply yourself (trailing assistant or model turn) OpenAI: GPT-6 Astra, Sol, Luna; GPT-6.1 Sol No prefill feature or restriction documented None documented Not applicable (no restriction documented) Not applicable
Start the reply yourself (trailing assistant or model turn) Google: Gemini 3.8 Flash. Since documented from the Gemini 3.6 Flash guide Validation error if the history ends with a model turn Error documented; text not documented Not documented End with a user turn; Google suggests server-side state (previous_interaction_id)
Edit earlier history while replaying reasoning Anthropic: Claude Opus 5.5, Sonnet 5.5, Haiku 5.5, Fable 5.1. Since Fable 5.1, 1 Sep 2026; enforced by default for accounts created from 31 Aug 2026 New accounts: 400. With drop_block: blocks dropped. Older accounts, field unset: blocks reach the model, flagged thinking_mismatch_allowed only under the beta header messages.{i}.content.{j}: Invalid `signature` in `thinking` block. The block is bound to a different conversation. … (documented: Claude API errors) "guards against distillation" (Anthropic, preserved thinking) Append only: mid-conversation system messages, tool_addition and tool_removal, server-side context editing or compaction. On a 400, retry once with drop_block and keep sending it for the session. On Sonnet 5.5 and Haiku 5.5, drop_block needs adaptive thinking
Edit earlier history while replaying reasoning OpenAI: GPT-6 Astra, Sol, Luna; GPT-6.1 Sol No validation documented; pass reasoning items back untouched. Two adjacent configuration_update items are rejected (GPT-6 family) None documented for edits; adjacent configuration_update: rejection documented, text not documented Not documented configuration_update items to change effort mid-conversation
Edit earlier history while replaying reasoning Google: Gemini 3.8 Flash Stateless Interactions: resend thought blocks exactly; result of an edit not documented for 3.8 (3.6 guide: invalid signatures error) None documented Not documented Resend thought blocks unchanged, or keep state on the server (previous_interaction_id)
Switch models mid-conversation Anthropic: Claude Opus 5.5, Sonnet 5.5, Haiku 5.5, Fable 5.1. Since Fable 5.1, 1 Sep 2026; account binding from Sonnet 5.5, 28 Sep 2026 Unreadable blocks dropped silently: Opus 5.5 cannot read Fable or Mythos blocks; Sonnet 5.5 and Haiku 5.5 cannot read Opus 5, Opus 5.5, Fable or Mythos blocks, and Haiku 5.5 not Sonnet 5.5's; Fable 5.1 reads Opus 5.5 blocks on the Claude API. Sonnet 5.5 and Haiku 5.5 blocks are also dropped in another account None: no error by design; listed as model_binding_mismatch or organization_binding_mismatch in input_transformations under the beta header "guards against distillation" (Anthropic, preserved thinking) Keep sending the full history; watch input_transformations
Switch models mid-conversation OpenAI: GPT-6 Astra, Sol, Luna; GPT-6.1 Sol Reasoning from another model family omitted silently; whether GPT-6.1 Sol shares a family with GPT-6 is not documented None: no error by design Not documented Expect the new family to start without the earlier reasoning
Switch models mid-conversation Google: Gemini 3.8 Flash. Since routing of gemini-3.7-flash, 8 Oct 2026 Resend the old model's thought blocks; Google says the backend manages compatibility. Requests to gemini-3.7-flash now run on 3.8 Flash None documented Not applicable (no restriction documented) No change needed; change gemini-3.7-flash to gemini-3.8-flash
Show users the model's notes between tool calls Anthropic: Claude Opus 5.5, Sonnet 5.5, Fable 5.1. Since Fable 5.1 since release, 1 Sep 2026; Opus 5.5, 22 Sep 2026 (Opus 5 returned text blocks); Sonnet 5.5, 28 Sep 2026 Notes between tool calls come back as progress-update thinking blocks, empty at the default display omitted; no error None: no error. display updates without its beta header: 400, text not documented Not documented display "updates" (beta header thinking-display-updates-2026-08-18) or "summarized"; render each non-empty thinking block before its tool_use

Provenance: "documented" strings are copied from the named vendor page; "reported" strings come from public GitHub issues (one reporter, Amazon Bedrock) and are not in vendor documentation. An ellipsis marks an exact leading part of a longer documented string. "Not documented" means no page read states it. Nothing here was sent to an API.

On Amazon Bedrock, check which API you are calling

Amazon Bedrock serves Claude through two integrations, the Messages API on bedrock-mantle and the older Converse and InvokeModel APIs, and every public report of Bedrock-specific parameter errors cited here comes from the older pair. Anthropic's Claude in Amazon Bedrock page describes the newer one as serving "Claude through the Messages API at /anthropic/v1/messages on AWS-managed infrastructure", at https://bedrock-mantle.{region}.api.aws/anthropic/v1/messages, open to all Bedrock customers since 16 April 2026; Converse and InvokeModel remain available.

Four Bedrock-specific points are documented:

  • Structured outputs are not available for Claude Opus 5.5, Sonnet 5.5 or Haiku 5.5 on Bedrock. Anthropic's Bedrock page lists structured outputs under "Features not supported", Anthropic's structured outputs page lists Bedrock availability only for Claude 4.5 and 4.6 models, and AWS's Opus 5.5 model card marks them unsupported on both endpoints. Anthropic's features overview defines structured outputs as covering both JSON outputs and strict tool use, so we read the exclusion as covering strict tools too. The Sonnet 5.5 and Haiku 5.5 pages state the exclusion directly and give the fallback: "describe the format in the prompt or use a tool without strict, and validate the output in your code." The consequence, our inference for Opus 5.5 and Sonnet 5.5: a Bedrock caller who forced a tool to get JSON has neither Anthropic replacement, and is left with tool_choice: auto, an explicit instruction and their own validation of the output. Haiku 5.5 still accepts a forced tool there, without thinking.
  • Mid-conversation system messages are available on Claude in Amazon Bedrock; the Files API is not.
  • On-demand compaction: the two vendors disagree. Anthropic's compaction page says it is not available on Amazon Bedrock; AWS's Opus 5.5 model card lists compaction on the bedrock-runtime endpoint with compact-2026-09-04. Check before relying on it.
  • The computer_20251124 computer-use tool version is still accepted for Opus 5.5, Sonnet 5.5 and Haiku 5.5 on Bedrock (for Haiku 5.5 it is the version to use there), while the Claude API and Google Cloud reject it.

Everything else about Bedrock in this guide is reported, not documented, and comes from the single issue author described in the first section, using Converse and InvokeModel:

  • For newer Claude models, temperature: 1.0 is reported as accepted and other values as failing with `temperature` is deprecated for this model. (trulens 2807, mem0 7417, dify-official-plugins 3925).
  • Forced tool use on Opus 5.5 and Fable 5.1 fails with the same text as the Claude API, and output_config.effort from low to max is accepted on Opus 5.5 (langchain-aws 1310).
  • For GPT-6, temperature is reported as rejected "at any value, 1 included", so the Claude habit of leaving it at 1.0 does not transfer. Effort goes in additionalModelRequestFields as reasoning={"effort": ...}, and GPT-6 Astra answers effort none with Unsupported value: 'none' is not supported with the 'us.openai.gpt-6-astra' model (onyx 15078).

Whether OpenAI's own API accepts temperature at its default on GPT-6 is not documented: OpenAI's changelog says Astra does not support "custom" values, which leaves the default open.

Check a request body before you deploy

The short Python script below checks a request body against the documented rules in this guide for Claude Opus 5.5, Sonnet 5.5, Haiku 5.5 and Fable 5.1, GPT-6 Astra, Sol and Luna, GPT-6.1 Sol, and Gemini 3.8 Flash (and gemini-3.7-flash, which now routes to it), without sending anything to an API. It uses only the standard library. Its rules are a documented-only subset of the rules file behind the reference table: no string from the Bedrock issue reports is in it. We compared its findings with the fuller checker behind the table on 79 illustrative cases, with no mismatches, using Python 3.11.7 on 27 September 2026, 28 September and 13 October 2026.

Save it as preflight.py:

#!/usr/bin/env python3
"""Check a request body against documented API rules, as of 13 October 2026.

Usage: python3 preflight.py MODEL BODY.json [--previous PREV.json]
MODEL: claude-opus-5-5, claude-sonnet-5-5, claude-haiku-5-5, claude-fable-5-1,
gpt-6-astra, gpt-6-sol, gpt-6-luna, gpt-6.1-sol, or gemini-3.8-flash or
gemini-3.7-flash (generateContent bodies; 3.7 is routed to 3.8). Nothing is sent
anywhere. A clean run means only that none of these documented rules fired. Not
checked: strict schema size limits, Bedrock, beta headers, model switches and
Gemini Interactions.
"""
import argparse, json, sys

ANTHROPIC_THINKING = ('"thinking.type.{}" is not supported for this model. Use "thinking.type.adaptive" '
                      'and "output_config.effort" to control thinking behavior.')
EFFORT = ("output_config.effort 'xhigh' is not supported when thinking is disabled on this model. "
          "Use effort 'high' or below, or enable thinking.")  # quoted for xhigh; max is also rejected
SONNET, HAIKU = "claude-sonnet-5-5", "claude-haiku-5-5"
RULES = {  # rule: (outcome, documented error text or None, what to send instead)
    "anthropic.temperature": ("rejected", None, "Omit it; guide the model in the prompt, or use structured outputs."),
    "anthropic.top_p": ("rejected", None, "Omit top_p (0.99 is the only accepted value)."),
    "anthropic.top_k": ("rejected", None, "Omit top_k."),
    "anthropic.temperature_with_top_p": ("rejected", None, "Send at most one of them, at its default."),
    "anthropic.thinking_type": ("rejected", ANTHROPIC_THINKING,
                                'Omit thinking (or send {"type": "adaptive"}) and set output_config.effort.'),
    "anthropic.thinking_enabled": ("rejected", ANTHROPIC_THINKING.format("enabled"),
                                   "Omit thinking and set output_config.effort."),
    "anthropic.sonnet55.thinking_disabled": ("rejected", 'To turn thinking off on this model, send "thinking": '
                                             '{"type": "between_tools"} instead of {"type": "disabled"}. The model does '
                                             "not think before responding. The short updates it writes between tool "
                                             "calls come back as thinking blocks.",
                                             'Send {"type": "between_tools"} at effort high or below.'),
    "anthropic.sonnet55.between_tools_effort": ("rejected", EFFORT, "Use effort high or below, or omit thinking."),
    "anthropic.haiku55.disabled_effort": ("rejected", EFFORT, "Use effort high or below, or omit thinking."),
    "anthropic.sonnet55.between_tools_fields": ("rejected", None, "Send between_tools with no other thinking field."),
    "anthropic.haiku55.block_binding_disabled": ("rejected", None, "Use block_binding only with adaptive thinking."),
    "anthropic.between_tools_other_models": ("rejected", '"thinking.type.between_tools" is not supported for this '
                                             "model.", "Omit thinking and set output_config.effort."),
    "anthropic.forced_tool": ("rejected", 'tool_choice: type "tool" and "any" are not supported for this model.',
                              "Keep tool_choice auto, set strict: true, say in the prompt when the tool applies."),
    "anthropic.prefill": ("rejected", "This model does not support assistant message prefill. "
                          "The conversation must end with a user message.",
                          "End with a user turn; ask for the format in instructions or use structured outputs."),
    "anthropic.strict_schema": ("rejected", None, "Set additionalProperties: false on every object; "
                                "drop numeric and string-length constraints."),
    "anthropic.prefix_check": ("account-dependent", "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".',
                               "Keep system and tools fixed and only append; use a mid-conversation system message."),
    "openai.astra.effort_none": ("rejected", None, "Use effort low."),
    "openai.sol61.effort": ("unsupported", None, "Use effort low."),
    "openai.astra.sampling": ("unsupported", None, "Remove the fields."),
    "openai.solluna.sampling": ("unsupported", None, "Remove the fields, or use reasoning effort none."),
    "openai.astra.chat_tools": ("unsupported", None, "Use the Responses API for tool calling."),
    "openai.solluna.chat_tools": ("unsupported", None, "Use the Responses API, or reasoning_effort none."),
    "gemini.sampling": ("ignored", None, "Remove them; use explicit system instructions, thinkingLevel, a schema."),
    "gemini.penalties": ("rejected", None, "Remove them."),
    "gemini.minimal": ("rejected", None, "Use low (levels: low, medium, high)."),
    "gemini.budget_and_level": ("rejected", None, "Keep thinkingLevel only."),
    "gemini.prefill": ("rejected", None, "End with a user turn."),
}
THINKING = ("thinking", "redacted_thinking")
RESTRICTED = ("minimum", "maximum", "multipleOf", "minLength", "maxLength")  # not allowed in strict schemas


def get(d, path):
    for k in path.split("."):
        d = d.get(k) if isinstance(d, dict) else None
    return d


def schemas(s, where="$"):  # every sub-schema, with its location
    if isinstance(s, dict):
        yield where, s
        for k, sub in (s.get("properties") or {}).items():
            yield from schemas(sub, f"{where}.{k}")
        for sub in [s.get("items"), *s.get("anyOf", []), *s.get("allOf", []), *s.get("oneOf", []),
                    *(s.get("$defs") or {}).values(), *(s.get("definitions") or {}).values()]:
            yield from schemas(sub, where + ".*")


def key(v, message=False):  # cache_control and thinking blocks are outside the checked prefix
    strip = lambda x: ({k: strip(y) for k, y in x.items() if k != "cache_control"} if isinstance(x, dict) else
                       [strip(y) for y in x if not (isinstance(y, dict) and y.get("type") in THINKING)]
                       if isinstance(x, list) else x)
    v = strip(v)
    if message and isinstance(v.get("content"), str):
        v = {**v, "content": [{"type": "text", "text": v["content"]}]}
    return json.dumps(v, sort_keys=True)


def prefix(body, prev, add):
    msgs, old = body.get("messages", []), prev.get("messages", [])
    old_tools = sorted(key(t) for t in prev.get("tools") or [])
    tools = sorted(key(t) for t in body.get("tools") or [] if key(t) in old_tools or t.get("defer_loading") is not True)
    moved = " and ".join(([] if key(body.get("system")) == key(prev.get("system")) else ["system"])
                         + ([] if tools == old_tools else ["tools"]))  # any change here invalidates every block
    first = next((i for i, m in enumerate(old) if i >= len(msgs) or key(m, True) != key(msgs[i], True)), None)
    blocks = [(i, j, b.get("signature") or b.get("data")) for i, m in enumerate(msgs)
              for j, b in enumerate(m["content"] if isinstance(m.get("content"), list) else [])
              if isinstance(b, dict) and b.get("type") in THINKING]
    bad = [(i, j) for i, j, _ in blocks if moved or (first is not None and i > first)]
    order = [b.get("signature") or b.get("data") for m in old if isinstance(m.get("content"), list)
             for b in m["content"] if isinstance(b, dict) and b.get("type") in THINKING]
    order += [s for _, _, s in blocks if s not in order]
    at = [order.index(s) for _, _, s in blocks]
    gap = next((n for n in range(1, len(at)) if at[n] != at[n - 1] + 1), None)
    bad += [(i, j) for i, j, _ in blocks[gap:]] if gap else []  # kept blocks must be one unbroken run
    if bad:
        where = moved or (f"messages[{first}]" if first is not None else "a thinking block removed mid-run")
        add("anthropic.prefix_check", f"changed: {where}; invalid thinking blocks: "
            + ", ".join(f"messages.{i}.content.{j}" for i, j in sorted(set(bad)))
            + ". A 400 by default for accounts created on or after 31 August 2026; older accounts only"
              " when prefix_mismatch_behavior is set")


def claude(model, b, prev, add):
    if "temperature" in b and b["temperature"] != 1.0:
        add("anthropic.temperature", f"temperature = {b['temperature']}")
    if "top_p" in b and b["top_p"] != 0.99:
        add("anthropic.top_p", f"top_p = {b['top_p']}")
    if "temperature" in b and "top_p" in b:
        add("anthropic.temperature_with_top_p", "temperature and top_p are both set")
    if "top_k" in b:
        add("anthropic.top_k", "top_k is set")
    thinking = b.get("thinking") if isinstance(b.get("thinking"), dict) else {}
    kind, new = thinking.get("type"), model in (SONNET, HAIKU)
    effort = get(b, "output_config.effort") or ("high" if model == SONNET else "medium")
    if model == SONNET and kind == "disabled":
        add("anthropic.sonnet55.thinking_disabled", "thinking.type = disabled")
    elif new and kind == "enabled":
        add("anthropic.thinking_enabled", "thinking.type = enabled")
    elif not new and kind in ("disabled", "enabled"):
        add("anthropic.thinking_type", f"thinking.type = {kind}", kind)
    elif model != SONNET and kind == "between_tools":
        add("anthropic.between_tools_other_models", "thinking.type = between_tools")
    off = {(SONNET, "between_tools"): "anthropic.sonnet55.between_tools_effort",
           (HAIKU, "disabled"): "anthropic.haiku55.disabled_effort"}.get((model, kind))
    if off and effort in ("xhigh", "max"):
        add(off, f"thinking.type = {kind} with effort {effort}")
    if model == SONNET and kind == "between_tools" and (extra := [k for k in ("display", "budget_tokens",
                                                                              "block_binding") if k in thinking]):
        add("anthropic.sonnet55.between_tools_fields", "between_tools with " + ", ".join(extra))
    if model == HAIKU and kind == "disabled" and "block_binding" in thinking:
        add("anthropic.haiku55.block_binding_disabled", "block_binding with thinking disabled")
    if (choice := get(b, "tool_choice.type")) in ("any", "tool") and model != HAIKU:  # Haiku 5.5 accepts it
        add("anthropic.forced_tool", f"tool_choice.type = {choice}")
    msgs = b.get("messages") or []
    if msgs and isinstance(msgs[-1], dict) and msgs[-1].get("role") == "assistant":
        add("anthropic.prefill", "the last message is an assistant turn")
    strict = [(t.get("name"), t.get("input_schema")) for t in b.get("tools") or [] if t.get("strict") is True]
    problems = []
    for name, schema in strict + [("output_config.format", get(b, "output_config.format.schema"))]:
        for where, n in schemas(schema):
            is_object = n.get("type") == "object" or "object" in (n.get("type") or []) or "properties" in n
            if is_object and n.get("additionalProperties") is not False:
                problems.append(f"{name} {where} lacks additionalProperties: false")
            problems += [f"{name} {where} uses {k}" for k in RESTRICTED if k in n]
    if problems:
        add("anthropic.strict_schema", "; ".join(problems))
    if prev is not None:
        prefix(b, prev, add)


def openai(model, b, add):
    chat = "messages" in b  # Chat Completions; a Responses body uses "input"
    effort = b.get("reasoning_effort") if chat else get(b, "reasoning.effort")
    fam = "openai.solluna." if model in ("gpt-6-sol", "gpt-6-luna") else "openai.astra."  # Astra, 6.1 Sol
    above_none = fam == "openai.astra." or effort != "none"  # GPT-6 Sol and Luna default to medium
    if model == "gpt-6-astra" and effort == "none":
        add("openai.astra.effort_none", "reasoning effort none (HTTP 400)")
    if model == "gpt-6.1-sol" and effort in ("none", "minimal"):
        add("openai.sol61.effort", f"reasoning effort {effort}")
    fields = [f for f in ("temperature", "top_p", "top_logprobs", "logprobs") if f in b]
    fields += ["include logprobs"] if "message.output_text.logprobs" in (b.get("include") or []) else []
    if fields and above_none:
        add(fam + "sampling", ", ".join(fields) + f" with reasoning effort {effort or 'unset'}")
    if chat and "tools" in b and above_none:
        add(fam + "chat_tools", f"tools on Chat Completions with reasoning effort {effort or 'unset'}")


def gemini(b, add):
    gc = b.get("generationConfig") or {}
    tc = gc.get("thinkingConfig") or {}
    if hits := [k for k in ("temperature", "topP", "topK") if k in gc]:
        add("gemini.sampling", ", ".join(hits))
    if hits := [k for k in ("frequencyPenalty", "presencePenalty", "candidateCount") if k in gc]:
        add("gemini.penalties", ", ".join(hits))
    if tc.get("thinkingLevel") in ("minimal", "MINIMAL"):
        add("gemini.minimal", "thinkingLevel minimal")
    if "thinkingBudget" in tc and "thinkingLevel" in tc:
        add("gemini.budget_and_level", "thinkingBudget with thinkingLevel")
    turns = b.get("contents") or []
    if turns and isinstance(turns[-1], dict) and turns[-1].get("role") == "model":
        add("gemini.prefill", "the last turn has role model")


def check(model, body, previous=None):
    found = []

    def add(rule, matched, fill=None):
        outcome, error, instead = RULES[rule]
        error = error.format(fill) if error and fill else error
        found.append({"rule": rule, "outcome": outcome, "matched": matched,
                      "error": error or "no error text documented", "instead": instead})

    if model in ("claude-opus-5-5", SONNET, HAIKU, "claude-fable-5-1"):
        claude(model, body, previous, add)
    elif model in ("gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol"):
        openai(model, body, add)
    elif model in ("gemini-3.8-flash", "gemini-3.7-flash"):
        gemini(body, add)
    else:
        sys.exit(f"unknown model: {model}")
    return found


if __name__ == "__main__":
    ap = argparse.ArgumentParser(usage="python3 preflight.py MODEL BODY.json [--previous PREV.json]")
    ap.add_argument("model")
    ap.add_argument("body")
    ap.add_argument("--previous")
    args = ap.parse_args()
    previous = json.load(open(args.previous)) if args.previous else None
    findings = check(args.model, json.load(open(args.body)), previous)
    for f in findings:
        print(f"{f['outcome'].upper()}: {f['matched']}\n  error: {f['error']}\n  instead: {f['instead']}")
    print(f"{len(findings)} finding(s) for {args.model}; exit status {1 if findings else 0}")
    sys.exit(1 if findings else 0)

Here is an illustrative body.json: the order A-1001 request to Claude Opus 5.5 as a structured-output wrapper might build it, with temperature: 0, a forced lookup_order call and a reply prefilled with {.

{
  "model": "claude-opus-5-5",
  "max_tokens": 1024,
  "temperature": 0,
  "system": "You are a support assistant for Acme Cloud. Today is 2026-09-26.",
  "tools": [{
    "name": "lookup_order",
    "description": "Look up an order by ID.",
    "input_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
                     "required": ["order_id"], "additionalProperties": false}
  }],
  "tool_choice": {"type": "tool", "name": "lookup_order"},
  "messages": [
    {"role": "user", "content": "Where is order A-1001?"},
    {"role": "assistant", "content": "{"}
  ]
}

Running the script on it prints three findings and exits with status 1:

$ python3 preflight.py claude-opus-5-5 body.json
REJECTED: temperature = 0
  error: no error text documented
  instead: Omit it; guide the model in the prompt, or use structured outputs.
REJECTED: tool_choice.type = tool
  error: tool_choice: type "tool" and "any" are not supported for this model.
  instead: Keep tool_choice auto, set strict: true, say in the prompt when the tool applies.
REJECTED: the last message is an assistant turn
  error: This model does not support assistant message prefill. The conversation must end with a user message.
  instead: End with a user turn; ask for the format in instructions or use structured outputs.
3 finding(s) for claude-opus-5-5; exit status 1

For the edited-history check, pass two consecutive request bodies from your logs, the later one first: python3 preflight.py claude-fable-5-1 turn3.json --previous turn2.json. Log them after your last library or gateway, where the API receives them, so that edits made in transit are visible. The script reports what changed and which thinking blocks are now invalid. On our illustrative turn-2 and turn-3 bodies of the support conversation above, with the date in system re-rendered between them, it printed the following (the turn-3 body carries thinking blocks 1 and 2, at messages 1 and 3):

ACCOUNT-DEPENDENT: changed: system; invalid thinking blocks: messages.1.content.0, messages.3.content.0. A 400 by default for accounts created on or after 31 August 2026; older accounts only when prefix_mismatch_behavior is set
  error: 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".
  instead: Keep system and tools fixed and only append; use a mid-conversation system message.
1 finding(s) for claude-fable-5-1; exit status 1

The error: line is the documented opening only; the API's message usually adds the closing sentence that names what changed. On an older account, "only when prefix_mismatch_behavior is set" means that otherwise the request succeeds and the model reads both invalid blocks. The append-only version of the conversation, with a new cache_control marker, a changed effort and an appended system message, produced no findings and exit status 0.

A clean run means that none of these documented rules fired. It does not mean the API will accept the request: a trailing assistant message sent to GPT-6 Sol passes only because OpenAI documents nothing about it. Nor does it mean a tool call will happen, since tool_choice: auto with a strict tool still leaves that to the model. As its docstring says, the script does not check strict-schema size limits, anything Bedrock-specific, beta headers, model switches or Gemini's Interactions API. It has other limits too:

  • For Gemini it reads only camelCase keys (generationConfig, thinkingConfig, thinkingLevel), so a body written with snake_case keys such as generation_config produces no findings.
  • It does not verify signatures. An empty one on the last thinking block goes unreported, and on an earlier block it is reported as a removed block rather than as the separate signature error.
  • It cannot see a thinking block moved to a different position within its own message.
  • It does not flag a history from which every thinking block was removed. The API accepts that too, but the model loses its earlier reasoning, as the section on serialisers above describes.
  • It treats a message whose content is a plain string and the same text sent as a single text block as equal, which Anthropic does not document.
  • It does not model server-side compaction blocks or the Message Batches defaults.
  • It reads requests, not responses, so it cannot tell you that notes between tool calls have moved into empty thinking blocks.

Before you change the model ID

The order of work that follows from the documentation, as of 13 October 2026:

  1. Log the body your library or gateway actually sends to the API, and run the preflight script on it.
  2. Remove the sampling fields: temperature, top_p and top_k on Claude and Gemini 3.8 Flash (plus the penalty fields and candidate_count on Gemini), and temperature, top_p, top_logprobs and logprobs on GPT-6 and GPT-6.1 Sol unless you run GPT-6 Sol or Luna at effort none. On Claude, send neither temperature nor top_p rather than relying on their defaults.
  3. Replace switched-off thinking: omit thinking and lower effort on Opus 5.5 and Fable 5.1, send between_tools instead of disabled on Sonnet 5.5, keep disabled on Haiku 5.5, and in both cases keep effort at high or below. On GPT-6 Astra and GPT-6.1 Sol, use effort low instead of none.
  4. End every request with a user turn.
  5. Replace forced tool choice with tool_choice: auto, a strict schema and an explicit instruction, or with structured outputs. On Haiku 5.5 a forced tool still works, but without thinking. On Bedrock, where structured outputs are not available for Opus 5.5, Sonnet 5.5 or Haiku 5.5, validate the output yourself.
  6. Keep system and tools fixed and history append-only. Send every block back as received, including empty thinking blocks, make sure no layer moves system messages into top-level system, and use mid-conversation system messages and server-side trimming for the jobs that edits used to do.
  7. On Anthropic, send the thinking-binding-controls-2026-08-01 header and log input_transformations. model_binding_mismatch, prefix_binding_mismatch and organization_binding_mismatch entries are blocks the API dropped; on accounts created before 31 August 2026, thinking_mismatch_allowed entries are edits that users on newer accounts will get as 400s. To see the new-account behaviour directly, set prefix_mismatch_behavior: "error" on a test request.
  8. Handle the signature 400 by retrying once with drop_block, storing that choice with the session, and then fixing the edit that caused it. On Sonnet 5.5 and Haiku 5.5 that needs adaptive thinking.
  9. If your interface shows the model's notes between tool calls, set display: "updates" with its beta header on Opus 5.5, Sonnet 5.5 and Fable 5.1, and render each non-empty thinking block before its tool_use.
  10. Change gemini-3.7-flash to gemini-3.8-flash in code and tests, since requests to the old name already run on the new model.

The cells most likely to change next are the ones that say "not documented" (OpenAI's error text and outcomes, the Gemini API's handling of sampling fields, and the missing reasons) and Anthropic's enforcement of the prefix check for accounts created before 31 August 2026.