Model routing
By default there is no router: every spawned session uses the Claude CLI with the machine's keychain OAuth or ANTHROPIC_API_KEY — Anthropic only. The router is opt-in. Activate it and the daemon routes each spawn to the cheapest backend that satisfies the request's declared needs, collapsing to an Anthropic safety net whenever config is absent, a provider is down, or a rule fails to match.
Activate
cp config/model-routing.yaml.example config/model-routing.yaml
maestro router validate # strict-validate before it takes effect
Restart the daemon and every new spawn respects the policy. Two single-step kill switches revert to Anthropic-only immediately:
- rename
config/model-routing.yamlto.disabled, or - export
MAESTRO_ROUTER_FORCE_ANTHROPIC=1— honoured before any rule runs, so it wins even over a corrupt config.
Catalog vs policy
Models are not defined in your config. The bundled catalog (lib/model-router/catalog/*.yaml, shipped with the SDK) is the source of truth for every model's identity, status, context window, cost per million tokens, compatibility flags, tool-reliability grade, and harness affordances. config/model-routing.yaml is policy only — aliases, data-class rules, the ordered chain rules, and the budget ladder. Upgrading the SDK refreshes the catalog without touching your policy.
Config anatomy (schema_version: 2)
schema_version: 2
aliases: # friendly names → catalog refs
frontier: anthropic/claude-opus-4-8
default: anthropic/claude-sonnet-4-6
fast: anthropic/claude-haiku-4-5
cheap: deepseek/deepseek-v4-flash # unreachable until its key exists
defaults:
needs_tool_use_for_sessions: true # session work defaults to tool-using
data_class: sensitive # deny-by-default: unlabeled work never leaves Anthropic
cache_ttl: 1h
backends: # compliance gate + credential binding, not price
anthropic: { allowed_data_classes: [public, internal, sensitive] }
deepseek: { allowed_data_classes: [public] }
moonshot: { allowed_data_classes: [public] }
routing_policy: # ordered, first match wins
- match: { agent_role_in: [ceo_pre_pass, audit, decision_writer, regulatory] }
chain: [frontier, default]
pin: true # exempt from the budget degradation ladder
- match: { task_class: classify.inbox }
harness: direct
chain: [fast, cheap, rules] # "rules" = deterministic fallback as terminal candidate
needs_tool_use: false
Each rule resolves to a candidate chain of aliases or refs. The resolver gates candidates in order — status, harness, capability and tool-grade, context window, data class, credential, circuit breaker, budget ladder — and picks the first survivor.
Three properties do the heavy lifting:
- Deny-by-default data classes.
defaults.data_class: sensitivemeans unlabeled work can never route to a backend whoseallowed_data_classesexcludes it. The gate is compliance, not price. - Key absence as enforcement. A third-party row is structurally unreachable until its key exists on the machine — locally in
.env, or leased from the org broker at spawn time (see Pairing and credentials). Sensitive roles stay pinned to Anthropic by rule; everything else cannot leak to a provider you never enabled. - Metadata-only matching. Rules match on structured request metadata (
task_class,agent_role,data_class, token estimates, capability needs) — message content never reaches the rule engine, so prompt injection cannot steer routing.
Inspect any decision
maestro router why session.responder
maestro router why '{"task_class":"classify.inbox","data_class":"public"}'
The decision print shows the chosen provider/model with its harness and transport, a one-line explain, the lane and estimated cost, the full resolved chain, and tried[] — every candidate that was rejected and why. maestro router validate strict-validates the config against the catalog and exits non-zero on errors.
Observability
Every routed spawn writes a routed event to logs/daemon/<date>-sessions.jsonl recording the backend, transport, model, the candidates tried, and any fallback reason — the raw material for cost-per-agent and failover counters. Router health and breaker state surface on the diagnostics dashboard alongside the rest of the observability spine.
Backends differ in tool-call reliability, and the catalog grades them. Session work defaults to needs_tool_use: true, which keeps low-grade rows structurally unreachable for tool-using sessions — opt out per rule (needs_tool_use: false) only for genuinely tool-less work like classification and lookups.