We moved an LLM backend from Claude Sonnet 4.6 to Opus 4.8. The model is configurable through a single environment variable, so this should have been a one-line change. Instead, every call started returning HTTP 400.
The error, once we read the full response body:
anthropic.BadRequestError: Error code: 400 - {'type': 'error', 'error':
{'type': 'invalid_request_error',
'message': '`temperature` is deprecated for this model.'}}
Opus 4.8 no longer accepts the temperature parameter. Send it, even temperature=0.2, and the API rejects the whole request. Our code passed temperature on every call (a habit, for slightly more deterministic synthesis), so the model swap broke everything until we stopped sending it.
A guard that keeps the swap reversible
You could just delete temperature everywhere. But we wanted to keep the ability to flip back to an older model without code changes, so we added a tiny per-model guard that omits the parameter only for models that reject it:
def temp(value: float, model: str) -> dict:
# Opus 4.8 deprecated temperature (API 400 if sent); omit it for that model.
return {} if "opus-4-8" in model else {"temperature": value}
client.messages.create(
model=model,
messages=[...],
max_tokens=4096,
**temp(0.2, model), # {} for Opus 4.8, {"temperature": 0.2} otherwise
)
Now the model is genuinely a config value again: claude-opus-4-8 or claude-sonnet-4-6, no code change either way.
Takeaways
- Model migrations aren’t always drop-in. A newer model can deprecate parameters, and the result is a 400 that has nothing to do with your prompt or your tokens, easy to chase in the wrong direction.
- Log the full Anthropic error body, not just the status code. The status was a generic
400; the message in the payload (temperature is deprecated for this model) is what pointed straight at the fix. - Make model selection a capability-guarded config value. A small helper that strips unsupported params per model turns a risky migration into a reversible toggle.