Vertex/Bedrock: /model picker collapses the Opus family to a single row because 2.1.211 seeds ANTHROPIC_DEFAULT_OPUS_MODEL itself
Summary
Since 2.1.211, on Vertex and Bedrock the /model picker shows only one Opus row — labelled <model-id> · Custom Opus model — instead of the full Opus catalogue. Newer Opus models cannot be selected at all.
The user has not set ANTHROPIC_DEFAULT_OPUS_MODEL. Claude Code sets it during startup, and the picker then mistakes its own value for a deliberate user pin.
This is not the (intended) behaviour where setting ANTHROPIC_DEFAULT_OPUS_MODEL yourself collapses the family to one row. The bug is that the pin is manufactured on the user's behalf and is then indistinguishable from a real one — even though the code already records a sentinel saying it wasn't.
The failure is self-reinforcing: the only selectable Opus is the pinned one, and selecting it re-saves it as the default, which re-seeds the same pin next launch. Once you land on an older Opus you cannot reach a newer one through the UI.
Edit 2026-07-28. The trigger is broader than first described, and the original workaround was wrong. Any concreteclaude-opus-*ID reaching startup seeds the pin — including one passed explicitly as--model claude-opus-5. Only a non-concrete value (theopusfamily alias) escapes. Worse, whether a given concrete ID seeds depends on the fetchedavailableModelsallowlist, so the same binary can flip from working to broken with no version change — which is what happened here. The Root cause and Workaround sections below have been corrected in place; the follow-up comment records what changed.
Environment
- Claude Code 2.1.220 (also reproduced on 2.1.211–2.1.216)
CLAUDE_CODE_USE_VERTEX=1,CLOUD_ML_REGION=global- macOS 15 (Darwin 25.3.0)
- No
ANTHROPIC_DEFAULT_OPUS_MODELin any shell rc file,settings.json, or the tmux environment
Steps to reproduce
- Run Claude Code against Vertex or Bedrock.
- Ensure
ANTHROPIC_DEFAULT_OPUS_MODELis unset — in your shell, yoursettings.json, and (if applicable) your tmux server environment. - Open
/modeland set any specific Opus version as the default (a session-only change is not persisted and will not reproduce this), so thatsettings.jsonrecords a concrete model ID rather than theopusfamily alias. Selecting the newest Opus is sufficient — this is not about picking an old model. In the run below the selection was Opus 5 (1M context), giving"model": "claude-opus-5[1m]". - Restart Claude Code with no
--modelflag. - Open
/model.
Expected: the full Opus catalogue (4.1, 4.6, 4.7, 4.8, 5, plus 1M variants), as in 2.1.210.
Actual: a single row, claude-opus-5[1m] · Custom Opus model. Every other Opus version is gone, including the plain Opus 5 row.
Inspecting the environment of the Claude Code process (a shell-level printenv will not show it — the value is written into process.env at startup, so check from a child of that process) shows:
ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-5[1m]
CLAUDE_CODE_3P_PROBE_WROTE_OPUS_DEFAULT=claude-opus-5[1m]
Neither was set by the user. Selecting the sole remaining row re-saves it as the default, which re-seeds the same pin on the next launch.
Setting "model": "opus" (the family alias) and restarting restores the full catalogue, because an alias carries no concrete version for the seed to write.
Bisection
| Version | Picker |
|---------|--------|
| 2.1.206 | Full catalogue |
| 2.1.209 | Full catalogue |
| 2.1.210 | Full catalogue |
| 2.1.211 | Collapsed to one row |
| 2.1.220 | Collapsed to one row |
Each build was downloaded and launched directly, with only the version changed. 2.1.210 is the last good build.
Root cause
From the minified bundle of 2.1.220. (Mangled identifiers are build-specific; the behaviour is what matters.)
1. Startup seeds the variable. A function added in 2.1.211 in the Vertex/Bedrock startup-probe module (apply3PDefaultFallbacks) resolves a model and writes it into process.env:
async function YUs(e){ // apply3PDefaultFallbacks
HHo();
let t = e?.pendingUserModel?.trim(),
n = gYn(t==="default"||t==="inherit"||t===""?void 0:t??void 0),
o = n!=null&&n!=="" ? n // --model resolved → seed THAT
: (t!==void 0 && n==null ? void 0 // --model unresolvable → seed nothing
: h9() ?? void 0); // nothing passed → ambient default
switch(xn()){
case"vertex": return { lines: await Guy(o), hasHardFailure:!1 } // → seedEnvDefaultForUserPin(o)
…
The first branch is the important one, and it is why passing --model explicitly is not an escape. The resolver returns an available concrete ID unchanged:
function gYn(e){ if(e && !Hl(e)) return ZW(e) ?? void 0; return e } // Hl = isAvailable, ZW = alias lookup
So --model claude-opus-5 sets o = "claude-opus-5" and seeds exactly as the ambient default would. The "explicit but unresolved" branch is only reached when the resolver returns null — i.e. for a value that is neither an available concrete ID nor a resolvable alias.
This also makes the bug non-deterministic across environments with an identical binary: Hl() tests the fetched availableModels allowlist. While claude-opus-5 was absent from that list, Hl() was false, ZW() returned null (it only resolves aliases), nothing was seeded, and --model claude-opus-5 appeared to be a valid workaround. Once Opus 5 entered the list, the same command on the same 2.1.220 build began seeding the pin. Anyone bisecting this should be aware that the version table alone does not determine the behaviour.
The write is accompanied by a sentinel, CLAUDE_CODE_3P_PROBE_WROTE_OPUS_DEFAULT, whose entire purpose is to record that the probe wrote the value rather than the user.
1b. Only aliases are exempt, and only incidentally. The seeder bails early because its canonicaliser cannot map a family alias to a registry key — not because of which branch fed it:
function Ouy(e){ // seedEnvDefaultForUserPin
let t=e?.trim(); if(!t) return;
if(Z.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST) return;
let r=UUs(t); if(!r) return; // ← alias ⇒ undefined ⇒ no seed
…
}
function UUs(e){ let t=JM(e); for(let r of nun) if(JM(Ul[r].firstParty)===t) return r; return }
UUs("opus") matches no first-party ID and returns undefined. That single early return is the entire reason the opus alias works, which is a fragile thing for the only workaround to rest on.
2. The picker ignores the sentinel. The Opus row-builder is unchanged from 2.1.210 and gates purely on truthiness:
function GBc(){
let e = Z.ANTHROPIC_DEFAULT_OPUS_MODEL;
if(xXn() && e){ // isThirdParty && env var set
let t = Wb(e);
return { value:"opus",
label: Z.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME ?? e,
description: … ?? `Custom Opus model${t?" (1M context)":""}` }
}
} // undefined ⇒ catalogue rows render
Returning a row here replaces the catalogue builders for Opus 4.1/4.6/4.7/4.8/5.
3. The means to tell them apart already exists, and is used by the probe module — just not by the picker:
function tme(e){ // "was this probe-written?"
let t = Z.ANTHROPIC_DEFAULT_OPUS_MODEL,
r = Z.CLAUDE_CODE_3P_PROBE_WROTE_OPUS_DEFAULT;
return t!==void 0 && t===r;
}
This appears to be a side effect of the 2.1.211 entry: "Fixed Claude Code on Vertex and Bedrock attempting the default Opus model at startup and printing a spurious fallback notice when a model is explicitly configured." Suppressing the redundant probe is reasonable; treating the ambient default as "explicitly configured", and writing it into a variable the picker reads as a user override, is what breaks.
Sonnet and Haiku are unaffected here only because those variables were genuinely user-set, which is the intended per-family pin.
Suggested fix
Either would do:
- In the picker — gate the custom-row branch on the value not being probe-written, i.e. consult the existing sentinel rather than testing truthiness alone. Note that sentinel-aware helpers already exist and are used by neighbouring code (
tme(), andlDc(), which returns exactly the per-family "genuinely user-pinned" booleans the picker needs); the row-builder simply does not call them. - At source — do not seed a variable that the picker reads as a user override. Restricting the seed to "genuinely explicit" invocations is not sufficient, since an explicit
--model claude-opus-5is precisely the case that breaks.
Workaround
Use the family alias, not a concrete ID:
claude --model opus
A concrete ID — including --model claude-opus-5 — does not work; it seeds the pin like any other. The alias escapes only because the seeder's canonicaliser cannot map it (see 1b above).
--model is session-scoped, so this does not persist. For a durable fix, set the saved default to the alias:
{ "model": "opus" }
One caveat, which is how you get re-pinned: catalogue rows in /model carry concrete IDs. Changing only the session model is harmless — that is not persisted. But changing the default model writes the selected row's concrete ID to settings.json, so escaping with --model opus and then making any Opus row your default re-arms the bug for the next launch. To keep a persistent Opus default, set "model": "opus" in settings.json by hand rather than choosing a version through the picker.
Related, but not duplicates
- #52310 — same symptom (one row per family, other versions hidden) on Bedrock, but filed 2026-04-28 and stale-closed 2026-05-27, weeks before 2.1.211. No probe/sentinel mechanism.
- #49566 —
ANTHROPIC_DEFAULT_*_MODELproducing a duplicate custom entry on Bedrock; inverse symptom, closed. - #60815, #57342 — individual 1M variants missing from the Bedrock picker; stale-closed.
- #68005 — Bedrock model-ID suffix handling; adjacent surface, different failure.
No open issue describes a self-seeded ANTHROPIC_DEFAULT_OPUS_MODEL or references the sentinel. The changelog through 2.1.220 (newest published) contains no fix; the 2.1.219 picker entry is a cosmetic label change only.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗