If Anthropic API calls start failing with temperature: Extra inputs are not permitted after you upgrade @anthropic-ai/sdk to 1.x, the cause is the API version the SDK now sends. SDK 1.0 moved to API version 2026-10-21, under which our requests with temperature were rejected. You can either stay on SDK 0.x and the old API version, or upgrade and stop sending temperature.
This is how we found it in Drift-CI, why our tests didn't, and which option we picked.
The error: temperature: Extra inputs are not permitted
Every request came back as a 400 with this body:
400 {"type":"error","error":{"type":"invalid_request_error","message":"temperature: Extra inputs are not permitted"}}The wording matters: it doesn't say the value 0 is invalid. It says temperature is an extra input, so the request is rejected for including the parameter at all.
What happened
Drift-CI's core package depended on @anthropic-ai/sdk ^0.104.2. Dependabot opened a pull request proposing @anthropic-ai/sdk 1.3.0, and the lockfile resolved to 1.6.0.
The SDK 1.0.0 changelog lists a single breaking change: "api: add support for new API version 2026-10-21". Per the changelog, the client now sends the header anthropic-version: 2026-10-21.
A one-line changelog entry about an API version is easy to wave through. The types compiled. All 587 unit tests passed. On paper, the upgrade was clean.
Why the unit tests passed
Drift-CI's tests mock the SDK, as most test suites do. That's the right call for unit tests: they should be fast, free and independent of the network. But it means the tests verify what our code sends, not what the API accepts.
Nothing in our code changed. We still built the same request with the same parameters. What changed was the contract on the other side: the same request under a new API version was no longer valid. A mock can't know that, because the mock was written against the old contract.
This is the general trap with SDK major versions for hosted APIs. The breaking change isn't always in the SDK's types or method signatures, where a compiler would catch it. Sometimes it's in the API version the SDK negotiates, and that only shows up when you make a real call.
How a live eval run caught it
Drift-CI is an eval tool, so we have a suite that makes real model calls: the dogfood repo's support-ticket triage suite, with eight cases, pinned to claude-sonnet-4-6. To test the upgrade against the real API, we opened a test pull request in the dogfood repo and pinned the Drift-CI GitHub Action to the exact commit of the upgrade branch instead of @v1:
- id: drift
uses: Drift-CI/drift-ci@29a7666813ab9c7647777c1caf4463e5db8c417f
with:
provider: anthropic
api-key: ${{ secrets.ANTHROPIC_API_KEY }}All eight cases came back as evaluator errors, each in about 100 to 210 ms, far too fast for a real completion. Drift-CI reports a rejected request like this as an error rather than a score, so no case was counted as a regression. That's the correct behaviour for a broken client, but it meant the PR comment showed what failed and not why.
To see the actual error, we added a step that prints the messages from the JUnit report the action writes:
- if: always()
run: grep -o 'message="[^"]*"' "${{ steps.drift.outputs.junit-path }}" | head -5 || cat "$RUNNER_TEMP/drift-junit.xml" | head -60That printed the 400 shown above.
A gotcha along the way: GitHub Actions doesn't accept a short commit SHA in uses:. You need the full 40-character SHA.
Why it showed up as errors, not regressions
It's worth spelling out why eight failed requests didn't produce eight red regressions, because the distinction is deliberate.
Drift-CI classifies every failed provider call. Rate limits, timeouts, 5xx responses and network failures are transient: they say nothing about your prompt, so they get their own statuses, and a run with too many of them aborts. Authentication failures get their own status too. Anything else, including a 400 for an invalid request, is reported as an error for that case. An errored case has no score, so it's left out of the comparison with its baseline entirely.
The alternative would have been worse. If a rejected request counted as a score of zero, every case would have dropped from its baseline at once, and the PR comment would have described a catastrophic change in model behaviour. Someone reading it might have gone looking for a prompt problem. As errors, they pointed straight at the client: the model never answered, so there was no behaviour to compare.
The cost of that choice is that the PR comment alone doesn't tell you what the error was. That's why the JUnit step above is worth keeping for debugging.
The fix options and the trade-offs
Drift-CI sends temperature: 0 on purpose. Eval scores need to be as repeatable as possible, and temperature 0 is the main lever for that on models that accept it. Drift-CI already skipped temperature for Claude 4.7-generation and newer models, which reject sampling parameters outright. Under the new API version, requests to claude-sonnet-4-6, which accepts temperature on the old version, were rejected too.
We had two options:
(a) Stay on SDK 0.x and the old API version. Nothing changes for users. Scores stay as repeatable as before, and existing baselines stay valid. The cost is staying on an older SDK line.
(b) Upgrade and stop sending temperature. We'd be on the current SDK, but older models that accept temperature would run at their default sampling. That means noisier scores on those models, and possibly a one-time shift in scores that would show up against users' committed baselines, without any change on their side.
For a regression-testing tool, (b) is the worse trade. Noisier scores mean more false regressions, and a baseline shift caused by an internal dependency bump is exactly the kind of unexplained change Drift-CI exists to prevent.
What we did
We chose (a). Drift-CI stays on @anthropic-ai/sdk 0.x, and our Dependabot config now ignores @anthropic-ai/sdk and @anthropic-ai/bedrock-sdk from version 1.0.0. We'll revisit when the old API version gets a retirement date.
The Dependabot rule is a small thing, but it matters. Without it, the same upgrade PR keeps coming back, and sooner or later someone merges it because the tests are green.
How to protect yourself
The lesson isn't specific to Anthropic. Any SDK major for a hosted model API can change behaviour that mocked unit tests can't see.
- Run a live eval suite on dependency-bump PRs. Dependabot PRs can't read your Actions secrets, so either add the provider key under Settings → Secrets and variables → Dependabot, or push the bump to a branch of your own and open the PR yourself, as we did. A broken client then shows up as a wall of errors before you merge. The GitHub Actions tutorial sets up the workflow.
- Pin the tool under test to a full commit SHA when you need to test an unreleased change against a real workload.
- Make errors readable. An eval tool should keep "the request failed" apart from "the model got worse". When everything errors, read the underlying message; here, the JUnit report had it.
- Treat API versions as part of the model. A new API version can change what a model accepts as much as a new model can. Testing an LLM model upgrade covers running the same suite before and after a switch.
- Keep your regression baselines honest. If you do change sampling parameters, expect score shifts and accept new baselines deliberately. The LLM regression testing guide covers baselines and thresholds.