[BUG] modelPicker skips an `opusplan` row as already covered, but the picker shows no Opus Plan Mode row
Summary
A modelPicker row whose model is opusplan never reaches the /model picker in append mode. Claude Code treats the row as covered by the built-in lineup. The built-in lineup carries no Opus Plan Mode row in an ordinary session, so nothing covers it.
opusplan is a mode, not a model. --model opusplan runs Opus during plan mode and Sonnet during execution. --model opus runs Opus throughout. The coverage check compares resolved models, so it treats the two as the same row.
Relationship to #26556
#26556 requests Opus Plan as a discoverable /model option. This issue is not a duplicate of it, and not a second request for the same feature.
modelPicker shipped in v2.1.242 as the documented way to add rows to the /model picker. It is the mechanism a user would reach for to satisfy #26556 themselves. It works for every other value tested. It does not work for opusplan, and it gives no indication why.
Environment
| | |
| --- | --- |
| Claude Code | 2.1.246, also reproduced on 2.1.243 and 2.1.245 |
| Platform | macOS 15.6, native install |
| Auth | Claude Max subscription, first-party API |
| Session model | opus[1m] |
| Built-in lineup | Default, Opus (1M context), Fable, Sonnet, Haiku |
Steps to reproduce
- Save this to
/tmp/mp.json:
{
"modelPicker": {
"options": [
{
"model": "opusplan",
"label": "Opus Plan Mode",
"description": "Use Opus in plan mode, Sonnet otherwise"
}
]
}
}
- Run
claude --settings /tmp/mp.json(absolute path required). - Run
/model.
Expected
The picker shows the built-in lineup, then an "Opus Plan Mode" row.
The settings reference states the contract for the model field:
Each row'smodelis taken verbatim, so it accepts anything--modelaccepts: an alias such asopus, an Anthropic model ID, or a provider-format ID for Amazon Bedrock, Google Cloud's Agent Platform, Microsoft Foundry, or an LLM gateway.
opusplan is such an alias, and Claude Code advertises it in its own help text:
$ claude -p "/model"
Usage: /model <name>. Available: sonnet, opus, haiku, fable, best,
sonnet[1m], opus[1m], fable[1m], opusplan, default, or a full model ID.
Actual
The picker shows the built-in lineup and nothing else. Five rows, no Opus Plan Mode row, and no message.
Why this is not the documented skip
The same page documents a skip:
With it off, Claude Code skips a listed model that the built-in lineup already covers.
That sentence correctly explains several other rows (see the table below). It does not explain this one, because the built-in lineup carries no Opus Plan Mode row in this session.
Claude Code does ship such a row, and offers it on one condition only: the session model is already opusplan. A session started with {"model": "opusplan"} boots as "Opus Plan" and its picker gains a sixth row, "Opus Plan Mode ✔".
So the row is reachable only by users who already run the mode it selects. modelPicker is the documented way out of that loop, and it is the one route that does not work.
Evidence
Each row below ran alone and in combination, in both merge modes, via --settings, on 2.1.246.
| model | Append | Replace | Reading |
| --- | --- | --- | --- |
| opusplan | dropped | shown | the defect |
| opus | dropped | shown | expected, lineup covers Opus |
| best | dropped | shown | expected, lineup covers it |
| claude-haiku-4-5-20251001 | dropped | shown | expected, lineup covers Haiku |
| claude-sonnet-4-5-20250929 | shown | shown | expected, outside the lineup |
| claude-opus-4-1-20250805 | dropped | dropped | expected, retired |
| claude-3-5-haiku-20241022 | dropped | dropped | expected, retired |
Replace mode renders the opusplan row, which proves the row parses, validates, and is servable. Only the coverage check rejects it:
1. Default (recommended)
2. Opus Plan Mode Use Opus in plan mode, Sonnet otherwise
3. Opus (1M context) ✔ Opus 5 with 1M context
Impact
There is no way to keep the maintained lineup and add Opus Plan Mode. The three available workarounds are each worse than the documented behavior:
replaceBuiltInOptions: trueand hand-maintain the whole lineup, so future model additions stop appearing."model": "opusplan", which changes the default for every session.- Do without the row.
Requested change
Treat opusplan as distinct from opus in the coverage check. A mode and a model are not the same row, and a user who names the mode explicitly has asked for something the lineup does not offer.
Secondary request (independent)
Say something when a row is skipped for coverage. The documentation describes two visible outcomes, a dropped row and a grayed-out row that "shows the reason". A row skipped for coverage produces neither, so a user cannot distinguish it from a typo in model or a settings file that failed to load.
Claude Code already emits a warning of the right shape for the exact-duplicate case:
model options: dropping duplicate row "<label>" (value <model>)
One line in that shape, for the coverage skip, would make the feature self-diagnosing.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Retest on 2.1.250: still reproduces
Retested 2026-08-28 on Claude Code 2.1.250, macOS 15.6, native install, Claude Max. The defect is unchanged.
The changelogs for 2.1.247, 2.1.248, and 2.1.250 carry no entry that names
modelPickeror the coverage check, so this is a confirmation on the current build, not a regression report. No 2.1.249 entry exists.Method
The exact row from the reproduction steps above, unchanged, via
claude --settingsin a pseudo-terminal that opens/modeland captures the rendered picker. Each merge mode ran as its own probe.Result
| mode | picker |
| --- | --- |
| append (
replaceBuiltInOptions: false) | Five built-in rows only. The row is dropped, with no message. || replace (
replaceBuiltInOptions: true) | The row renders:Opus Plan Mode Use Opus in plan mode, Sonnet otherwise. |The replace run is the positive control. It proves the row parses, validates, and is servable on this build, so the append-mode absence is the coverage check, not row validity.
One new data point: an
opusplan[1m]row behaves identically in both modes, so the suffix does not change the coverage collision.Verified versions now: 2.1.243, 2.1.245, 2.1.246, 2.1.250. Both requested changes stand: treat
opusplanas distinct fromopusin the coverage check, and emit one warning line when a row is skipped for coverage.Retest on 2.1.257: still reproduces
Retested 2026-09-01 on Claude Code 2.1.257, macOS 15.6, native install, Claude Max. The defect is unchanged.
The changelogs for 2.1.251, 2.1.252, and 2.1.257 carry no entry that names
modelPicker, the coverage check, or a skip warning. No 2.1.253 through 2.1.256 entries exist.Method
The exact row from the reproduction steps above, unchanged, via
claude --settingsin a pseudo-terminal that opens/model, scrolls the list, and captures the rendered picker. Each merge mode ran as its own probe. Session modelopus[1m], built-in lineup: Default, Opus (1M context), Fable, Sonnet, Haiku.Result
| mode | picker |
| --- | --- |
| append (
replaceBuiltInOptions: false) | Five built-in rows only. The row is dropped, with no message. The… +2 modelsfold marker confirms the count. || replace (
replaceBuiltInOptions: true) | The row renders:Opus Plan Mode Use Opus in plan mode, Sonnet otherwise. |The replace run is the positive control. It proves the row parses, validates, and is servable on this build, so the append-mode absence is the coverage check, not row validity.
One new data point: the picker header text "For other/previous model names, specify with --model." is present in the 2.1.251 and 2.1.257 binaries. It is not new, so it does not explain a change in behavior.
Verified versions now: 2.1.243, 2.1.245, 2.1.246, 2.1.250, 2.1.257. Both requested changes stand: treat
opusplanas distinct fromopusin the coverage check, and emit one warning line when a row is skipped for coverage.Correction: the collision partner is the Sonnet row, not the Opus row
The report above says the coverage check treats
opusplanandopusas the same row. I read the picker code in the 2.1.257 bundle, and the partner is the Sonnet row. The requested fix does not change, but the wording does.The snippets below come from the minified 2.1.257 bundle. The function names are minified and differ per build, so treat them as landmarks, not identifiers.
Code path (2.1.257)
``
js
``let v=[...e];for(let C of _)if(!v.some((I)=>tF(I,C)))v.push(C);return v
[1m]suffix on both sides or on neither side returns true. Reformatted, Fable-family branch elided:``
js
``function tF(e,n){
if(e.value===n.value)return!0;
let d=At(e.value);if(hr(d)!==hr(At(n.value)))return!1;
return Qc(e.value)===Qc(n.value)||D_(d)}
Qcis the[1m]suffix test.opusplanto the Sonnet model ID, on the same branch assonnet:``
js
``case"opusplan":return o?lS(Ye(Nf())):Nf();
case"sonnet":return o?lS(Ye(Nf())):Nf();
Nf()returnsANTHROPIC_DEFAULT_SONNET_MODELor the current Sonnet.[1m], so the comparator returns true. Theopusplanrow matches the built-in Sonnet row and is dropped. This path logs nothing. Thedropping duplicate rowwarning fires only on the exact-value duplicate pass that runs later.opusplan:``
js
``else if(C==="opusplan")return hH([...o,W9r()],n);
Revised request
Treat
opusplanas distinct from any resolved model in the coverage check. A mode row and the row for its execution-mode model are not the same row. The secondary request stands: emit one warning line when a row is skipped for coverage.The 2.1.258 changelog carries no entry that names
modelPickeror the/modelpicker, so this reading applies to 2.1.258 as well.