Per-role model routing
Model routing means giving each task the model it needs, and it is
the 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, so cheap even on a
frontier model); the workers execute (long outputs, which is where the
cheap model saves real money):
from loomflow import Agent, BudgetConfig, StandardBudget
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. Nothing special is needed, because 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.
Each role takes its own model: a spec string, a Model instance, or
None (the default), which uses the agent’s main model. A spec string
gets its API key the way the main model does, through the agent’s
Tuning(secrets=...) backend when one is set:
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"), RetryPolicy()),
or a whole FallbackModel chain.
The cheap-classifier router
Team.router’s own model does exactly one thing: classify the
request. Each route’s agent carries its own model, so the frontier
model never spends tokens on the routing decision:
from loomflow import Agent
from loomflow.architecture import RouterRoute
from loomflow.team import Team
team = Team.router(
[
RouterRoute(
"billing",
Agent("You handle billing questions.", model="claude-sonnet-4-6"),
description="Invoices, refunds, payment methods.",
),
RouterRoute(
"escalation",
Agent("You handle angry or complex cases.", model="claude-fable-5"),
description="Complaints, legal threats, anything unusual.",
),
],
model="claude-haiku-4-5", # ONLY classifies
)Sidecar models
The framework’s helper calls each accept their own (usually cheap) model. Same idea, smaller scopes:
Agent(run_until={"checker": "claude-haiku-4-5", ...}): the goal-met checkAgent(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.