[FEATURE] Intelligent Model Routing — Claude Code routes 93.8% of Max subscriber tokens to Opus with no automatic optimization
Preflight
- [x] I've searched existing issues for duplicates (there are 30+ related issues — this consolidates them)
- [x] I've verified this with real usage data via
ccusage - [x] I've read the binary to confirm the routing architecture
What
Claude Code has no automatic model routing. Every main-loop turn uses the session model for the full context resend, regardless of task complexity. On Max plans that default to Opus, this means simple file reads, "yes do it" confirmations, and grep result processing all burn Opus-tier tokens.
This isn't just an opusplan bug — it affects every Max subscriber running on default settings. opusplan was the one attempt at routing, and it's broken. But even if it worked, the routing granularity (plan vs. non-plan) is far too coarse.
Evidence: Real Usage Data from a Max Subscriber
17 days of usage on a $200/month Max plan (via ccusage):
Model Total Tokens Pct Virtual Cost
──────────────────────────────────────────────────────────────
opus-4-6 1,405,957,116 77.0% $1,011.59
opus-4-5 305,903,815 16.8% $234.43
haiku-4-5 94,216,971 5.2% $16.02
sonnet-4-5 17,285,048 0.9% $11.44
sonnet-4-6 1,046,090 0.1% $0.92
──────────────────────────────────────────────────────────────
TOTAL 1,825,500,411 100.0% $1,274.40
By tier:
Opus: 93.8% of tokens, 97.8% of cost ($1,246)
Sonnet: 1.0% of tokens, 1.0% of cost ($12)
Haiku: 5.2% of tokens, 1.3% of cost ($16)
93.8% of all tokens go to Opus. The 5.2% Haiku is background tasks (via CLAUDE_CODE_SUBAGENT_MODEL). The 1% Sonnet is from a handful of manual /model sonnet switches. There is no automatic routing happening.
96% of those Opus tokens are cache reads — the full conversation context being re-sent every turn. A 200K context session with 50 turns generates ~10M tokens in cache reads alone, all at Opus rates, even when the turn is a trivial "run this grep" orchestration.
Binary Analysis: Why Routing Doesn't Exist
I decompiled the v2.1.50 binary and traced the model selection functions. Here's what I found.
The main loop model selector (getRuntimeMainLoopModel):
function getRuntimeMainLoopModel({ permissionMode, mainLoopModel, exceeds200kTokens }) {
// ONLY upgrades to Opus when user selected "opusplan" AND is inside EnterPlanMode
if (getUserSpecifiedModel() === "opusplan" && permissionMode === "plan" && !exceeds200kTokens)
return getDefaultOpusModel();
if (getUserSpecifiedModel() === "haiku" && permissionMode === "plan")
return getDefaultSonnetModel();
return mainLoopModel; // ← everything else: whatever the session model is
}
This function is called on every turn. If you selected opus (or Max defaulted you to it), mainLoopModel is always Opus. There's no complexity check, no task-type check, no "is this turn just processing tool output" check. Just: return the session model.
The model resolver (parseUserSpecifiedModel):
case "opusplan": return getDefaultSonnetModel(); // resolves to Sonnet as base
case "sonnet": return getDefaultSonnetModel();
case "opus": return getDefaultOpusModel();
The subagent model selector:
function getSubagentModel(config, parentModel, frontmatterModel, permissionMode) {
if (process.env.CLAUDE_CODE_SUBAGENT_MODEL)
return parse(process.env.CLAUDE_CODE_SUBAGENT_MODEL); // env var overrides everything
if (frontmatterModel) return parse(frontmatterModel); // agent .md model field
if (config === "inherit")
return getRuntimeMainLoopModel({ permissionMode, mainLoopModel: parentModel });
return parse(config);
}
Subagents inherit the parent model by default. If the parent is Opus, subagents are Opus. The only escapes are CLAUDE_CODE_SUBAGENT_MODEL env var or per-agent model: frontmatter. Neither is automatic.
What opusplan Actually Does (And Doesn't)
The docs promise: "Uses opus during plan mode, then switches to sonnet for execution."
What actually happens:
| Context | Model Used | Why |
|---------|-----------|-----|
| Inside EnterPlanMode → ExitPlanMode | Opus | permissionMode === "plan" |
| Regular conversation turns | Sonnet | Falls through to mainLoopModel |
| Tool calls (Bash, Read, Edit) | Sonnet | Not in plan mode |
| Subagent/Task spawning | Sonnet (inherited) | Not in plan mode |
| After exiting plan mode | Sonnet | permissionMode reverts |
So opusplan gives you Opus for the ~5% of the session spent inside plan mode, and Sonnet for everything else. Users who selected opusplan expecting "Opus for reasoning, Sonnet for execution" get "Sonnet for almost everything, Opus for the brief planning window."
Users who selected opus directly (or got it via Max default) get the opposite problem: Opus for everything, no routing at all.
What Users Are Building Instead
Because no automatic routing exists, users are building their own:
- Custom skills that spawn
Taskwith explicitmodel: "haiku"ormodel: "sonnet"for different task types CLAUDE_CODE_SUBAGENT_MODEL=sonnetenv var to at least route subagents away from Opus- Manual
/modelswitching mid-session (tedious, state is global) - Wrapper scripts around the
claudebinary - Third-party router plugins like
claude-router(28 stars, single maintainer)
None of these should be necessary. The model should be smart enough — or at least configurable enough — to route by task type.
Proposed Solution: Tiered Approach
Tier 1 — Quick wins (one-line to small changes):
- Default subagents to Sonnet, not inherit Opus (#26179) — the biggest bang-for-buck change
- Fix opusplan to actually route based on plan/execute phase reliably
- Support
model:in skill/agent frontmatter (#23462) — let authors specify
Tier 2 — Per-message routing:
- Per-message model override (#25410, #26961) — lightweight syntax like
//s do this with sonnetor//h quick lookup
Tier 3 — User-configurable routing:
- Routing rules in settings (#19269, #26740):
{
"modelRouting": {
"plan": "opus",
"execute": "sonnet",
"subagents": "sonnet",
"skills": {
"brainstorming": "opus",
"research": "sonnet"
}
}
}
Tier 4 — Automatic routing:
- Complexity-based routing (#25986, #15721) — analyze the prompt/turn and route automatically. Even a simple heuristic (tool-output-processing turns → Sonnet) would help.
30+ Related Issues
This is the most-requested class of feature across the Claude Code issue tracker.
opusplan bugs (the one routing attempt, broken):
| # | Title | Status |
|---|-------|--------|
| #6108 | Opus Plan Mode: Automatic Model Switching Failure | CLOSED (cosmetic fix only) |
| #5990 | opusplan falls back to sonnet 3.7 instead of sonnet 4 | CLOSED |
| #16982 | opusplan doesn't switch to Opus during plan mode | OPEN |
| #16183 | "opusplan" not recognized in plan mode | CLOSED |
| #25866 | Case-sensitive "OpusPlan" accepted but doesn't work | OPEN |
| #26556 | Show Opus Plan in /model command options | OPEN |
| #27183 | 100% traffic routed to Opus, Sonnet quota never consumed | OPEN |
| #27237 | opusplan uses sonnet-4-5 even when sonnet-4-6 available | OPEN |
Model routing feature requests:
| # | Title | Status |
|---|-------|--------|
| #19269 | User-Configurable Model Routing for Skills and Tools | OPEN (high-priority, stale) |
| #25410 | Per-Prompt Model Override Syntax (//s, //o, //h) | OPEN |
| #26961 | Per-message model override without switching session model | OPEN |
| #26740 | Per-tool model routing to optimize cost and latency | OPEN |
| #25986 | Automatic model suggestions based on query complexity | OPEN |
| #15721 | Automatic Model Switching for Plan Mode | OPEN |
| #27274 | Automatic Model Switching Between Plan and Execution Modes | OPEN |
| #20408 | Auto-select optimal models for Plan mode and context-clear | OPEN |
| #22206 | Programmatic Model Switching | OPEN |
| #17772 | Programmatic Model Switching for Autonomous Agents | OPEN |
| #12645 | Switch models mid-session without persisting to settings | OPEN |
Subagent model issues:
| # | Title | Status |
|---|-------|--------|
| #26179 | Subagents should default to Sonnet, not inherit Opus | OPEN |
| #24160 | Allow overriding the small/fast model used for subagents | OPEN |
| #23462 | Model selection in skill frontmatter | OPEN |
| #4937 | Model selection support for custom commands | OPEN |
| #18873 | Task tool model parameter returns 404 | OPEN |
| #17562 | Agent model shorthand names cause 404 errors | OPEN |
| #10993 | Subagent model selection ambiguity (docs) | CLOSED (not planned) |
| #19174 | Ambiguity regarding default model behavior for Subagents | OPEN |
Token consumption / quota impact:
| # | Title | Status |
|---|-------|--------|
| #23706 | Opus 4.6 token consumption significantly higher than 4.5 | OPEN |
| #24243 | Regression: increased per-turn token consumption | OPEN |
| #13761 | Task tool consumed ~77,600 tokens instead of ~15,000 | OPEN |
| #1109 | Usage Metrics Visibility for Max Subscribers | OPEN |
| #18744 | Transparent Context Usage & Tool Loading Optimization | OPEN |
| #22625 | Per-Subagent Token Usage Tracking | OPEN |
Hook/extensibility requests (users trying to build routing themselves):
| # | Title | Status |
|---|-------|--------|
| #17902 | Smart Tool Introduction with BeforeToolSelection Hook | OPEN |
| #21537 | BeforeToolSelection Hook for Dynamic Tool Filtering | OPEN |
| #24728 | Show active model in status line when skills/subagents override | OPEN |
| #27429 | MAX_THINKING_TOKENS global — causes subagent crashes | OPEN |
Why This Matters
30+ open issues. The single routing feature (opusplan) is broken. Max subscribers default to Opus for everything. Users are building DIY routers out of skills, env vars, and wrapper scripts.
The fix doesn't need to be ambitious. Even just:
- Default subagents to Sonnet (#26179) — one-line change
- Support
model:in skill frontmatter (#23462) — small feature - Fix opusplan to actually route based on plan/execute phase
...would address the majority of these issues.
Environment
- Claude Code v2.1.50
- macOS (Apple Silicon)
- Max plan ($200/month)
- Model: opus (Max default)
This issue has 11 comments on GitHub. Read the full discussion on GitHub ↗