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.yaml to .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: sensitive means unlabeled work can never route to a backend whose allowed_data_classes excludes 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.

Route tool-using work carefully

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.