[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)
11 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
I know, this may be in vain, but allowing subagents to be routed to other endpoints would be a game changer for many as well.
Local models usually cant replace the main drivers such as opus and sonnet, but can run long, easy or parallelized tasks. Enabling direct routing via claude code would allow using opus & sonnet for the main tasks, while offloading dumb stuff on local / other models.
The alternative for me is as of now to fully replace my subscription with routing via the API, at which point I can just as well route everything to another provider...
Still relevant and impacts me. So not https://github.com/anthropics/claude-code/labels/stale
Let me extend this to the point of thinking that context / content aware routing across models might be nice.
Not "intelligent" but works well for local models: https://github.com/oussama-kh/mcp-llama-swap
Field data — confirming the problem
Ran analysis on my own Max sessions (April 7, 2026):
This confirms the issue author's findings — Opus dominates even where Sonnet performs identically (SWE-bench: 80.9% vs 79.6%).
Workaround via
.claude/agents/frontmatterBuilt better-model — zero-dependency CLI that installs custom agents with
model:frontmatter and a CRITICAL routing block in CLAUDE.md:What it does:
sonnet-coder(model: sonnet) andhaiku-explorer(model: haiku) agentsEvidence-based routing matrix citing SWE-bench, GPQA Diamond, ARC-AGI-2, RouteLLM research. Expected improvement: Opus 71% → ~25-30%.
Not a replacement for proper built-in routing (which this issue requests), but a functional workaround using existing Claude Code primitives.
+1 to this, but want to add an angle I haven't seen surfaced in the discussion: even users who try to build their own routing layer hit a wall, because the prompt cache is per-model.
I've been prototyping a local proxy that sits in front of Claude Code and routes each turn to Haiku/Sonnet/Opus based on task difficulty. The classifier part works fine, but the economics collapse fast: switching models mid-conversation invalidates the cache entirely, and the whole history gets re-billed at full input rate on the new model. This actually compounds the exact cost pattern this issue documents — @talkstream's data shows 96% of Opus spend is on cache reads of conversation history, which is precisely the cost that naive cross-model routing would re-multiply rather than save. A few switches in a 50K–200K token Claude Code session and the router loses money vs. fixed-Opus.
This means any third-party routing layer is structurally capped at "lock the tier for the whole session" — which is exactly the coarse
opusplan-style behavior this issue is trying to move beyond. The cleanest fix has to come from Anthropic side.Related, #29550 noted that lightweight Haiku routing was actually present inside Claude Code before v2.1.63 and was silently removed, and that question has been open without a maintainer response. It would really help to understand whether the removal was related to cache invalidation cost, quality consistency, or something else — because that constraint shapes what any external routing solution can realistically do.
Concrete forms a server-side fix could take: (a) intra-Anthropic auto-routing as requested here, (b) some form of cross-model cache prefix portability so external routers become viable, or (c) an explicit
model_candidates: [haiku, sonnet, opus]API where the server picks while keeping the cache unified.Would love to hear if any of these are on the roadmap, or what the recommended pattern for routing-style tooling is today.
93.8% of tokens going to Opus with no routing optimization is a real cost problem. While intelligent routing is ultimately an Anthropic-side fix, you can reduce the per-turn cost right now by keeping session context lean. Cozempic prunes the session JSONL with 18 strategies — thinking-blocks, metadata-strip, tool-result-age, compact-summary-collapse (85-95% savings on compaction summaries alone). Less context per Opus turn = less waste on simple tasks.
pip install cozempic && cozempic guardto auto-prune in the background. Would love feedback on whether the token savings are noticeable.It is, but I got some success for myself, anyway. Take a look, maybe it would be useful for you, too: https://github.com/talkstream/better-model (npm is recommended, it install/uninstall router per project basis).
Worth noting that the
ANTHROPIC_BASE_URLenv var already gives you external control over routing today — no need to wait for native support.The pattern:
ANTHROPIC_BASE_URLat an API gateway/hub that supports multiple backendsThis is how we handle it at FuturMix — one OpenAI-compatible endpoint that routes to 22+ models across Anthropic, OpenAI, Google, and DeepSeek. Set
ANTHROPIC_BASE_URLonce and the gateway handles model selection.For anyone interested in the DIY route, LiteLLM (open source) can do similar routing with custom rules. The key insight is: model routing is a gateway-layer concern, not an editor concern.
Got tired of waiting for this, so I built it: MIT-licensed router that classifies each task (Haiku call, ~5 tokens) and dispatches to the cheapest model that holds quality — doubt routes up, never down. Includes a CLAUDE.md policy for Code/Cowork sessions, a UserPromptSubmit hook (zero API calls, ~ to bypass), and a usage log + dashboard showing your actual tier split. Test report in the repo: 9/9 routing cases, fallback and escalation verified. https://github.com/edagher92-coder/claude-model-router. Hope it helps until this ships natively.