![[Quick Tip] I tried out the Model Routing feature that was added to Strands Agents](https://devio2024-media.developers.io/image/upload/f_auto,q_auto,w_3840/f_auto,q_auto/v1790874980/eyecatch/jwxssnrw4pk9ej29hn8k.png)
[Quick Tip] I tried out the Model Routing feature that was added to Strands Agents
This page has been translated by machine translation. View original
Introduction
Hello, I'm Kamino from the Consulting Division, currently practicing my driving. It feels great when you nail parking on the first try...!
While working through a hands-on article for Strands Decider the other day, I noticed a class called ModelRouter being used for model selection!
Hmm, I didn't remember model routing being a thing... So I looked into it, and it turns out that a Model Routing feature had been added to Strands Agents!
Looking back through the release notes, ModelRouter was added in v1.52.0 on August 12, 2026, and the documentation page was added in v1.55.0 on September 8. It seems it was added while I was away... I'm so frustrated I missed it...
Let's give it a try right away!
Overview of Model Routing
Model Routing is a mechanism that selects "which model should answer" from among candidates on each call. You simply group the candidate models into a ModelRouter and pass it to Agent(model=router), so the code on the calling side remains unchanged from before.
The image below gives you an idea of what it looks like.
By default you can use Sonnet, and fall back to Haiku when it's unavailable, or you can route based on the content of requests!

Previously, you had to write your own logic to retry with a different model when the main model failed, or to select a model based on the request content before creating an Agent — so it's great to be able to delegate this to the SDK!
Which model to select is specified by the routing strategy (RoutingStrategy). There are three types of strategies available:
| Strategy | How it selects |
|---|---|
| FallbackStrategy | Default. Tries models in declared order, switching to the next candidate on failure. |
| ClassifierStrategy | A classifier model looks at the request content and selects the first model to answer. Does not switch on failure. |
| Custom RoutingStrategy | Write your own. You can also combine the two above. |
Note that only stateless models (those that do not maintain conversation history on the provider side) can be used as candidates.
Prerequisites
Here are the prerequisites for this walkthrough!
| Item | Version / Setting |
|---|---|
| Python | 3.13.13 |
| strands-agents | 1.59.0 |
| Models | Amazon Bedrock Claude Haiku 4.5 / Claude Sonnet 4.6 |
| Region | ap-northeast-1 |
uv init --python 3.13 strands-model-routing
cd strands-model-routing
uv add strands-agents
Switching to a backup model on failure with FallbackStrategy
FallbackStrategy is used when no strategy is specified. It tries models in the declared order and switches to the next candidate if a call fails.
To see the switching in action, I intentionally specified a non-existent model ID as the first choice.
import logging
from strands import Agent
from strands.models import BedrockModel, ModelRouter
logging.basicConfig(format="%(name)s | %(message)s")
logging.getLogger("strands.models.routing").setLevel(logging.INFO)
# Intentionally use a non-existent model ID as the first choice
primary_model = BedrockModel(model_id="jp.anthropic.claude-not-exist-v1:0")
backup_model = BedrockModel(model_id="jp.anthropic.claude-haiku-4-5-20251001-v1:0")
router = ModelRouter(models=[primary_model, backup_model], max_switches=1)
agent = Agent(model=router, callback_handler=None)
result = agent("Please answer the capital of Japan in one word.")
print(result)
max_switches is the maximum number of times candidates can be switched within a single call.
Let's try it!
uv run fallback.py
strands.models.routing.router | strategy=<FallbackStrategy>, candidate=<BedrockModel/jp.anthropic.claude-not-exist-v1:0>, model=<BedrockModel/jp.anthropic.claude-not-exist-v1:0> | candidate selected
strands.models.routing.router | from_candidate=<BedrockModel/jp.anthropic.claude-not-exist-v1:0>, to_candidate=<BedrockModel/jp.anthropic.claude-haiku-4-5-20251001-v1:0>, error=<ValidationException> | model call failed, switching candidate
Tokyo
On the first line, the non-existent model is selected, and on the second line, a ValidationException triggers a switch to Haiku 4.5. It returned "Tokyo" without throwing an error! It's convenient not having to handle this yourself.
Routing based on request content with ClassifierStrategy
Next, let's try this approach when you want to route based on the content of the request. A classifier model receives the request content and candidate descriptions, then selects which candidate should answer. This time, I used Haiku 4.5 as the classifier, routing simple requests to Haiku 4.5 and requests requiring more reasoning to Sonnet 4.6.
import logging
from strands import Agent
from strands.models import BedrockModel, ClassifierStrategy, ModelRouter, RoutingCandidate
logging.basicConfig(format="%(name)s | %(message)s")
logging.getLogger("strands.models.routing").setLevel(logging.INFO)
# Model that decides the routing
classifier_model = BedrockModel(
model_id="jp.anthropic.claude-haiku-4-5-20251001-v1:0",
max_tokens=64,
streaming=False,
temperature=0,
)
router = ModelRouter(
models=[
RoutingCandidate(
BedrockModel(model_id="jp.anthropic.claude-haiku-4-5-20251001-v1:0"),
name="fast",
description="Requests that can be answered quickly, such as greetings, short questions, and simple translations",
metadata={"cost": "low", "latency": "low"},
),
RoutingCandidate(
BedrockModel(model_id="jp.anthropic.claude-sonnet-4-6"),
name="strong",
description="Requests requiring a lot of reasoning, such as multi-condition designs, comparative analysis, and debugging",
metadata={"cost": "high", "latency": "medium"},
),
],
strategy=ClassifierStrategy(classifier_model),
)
requests = [
"Please translate 'Good morning' into English.",
"Please compare how to design DynamoDB write conflicts in a multi-region active/active configuration.",
]
for request in requests:
print(f"\n### {request}")
agent = Agent(model=router, callback_handler=None)
result = agent(request)
print(str(result)[:80].replace("\n", " "), "...")
Candidate models are specified with RoutingCandidate, with a name, description, and metadata attached. The classifier model uses these three pieces of information to select the routing destination, so make sure to write concretely what kind of requests each model is suited for.
metadata can contain criteria like cost and latency, with string keys and JSON-serializable values.
uv run classifier.py
### Please translate 'Good morning' into English.
strands.models.routing.router | strategy=<ClassifierStrategy>, candidate=<fast>, model=<BedrockModel/jp.anthropic.claude-haiku-4-5-20251001-v1:0> | candidate selected
The English translation of 「おはようございます」is: **Good morning.** For a more polite expression: - **Good morning, sir/madam.* ...
### Please compare how to design DynamoDB write conflicts in a multi-region active/active configuration.
strands.models.routing.router | strategy=<ClassifierStrategy>, candidate=<strong>, model=<BedrockModel/jp.anthropic.claude-sonnet-4-6> | candidate selected
# DynamoDB Multi-Region Active/Active Write Conflict Design ## Conflict Mechanism ``` Region A (ap-nort ...
The translation request was routed to fast (Haiku 4.5), and the DynamoDB design comparison was routed to strong (Sonnet 4.6). The routing worked exactly as intended!
Since ClassifierStrategy makes one additional call to the classifier model before the actual response, this adds latency and token usage. Also, if the selected model fails, it does not switch to another candidate. These are trade-offs, so consider adopting this approach in light of what you want to do and the speed requirements.
Supporting both routing and fallback with a custom RoutingStrategy
With the built-in strategies, you cannot use routing and fallback switching at the same time. When you need both, you write your own class with a single select method as a RoutingStrategy.
Following the documentation example, I created a class that delegates the initial selection to ClassifierStrategy and subsequent selections after failure to FallbackStrategy. If there have been no attempts yet (context.attempts is empty), we know it's the initial selection.
class ClassifyThenFallback:
def __init__(self, classifier: ClassifierStrategy) -> None:
self._classifier = classifier
self._fallback = FallbackStrategy()
async def select(self, context: RoutingContext, **kwargs: Any) -> RoutingCandidate | None:
# Initial selection uses classification, post-failure selection uses fallback
if not context.attempts:
return await self._classifier.select(context, **kwargs)
return await self._fallback.select(context, **kwargs)
Pass strategy=ClassifyThenFallback(ClassifierStrategy(classifier_model)) to ModelRouter. To see the switching happen, I intentionally used a non-existent model ID for the strong side so it would fail after being selected.
Full custom.py
import logging
from typing import Any
from strands import Agent
from strands.models import (
BedrockModel,
ClassifierStrategy,
FallbackStrategy,
ModelRouter,
RoutingCandidate,
RoutingContext,
RoutingStrategy,
)
logging.basicConfig(format="%(name)s | %(message)s")
logging.getLogger("strands.models.routing").setLevel(logging.INFO)
class ClassifyThenFallback:
def __init__(self, classifier: ClassifierStrategy) -> None:
self._classifier = classifier
self._fallback = FallbackStrategy()
async def select(self, context: RoutingContext, **kwargs: Any) -> RoutingCandidate | None:
# Initial selection uses classification, post-failure selection uses fallback
if not context.attempts:
return await self._classifier.select(context, **kwargs)
return await self._fallback.select(context, **kwargs)
classifier_model = BedrockModel(
model_id="jp.anthropic.claude-haiku-4-5-20251001-v1:0",
max_tokens=64,
streaming=False,
temperature=0,
)
strategy: RoutingStrategy = ClassifyThenFallback(ClassifierStrategy(classifier_model))
router = ModelRouter(
models=[
RoutingCandidate(
BedrockModel(model_id="jp.anthropic.claude-haiku-4-5-20251001-v1:0"),
name="fast",
description="Requests that can be answered quickly, such as greetings, short questions, and simple translations",
),
RoutingCandidate(
# Intentionally use a non-existent model ID to cause failure after selection
BedrockModel(model_id="jp.anthropic.claude-not-exist-v1:0"),
name="strong",
description="Requests requiring a lot of reasoning, such as multi-condition designs, comparative analysis, and debugging",
),
],
strategy=strategy,
max_switches=1,
)
agent = Agent(model=router, callback_handler=None)
result = agent("Please compare how to design DynamoDB write conflicts in a multi-region active/active configuration.")
print(str(result)[:80].replace("\n", " "), "...")
uv run custom.py
strands.models.routing.router | strategy=<ClassifyThenFallback>, candidate=<strong>, model=<BedrockModel/jp.anthropic.claude-not-exist-v1:0> | candidate selected
strands.models.routing.router | from_candidate=<strong>, to_candidate=<fast>, error=<ValidationException> | model call failed, switching candidate
# DynamoDB Multi-Region Configuration Write Conflict Design Comparing multiple approaches for write conflict countermeasures in a multi-region active/active configuration ...
On the first line, the DynamoDB design comparison was classified and routed to strong. When strong fails with a ValidationException, the second line shows a switch to fast, and a response is successfully returned!
Beyond this use case, it's also useful whenever you want to build your own custom routing logic!
Conclusion
I tried out the Model Routing feature that was added while I was away!
It seems genuinely useful, and I'm happy it's here.
The hands-on article at the beginning also introduced an example of delegating routing decisions to Strands Decider. It might be interesting to think about whether to use an LLM, Jev, or Decider as the classifier!
I hope this article was helpful in some way. Thank you for reading to the end!
