[Platform] Add ModelFailoverPlatform bridge#1864
Conversation
6e00a10 to
01539f3
Compare
a48bcec to
fba33db
Compare
Add a new bridge that complements FailoverPlatform (which chains platforms) by chaining models on a single platform. When the requested model fails, it tries each fallback model in order on the same underlying platform. This is useful when a provider offers multiple models and some may be temporarily unavailable or rate-limited (e.g. OpenRouter free models, or trying GPT-4o before falling back to GPT-4o-mini on the same OpenAI platform). The bridge includes: - ModelFailoverPlatform (PlatformInterface decorator) - ModelFailoverPlatformFactory - Bundle configuration (model_failover platform type) - Deptrac layer definition - Comprehensive test suite (7 tests)
fba33db to
8f1ba28
Compare
|
Could we work toward a fusion between this PR and #1808? |
I think they solve two different use cases, no? |
|
From my POV, you're trying to solve an issue on |
I made it to draft for now as I'm still refining the functionality for my project, generally speaking I like your approach more but for my usecase this seems the way to go. |
|
I pushed a new commit that contains the model override on the current implementation: da951e5, not sure if it will lend on |
…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
Summary
FailoverPlatformchains multiple platforms — when one provider fails, it tries the next provider. But there's no native support for chaining multiple models on a single platform.ModelFailoverPlatformfills this gap: aPlatformInterfacedecorator that tries multiple models in sequence on the same underlying platform. When the requested model fails, it tries each fallback model in order.Use cases:
openrouter/free→minimax:free→glm:free)gpt-4o-minibeforegpt-4oon the same OpenAI platform)Usage
Composes naturally with
FailoverPlatform:What's included
ModelFailoverPlatform—PlatformInterfacedecorator (same patterns asFailoverPlatform)ModelFailoverPlatformFactory— static factorymodel_failoverplatform type)splitsh.jsonentryRelationship to FailoverPlatform