[FEATURE] Per-model effort levels: allow `effortLevel` to accept a model-keyed map

Status Closed — duplicate
Reported on v2.1.235
Maintainer reply None cached
Activity 1 comment · opened Aug 18, 2026 · closed Aug 20, 2026

Preflight Checklist

  • [ ] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Leaving the first box unchecked deliberately, because checking it would not be true. This capability has been requested four times to my knowledge: #77067 and #82114 are open, and #51059 and #67070 were both closed NOT_PLANNED, both proposing the same model-keyed map shape this issue proposes.

Filing anyway, with that history stated up front rather than hidden, because the measured evidence below is not in any of the four and it changes the argument rather than repeating it:

  • The per-family capability ceiling, which is the one genuinely per-model lever that exists, cannot serve as a workaround. Measured, and filed separately as #87788.
  • Once max is persisted, neither a shell export nor --effort can supply a per-launch override, so the usual "just use a wrapper script" answer does not hold either.
  • The bundle's own effort_cost_index shows the cost of raising effort differs by more than 3x between Opus 5 and Sonnet 5, which is a concrete argument for why one global dial is the wrong shape.

If the NOT_PLANNED decisions on #51059 and #67070 still stand, closing this the same way is a reasonable outcome; I would just ask that #87788, the capability-ceiling bug, be triaged on its own merits, since it is a divergence from documented behavior rather than a feature ask.

Problem Statement

Effort is a single global dial while /model switches models freely, so running two models at two different levels in one session is not expressible.

The concrete goal that cannot be met: Opus 5 at xhigh and Sonnet 5 at max, persistently, surviving /model switches, with no manual /effort after each switch.

What makes this harder than it looks is that every adjacent mechanism is either global or a ceiling:

  • effortLevel and CLAUDE_CODE_EFFORT_LEVEL are flat scalars. One dial, every model.
  • ANTHROPIC_DEFAULT_<FAMILY>_MODEL_SUPPORTED_CAPABILITIES is genuinely per-family, but it is a ceiling, and it cannot express a lower preferred level because an unsupported request falls back to high rather than stepping down. Measured on Bedrock with 2.1.235: capability string effort,xhigh_effort,... plus CLAUDE_CODE_EFFORT_LEVEL=max sends effort: "high", not xhigh, even though xhigh is reachable on that same string when requested directly. Filed separately as #87788.
  • The model catalog carries a per-model default_effort, but it is baked into the binary with no documented override, and Opus 5 and Sonnet 5 both ship "high", so CLAUDE_CODE_EFFORT_LEVEL=auto yields high everywhere.
  • Skill and subagent frontmatter effort: is scoped to that skill or subagent, not to a model, so a plain /model opus does not trigger it.
  • The agent settings key applies an agent's model to the main thread but not its effort.
  • Hooks can read effort via $CLAUDE_EFFORT but no hook event can set it, so a hook-based binder is not possible. Two further details close that door: SessionStart is the only event that can receive a model field and it "is not guaranteed to be present", and per hooks.md there is no $CLAUDE_MODEL variable while $ANTHROPIC_MODEL "doesn't change when you switch models with /model during a session", so nothing observes a mid-session model change either.
  • claude agents --effort <level> pairs model and effort per background-dispatched session, which works, but only for background dispatch rather than the interactive session.

Proposed Solution

Let effortLevel accept a model-keyed object alongside the current flat string:

{
  "effortLevel": {
    "opus": "xhigh",
    "sonnet": "max",
    "default": "high"
  }
}

Keys accept the same forms availableModels already accepts, meaning family aliases, version prefixes, and full model IDs, with the most specific match winning. default covers unmapped models. The flat-string form stays valid and behaves exactly as today.

On any model change, by /model, the picker, --model, or an agent's model:, effort follows the mapping. An explicit /effort still overrides until the next model switch.

Two details that matter for this to be usable:

  1. The map should accept max. Today effortLevel rejects it, so max can only be persisted through CLAUDE_CODE_EFFORT_LEVEL, which is exactly the mechanism that makes per-model impossible. Tracked separately as #35904.
  2. The environment variable should keep winning when it is set, so existing setups do not change behavior. The map is a replacement for the flat setting, not for the override.

Alternative Solutions

Each of these was tried and measured on 2.1.235 rather than assumed.

  • Per-family capability ceiling. Removing max_effort from the Opus string while leaving xhigh_effort should, per the documented fall-back rule, yield xhigh for Opus while Sonnet keeps max. It yields high. This is the closest thing to a working answer and it does not work.
  • Shell export per launch. Does not work once max is persisted, because a settings.json env value replaces the value inherited from the shell. Verified: settings file holding max, shell exporting xhigh, wire sends max.
  • --effort at launch. Also loses to the environment variable, which the docs say takes precedence over all other methods.
  • A --settings file per launch. This does work, since --settings is the highest-precedence non-managed layer and merges env per key. But it binds to the launch rather than the model, so a /model switch mid-session keeps the launch level, and it means one settings file per model-and-level combination.
  • Subagent frontmatter effort:. Works for subagents and is genuinely useful, but it loses to the environment variable, so it is unavailable to anyone who has persisted max. It also does not touch the interactive session.
  • Manually pairing /model and /effort. Works, and is the current answer, but it is one forgettable step with cost consequences in both directions.

Priority

Medium - Would be very helpful

Feature Category

Configuration and settings

Use Case Example

  1. Opus 5 is the default model for hard reasoning work, where the jump from high to xhigh is cheap.
  2. Sonnet 5 runs a different class of task in the same session, where a different level is correct.
  3. Today one dial serves both, so choosing a level for one model silently sets it for the other.
  4. With a per-model map, each runs at its intended level and no /effort is needed after a /model switch.

The bundled catalog's own effort_cost_index is the sharpest argument for per-model configuration, because the cost of raising effort is wildly model-dependent:

| Model | xhigh | max |
|---|---|---|
| claude-opus-5 | 1.6x | 1.7x |
| claude-sonnet-5 | 2.41x | 5.59x |

Both figures are relative to that same model's high. Moving Opus from high to max costs 1.7x, while the identical move on Sonnet costs 5.59x. A single global dial therefore has a completely different consequence depending on which model happens to be active, which is exactly the decision a per-model setting would let users make deliberately instead of by accident.

Additional Context

Full prior art, so triage does not have to rediscover it:

| Issue | State | Relation |
|---|---|---|
| #51059 | closed NOT_PLANNED | Per-model effort configuration, modelEffort block. Same shape as this. |
| #67070 | closed NOT_PLANNED | Per-model effort level in settings.json, modelEffort map. Same shape as this. |
| #77067 | open | effortLevel-as-map. Same shape as this. |
| #82114 | open | modelEffortDefaults. Same shape as this. |
| #35904 | open | effortLevel rejects max. This proposal depends on it. |
| #54249 | closed COMPLETED | Earlier round of the max-dropped problem. Not fully fixed; effortLevel still rejects max in 2.1.235. |
| #83489 | open | Named model presets with preset effort levels. Solves this from a different direction. |
| #82259 | open | claude -p --agent ignores frontmatter effort:. Relevant because frontmatter is the nearest existing per-scope mechanism. |

One note for anyone searching the tracker: the string modelEffort does appear in the shipped bundle, but it is an unrelated internal field on a per-model-and-level routing table, not a settings key. It is not evidence that this feature partially shipped.

Environment for every measurement above: Claude Code 2.1.235 on macOS, AWS Bedrock with pinned inference-profile IDs, effort read off the wire from output_config.effort in the verbose debug log rather than from the /effort menu or the spinner label.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗