Per-role model routing
Model routing — the right model for the right task — is the
single biggest lever on an agent bill. Planning and judging need your
best model; typing out code, scoring a thought 0–1, or classifying
a request into a bucket does not. Output tokens cost ~5× input
tokens, and execution is output-heavy while planning is input-heavy —
so putting the frontier model where judgment lives and a cheap model
where typing lives routinely cuts 60–90% of a run’s cost.
Loomflow gives you this at two levels.
Between agents — every Team seat has its own model
Each worker in a team is a full Agent with its own model=. The
coordinator plans and reviews (short outputs — cheap even on a
frontier model); the workers execute (long outputs — where the cheap
model saves real money):
from loomflow import Agent
from loomflow.team import Team
team = Team.supervisor(
model="claude-fable-5", # plans, delegates, reviews
workers={
"coder": Agent(
"Implement exactly what the delegation says.",
model="claude-sonnet-4-6", # cheap executor
tools=[read_tool, write_tool, bash_tool],
),
"tester": Agent(
"Run tests, report failures verbatim.",
model="claude-haiku-4-5", # mechanical task
tools=[bash_tool],
),
},
budget=StandardBudget(BudgetConfig(max_cost_usd=5.0)),
)The coordinator’s delegate(worker, instructions) calls are the
spec-writing; worker spend decrements the same budget; one audit log
covers the whole tree. This has always worked — every seat is just an
Agent.
Inside one agent — <role>_model= on the reasoning architectures
The single-agent architectures make several kinds of internal call
on what used to be one model. Each role now takes its own model —
spec string, Model instance, or None (= the agent’s main model,
exactly the previous behavior):
from loomflow import Agent
from loomflow.architecture import PlanAndExecute, TreeOfThoughts
# Plan with a frontier model, execute the steps with a cheap one.
agent = Agent(
"You build features.",
model="claude-sonnet-4-6", # synthesis + default
architecture=PlanAndExecute(
planner_model="claude-fable-5", # thinks
executor_model="claude-haiku-4-5", # types
),
)
# ToT's evaluator makes branch_factor × beam_width × depth "rate this
# thought 0-1" calls per run — pure cheap-model territory.
agent = Agent(
"You solve hard problems.",
model="claude-opus-4-8", # proposals + synthesis
architecture=TreeOfThoughts(
evaluator_model="claude-haiku-4-5", # ~12 scoring calls/run
),
)The full kwarg map:
| Architecture | Role kwargs | What each role does |
|---|---|---|
PlanAndExecute | planner_model, executor_model, synthesizer_model | decompose · run each step · final answer |
TreeOfThoughts | proposer_model, evaluator_model, synthesizer_model | propose thoughts · score 0–1 · final answer |
ReWOO | planner_model, solver_model | plan tool steps · solve over results |
Reflexion | evaluator_model, reflector_model | pass/fail check · one-sentence lesson |
SelfRefine | critic_model, refiner_model | critique the draft · apply the critique |
The base attempt / initial draft / main tool loop always stays on the
agent’s model= — that is where quality lives, and keeping the hot
path on one model preserves its
prompt-cache prefix.
Role models are not auto-wrapped with retries (same convention as
the run_until checker). Pass a wrapped instance for resilience:
evaluator_model=RetryingModel(AnthropicModel("claude-haiku-4-5")) —
or a whole FallbackModel chain.
The cheap-classifier router
Team.router’s own model does exactly one thing: classify the
request. Its workers each carry their own model — so the frontier
never sees the routing decision at all:
team = Team.router(
model="claude-haiku-4-5", # ONLY classifies
workers={
"billing": Agent(..., model="claude-sonnet-4-6"),
"escalation": Agent(..., model="claude-fable-5"),
},
)Sidecar models (already routable)
The framework’s helper calls have accepted their own cheap models since before this release — same idea, smaller scopes:
Agent(run_until={"checker": "claude-haiku-4-5", ...})— the goal-met checkTuning(tool_result_summarizer=...)— tool-output compressionTuning(auto_compact_summariser=...)— context compactionLLMJudge(model=...)in the eval harnessModerationGuard(model=...)in guardrails
Routing is explicit by design — loomflow never silently swaps
models on you. Every call’s serving model is visible on its telemetry
span (gen_ai.request.model), so you can verify exactly where tokens
went. Automatic heuristic routing (“auto mode”) is deliberately out of
scope until it can be validated against the eval harness.
See examples/31_model_routing.py
for a runnable offline demo that proves which model served which call.