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
{...}
}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: ... |
messageMapper()andtoolPayloadMapper()throw — 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.
