feat(providers): add multi-model router for task-based provider routing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Argenis 2026-02-15 11:40:58 -05:00 committed by GitHub
parent eadeffef26
commit 1cfc63831c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 537 additions and 9 deletions

View file

@ -5,6 +5,7 @@ pub mod ollama;
pub mod openai;
pub mod openrouter;
pub mod reliable;
pub mod router;
pub mod traits;
pub use traits::Provider;
@ -153,7 +154,7 @@ fn resolve_api_key(name: &str, api_key: Option<&str>) -> Option<String> {
/// Factory: create the right provider from config
#[allow(clippy::too_many_lines)]
pub fn create_provider(name: &str, api_key: Option<&str>) -> anyhow::Result<Box<dyn Provider>> {
let resolved_key = resolve_api_key(name, api_key);
let _resolved_key = resolve_api_key(name, api_key);
match name {
// ── Primary providers (custom implementations) ───────
"openrouter" => Ok(Box::new(openrouter::OpenRouterProvider::new(api_key))),
@ -316,6 +317,71 @@ pub fn create_resilient_provider(
)))
}
/// Create a RouterProvider if model routes are configured, otherwise return a
/// standard resilient provider. The router wraps individual providers per route,
/// each with its own retry/fallback chain.
pub fn create_routed_provider(
primary_name: &str,
api_key: Option<&str>,
reliability: &crate::config::ReliabilityConfig,
model_routes: &[crate::config::ModelRouteConfig],
default_model: &str,
) -> anyhow::Result<Box<dyn Provider>> {
if model_routes.is_empty() {
return create_resilient_provider(primary_name, api_key, reliability);
}
// Collect unique provider names needed
let mut needed: Vec<String> = vec![primary_name.to_string()];
for route in model_routes {
if !needed.iter().any(|n| n == &route.provider) {
needed.push(route.provider.clone());
}
}
// Create each provider (with its own resilience wrapper)
let mut providers: Vec<(String, Box<dyn Provider>)> = Vec::new();
for name in &needed {
let key = model_routes
.iter()
.find(|r| &r.provider == name)
.and_then(|r| r.api_key.as_deref())
.or(api_key);
match create_resilient_provider(name, key, reliability) {
Ok(provider) => providers.push((name.clone(), provider)),
Err(e) => {
if name == primary_name {
return Err(e);
}
tracing::warn!(
provider = name.as_str(),
"Ignoring routed provider that failed to create: {e}"
);
}
}
}
// Build route table
let routes: Vec<(String, router::Route)> = model_routes
.iter()
.map(|r| {
(
r.hint.clone(),
router::Route {
provider_name: r.provider.clone(),
model: r.model.clone(),
},
)
})
.collect();
Ok(Box::new(router::RouterProvider::new(
providers,
routes,
default_model.to_string(),
)))
}
#[cfg(test)]
mod tests {
use super::*;