[FEATURE] Intelligent Model Routing — Claude Code routes 93.8% of Max subscriber tokens to Opus with no automatic optimization

Status Open
Maintainer reply None cached
Activity 11 comments · opened Feb 22, 2026

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 EnterPlanModeExitPlanMode | 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 Task with explicit model: "haiku" or model: "sonnet" for different task types
  • CLAUDE_CODE_SUBAGENT_MODEL=sonnet env var to at least route subagents away from Opus
  • Manual /model switching mid-session (tedious, state is global)
  • Wrapper scripts around the claude binary
  • 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):

  1. Default subagents to Sonnet, not inherit Opus (#26179) — the biggest bang-for-buck change
  2. Fix opusplan to actually route based on plan/execute phase reliably
  3. Support model: in skill/agent frontmatter (#23462) — let authors specify

Tier 2 — Per-message routing:

  1. Per-message model override (#25410, #26961) — lightweight syntax like //s do this with sonnet or //h quick lookup

Tier 3 — User-configurable routing:

  1. Routing rules in settings (#19269, #26740):
{
  "modelRouting": {
    "plan": "opus",
    "execute": "sonnet",
    "subagents": "sonnet",
    "skills": {
      "brainstorming": "opus",
      "research": "sonnet"
    }
  }
}

Tier 4 — Automatic routing:

  1. 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:

  1. Default subagents to Sonnet (#26179) — one-line change
  2. Support model: in skill frontmatter (#23462) — small feature
  3. 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)

View original on GitHub ↗

11 Comments

github-actions[bot] · 6 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/27183
  2. https://github.com/anthropics/claude-code/issues/26179
  3. https://github.com/anthropics/claude-code/issues/19269

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

FilippTrigub · 6 months ago

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...

RobinHoutevelts · 5 months ago

Still relevant and impacts me. So not https://github.com/anthropics/claude-code/labels/stale

lenucksi · 5 months ago

Let me extend this to the point of thinking that context / content aware routing across models might be nice.

oussama-kh · 4 months ago

Not "intelligent" but works well for local models: https://github.com/oussama-kh/mcp-llama-swap

talkstream · 4 months ago

Field data — confirming the problem

Ran analysis on my own Max sessions (April 7, 2026):

  • 8 sessions, 3,194 API calls across 5 projects
  • Opus: 71.2% of calls, 71.9% of output tokens
  • Sonnet: 6.6% (target: 60%)
  • Haiku: 22.2% (Claude Code's native Explore agent)

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/ frontmatter

Built better-model — zero-dependency CLI that installs custom agents with model: frontmatter and a CRITICAL routing block in CLAUDE.md:

npx better-model init

What it does:

  • Ships sonnet-coder (model: sonnet) and haiku-explorer (model: haiku) agents
  • Adds routing block to CLAUDE.md with effort levels per tier
  • Frontmatter enforcement gives ~100% compliance vs ~70% from CLAUDE.md instructions alone

Evidence-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.

warku123 · 3 months ago

+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.

junaidtitan · 3 months ago

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 guard to auto-prune in the background. Would love feedback on whether the token savings are noticeable.

talkstream · 3 months ago
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.

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).

FuturMix · 3 months ago

Worth noting that the ANTHROPIC_BASE_URL env var already gives you external control over routing today — no need to wait for native support.

The pattern:

  1. Point ANTHROPIC_BASE_URL at an API gateway/hub that supports multiple backends
  2. The gateway inspects the request and routes to the model you configure (Opus for planning, Sonnet for code, Haiku for confirmations)
  3. Claude Code itself doesn't know anything changed — it just calls the endpoint

This 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_URL once 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.

edagher92-coder · 1 month ago

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.