Auto-following to the newest Opus moved the failure rather than removing it.
The deploy's own smoke output showed it: 404 "model not found" became
HTTP 400: `temperature` is deprecated for this model.
Anthropic removed temperature/top_p/top_k on the current generation (Opus 5,
Sonnet 5, Opus 4.7/4.8, Fable 5), and the adapter sends temperature: 0
unconditionally. So the gate's prompt-selection was still degrading on every
call -- just with a different status code.
A list of which models accept sampling would rot exactly the way the pinned
model ids did, which is the whole thing this branch is trying to stop. So the
provider learns it instead: the first 400 naming a sampling parameter drops it
and retries, and remembers the model so every later call omits it up front.
One wasted call, once, rather than a hardcoded table to maintain.
The match is deliberately narrow -- a 400 must actually name temperature/top_p/
top_k. An unrelated 400 (missing max_tokens, bad schema) propagates untouched;
there is a test for that, because a broad match here would silently swallow
real request errors and retry them pointlessly.
Verified live: claude-opus-latest -> claude-opus-5, first attempt 400 on
temperature, retry succeeds past it. The retry then hit HTTP 429 -- the
personal OAuth token's rate limit, which is the pre-existing credential-tiering
issue, not this path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GqMidYEGUJG5fxeoTELBu2
`claude-opus-4-20250514` was pinned as the heavy provider and had been
returning 404 on every gate ranking and every pagination title. The only
visible symptom was a fallback that looked like an ordinary one -- it surfaced
here because the degradation notice added earlier in this branch finally
printed the reason.
Checked against GET /v1/models: BOTH pins were dead. The fast tier's
claude-haiku-3-5-20241022 is gone too, so that tier had been silently 404ing
as well.
The provider's listModels() asserted "Anthropic doesn't have a models listing
endpoint" and returned four hardcoded dated ids. That endpoint does exist and
answers fine with the OAuth token this deployment uses; the hardcoded list was
simply stale, and a test pinned it in place.
So: `claude-<family>-latest` (or a bare `opus` / `sonnet` / `haiku` / `fable`)
now resolves against the live list. Exact ids pass through untouched, so
pinning still works when someone wants it.
Newest is decided by `created_at`, never by parsing the version out of the id.
That is not incidental: `claude-opus-4-5` sorts ABOVE `claude-opus-5` as a
string, and "4-5" parses as a larger minor than "5". There is a test for
exactly that trap.
Resolution is cached (12h, MCPCTL_ANTHROPIC_MODEL_TTL_MS) so it is not a
per-call network hop, and shared across instances since the model list is
account-wide. When the endpoint is unreachable it falls back to a pinned
known-good id per family and says so on stderr -- the map going stale can then
only cost availability, never correctness.
The constructor default was `claude-sonnet-4-20250514`, also retired; it now
tracks the family too.
Local config: heavy -> claude-opus-latest, fast -> claude-haiku-latest.
Verified live: opus-latest -> claude-opus-5, haiku-latest ->
claude-haiku-4-5-20251001, sonnet-latest -> claude-sonnet-5, and an exact id
passes through.
Also found while there: both anthropic entries were named "anthropic", and the
registry keys by name, so the second silently OVERWROTE the first and one
tier's model was discarded entirely. Renamed to anthropic-fast /
anthropic-heavy so NamedProvider keeps them distinct and the tier split is
real.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GqMidYEGUJG5fxeoTELBu2