| name | explain-modelmux |
| description | Use when someone wants to understand how modelmux works — why a proxy is needed, what signals it routes on, and how the cascade keeps the orchestrator on Claude while diverting chosen subagents. |
How modelmux works
Explain the mental model, adapting depth to the audience. The whole system is
small and readable — point people at the actual files.
The constraint
Claude Code picks its upstream from one global environment variable,
ANTHROPIC_BASE_URL. There's no per-subagent model setting on the client side.
So if you want some agents on Claude and others on a different model, the
decision has to happen outside the client — in a proxy that every request
flows through.
The proxy
src/server.ts is a Bun.serve reverse proxy. For each request it:
- Extracts routing signals (
src/signals.ts).
- Picks a target via a first-match cascade (
src/route.ts + routes.toml).
- Rewrites auth for the chosen upstream (
src/upstreams.ts) and forwards it.
SSE streams pass straight through.
Two upstreams: Anthropic (api.anthropic.com, "passthrough" = keep the
model Claude Code asked for) and OpenRouter (its Anthropic-compatible
endpoint, so no request translation is needed).
The signals (why it routes by header, not model name)
From each request (src/signals.ts):
x-claude-code-agent-id — present only on subagent requests, so it marks
isSubagent. This is the key signal, and routing on it sidesteps a Claude Code
bug where a subagent's requested model can silently fall back to the parent's.
x-app — cli for foreground, cli-bg for background work.
<<route:alias>> — an explicit tag placed in an agent's system prompt.
- Work-type signals derived from the request:
background (x-app: cli-bg),
longContext (estimated tokens > longContextThreshold), think (an
extended-thinking block), webSearch (a web-search tool).
The cascade
route() walks routes.toml's routes top-to-bottom, first match wins:
- Tag rules —
<<route:flagship|max|reasoner|review|claude-review>> →
that model. <<route:control>> → orchestrator (stays on Claude).
workType: background → the cheap model.
anySubagent → the flagship model (any untagged subagent).
- Default →
orchestrator (Anthropic passthrough).
All four work types (background, longContext, think, webSearch) are
implemented in route.ts, but only background is enabled in the shipped
routes.toml — the others are commented-out examples an operator can turn on
(e.g. send big-context requests to a roomier model). That's what
longContextThreshold configures; it does nothing until a longContext rule
exists.
The main loop has no x-claude-code-agent-id, so it never matches a subagent
rule — it falls to the default and stays on Claude. That's the core promise:
smart orchestrator on Claude, chosen subagents on cheaper/other models.
The menu
routes.toml maps friendly aliases (flagship, cheap, …) to
<upstream>:<slug>. Swap a model in one place, or override at runtime with
MUX_MODEL_<ALIAS>. See the switch-models skill for day-to-day changes.
Verifying it
Every decision is appended to decisions.jsonl with isSubagent, matchedRule,
upstream, and resolvedModel — the ground truth for "did this actually route
where I think?"