use crate::chat::request::ChatRequest;
use crate::chat::response::ChatResponse;
use crate::error::{Error, Result};
use crate::model::{ModelCapabilities, ModelId};
use crate::observe;
use crate::provider::{Access, Provider};
use crate::retry::Retry;
use std::sync::Arc;
pub struct Route {
provider: Arc<dyn Provider>,
model: ModelId,
}
impl Route {
pub fn new(provider: Arc<dyn Provider>, model: impl Into<ModelId>) -> Self {
Self {
provider: provider.clone(),
model: model.into(),
}
}
pub fn capabilities(&self) -> Option<ModelCapabilities> {
self.provider.capabilities(&self.model)
}
pub fn name(&self) -> String {
format!("{}/{}", self.provider.id(), self.model)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct Requirements {
pub tools: bool,
pub structured_output: bool,
pub prompt_caching: bool,
pub thinking: bool,
pub images: bool,
pub streaming: bool,
pub must_stay_on_device: bool,
}
impl Requirements {
pub fn of(request: &ChatRequest) -> Requirements {
let needs = request.needs();
Requirements {
tools: needs.tools,
structured_output: needs.structured_output,
prompt_caching: needs.prompt_caching,
thinking: needs.thinking,
images: needs.images,
streaming: false,
must_stay_on_device: false,
}
}
#[must_use]
pub fn on_device(mut self) -> Self {
self.must_stay_on_device = true;
self
}
#[must_use]
pub fn streaming(mut self) -> Self {
self.streaming = true;
self
}
pub fn met_by(self, have: &ModelCapabilities) -> bool {
if self.must_stay_on_device && !have.reach.is_on_device() {
return false;
}
(!self.tools || have.tools)
&& (!self.structured_output || have.structured_output)
&& (!self.prompt_caching || have.prompt_caching)
&& (!self.thinking || have.thinking)
&& (!self.streaming || have.streaming)
&& (!self.images || have.images)
}
pub fn unmet_by(self, have: &ModelCapabilities) -> Vec<&'static str> {
let mut missing = Vec::new();
if self.must_stay_on_device && !have.reach.is_on_device() {
missing.push("on-device");
}
if self.tools && !have.tools {
missing.push("tools");
}
if self.structured_output && !have.structured_output {
missing.push("structured_output");
}
if self.prompt_caching && !have.prompt_caching {
missing.push("prompt_caching");
}
if self.streaming && !have.streaming {
missing.push("streaming");
}
if self.thinking && !have.thinking {
missing.push("thinking");
}
if self.images && !have.images {
missing.push("images");
}
missing
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Attempted {
pub route: String,
pub why: String,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Routed {
pub response: ChatResponse,
pub route: String,
pub fell_through: Vec<Attempted>,
pub attempts: u32,
}
pub struct Router {
routes: Vec<Route>,
retry: Option<Retry>,
}
impl Router {
pub fn new(routes: Vec<Route>) -> Self {
Self {
routes,
retry: None,
}
}
#[must_use]
pub fn retrying(mut self, policy: Retry) -> Self {
self.retry = Some(policy);
self
}
pub fn unusable(&self) -> Vec<String> {
self.routes
.iter()
.filter(|route| route.capabilities().is_none())
.map(Route::name)
.collect()
}
pub fn routes(&self) -> impl Iterator<Item = (String, Option<ModelCapabilities>)> + '_ {
self.routes
.iter()
.map(|route| (route.name(), route.capabilities()))
}
pub async fn preflight(&self) -> Vec<(String, Access)> {
let mut reached = Vec::with_capacity(self.routes.len());
for route in &self.routes {
reached.push((route.name(), route.provider.validate(&route.model).await));
}
reached
}
pub async fn chat(&self, request: ChatRequest, needs: Requirements) -> Result<Routed> {
let span = observe::routing(&request.model);
observe::inside(span.clone(), self.route(request, needs, span)).await
}
async fn route(
&self,
request: ChatRequest,
needs: Requirements,
span: observe::Span,
) -> Result<Routed> {
let mut fell_through = Vec::new();
let mut last: Option<Error> = None;
let mut attempts = 0;
for route in &self.routes {
let Some(capabilities) = route.capabilities() else {
fell_through.push(Attempted {
route: route.name(),
why: "the provider does not know this model".into(),
});
continue;
};
let missing = needs.unmet_by(&capabilities);
if !missing.is_empty() {
fell_through.push(Attempted {
route: route.name(),
why: format!("cannot do {}", missing.join(", ")),
});
continue;
}
let mut sending = request.clone();
sending.model = route.model.clone();
let allowed = self.retry.as_ref().map_or(1, Retry::attempts);
for attempt in 1..=allowed {
attempts += 1;
match route.provider.chat(sending.clone()).await {
Ok(response) => {
let name = route.name();
observe::routed(
&span,
&name,
response.usage.coverage(),
attempts,
fell_through.len(),
);
return Ok(Routed {
response,
route: name,
fell_through,
attempts,
});
}
Err(error) => {
if matches!(error, Error::Refused { .. }) {
return Err(error);
}
let waiting = self
.retry
.as_ref()
.and_then(|policy| policy.wait_before(attempt + 1, &error));
fell_through.push(Attempted {
route: route.name(),
why: match waiting {
Some(wait) => format!(
"{error} (attempt {attempt} of {allowed}, waiting {}ms)",
wait.as_millis()
),
None => error.to_string(),
},
});
last = Some(error);
match waiting {
Some(wait) => {
if let Some(policy) = &self.retry {
policy.sleep(wait).await;
}
}
None => break,
}
}
}
}
}
match last {
Some(error) => Err(error),
None => Err(Error::Unsupported(format!(
"no route can serve this request. Tried: {}",
fell_through
.iter()
.map(|a| format!("{} ({})", a.route, a.why))
.collect::<Vec<_>>()
.join("; ")
))),
}
}
}