On a Sunday night I was going through the small scripts my indie-developer backend uses to call the API, and temperature=0.2 turned up far more often than I expected.
I wrote every one of those lines. Yet for about half of them I couldn't remember which script wanted 0.2, or why.
What sent me looking was the official migration page for API version 2026-10-21. It says that sending temperature, top_p, or top_k returns a 400, even when the value equals the default.
What I'd like to say up front is that you hit this when you upgrade the SDK, not when you change the model. You can leave the model ID untouched and still be stopped by the next deploy after a dependency bump.
What changes (as far as the migration page confirms)
The page gives no cutover date and no deadline. It names the new version string, 2026-10-21, and the previous one, 2023-06-01. With no date, the question isn't "when will it break" but "when do I choose to switch."
There are two ways the switch happens. With an SDK, the API version is tied to the SDK's major version; for Python, upgrading anthropic to 2.0.0 is what starts sending 2026-10-21. Without an SDK, it happens when you change the anthropic-version header yourself.
Three things in the Messages API now return a 400.
| What you send | Behavior in 2026-10-21 | Fix |
|---|---|---|
temperature / top_p / top_k | 400 even at the default (token counting and batches too) | Remove the parameter |
A document block directly in an assistant message | 400 | Move it into a user message |
A non-empty citations array on a user text block | 400 | Drop the citations; assistant-side citations and citations.enabled on documents still work |
One more point on sampling. Claude 4.7 and later models already accept only the defaults, so calls to those behave the same and you just delete the argument. The place to be careful is a model that did accept other values (the page's example is Sonnet 4.6) where you had lowered temperature. Delete the argument and it runs at the default of 1.0, so nothing errors, but the spread of the output changes.
Drawing a line before searching
At first I assumed "delete them all" was the whole job, and the result wasn't great. Sorting which lines are safe to delete from which lines should stay on the old version took longer than the deleting itself.
This is the line I use now.
- Calls to 4.7 or later: delete the argument (behavior doesn't change)
- Calls to 4.6 or earlier that don't depend on output variance: delete it, then compare a few outputs
- Calls to 4.6 or earlier that depend on
temperaturenear 0: keep that one request on2023-06-01, or pin the output shape in the prompt
The third follows the page, which says that if a request depends on a non-default value, you should stop sending the parameter or keep that request on 2023-06-01.
A scanner to find them
Having a machine list candidates and a person judge them worked better than eyeballing grep output. I turned the items on the migration page that are easy to spot in code into regular expressions. It isn't a parser, so comments and variable names will give false positives; I accepted that to miss fewer real ones.
#!/usr/bin/env python3
"""Rough scanner for code that may return a 400 or change behavior on API version 2026-10-21.
Usage: python3 scan_20261021.py <dir> [<dir> ...] exits 1 if any candidate is found"""
import re, sys, pathlib
EXTS = {".py", ".ts", ".tsx", ".js", ".mjs", ".rb", ".go", ".java", ".json", ".yaml", ".yml", ".sh"}
SKIP = {"node_modules", ".git", ".venv", "dist", "build", ".next"}
RULES = [
# (id, regex, what to do next)
("sampling", r"""["']?\b(temperature|top_p|top_k)\b["']?\s*[:=]""", "remove it; if you rely on a non-default value, keep only that request on the old version"),
("version", r"anthropic-version.{0,20}2023-06-01", "change the header to 2026-10-21 together with the fixes"),
("auth-hdr", r"""x-api-key""", "new SDKs send Authorization: Bearer; check what your proxy reads"),
("msgparam", r"\bMessageParam\(", "use a dict literal or a typed assignment"),
("compaction", r"\bcompactionControl\b", "replace with context_management compaction"),
("completions",r"\bcompletions\.create\b", "Text Completions was removed; use the Messages API"),
("assist-doc", r"""["']role["']\s*:\s*["']assistant["'].{0,200}["']type["']\s*:\s*["']document["']""", "move document blocks to a user message"),
]
def scan(root: pathlib.Path):
hits = []
for p in root.rglob("*"):
if not p.is_file() or p.suffix not in EXTS or SKIP & set(p.parts):
continue
try:
lines = p.read_text(encoding="utf-8", errors="ignore").splitlines()
except OSError:
continue
for no, line in enumerate(lines, 1):
for rid, rx, todo in RULES:
if re.search(rx, line, re.I if rid == "auth-hdr" else 0):
hits.append((str(p), no, rid, line.strip()[:90], todo))
return hits
if __name__ == "__main__":
roots = [pathlib.Path(a) for a in sys.argv[1:]] or [pathlib.Path(".")]
found = [h for r in roots for h in scan(r)]
for path, no, rid, text, todo in found:
print(f"{path}:{no} [{rid}] {text}\n -> {todo}")
print(f"\n{len(found)} candidates")
sys.exit(1 if found else 0)The assist-doc rule only catches a dict literal on a single line. Messages wrapped across several lines will slip through, so check anywhere you use document by eye as well.
The run on a tiny sample
I put three files in a folder: a Python file passing temperature=0.2, a TypeScript file with top_p, and a file that merely contains the word temperature. Then I ran the scanner.
t/app.py:6 [sampling] temperature=0.2,
-> remove it; if you rely on a non-default value, keep only that request on the old version
t/app.py:9 [msgparam] m = anthropic.types.MessageParam(role="user", content="x")
-> use a dict literal or a typed assignment
t/proxy.ts:1 [auth-hdr] const key = req.headers["x-api-key"];
-> new SDKs send Authorization: Bearer; check what your proxy reads
t/proxy.ts:2 [sampling] const body = { model: "m", top_p: 0.9, messages: [] };
-> remove it; if you rely on a non-default value, keep only that request on the old version
4 candidatesThe file with only the word was not flagged, and the exit code was 1. Four hits, as expected.
The auth-hdr rule is the easy one to miss. New SDKs send the API key as Authorization: Bearer rather than X-Api-Key. If a proxy or gateway of your own validates the key by reading X-Api-Key, it may stop requests at authentication, before the sampling 400 is ever reached. If you run a thin relay in front of your backend, check that before you bump the SDK.
The order I fix things in
Fixing everything at once leaves you unsure what helped. I go in this order.
- Run the scanner and count
- Sort lines to keep from lines to delete, using the line above
- Delete the arguments while still on the old SDK and commit (the change is safe on both versions)
- Upgrade the SDK last; the migration page also says to release the upgrade together with the fixes, so I ship steps 3 and 4 the same day
Putting step 3 in first means that if a 400 shows up on upgrade day, you can rule out sampling arguments as the cause.
A few other SDK changes are worth jotting down. Calling MessageParam(...) as a class raises TypeError. Passing two credentials to one client, such as api_key and auth_token, raises ValueError. The CLI's --api-key and --auth-token flags are gone.
I covered how to choose temperature in another article, and the 400s that come with swapping model IDs in the scanner for Sonnet 4.5's retirement. The two scanners look for different things, so running both doesn't double up.
Next step
Run this scanner once on your own repository. If the count is 0, all that's left is choosing the day to upgrade. If it isn't, that number is the size of the job before the switch.
Before you pick the day to switch, count where your code leans on those arguments—that's a line I intend to keep for the next version too.
Sources: API version 2026-10-21 migration