use std::collections::BTreeMap;
use crate::provider::{
CompletionRequest, CompletionResponse, ModelProvider, ModelSelection, ProviderError,
};
pub struct RoutingProvider {
routes: BTreeMap<String, Box<dyn ModelProvider>>,
fallback: Option<Box<dyn ModelProvider>>,
fallback_name: String,
last: String,
}
impl RoutingProvider {
pub fn new() -> RoutingProvider {
RoutingProvider {
routes: BTreeMap::new(),
fallback: None,
fallback_name: "none".to_string(),
last: "router".to_string(),
}
}
pub fn with(mut self, vendor: impl Into<String>, provider: Box<dyn ModelProvider>) -> Self {
self.routes.insert(vendor.into(), provider);
self
}
pub fn or_else(mut self, vendor: impl Into<String>, provider: Box<dyn ModelProvider>) -> Self {
self.fallback_name = vendor.into();
self.fallback = Some(provider);
self
}
pub fn vendors(&self) -> Vec<&str> {
let mut vendors: Vec<&str> = self.routes.keys().map(String::as_str).collect();
if self.fallback.is_some() && !vendors.contains(&self.fallback_name.as_str()) {
vendors.push(&self.fallback_name);
}
vendors.sort_unstable();
vendors
}
pub fn is_empty(&self) -> bool {
self.routes.is_empty() && self.fallback.is_none()
}
pub fn describe(&self) -> String {
match (self.routes.is_empty(), self.fallback.is_some()) {
(true, true) => format!("model calls go to {}", self.fallback_name),
(_, true) => format!(
"model calls route by vendor ({}), otherwise {}",
self.vendors().join(", "),
self.fallback_name
),
(_, false) => format!(
"model calls route by vendor ({}); the artifact must pin one",
self.vendors().join(", ")
),
}
}
fn vendor_of(selection: &ModelSelection) -> Option<&str> {
match selection {
ModelSelection::Exact(reference) => reference.split_once('/').map(|(vendor, _)| vendor),
_ => None,
}
}
fn pick(
&mut self,
selection: &ModelSelection,
) -> Result<&mut dyn ModelProvider, ProviderError> {
if let Some(vendor) = RoutingProvider::vendor_of(selection) {
let vendor = vendor.to_string();
if self.routes.contains_key(&vendor) {
let provider = self.routes.get_mut(&vendor).expect("just checked");
return Ok(provider.as_mut());
}
if self.fallback.is_some() && self.fallback_name == vendor {
return Ok(self
.fallback
.as_mut()
.map(Box::as_mut)
.expect("checked immediately above"));
}
let reachable = match self.vendors() {
vendors if vendors.is_empty() => "none".to_string(),
vendors => vendors.join(", "),
};
return Err(ProviderError::Configuration(format!(
"the artifact pins `{vendor}/…`, and this run has no provider for `{vendor}`\n \
available: {reachable}\n \
export that vendor's key, or override the model with --model"
)));
}
if self.fallback.is_none() {
let reachable = if self.routes.is_empty() {
"nothing".to_string()
} else {
self.vendors().join(", ")
};
return Err(ProviderError::Configuration(format!(
"this artifact names no vendor and no default provider is configured\n \
pin one with `model exact \"<vendor>/<model>\"`; this run can reach: {reachable}"
)));
}
Ok(self
.fallback
.as_mut()
.map(Box::as_mut)
.expect("checked immediately above"))
}
}
impl Default for RoutingProvider {
fn default() -> Self {
RoutingProvider::new()
}
}
impl ModelProvider for RoutingProvider {
fn name(&self) -> &str {
&self.last
}
fn complete(
&mut self,
request: &CompletionRequest,
) -> Result<CompletionResponse, ProviderError> {
let provider = self.pick(&request.model)?;
let name = provider.name().to_string();
let response = provider.complete(request);
self.last = name;
response
}
fn streams(&self) -> bool {
!self.is_empty()
&& self.routes.values().all(|provider| provider.streams())
&& self
.fallback
.as_ref()
.is_none_or(|provider| provider.streams())
}
fn complete_streaming(
&mut self,
request: &CompletionRequest,
on_delta: crate::provider::DeltaSink<'_>,
) -> Result<CompletionResponse, ProviderError> {
let provider = self.pick(&request.model)?;
let name = provider.name().to_string();
let response = provider.complete_streaming(request, on_delta);
self.last = name;
response
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::provider::Usage;
use serde_json::{json, Value};
struct Echo(&'static str);
impl ModelProvider for Echo {
fn name(&self) -> &str {
self.0
}
fn complete(
&mut self,
_request: &CompletionRequest,
) -> Result<CompletionResponse, ProviderError> {
Ok(CompletionResponse {
value: Value::String(self.0.to_string()),
usage: Usage::default(),
model: self.0.to_string(),
})
}
}
fn request(model: ModelSelection) -> CompletionRequest {
CompletionRequest {
node: "n0".into(),
model,
system: None,
prompt: "hello".into(),
context: Vec::new(),
response_type: "markdown".into(),
shape: crate::schema::ResponseShape::Prose,
max_tokens: 100,
}
}
fn router() -> RoutingProvider {
RoutingProvider::new()
.with("openai", Box::new(Echo("openai")))
.with("anthropic", Box::new(Echo("anthropic")))
}
#[test]
fn a_pinned_vendor_decides_which_provider_answers() {
let mut router = router();
for (reference, expected) in [
("openai/gpt-test", "openai"),
("anthropic/claude-test", "anthropic"),
] {
let response = router
.complete(&request(ModelSelection::Exact(reference.into())))
.unwrap();
assert_eq!(response.value, json!(expected));
assert_eq!(router.name(), expected, "the run log names who answered");
}
}
#[test]
fn a_vendor_nobody_serves_is_an_error_rather_than_a_silent_substitution() {
let error = router()
.complete(&request(ModelSelection::Exact("mistral/large".into())))
.unwrap_err();
let text = error.to_string();
assert!(text.contains("mistral"), "{text}");
assert!(text.contains("anthropic, openai"), "{text}");
}
#[test]
fn an_unpinned_call_goes_to_the_fallback() {
let mut router = router().or_else("anthropic", Box::new(Echo("anthropic")));
let response = router.complete(&request(ModelSelection::Default)).unwrap();
assert_eq!(response.value, json!("anthropic"));
}
#[test]
fn an_unpinned_call_with_no_fallback_says_how_to_pin_one() {
let error = router()
.complete(&request(ModelSelection::Default))
.unwrap_err();
let text = error.to_string();
assert!(text.contains("model exact"), "{text}");
assert!(text.contains("anthropic, openai"), "{text}");
}
#[test]
fn a_single_provider_serves_its_own_vendor_without_being_registered_twice() {
let mut router = RoutingProvider::new().or_else("anthropic", Box::new(Echo("anthropic")));
assert_eq!(router.vendors(), vec!["anthropic"]);
let pinned = router
.complete(&request(ModelSelection::Exact("anthropic/claude-x".into())))
.unwrap();
assert_eq!(pinned.value, json!("anthropic"));
let unpinned = router.complete(&request(ModelSelection::Default)).unwrap();
assert_eq!(unpinned.value, json!("anthropic"));
let error = router
.complete(&request(ModelSelection::Exact("openai/gpt-x".into())))
.unwrap_err();
assert!(
error.to_string().contains("no provider for `openai`"),
"{error}"
);
}
#[test]
fn several_providers_and_no_fallback_ask_the_artifact_to_pin() {
let described = router().describe();
assert!(described.contains("must pin one"), "{described}");
}
#[test]
fn a_bare_model_name_is_not_read_as_a_vendor() {
let mut router = router().or_else("anthropic", Box::new(Echo("anthropic")));
let response = router
.complete(&request(ModelSelection::Exact("claude-test".into())))
.unwrap();
assert_eq!(response.value, json!("anthropic"));
}
#[test]
fn the_description_says_what_this_run_can_reach() {
let described = router()
.or_else("anthropic", Box::new(Echo("anthropic")))
.describe();
assert!(described.contains("anthropic, openai"), "{described}");
}
#[test]
fn an_empty_router_is_empty() {
assert!(RoutingProvider::new().is_empty());
assert!(!router().is_empty());
}
}