Skip to Content
ModelsPer-role model routing

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:

ArchitectureRole kwargsWhat each role does
PlanAndExecuteplanner_model, executor_model, synthesizer_modeldecompose · run each step · final answer
TreeOfThoughtsproposer_model, evaluator_model, synthesizer_modelpropose thoughts · score 0–1 · final answer
ReWOOplanner_model, solver_modelplan tool steps · solve over results
Reflexionevaluator_model, reflector_modelpass/fail check · one-sentence lesson
SelfRefinecritic_model, refiner_modelcritique 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 check
  • Agent(tool_result_summarizer=...): tool-output compression
  • Tuning(auto_compact_summariser=...): context compaction
  • LLMJudge(model=...) in the eval harness
  • ModerationGuard(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.

Last updated on