use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Family {
Anthropic,
OpenAi,
Zhipu,
Alibaba,
Google,
Custom(String),
}
impl Family {
pub fn named(label: &str) -> Result<Self, RouteError> {
if !identifier(label) {
return Err(RouteError::FamilyDeclaration(format!("invalid family label `{label}`")));
}
Ok(match label.to_ascii_lowercase().as_str() {
"anthropic" | "claude" => Self::Anthropic,
"openai" | "gpt" | "codex" => Self::OpenAi,
"zhipu" | "zai" | "glm" => Self::Zhipu,
"alibaba" | "qwen" => Self::Alibaba,
"google" | "gemini" => Self::Google,
_ => Self::Custom(label.to_ascii_lowercase()),
})
}
fn canonical(self) -> Result<Self, RouteError> {
match self {
Self::Custom(name) => Self::named(&name),
other => Ok(other),
}
}
}
impl fmt::Display for Family {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Anthropic => "anthropic",
Self::OpenAi => "openai",
Self::Zhipu => "zhipu",
Self::Alibaba => "alibaba",
Self::Google => "google",
Self::Custom(label) => label,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum RouteError {
#[error("`{0}` is not a route (expected `provider:model` or `provider:model -> provider:model`)")]
Grammar(String),
#[error("`{0}` is outside the Agentbox route policy (providers: claude, codex, loom, zai; simple model ids)")]
AgentboxPolicy(String),
#[error("unknown model family for `{rung}`: refusing (the check fails closed)")]
UnknownFamily {
rung: String,
},
#[error("`{rung}`: provider declares {host} but the model declares {model}")]
HostModelMismatch {
rung: String,
host: Family,
model: Family,
},
#[error("invalid model-family declaration: {0}")]
FamilyDeclaration(String),
#[error("reviewer `{0}` must name exactly one model")]
ReviewerRoute(String),
#[error(
"seal review refused: author `{author}` and reviewer `{reviewer}` are the same model family ({family}); a seal review must come from a different family"
)]
SameFamily {
author: String,
reviewer: String,
family: Family,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Rung {
pub host: String,
pub model: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Route {
pub rungs: Vec<Rung>,
}
const AGENTBOX_HOSTS: [&str; 4] = ["claude", "codex", "loom", "zai"];
fn identifier(text: &str) -> bool {
text.as_bytes().first().is_some_and(u8::is_ascii_alphabetic)
&& text.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
}
fn rung(text: &str) -> Option<Rung> {
let (host, model) = text.trim().split_once(':')?;
let model_ok = !model.is_empty()
&& model
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-' | b'/' | b':' | b'@' | b'+'));
(identifier(host) && model_ok).then(|| Rung { host: host.into(), model: model.into() })
}
fn named_stem(model: &str, stem: &str) -> bool {
model == stem
|| model.strip_prefix(stem).and_then(|rest| rest.strip_prefix('-')).is_some_and(|rest| !rest.is_empty())
}
fn numbered_model(model: &str, prefix: &str, omni_suffix: bool) -> bool {
let Some(rest) = model.strip_prefix(prefix) else { return false };
let version = match rest.split_once('-') {
Some((version, suffix)) if !suffix.is_empty() => version,
Some(_) => return false,
None => rest,
};
let version = if omni_suffix { version.strip_suffix('o').unwrap_or(version) } else { version };
version.split('.').all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit()))
}
fn by_model(model: &str) -> Option<Family> {
let m = model.rsplit('/').next()?.split([':', '@']).next()?.to_ascii_lowercase();
if named_stem(&m, "claude") {
Some(Family::Anthropic)
} else if numbered_model(&m, "gpt-", true)
|| named_stem(&m, "gpt-oss")
|| named_stem(&m, "codex")
|| numbered_model(&m, "o", false)
{
Some(Family::OpenAi)
} else if named_stem(&m, "glm") {
Some(Family::Zhipu)
} else if named_stem(&m, "qwen") || numbered_model(&m, "qwen", false) {
Some(Family::Alibaba)
} else if named_stem(&m, "gemini") {
Some(Family::Google)
} else {
None
}
}
fn by_provider(host: &str) -> Option<Family> {
match host.to_ascii_lowercase().as_str() {
"claude" | "anthropic" => Some(Family::Anthropic),
"codex" | "openai" => Some(Family::OpenAi),
"zai" | "zhipu" => Some(Family::Zhipu),
"gemini" | "google" => Some(Family::Google),
_ => None,
}
}
fn check_provider(rung: &Rung, model: &Family) -> Result<(), RouteError> {
if let Some(host) = by_provider(&rung.host) {
if &host != model {
return Err(RouteError::HostModelMismatch {
rung: format!("{}:{}", rung.host, rung.model),
host,
model: model.clone(),
});
}
}
Ok(())
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FamilyCatalogue {
declarations: BTreeMap<(String, String), Family>,
}
impl FamilyCatalogue {
pub fn declare(&mut self, alias: &str, family: Family) -> Result<(), RouteError> {
let route = Route::parse(alias)?;
if route.rungs.len() != 1 {
return Err(RouteError::FamilyDeclaration("an alias must name exactly one model".into()));
}
let r = &route.rungs[0];
let family = family.canonical()?;
check_provider(r, &family)?;
if by_model(&r.model).is_some_and(|known| known != family) {
return Err(RouteError::FamilyDeclaration(format!("`{alias}` contradicts its recognised model family")));
}
let key = (r.host.clone(), r.model.clone());
if let Some(existing) = self.declarations.get(&key) {
if existing != &family {
return Err(RouteError::FamilyDeclaration(format!("`{alias}` already declares {existing}")));
}
}
self.declarations.insert(key, family);
Ok(())
}
}
impl Rung {
pub fn family(&self) -> Result<Family, RouteError> {
self.family_with(&FamilyCatalogue::default())
}
pub fn family_with(&self, catalogue: &FamilyCatalogue) -> Result<Family, RouteError> {
let label = format!("{}:{}", self.host, self.model);
if rung(&label).as_ref() != Some(self) {
return Err(RouteError::Grammar(label));
}
let model = catalogue
.declarations
.get(&(self.host.clone(), self.model.clone()))
.cloned()
.or_else(|| by_model(&self.model))
.ok_or(RouteError::UnknownFamily { rung: label })?;
check_provider(self, &model)?;
Ok(model)
}
}
impl Route {
pub fn parse(text: &str) -> Result<Self, RouteError> {
let bad = || RouteError::Grammar(text.to_owned());
let parts: Vec<&str> = text.split("->").collect();
if parts.len() > 2 {
return Err(bad());
}
let rungs = parts.into_iter().map(|p| rung(p).ok_or_else(bad)).collect::<Result<Vec<_>, _>>()?;
Ok(Self { rungs })
}
pub fn parse_agentbox(text: &str) -> Result<Self, RouteError> {
let parsed = Self::parse(text)?;
if parsed.rungs.iter().any(|r| {
!AGENTBOX_HOSTS.contains(&r.host.as_str())
|| !r.model.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
}) {
return Err(RouteError::AgentboxPolicy(text.into()));
}
Ok(parsed)
}
pub fn families(&self) -> Result<BTreeSet<Family>, RouteError> {
self.families_with(&FamilyCatalogue::default())
}
pub fn families_with(&self, catalogue: &FamilyCatalogue) -> Result<BTreeSet<Family>, RouteError> {
if self.rungs.is_empty() || self.rungs.len() > 2 {
return Err(RouteError::Grammar("expected one or two model rungs".into()));
}
self.rungs.iter().map(|r| r.family_with(catalogue)).collect()
}
}
pub fn cross_family(author: &str, reviewer: &str) -> Result<(), RouteError> {
cross_family_with(author, reviewer, &FamilyCatalogue::default())
}
pub fn cross_family_with(author: &str, reviewer: &str, catalogue: &FamilyCatalogue) -> Result<(), RouteError> {
let a = Route::parse(author)?.families_with(catalogue)?;
let review = Route::parse(reviewer)?;
if review.rungs.len() != 1 {
return Err(RouteError::ReviewerRoute(reviewer.trim().into()));
}
let r = review.families_with(catalogue)?;
match a.intersection(&r).next() {
None => Ok(()),
Some(family) => Err(RouteError::SameFamily {
author: author.trim().into(),
reviewer: reviewer.trim().into(),
family: family.clone(),
}),
}
}