[FEATURE] Per-model effort levels: allow `effortLevel` to accept a model-keyed map
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
maxis persisted, neither a shell export nor--effortcan supply a per-launch override, so the usual "just use a wrapper script" answer does not hold either. - The bundle's own
effort_cost_indexshows 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:
effortLevelandCLAUDE_CODE_EFFORT_LEVELare flat scalars. One dial, every model.ANTHROPIC_DEFAULT_<FAMILY>_MODEL_SUPPORTED_CAPABILITIESis genuinely per-family, but it is a ceiling, and it cannot express a lower preferred level because an unsupported request falls back tohighrather than stepping down. Measured on Bedrock with 2.1.235: capability stringeffort,xhigh_effort,...plusCLAUDE_CODE_EFFORT_LEVEL=maxsendseffort: "high", notxhigh, even thoughxhighis 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", soCLAUDE_CODE_EFFORT_LEVEL=autoyieldshigheverywhere. - Skill and subagent frontmatter
effort:is scoped to that skill or subagent, not to a model, so a plain/model opusdoes not trigger it. - The
agentsettings key applies an agent's model to the main thread but not its effort. - Hooks can read effort via
$CLAUDE_EFFORTbut no hook event can set it, so a hook-based binder is not possible. Two further details close that door:SessionStartis the only event that can receive amodelfield and it "is not guaranteed to be present", and perhooks.mdthere is no$CLAUDE_MODELvariable while$ANTHROPIC_MODEL"doesn't change when you switch models with/modelduring 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:
- The map should accept
max. TodayeffortLevelrejects it, somaxcan only be persisted throughCLAUDE_CODE_EFFORT_LEVEL, which is exactly the mechanism that makes per-model impossible. Tracked separately as #35904. - 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_effortfrom the Opus string while leavingxhigh_effortshould, per the documented fall-back rule, yieldxhighfor Opus while Sonnet keepsmax. It yieldshigh. This is the closest thing to a working answer and it does not work. - Shell export per launch. Does not work once
maxis persisted, because asettings.jsonenvvalue replaces the value inherited from the shell. Verified: settings file holdingmax, shell exportingxhigh, wire sendsmax. --effortat launch. Also loses to the environment variable, which the docs say takes precedence over all other methods.- A
--settingsfile per launch. This does work, since--settingsis the highest-precedence non-managed layer and mergesenvper key. But it binds to the launch rather than the model, so a/modelswitch 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 persistedmax. It also does not touch the interactive session. - Manually pairing
/modeland/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
- Opus 5 is the default model for hard reasoning work, where the jump from
hightoxhighis cheap. - Sonnet 5 runs a different class of task in the same session, where a different level is correct.
- Today one dial serves both, so choosing a level for one model silently sets it for the other.
- With a per-model map, each runs at its intended level and no
/effortis needed after a/modelswitch.
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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗