Skip to content

[Agent] Add Model Router system for intelligent model selection#1090

Draft
wachterjohannes wants to merge 1 commit intosymfony:mainfrom
wachterjohannes:feature/model-router
Draft

[Agent] Add Model Router system for intelligent model selection#1090
wachterjohannes wants to merge 1 commit intosymfony:mainfrom
wachterjohannes:feature/model-router

Conversation

@wachterjohannes
Copy link
Copy Markdown
Contributor

Q A
Bug fix? yes/no
New feature? yes/no
Docs? yes/no
Issues Fix #...
License MIT

Implements the Model Router architecture, enabling intelligent model selection based on input characteristics. This allows agents to automatically route requests to appropriate models (e.g., vision models for images, speech models for audio).

Core Infrastructure:

  • RouterInterface: Universal contract for all router types
  • RoutingResult: Result with model name, optional transformer, reason, and confidence
  • TransformerInterface: Contract for input transformation
  • RouterContext: Provides platform access, model catalog, and metadata
  • SimpleRouter: Callable-based router for flexible routing logic
  • ChainRouter: Composite router for trying multiple routers in sequence
  • ModelRouterInputProcessor: Integration with Agent's input processing pipeline

Input Enhancement:

  • Add optional platform property to Input class (backward compatible)
  • Update Agent to pass platform when creating Input
  • Platform available to input processors via Input.getPlatform()

Includes 6 examples demonstrating different routing patterns and comprehensive test coverage (14 router tests, all 187 agent tests passing).

Key Features:

  • No breaking changes - all changes are backward compatible
  • Extensible design - custom routers in <20 lines
  • Multi-provider support - works with any Platform implementation
  • Transformation support - routers can transform input before invocation
  • Default model fallback - routers can access agent's default model
  • Capability-based routing - find models supporting specific capabilities

Architecture Decisions:

  • Single RouterInterface for all router types enables composition
  • Transformer in RoutingResult (router decides both model and transformation)
  • Platform passed via Input (no breaking changes to interfaces)
  • RouterContext provides metadata including default model
  • Start simple with SimpleRouter, add complexity gradually

Implements Phase 1 of the Model Router architecture, enabling intelligent
model selection based on input characteristics. This allows agents to
automatically route requests to appropriate models (e.g., vision models
for images, speech models for audio).

Core Infrastructure:
- RouterInterface: Universal contract for all router types
- RoutingResult: Result with model name, optional transformer, reason, and confidence
- TransformerInterface: Contract for input transformation
- RouterContext: Provides platform access, model catalog, and metadata
- SimpleRouter: Callable-based router for flexible routing logic
- ChainRouter: Composite router for trying multiple routers in sequence
- ModelRouterInputProcessor: Integration with Agent's input processing pipeline

Input Enhancement:
- Add optional platform property to Input class (backward compatible)
- Update Agent to pass platform when creating Input
- Platform available to input processors via Input.getPlatform()

Includes 6 examples demonstrating different routing patterns and comprehensive
test coverage (14 router tests, all 187 agent tests passing).

Key Features:
- No breaking changes - all changes are backward compatible
- Extensible design - custom routers in <20 lines
- Multi-provider support - works with any Platform implementation
- Transformation support - routers can transform input before invocation
- Default model fallback - routers can access agent's default model
- Capability-based routing - find models supporting specific capabilities

Architecture Decisions:
- Single RouterInterface for all router types enables composition
- Transformer in RoutingResult (router decides both model and transformation)
- Platform passed via Input (no breaking changes to interfaces)
- RouterContext provides metadata including default model
- Start simple with SimpleRouter, add complexity gradually
@wachterjohannes
Copy link
Copy Markdown
Contributor Author

@chr-hertel this is my first protoype of the router - don't look to deep into the code - just the architecture stuff and there will be more as soon as we merged the first part.

following things i have planned:

  1. Basic routing
  2. Rule-based declarative routing (basically modelflow-ai decission tree)
  3. AI-powered routing

Another time - i have to refactor this code as soon as we are OK with the patterns behind it :)

chr-hertel added a commit that referenced this pull request Apr 20, 2026
…ting layer (chr-hertel)

This PR was squashed before being merged into the main branch.

Discussion
----------

[Platform] Introduce Provider abstraction and model routing layer

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| Docs?         | no
| Issues        |
| License       | MIT

## Summary

This PR introduces a **Provider** abstraction layer between `PlatformInterface` and the actual inference backends, along with a **ModelRouter** for routing. This is the foundational first step toward solving several long-standing architectural pain points.

### The Problem

The Platform component has a fundamental 1:1 coupling: one `Platform` instance = one inference backend. This causes:

- **No unified entry point for multi-provider setups** — users needing OpenAI + Anthropic must manage two separate `Platform` instances
- **Static ModelCatalogs are a maintenance burden** (#1692) — 38+ hardcoded catalogs need constant updates as models change faster than library releases
- **Model-level failover/routing has no natural home** — `FailoverPlatform` only fails over between whole platforms; #1864 wants model-level failover, #1808 wants model override in failover
- **Input-based model routing belongs at Platform level** — #1090 puts it at Agent level, but it's a Platform concern
- **Connector concept** (#646) — old sketch to introduce a layer below Platform for connection concerns never materialized

### The Solution

```
User Code
    |
PlatformInterface (UNCHANGED)
    |
Platform (refactored: router over providers)
    |-- ModelRouterInterface (routes model name → provider)
    |-- CompositeModelCatalog (merges provider catalogs)
    |-- ModelRoutingEvent (pre-routing hook, can set provider to skip router)
    |
ProviderInterface (NEW)
    |
Provider (extracted from current Platform logic)
    |-- ModelClientInterface[] / ResultConverterInterface[] / Contract / ModelCatalog
```

**`ProviderInterface`** encapsulates what `PlatformFactory::create()` currently produces — a complete, self-contained connection to a single inference backend (model clients, result converters, contract, catalog).

**`Platform`** becomes a thin routing layer over multiple providers, using a `ModelRouterInterface` to determine which provider handles each request.

**`ModelRoutingEvent`** is dispatched before routing, allowing listeners to modify the model/input/options or set a provider directly to skip the router entirely.

**`PlatformInterface`** remains completely unchanged — all downstream code (Agent, Chat, Store, user code) continues to work without modification.

### What's Included

**Core layer:**
- `ProviderInterface` + `Provider` (extracted from current Platform logic)
- `ModelRouterInterface` + `CatalogBasedModelRouter` (default: routes by catalog lookup)
- `CompositeModelCatalog` (merges catalogs from multiple providers)
- `ModelRoutingEvent` (Platform-level event with optional provider to skip routing)
- Refactored `Platform` as router over providers

**Reference bridge implementation:**
- `OpenAi\ProviderFactory` (replaces `OpenAi\PlatformFactory`)

**30 new tests**, all 510 tests pass (including existing).

### User-Facing DX

```php
// Single provider
$provider = OpenAi\ProviderFactory::create(apiKey: 'sk-...');
$platform = new Platform([$provider]);

// Multi-provider with auto-routing
$openai = OpenAi\ProviderFactory::create(apiKey: 'sk-...');
$anthropic = Anthropic\ProviderFactory::create(apiKey: 'sk-...');

$platform = new Platform([$openai, $anthropic]);

$platform->invoke('gpt-4o', $messages);            // → OpenAI
$platform->invoke('claude-3-5-sonnet', $messages);  // → Anthropic
```

### How This Addresses Existing Work

| PR/Issue | How it's addressed |
|----------|--------------------|
| #646 (Connectors) | `Provider` is the connector concept — encapsulates connection concerns |
| #1692 (ModelCatalog burden) | `CompositeModelCatalog` + dynamic catalogs become first-class; static catalogs per bridge become optional |
| #1753 (BC for model IDs) | Model ID resolution is now a pluggable strategy, not hardcoded |
| #1808 (Model override in failover) | Enables `FailoverModelRouter` with model name translation (follow-up) |
| #1864 (ModelFailoverPlatform) | Enables model-level failover natively via router (follow-up) |
| #1090 (Model Router) | Enables input-based routing at Platform level via `CallableModelRouter` (follow-up) |

## Use-Case: Load Balancing

The `ModelRouterInterface` enables load balancing between providers. For example, round-robin between two OpenAI providers with different API keys:

```php
$openai1 = OpenAi\ProviderFactory::create(apiKey: 'sk-key1');
$openai2 = OpenAi\ProviderFactory::create(apiKey: 'sk-key2');

$platform = new Platform(
    [$openai1, $openai2],
    new RoundRobinModelRouter(),
);

$platform->invoke('gpt-4o', $messages); // → openai1
$platform->invoke('gpt-4o', $messages); // → openai2
$platform->invoke('gpt-4o', $messages); // → openai1
```

The router implementation:

```php
final class RoundRobinModelRouter implements ModelRouterInterface
{
    private int $counter = 0;

    public function resolve(string $model, iterable $providers, array|string|object $input, array $options = []): ProviderInterface
    {
        $candidates = [];
        foreach ($providers as $provider) {
            if ($provider->supports($model)) {
                $candidates[] = $provider;
            }
        }

        if ([] === $candidates) {
            throw new ModelNotFoundException(\sprintf('No provider found for model "%s".', $model));
        }

        return $candidates[$this->counter++ % \count($candidates)];
    }
}
```

The same pattern works for weighted or random distribution — just a different `ModelRouterInterface` implementation. Composes with `ChainModelRouter` too (e.g. explicit routes for some models, load-balanced for the rest).

## Use-Case: Failover / Rate Limit Handling

Failover is a provider-level concern, implemented as a `ProviderInterface` decorator — not a router concern (since routing happens before invocation, it can't react to runtime failures).

**Cross-provider failover** (OpenAI rate-limited → fall back to Anthropic):

```php
$failover = new FailoverProvider('main', [
    OpenAi\ProviderFactory::create(apiKey: 'sk-...'),
    Anthropic\ProviderFactory::create(apiKey: 'sk-...'),
], $rateLimiterFactory);

$platform = new Platform([$failover]);
```

**Nested composition** (load-balanced OpenAI pool with Anthropic as final fallback):

```php
$openaiPool = new FailoverProvider('openai', [$openai1, $openai2], $rateLimiterFactory);
$main = new FailoverProvider('main', [$openaiPool, $anthropic], $rateLimiterFactory);

$platform = new Platform([$main]);
```

This subsumes the existing `FailoverPlatform` entirely — same pattern, but as a `ProviderInterface` decorator instead of a `PlatformInterface` decorator. More composable since it nests with other providers and works seamlessly with the routing layer.

## Use-Case: ModelsDev as Universal Catalog

The Provider/ModelRouter rework shifts the role of the ModelsDev bridge. Its core value becomes clear — it's a **universal, dynamic model catalog** backed by the `symfony/models-dev` database. No hardcoded model lists, no API calls. New models appear with `composer update symfony/models-dev`.

Routing and provider construction move to where they belong: `Platform` and bridge-specific `ProviderFactory` classes.

```php
use Symfony\AI\Platform\Bridge\ModelsDev\ModelCatalog;
use Symfony\AI\Platform\Bridge\OpenAi\ProviderFactory as OpenAiProviderFactory;
use Symfony\AI\Platform\Bridge\Anthropic\ProviderFactory as AnthropicProviderFactory;
use Symfony\AI\Platform\Platform;

// Each bridge handles its own connection concerns (API keys, HTTP, contracts)
// ModelsDev provides the catalog — always up-to-date, no static model lists
$openai = OpenAiProviderFactory::create(
    apiKey: $_ENV['OPENAI_API_KEY'],
    modelCatalog: new ModelCatalog('openai'),
);
$anthropic = AnthropicProviderFactory::create(
    apiKey: $_ENV['ANTHROPIC_API_KEY'],
    modelCatalog: new ModelCatalog('anthropic'),
);

// Platform routes automatically — CatalogBasedModelRouter checks each provider's catalog
$platform = new Platform([$openai, $anthropic]);

$platform->invoke('gpt-4o', $messages);            // → openai (catalog knows it)
$platform->invoke('claude-3-5-sonnet', $messages);  // → anthropic (catalog knows it)
```

All `ProviderFactory` signatures already accept `ModelCatalogInterface` — no new abstraction needed. The static catalogs in each bridge become the fallback default, ModelsDev becomes the dynamic option.

**What remains valuable in ModelsDev:**
- `ModelCatalog` — the universal catalog (core value)
- `CapabilityMapper` — maps models-dev metadata to Capability enum
- `DataLoader` — loads/caches the JSON
- `ProviderRegistry` — discovery ("what providers exist?")

**What shrinks or goes away:**
- `PlatformFactory` — Platform + router handles routing natively now
- `BridgeResolver` — routing is Platform's job, not the catalog's
- `ModelResolver` with `"provider::model"` syntax — catalog-based routing makes this redundant for most cases

The ModelsDev bridge becomes a focused, lean package: **a catalog, not a platform**.

### Follow-Up PRs

This is scoped to the core layer + one bridge. Planned follow-ups:
- Convert remaining 37 bridges from `PlatformFactory` to `ProviderFactory`
- Add `ExplicitModelRouter`, `FailoverModelRouter`, `ChainModelRouter`, `CallableModelRouter`
- `FailoverProvider` decorator (subsumes `FailoverPlatform`)
- AI Bundle integration (DI wiring, routing config section)
- Refocus ModelsDev bridge as universal catalog

Commits
-------

94168f2 [Platform] Introduce Provider abstraction and model routing layer
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant