Introduction
As AI applications move from prototypes to production, routing becomes an important engineering problem.
A simple application may send every request to one model:
Application
|
v
AI Model
That architecture works until the model becomes unavailable, too expensive, too slow, or unsuitable for a particular request.
A production application may instead use several providers or models:
+--> Model A
|
Application --> Router
|
+--> Model B
|
+--> Model C
The difficult question is not simply which model should be used. It is also why the router selected that model and what happens when the selected model cannot complete the request.
This leads to two different routing strategies:
Semantic routing: choose a model based on the meaning, complexity, or characteristics of the request.
Failover routing: use an alternate model when the preferred model fails.
Microsoft.Extensions.AI provides abstractions that make it easier to build provider-independent AI applications and compose AI services. In a multi-provider application, those abstractions can be used as the foundation for routing, fallback, telemetry, and resilience patterns.
The important engineering insight is that semantic routing and failover routing solve different problems. Semantic routing optimizes model selection, while failover routing optimizes availability.
A production system often needs both.
What Is Semantic Routing?
Semantic routing evaluates the request before selecting an AI model.
For example:
User Request
|
v
Classify Request
|
+---- Simple question ----> Fast Model
|
+---- Coding task --------> Coding Model
|
+---- Complex reasoning --> Reasoning Model
The router might consider:
Request category
Prompt complexity
Required context size
Coding requirements
Expected reasoning depth
Latency requirements
Cost constraints
Output format
User tier
A simple routing policy might look like:
if request is simple:
use fast model
else if request is coding-related:
use coding model
else:
use reasoning model
The advantage is that every request does not automatically consume the most expensive model.
What Is Failover Routing?
Failover routing starts with a preferred provider or model and switches to another when the first option cannot serve the request.
Request
|
v
Primary Model
|
X
Failure
|
v
Fallback Model
|
v
Response
Failures may include:
Timeout
Rate limiting
Provider outage
Network failure
Service unavailable response
Temporary capacity problems
The important distinction is that failover does not necessarily determine which model is best for the request.
It determines what should happen when the preferred path fails.
Semantic Routing vs Failover Routing
| Characteristic | Semantic Routing | Failover Routing |
|---|---|---|
| Main goal | Better model selection | Higher availability |
| Decision time | Before execution | During failure recovery |
| Primary input | Request characteristics | Runtime failure |
| Optimization | Cost, quality, latency | Reliability |
| Requires classification | Usually | No |
| Handles outages | Indirectly | Directly |
| Can reduce cost | Yes | Sometimes |
| Can improve availability | Sometimes | Yes |
These strategies should not be treated as competitors.
They can operate together.
A Combined Architecture
A production architecture can look like this:
User Request
|
v
Semantic Router
|
+---------------+---------------+
| | |
v v v
Fast Model Coding Model Reasoning Model
| | |
+---------------+---------------+
|
Failure?
|
+----+----+
| |
No Yes
| |
v v
Response Failover
|
v
Backup Model
This separates two responsibilities:
Semantic Router = Which model should handle this?
Failover Router = What should happen if it cannot?
That separation makes the system easier to test.
Building a Routing Abstraction
A simple .NET application can start with a routing interface:
public interface IAIRouter
{
Task<IChatClient> SelectClientAsync(
string request,
CancellationToken cancellationToken = default);
}
A semantic implementation could classify the request:
public sealed class SemanticAIRouter : IAIRouter
{
private readonly IChatClient _fastClient;
private readonly IChatClient _reasoningClient;
public SemanticAIRouter(
IChatClient fastClient,
IChatClient reasoningClient)
{
_fastClient = fastClient;
_reasoningClient = reasoningClient;
}
public Task<IChatClient> SelectClientAsync(
string request,
CancellationToken cancellationToken = default)
{
var client = request.Length > 1000
? _reasoningClient
: _fastClient;
return Task.FromResult(client);
}
}
This example is intentionally simple.
Production semantic routing should not rely only on prompt length. It could combine classification, historical performance, request type, token estimates, and business rules.
Adding Failover
The next layer can handle runtime failures:
public async Task<ChatResponse> ExecuteWithFailoverAsync(
IChatClient primary,
IChatClient fallback,
IEnumerable<ChatMessage> messages,
CancellationToken cancellationToken)
{
try
{
return await primary.GetResponseAsync(
messages,
cancellationToken: cancellationToken);
}
catch (Exception)
{
return await fallback.GetResponseAsync(
messages,
cancellationToken: cancellationToken);
}
}
The important production improvement is to avoid catching every exception blindly.
For example, an authentication error may not be recoverable by retrying another model.
A better design categorizes failures.
Failure Classification
Consider:
Request Failure
|
+--------------+--------------+
| | |
Retryable Provider Permanent
Failure Failure Failure
| | |
v v v
Retry Failover Return
Examples:
| Failure | Likely Action |
|---|---|
| Timeout | Retry or failover |
| Rate limit | Backoff or failover |
| Temporary service error | Failover |
| Invalid API key | Alert/configuration failure |
| Invalid request | Do not failover blindly |
| Content policy rejection | Apply application policy |
| Malformed response | Retry/failover depending on provider |
Join the conversation! Your thoughts help the community grow.