use std::time::Duration;
use serde::Deserialize;
use super::{ClientError, ClientKind, ClientManager, compact_body, normalize_base_url};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct RouterModel {
pub id: String,
#[serde(default)]
pub owned_by: String,
}
#[derive(Deserialize)]
struct RouterCatalog {
data: Vec<RouterModel>,
}
impl ClientManager {
pub(crate) async fn catalog(
&self,
base_url: &str,
token: &str,
) -> Result<Vec<RouterModel>, ClientError> {
let base_url = normalize_base_url(base_url)?;
let url = models_url(&base_url);
let response = reqwest::Client::new()
.get(&url)
.bearer_auth(token)
.timeout(Duration::from_secs(15))
.send()
.await
.map_err(|error| {
ClientError::message(format!("router catalog is not reachable at {url}: {error}"))
})?;
let code = response.status();
let response_body = response.text().await.unwrap_or_default();
if !code.is_success() {
return Err(ClientError::message(format!(
"router catalog request failed at {url} ({code}): {}",
compact_body(&response_body)
)));
}
let catalog: RouterCatalog = serde_json::from_str(&response_body).map_err(|error| {
ClientError::message(format!("router returned an invalid model catalog: {error}"))
})?;
let mut models = catalog
.data
.into_iter()
.filter(|model| !model.id.trim().is_empty())
.collect::<Vec<_>>();
models.sort_by(|left, right| {
left.id
.cmp(&right.id)
.then_with(|| left.owned_by.cmp(&right.owned_by))
});
models.dedup_by(|left, right| left.id == right.id && left.owned_by == right.owned_by);
if models.is_empty() {
return Err(ClientError::message(
"router catalog contains no models from healthy subscriptions",
));
}
Ok(models)
}
}
fn models_url(base_url: &str) -> String {
let base_url = base_url.trim_end_matches('/');
if base_url.ends_with("/v1") {
format!("{base_url}/models")
} else {
format!("{base_url}/v1/models")
}
}
pub(super) fn doctor_model(
client: ClientKind,
catalog: &[RouterModel],
) -> Result<&str, ClientError> {
select_model(client, catalog).ok_or_else(|| ClientError::message(unavailable(client, catalog)))
}
#[must_use]
pub fn select_model(client: ClientKind, catalog: &[RouterModel]) -> Option<&str> {
let integration = client.integration();
for owner in integration.model_owners {
if let Some(model) = catalog.iter().find(|model| &model.owned_by == owner) {
return Some(model.id.as_str());
}
}
if integration.strict_owner && !catalog.iter().all(|model| model.owned_by.is_empty()) {
return None;
}
catalog.first().map(|model| model.id.as_str())
}
#[must_use]
pub fn usable_models(client: ClientKind, catalog: &[RouterModel]) -> Vec<RouterModel> {
let integration = client.integration();
if integration.model_owners.is_empty() {
return catalog.to_vec();
}
let mut preferred: Vec<RouterModel> = Vec::new();
for owner in integration.model_owners {
preferred.extend(
catalog
.iter()
.filter(|model| &model.owned_by == owner)
.cloned(),
);
}
if preferred.is_empty() && !integration.strict_owner {
return catalog.to_vec();
}
preferred
}
#[must_use]
pub fn unavailable(client: ClientKind, catalog: &[RouterModel]) -> String {
let mut advertised: Vec<&str> = catalog
.iter()
.map(|model| model.owned_by.as_str())
.filter(|owner| !owner.is_empty())
.collect();
advertised.sort_unstable();
advertised.dedup();
let holdings = if advertised.is_empty() {
"the catalog is empty".to_string()
} else {
format!("it advertises only {} models", advertised.join(", "))
};
let wanted = client.integration().model_owners.join(", ");
format!(
"the router advertises no model for {} ({wanted} models): {holdings}. Authorize a \
matching subscription on the router host, or pass --model explicitly to use one of \
the models it does advertise",
client.integration().name
)
}