Mechanism verified 2026-07-05 against the live Workflow and Agent tool schemas (Claude Code v2.1.x session surface). Model strings are aliases the harness resolves at spawn time — this doc deliberately never names a dated model ID.
Scope: the in-process half of routing (native
agent()calls sharing the session's provider). The cross-provider half (GLM/Codex process workers) and the shared work-class taxonomy live in fleet-worker/references/model-routing.md.
A 7-day session-log audit (2026-06-28..07-05, all projects on this machine):
Root cause is a default, not a decision: a Workflow agent() call inherits
the main-loop model and effort unless overridden, and nobody sets overrides.
On a Fable/Opus session, every "grep the logs and return JSON" stage runs on the
most expensive brain available. The fix is one line per collect-stage.
await agent(prompt, {
model: 'haiku', // 'sonnet' | 'opus' | 'haiku' | 'fable' — omit to inherit session model
effort: 'low', // 'low' | 'medium' | 'high' | 'xhigh' | 'max' — omit to inherit session effort
// ...label, phase, schema, isolation, agentType as usual
});
Facts that matter (all schema-sourced):
'opus' means "the current Opus slot", resolved by the
harness. Never hard-code dated model IDs in a workflow script — they rot.meta.phases[].model is display metadata. It annotates the phase in the
/workflows progress UI so the override is visible; it does not route
anything. opts.model on the agent() call is what routes. Set both — the
annotation is how a reader audits the run's routing at a glance.model param for single subagent spawns,
and it composes with custom agentType/subagent_type (an Explore scout on
haiku is legal). Exception: fork-type agents always inherit — model is
ignored for subagent_type: 'fork'.effort is a reasoning-depth knob, not just a price knob. Low effort
buys shallower thinking. Cheap and shallow is exactly right for extraction;
it is exactly wrong for a verdict.opts.schema, validation runs at the tool-call layer and re-asks on mismatch
— a haiku extractor can't hand back malformed JSON, only wrong content, which
is the verifier's job to catch anyway.| Stage type | Typical stages | Override |
|---|---|---|
| Mechanical collect | StructuredOutput extraction, log scans, dedup, classification, format conversion, count/enumerate | { model: 'haiku', effort: 'low' } |
| Broad sweep | finders, per-dimension reviewers, read-and-summarize, multi-modal search legs | { model: 'sonnet' } (+ effort: 'low' when the sweep is wide and mechanical) |
| Decide | adversarial verifiers, judges, synthesis, final report, anything expensive-if-wrong | omit both — inherit the session's premium model |
The rule of thumb: the stage that DECIDES stays premium; the stages that COLLECT go cheap. When unsure, omit the override — a wrong cheap answer that survives verification costs more than the override saves.
Corollaries (carried over from the shared taxonomy, they bind here too):
'opus', because on a Fable session that's a downgrade.effort: 'low' is often the bigger saving with no quality cliff.Extraction stage (the 729-calls-a-week shape):
// before — inherits Fable/Opus for a grep-and-format job
const flaky = await agent('Scan CI logs for retry markers; return the list.', { schema: FLAKY });
// after — one line
const flaky = await agent('Scan CI logs for retry markers; return the list.',
{ schema: FLAKY, model: 'haiku', effort: 'low' });
Review fan-out — sweep cheap, verify premium:
const results = await pipeline(
DIMENSIONS,
d => agent(d.prompt, { label: `review:${d.key}`, phase: 'Review',
schema: FINDINGS, model: 'sonnet', effort: 'low' }),
review => parallel(review.findings.map(f => () =>
agent(`Adversarially verify: ${f.title}`,
{ label: `verify:${f.file}`, phase: 'Verify', schema: VERDICT })
// no model/effort — the decider inherits the premium session model
))
);
Single Agent-tool spawn (same param, same doctrine):
Agent({ subagent_type: 'Explore', model: 'haiku',
prompt: 'List every file that constructs a cache key…' })
loop-estimate.py + assets/model-pricing.json.opts.model picks a tier on the session's
provider; pointing a stage at GLM/Codex requires an OS-process worker
(fleetflow/fleet-worker) — see the locus rule in
fleet-worker/references/model-routing.md.opts.model/opts.effort sit in the progress/structure surface.route('mechanical') etc.).