Smart Routing

Smart routing runs an agent's background turns — scheduled tasks, companion outbox generation, anything the runtime starts on its own — on a cheaper model, and escalates to the agent's own model if that fails. It never touches a turn you are watching.

It is off by default. Turn it on when you want it:

mur model smart on                     # global; cheap model auto-picked
mur model smart on --cheap deepseek_v4_flash   # or pin the cheap model
mur model smart off

mur agent smart researcher on          # this agent, regardless of the global
mur agent smart researcher off
mur agent smart researcher follow      # clear the override, inherit the global

What it will not do

A router may only substitute a model that can serve the request. Price orders the eligible set; it does not decide who is in it.

That rule exists because the alternative is silent. A scheduled image-recognition turn once got handed to a cheap text-tier model: the picker ranked candidates by cost and never asked whether they could see. Nothing downstream catches it — escalation triggers on a malformed reply, and a confidently wrong recognition is perfectly well-formed — and the routing caption only renders in Hub chat, which a scheduled turn never reaches. (You can now read those decisions back with mur agent routing, which is the other half of the fix.)

So before substituting, MUR reads what the request actually needs:

The request carriesThe substitute must
an imagedeclare vision in its registry capabilities
a tool listnot have declared capabilities that omit tools

The two are treated differently on purpose, and the difference follows how each one fails:

  • Vision — silence disqualifies. A model that cannot see answers with confident nonsense. That is silent and unrecoverable for that turn, so an entry that says nothing about vision is assumed not to have it. No provider catalog fills this in yet, which means image requests currently find no cheap candidate at all and simply run on the agent's own model.
  • Tools — silence is permitted. A model that cannot call tools is rejected by the provider, loudly, and the fallback chain advances on its own. Treating silence as incapacity would drop every entry written before capabilities existed — most of a real registry — out of the chain of every tool-carrying turn.

An entry that does list capabilities is taken at its word either way: enumerating what you can do and leaving tools out is a statement, not silence.

What is never filtered

Your explicit choices. The agent's model_ref, the global models.default, and a pinned re-run all go through untouched — if one of them cannot serve the request, it fails loudly rather than being quietly swapped. Filtering applies only to models MUR chose: the Smart cheap model, a difficulty-routed pick, and fallback-chain members.

If filtering leaves nothing, the request fails with the real error. A router that answers an image question with a blind model is worse than one that stops.

Seeing what the router did

The capability gate stops the failure MUR can recognise: a model that cannot serve the request. It cannot tell you the cheap model was simply worse at something — no automated check can, without paying to run the turn twice. For that you need to be able to look.

mur agent routing researcher                    # newest 20 decisions
mur agent routing researcher --downgrades-only  # only turns Smart chose for you
mur agent routing researcher --limit 100
TIME                  INTENT                  MODEL                REASON             OUTCOME   TASK
2026-09-01T13:55:0…   background/scheduled    ↓deepseek-v4-flash   smart-background   ok        the scheduled turn's prompt…
2026-09-01T02:00:0…   background/scheduled    ↓deepseek-v4-flash   smart-background   ok        …

1850 routing decisions · ↓ 4 chosen for you by Smart, on deepseek-v4-flash

↓ marks a model MUR chose. fallback-advance and explicit are not downgrades — those are your own configuration taking effect after a failure or a pin.

The summary line counts every decision on disk, not the rows printed. A "no downgrades" line that spoke only for the last twenty turns would be the same kind of half-truth the command exists to remove.

This reads ~/.mur/agents/<name>/telemetry/*.jsonl, which the runtime has been writing all along. Nothing new is recorded; what was missing was a way to read it outside the Hub chat caption that background turns never reach.

Per-agent inheritance

An agent has three states, and follow is a real one rather than something inferred from a blank field:

mur agent smart <name> …Effect
followinherit models.smart (the default for every agent)
onSmart on for this agent, whatever the global says
offSmart off for this agent, whatever the global says

Overrides merge field by field, so setting one field never resets the others: pinning a cheap model for one agent does not silently disable Smart for it. The same applies to the difficulty-routing override in models.routing.

The Hub shows the same three states under an agent's model settings.

Configuration

# ~/.mur/config.yaml
models:
  smart:
    enabled: false        # opt-in
    cheap: null           # null = auto-pick the cheapest eligible chat model
    max_escalations: 1    # escalate at most once on a structural failure
# ~/.mur/agents/<name>/profile.yaml — every field optional, absent = inherit
smart:
  enabled: true

Profiles written before this field existed carry the override nested under routing.smart. That location is still read, permanently — exported .muragent bundles contain it — and the first time you set the value through the CLI or Hub it is migrated up, so the setting ends up living in one place.