This package provides you with a RouterProvider component. It is a proxy that implements AIProviderInterface and routes inference calls (chat, stream, structured) to different underlying providers based on a routing strategy you define.
The agent doesn't know it's talking to a router, it's a drop-in replacement for any Neuron AI provider.
This is possible thanks to the Unified Messaging Layer that Neuron AI provides, with full support for multi-modality. Documentation here: https://docs.neuron-ai.dev/agent/messages
- Route
structured()calls to a provider with better structured output support (e.g., OpenAI) while using another provider forchat() - Use different providers depending on the content of the messages (e.g., route image-heavy requests to Gemini)
- Switch providers based on whether tools are present in the request
- Implement cost-based or latency-based routing logic
Install the composer package:
composer require neuron-ai/router
use NeuronAI\Router\RouterProvider;
use NeuronAI\Router\Rules\MethodRule;
use NeuronAI\Providers\Anthropic\Anthropic;
use NeuronAI\Providers\OpenAI\OpenAI;
class MyAgent extens Agent
{
protected function provider(): AIProviderInterface
{
return RouterProvider::make()
->addProvider('anthropic', new Anthropic(
key: 'ANTHROPIC_API_KEY',
model: 'claude-sonnet-4-20250514',
))
->addProvider('openai', new OpenAI(
key: 'OPENAI_API_KEY',
model: 'gpt-4o',
))
->setRule(
new RoundRobinRule(['anthropic', 'openai'])
);
}
protected function instructions(): string
{...}
protected function tools(): array
{...}
}The router delegates messageMapper() and toolPayloadMapper() to an underlying provider. After each inference call, these delegate to whichever provider the routing rule selected. If you need the mappers available before any inference call (e.g., during agent bootstrapping), set a default:
RouterProvider::make()
->addProvider('anthropic', new Anthropic(...))
->addProvider('openai', new OpenAI(...))
->setDefaultProvider('anthropic')
->setRule(new RoundRobinRule(['anthropic', 'openai']));The default is overwritten each time the routing rule resolves a provider, so it only acts as the initial fallback.
Routing logic is defined via the RoutingRuleInterface. The router calls resolveProvider() on the rule, passing context about the current request:
interface RoutingRuleInterface
{
public function resolveProvider(string $method, array $messages, array $tools): string;
}| Parameter | Type | Description |
|---|---|---|
$method |
string |
The inference method: 'chat', 'stream', or 'structured' |
$messages |
array |
The messages being sent to the provider |
$tools |
array |
The tools configured for this request |
The method must return the name of a registered provider (as a string).
Routes based on the inference method. Set a default provider and optionally override specific methods:
use NeuronAI\Router\Rules\MethodRule;
$router = RouterProvider::make()->addProvider(...);
// Use Anthropic for everything, except structured output which goes to OpenAI
$router->setRule(
new MethodRule('anthropic')->structured('openai')
)
// Override each method individually
$router->setRule(
new MethodRule('openai')
->chat('anthropic')
->stream('anthropic')
->structured('openai')
)Wraps a callable for maximum flexibility. Use this when you need to inspect messages or tools:
use NeuronAI\Router\Rules\CallbackRule;
// Route based on tools presence
$router->setRule(new CallbackRule(function (string $method, array $messages, array $tools): string {
if (count($tools) > 0) {
return 'anthropic';
}
return 'openai';
}))Distributes requests evenly across providers in sequence. Each call cycles to the next provider:
use NeuronAI\Router\Rules\RoundRobinRule;
// Alternate between Anthropic and OpenAI for each request
$router->setRule(
new RoundRobinRule(['anthropic', 'openai'])
)Routes based on the content blocks inside messages (images, files, audio, video). When a message contains a content type that not all providers support, you can route it to one that does:
use NeuronAI\Router\Rules\ContentRule;
// Use Anthropic by default, route images and video to Gemini, files to OpenAI
$router->setRule(
new ContentRule('anthropic')
->image('gemini')
->video('gemini')
->file('openai')
)When multiple content types are present in the same request, precedence is: video → audio → image → file → default. Content types without a configured provider are ignored and fall through to the next type in the precedence order.
Implement RoutingRuleInterface to create your own routing logic:
use NeuronAI\Router\Rules\RoutingRuleInterface;
class ImageAwareRule implements RoutingRuleInterface
{
public function __construct(
private string $defaultProvider,
private string $imageProvider,
) {}
public function resolveProvider(string $method, array $messages, array $tools): string
{
foreach ($messages as $message) {
foreach ($message->getContents() as $content) {
if ($content instanceof ImageContent) {
return $this->imageProvider;
}
}
}
return $this->defaultProvider;
}
}Then use it:
$router->setRule(
new ImageAwareRule(
defaultProvider: 'anthropic',
imageProvider: 'gemini',
)
)Inject the router just like any other provider — either via setAiProvider() or by overriding the provider() method:
class MyAgent extends Agent
{
protected function provider(): AIProviderInterface
{
return RouterProvider::make()
->addProvider('anthropic', new Anthropic(
key: 'ANTHROPIC_API_KEY',
model: 'claude-sonnet-4-20250514',
))
->addProvider('openai', new OpenAI(
key: 'OPENAI_API_KEY',
model: 'gpt-4o',
))
->setRule(
new RoundRobinRule(['anthropic', 'openai'])
);
}
}The router throws ProviderException with clear messages for misconfiguration:
| Scenario | Error Message |
|---|---|
| No routing rule set | no routing strategy configured. Call setRule() to set one. |
| No providers registered | no providers registered. Call addProvider() to add one. |
| Rule returns unknown name | unknown provider 'name'. Available: ... |
| Unknown default provider | unknown provider 'name'. Available: ... |
| Mapper called with no default or prior call | no provider available for delegation. Call setDefaultProvider() or make an inference call first. |
messageMapper()andtoolPayloadMapper()delegate to the last-resolved provider (or the default). These are internal to each concrete provider and are never called by the agent directly.setHttpClient()is forwarded to all registered providers.- The routing rule is called on every inference request, so keep it fast.
