Pattern · last reviewed 2026-11-01
Pin the API version, not just the model
A model id is one of three version pins in a vendor integration, and the other two can move without anyone editing your code. The model id says which weights answer. The API version says what the request and response rules are. The SDK major says which of those versions your client sends, and for Anthropic's SDKs the vendor says plainly that the API version follows the major release. A Dependabot pull request that bumps an SDK major is therefore an API migration wearing a dependency label.
The pattern has four parts. Write the three pins down for each integration. Review an SDK-major bump as a migration: read the vendor's breaking-changes list for the version it selects, search your own code for each affected feature, and run the behaviour checks for any path that used a knob the new version removes. Set the API version yourself wherever your code builds the request. And where a proxy sits in between, say which pin you do not control and what you watch instead. The honest limit: behind a gateway, the API version is chosen by someone else.
Context & problem
The decision this page improves is a small one that arrives often: a bot opens a pull request that raises your LLM SDK from one major version to the next, the tests pass, and someone has to decide whether to merge it as routine upkeep or to slow down.
For most dependencies, "the tests pass" is a fair answer. For an LLM SDK it is a weak one, because the SDK is also the thing that decides what your requests say. On 2026-10-21 Anthropic released the first new Messages API version since 2023-06-01. Its migration guide states the coupling in one sentence: “if you use an SDK, your requests use the API version that was current when your SDK's major version was released.” The guide then lists, per language, the first SDK release that uses the new version: Python 2.0.0, TypeScript 1.0.0, Go 2.0.0, Java 3.0.0, C# 13.0.0, PHP 1.0.0 and Ruby 2.0.0.
So the bump that looks like maintenance changes the rules your traffic is judged by. Pinning a model id does not help here, because nothing about the id changed. That page is about what drifts under a fixed id: defaults, serving stack, tokenizer. This one is about the layer above it: the contract your client signs each time it sends a request, and who gets to rewrite that contract.
How a team learns that a vendor changed something, and how widely to search once it has, is covered in the vendor change triage workflow. This page starts after that: the change is known, and the question is where in your dependency graph it lands.
Forces
- Three pins, three owners. You choose the model id in config. The vendor chooses the API version, and publishes it on a versions page. A package manager chooses the SDK release, within the range you allowed. A lockfile records the third and says nothing about the second.
- Version bumps arrive as automation. The pull request has a changelog link and a green check. It does not have a review question attached to it unless you add one.
- A 400 is the loud half of a removal. The quieter half is the fix. When a request fails because a parameter is gone, the quickest repair is to delete the parameter, and that repair changes what the model does without producing any error at all.
- Platforms lag the vendor. A cloud marketplace that serves the same model may support the new version for some models only. One codebase can end up needing two versions at once.
- Not everything in a major bump is an API rule. Authentication headers, import paths, transitive package pairing and licences move in the same release, and none of them show up in a request diff.
The pattern
Four decisions, in the order they pay back.
- Write the three pins down, with an owner and a place to look. For each integration, record the model id, the API version your requests carry, and the SDK major (or "no SDK, raw HTTP"). Add one line for who or what moves each, and one for where you would see the change. The table below is the shape of it. The first row is the one most config files already record; the other two rarely appear anywhere.
- Review an SDK-major bump as a migration. Four questions, in order. Which API version does the new major select? What does the vendor's breaking-changes list say for that version? Which of those features does our code use, by search rather than by recollection? For each hit, what behaviour test runs before merge? A bump with zero hits can merge fast. A bump with hits needs the test, not just a green build.
- Set the API version yourself wherever you build the request. If your code calls the HTTP API directly, the header is yours. Keep the value in one named constant and change it in a commit that someone reviews on purpose. An SDK that picks the version for you is a convenience you opted into; it is not a reason to stop knowing the value.
- Treat removing a knob as a behaviour change, not a fix. A request that must stop sending a parameter needs an evaluation result in the same change, because "the 400 is gone" says nothing about whether the answers are still the ones you shipped.
The three pins
| Pin | What it fixes | What moves it | Where you see it move |
|---|---|---|---|
| Model id | Which weights answer | Your config, or a retirement you were notified of | A config diff; the vendor's lifecycle page |
| API version | Which request and response rules apply | The vendor adding a version; your header; or an SDK major that sends a newer one | The vendor's versions page; the anthropic-version header on the wire |
| SDK major | Which client code builds the request, and which version it sends by default | A range in package.json and a bot that raises it | A lockfile diff, which says nothing about the first two rows |
What one version bump carried
Is a version bump more than its headline change? For 2026-10-21 it is. The vendor's own migration guide carries at least eight distinct items that a version or SDK move drags in. The first is the famous one; the rest are the reason the review needs a list instead of a keyword search.
| Change in the bump | How it shows up | What the review has to ask |
|---|---|---|
temperature, top_p, top_k are rejected, even at their default values | A 400 on any request that sends one. The guide says a request that lowered temperature on a model that honoured it, such as Sonnet 4.6, loses that effect once the parameter is dropped, and that “No other setting replaces it.” | Who relied on low temperature for consistency, and what test would notice if their output became less stable? |
A document block placed directly in an assistant message is rejected | A 400 on replayed conversations that stored such a block | Does any stored history contain one? |
A text block with non-empty citations in a user message is rejected | A 400, including inside search-result blocks | Do we echo citations back in user turns? |
SDKs send the key as Authorization: Bearer instead of X-Api-Key | A proxy, gateway or log filter that reads the old header stops seeing the key. The API accepts either header, so the vendor does not complain. | What sits between us and the vendor, and which header does it read? |
| The TypeScript platform packages (Bedrock, Vertex, Foundry and others) must be on the same major as the core SDK | A package manager installs a second, older copy of the core SDK for the one you did not move | Is there one copy of the SDK in the tree after the bump, or two? |
The CLI's Go module path gains a /v2 suffix | The v1 path still resolves and silently installs v1, so a script can look upgraded and not be | Which tool versions do our scripts install, and do they print them? |
| On Amazon Bedrock the new version exists only for Claude 4.7 and later models | Calls to older Bedrock models must stay on the SDK version or header they use today | Does one codebase now need two versions? |
| The SDK and CLI licence moves from MIT to Apache 2.0 | Nothing in the code. A line in a dependency licence inventory goes stale. | Who tracks licences, and do they read version bumps? |
Only the first row is a model-behaviour question, and it is the one that needs an evaluation. The others are contract, plumbing, packaging and legal questions that a diff of your own request code would never raise. That is the argument for a written list over trusting the test run.
Reference implementation notes
Setting the version yourself
Anthropic's versions page says every request must carry an anthropic-version header, and that the client SDKs handle it for you. When you make the call yourself, the header is yours to pin. The shape is small:
// one constant, changed only by a reviewed commit
const ANTHROPIC_VERSION = '2026-10-21';
// ISO dates sort as strings; from 2026-10-21 the sampling knobs are rejected on every model
function checkBodyFitsVersion(aVersion, aBody) {
if (aVersion < '2026-10-21') return;
for (const k of ['temperature', 'top_p', 'top_k']) {
if (k in aBody) throw new Error(`${k} is rejected from API version 2026-10-21`);
}
}
async function callMessages(aKey, aBody) {
checkBodyFitsVersion(ANTHROPIC_VERSION, aBody);
return fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: { 'x-api-key': aKey, 'anthropic-version': ANTHROPIC_VERSION, 'content-type': 'application/json' },
body: JSON.stringify(aBody),
});
}
The check is our own design, not a vendor feature. It turns a vendor 400 into a failure in your own tests, before a request leaves the building. The header names and the endpoint are the ones in the vendor's migration guide example. The same guide says that if a request relies on a non-default sampling value, you drop the parameter or hold that route on 2023-06-01, which is the second reason to hold the version in one place: a mixed fleet is a per-route value, not a global one.
The pages we read give no retirement date for 2023-06-01. Do not read the absence as permanence. Check the versions page before you plan on staying.
The pull-request review rule
Put the four review questions in the pull request template for dependency bumps that touch an LLM client, or in the bot's grouping rule so those bumps never merge alone. The useful output is a short comment on the pull request: the version the new major selects, the features searched for, the hits, and the test that ran for each hit. A bump with no hits says so in one line. That comment is the artefact; without it the pin lives in somebody's head.
Platforms in the middle
A cloud marketplace, a gateway or an aggregator changes who holds each pin. If your calls go through one, find out whether you send the version header at all, whether the intermediary sends one upstream, and whether it rewrites authentication headers. If the vendor's documentation for that intermediary does not say, write "unverified" in the pin table instead of leaving the cell blank. A blank cell reads as "nothing to know".
Trade-offs
- Pinning delays good changes too. A version you hold is a version whose fixes and new features you do not get. The vendor recommends the latest version; pinning is a choice to upgrade on your schedule, and the schedule needs an owner or it quietly becomes never.
- The review is manual and hit-or-miss. A search for affected features finds what you remembered to search for. The breaking-changes list is the control; the search is only as complete as the list is.
- Explicit headers duplicate the SDK. Setting the version yourself on a path where an SDK also sets it is two sources of truth. Pick one per call path.
- Mixed fleets cost a test matrix. Two versions at once means every shared code path is tested against both, or the ones that differ are named.
- Evaluations cost real calls. A behaviour check for a removed knob runs against the live model. That is the point, and it is also a spend line.
When not to use this
When a proxy owns the version. If you talk to the model through a gateway that translates your request into the vendor's, you cannot pin the version the vendor sees. Pin what you control, which is the model identifier the proxy accepts and the request fields you send, and watch the proxy's own changelog for the day it changes upstream. Trying to apply this pattern in full there would give you a constant that nothing reads.
When one request shape is all you will ever send. A single call with no optional features, no stored history and no sampling settings has little to migrate. A written pin table is still cheap, but the review ritual is not worth running per bump.
When the problem is a retired model, not a changed contract. If the thing that will break you is a retirement date, the model deprecations page and the model-pin pattern are the right tools. Version pinning does nothing for an id that stops answering.
When you have no way to run a behaviour check. The pattern's last step needs an evaluation. Without one, you are choosing between merging blind and never upgrading, and a pin table does not change that. Build the check first. If the open question is which model to measure on, how to choose between Opus, Sonnet and Haiku is the place to start.
As-built: what this platform runs
We do not run this pattern, and the reason is instructive. Our model calls do not go to Anthropic directly. endpoint() at src/lib/llm.ts:205 returns either Cloudflare AI Gateway's OpenRouter passthrough or OpenRouter's chat-completions URL, and headers() at src/lib/llm.ts:213 builds an OpenRouter request: a bearer token, a content type, two OpenRouter attribution headers and, on the gateway path, cf-aig-* headers. There is no anthropic-version header, and src/CLAUDE.md says so in its model-ids section. Searching package.json and package-lock.json for "anthropic" on 2026-11-01 found no match, so there is no Anthropic SDK in the tree at all. With no SDK, the SDK pin does not exist for us, and we set no API version ourselves. The model id is the one we hold: wireModel() in src/lib/llm.ts maps our canonical ids to OpenRouter slugs.
The API version still exists upstream; it belongs to OpenRouter. We read OpenRouter's parameters page on 2026-11-01: it says that when a sampling parameter is omitted OpenRouter does not insert a value, and it does not name an Anthropic API version. We also searched the first 100,000 characters of its llms-full.txt documentation dump for the header and for any Anthropic-version statement, and found none; that file is far longer, so this is not a full search. What version OpenRouter sends upstream is therefore unverified. We do not know when it will change, and the pattern's answer for that case is the one in the when-not section: watch the intermediary.
The 2026-10-21 change was a no-op for us, and only because of what our code never does. Nothing under src/ sends temperature, top_p or top_k. The one guard against someone adding them is a comment at src/lib/llm.ts:72-74 that says not to. A search of test/ on 2026-11-01 found no test that mentions any of the three. A comment is a reminder to a person, which is the same weakness the cost-meter page found in comments in src/lib/cost.ts, and the check sketched above would close it.
We do see the coupled-bump shape in our own dependency graph, one layer over. Three Dependabot pull requests (#98, #99 and #105, opened between 2026-10-13 and 2026-10-20 and since closed) were each titled as a bump of an MCP package together with agents, the Cloudflare Agents SDK our coach runs on. We read their titles, not their diffs, so this is a pointer to the shape and not a measurement of what changed.
Changelog
- 2026-11-01 — Initial publication. Anthropic's versions page, migration guide for 2026-10-21 and model-deprecations page read 2026-11-01. OpenRouter's parameters page read the same day. As-built claims verified against the tree the same day.
- The
anthropic-versionheader requirement; the version history (2026-10-21,2023-06-01,2023-01-01); the removal oftemperature,top_pandtop_kon every model; the Bedrock availability limit; the document-block and citations rejections: Anthropic, Versions, fetched 2026-11-01. - The sentence tying SDK major to API version; the per-language first versions; the Bearer header change; the TypeScript platform-package pairing; the CLI Go path behaviour; "no other setting replaces it"; the keep-on-
2023-06-01option; the MIT to Apache 2.0 licence change; the curl example with the header names: Anthropic, Migrating to API version 2026-10-21, fetched 2026-11-01. - The API-parameter-deprecation entry and its statement that a new API version can remove a parameter for every model: Anthropic, Model deprecations, fetched 2026-11-01.
- That OpenRouter does not insert a value for an omitted sampling parameter, and that its parameters page names no Anthropic API version: OpenRouter, Parameters, fetched 2026-11-01. The absence of a statement is the finding; we did not read OpenRouter's whole documentation set.
- Every claim about our code in the as-built section rests on this platform's own tree, read at publication:
src/lib/llm.ts,src/CLAUDE.md,package.json, the lockfile andtest/. The Dependabot pull request numbers, titles, dates and states are from the repository's pull request list on the same day. - The review rule and the version check are our own design, and the three-pin split is our framing, drawn from the 2026-10-21 change set, not a vendor recommendation. We do not run them; see the as-built section.
Version numbers, SDK releases and platform support change. Re-read the cited pages before relying on a number. Corrections: hello@aiarch.dev.
Learn to keep a model integration upgradeable.
aiArch teaches model routing, vendor-change handling and the operational controls that keep an LLM product stable under change — by building, for senior engineers moving into AI.
See how aiArch helps senior engineers become AI-native, or compare Professional Membership pricing.
Free with an account: Track 0 (The Mindset Shift) and the Track 0 coach allowance, no card · every claim cited · full curriculum with membership