[BUG] modelPicker skips an `opusplan` row as already covered, but the picker shows no Opus Plan Mode row

Status Open
Reported on v2.1.246
Maintainer reply None cached
Activity 14 comments · opened Aug 26, 2026

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

  1. Save this to /tmp/mp.json:
{
  "modelPicker": {
    "options": [
      {
        "model": "opusplan",
        "label": "Opus Plan Mode",
        "description": "Use Opus in plan mode, Sonnet otherwise"
      }
    ]
  }
}
  1. Run claude --settings /tmp/mp.json (absolute path required).
  2. 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's model is taken verbatim, so it accepts anything --model accepts: an alias such as opus, 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:

  1. replaceBuiltInOptions: true and hand-maintain the whole lineup, so future model additions stop appearing.
  2. "model": "opusplan", which changes the default for every session.
  3. 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.

View original on GitHub ↗

3 Comments

urda · 1 month ago

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 modelPicker or 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 --settings in a pseudo-terminal that opens /model and 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 opusplan as distinct from opus in the coverage check, and emit one warning line when a row is skipped for coverage.

urda · 1 month ago

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 --settings in a pseudo-terminal that opens /model, scrolls the list, and captures the rendered picker. Each merge mode ran as its own probe. Session model opus[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 models fold 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 opusplan as distinct from opus in the coverage check, and emit one warning line when a row is skipped for coverage.

urda · 1 month ago

Correction: the collision partner is the Sonnet row, not the Opus row

The report above says the coverage check treats opusplan and opus as 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)

  1. The curated rows merge into the built-in list before any session-model row is added. In append mode a curated row survives only when no built-in row matches it through the row comparator:

``js
let v=[...e];for(let C of _)if(!v.some((I)=>tF(I,C)))v.push(C);return v
``

  1. The comparator resolves both values through the alias resolver and compares the resolved model IDs. When they match, a [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)}
``

Qc is the [1m] suffix test.

  1. The alias resolver maps opusplan to the Sonnet model ID, on the same branch as sonnet:

``js
case"opusplan":return o?lS(Ye(Nf())):Nf();
case"sonnet":return o?lS(Ye(Nf())):Nf();
``

Nf() returns ANTHROPIC_DEFAULT_SONNET_MODEL or the current Sonnet.

  1. For the repro row, both sides resolve to the same Sonnet ID and neither carries [1m], so the comparator returns true. The opusplan row matches the built-in Sonnet row and is dropped. This path logs nothing. The dropping duplicate row warning fires only on the exact-value duplicate pass that runs later.
  1. The built-in "Opus Plan Mode" row is appended after the merge, and only when the session model is already opusplan:

``js
else if(C==="opusplan")return hH([...o,W9r()],n);
``

Revised request

Treat opusplan as 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 modelPicker or the /model picker, so this reading applies to 2.1.258 as well.

Showing cached comments. Read the full discussion on GitHub ↗