pub const DEFAULT_ALIASES: &[(&str, &str)] = &[
("opus", "anthropic/claude-opus-4-8"),
("sonnet", "anthropic/claude-sonnet-4-6"),
("haiku", "anthropic/claude-haiku-4-5"),
("gpt", "openai/gpt-5.5"),
("gpt-5.5", "openai/gpt-5.5"),
("gpt-5", "openai/gpt-5"),
("gemini", "google/gemini-2.5-pro"),
("flash", "deepseek/deepseek-v4-flash"),
("deepseek-flash", "deepseek/deepseek-v4-flash"),
("deepseek", "deepseek/deepseek-v4-pro"),
("llama", "meta-llama/llama-4-maverick"),
];
pub fn resolve_alias(model: &str) -> String {
Routing::default().resolve_alias(model)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Resolution {
pub model: String,
pub small_model: Option<String>,
pub fallback: Vec<String>,
pub base_prompts: std::collections::BTreeMap<String, String>,
pub routing: Routing,
pub refusal: Option<String>,
}
pub fn resolve(
capabilities: &std::collections::BTreeMap<String, crate::configfile::CapabilityConfig>,
base_model: &str,
) -> Resolution {
let routing = Routing::from_capabilities(capabilities);
let mut out = Resolution {
model: if base_model.is_empty() {
String::new()
} else {
routing.resolve_alias(base_model)
},
small_model: None,
fallback: Vec::new(),
base_prompts: std::collections::BTreeMap::new(),
refusal: None,
routing,
};
if let Some(cap) = capabilities.get("model_catalog") {
if let Some(sm) = cap.settings.get("small_model").and_then(|v| v.as_str()) {
if !sm.is_empty() {
out.small_model = Some(out.routing.resolve_alias(sm));
}
}
if let Some(table) = cap.settings.get("base_prompts").and_then(|v| v.as_object()) {
out.base_prompts = table
.iter()
.filter_map(|(pattern, body)| {
body.as_str()
.filter(|text| !text.is_empty())
.map(|text| (pattern.clone(), text.to_string()))
})
.collect();
}
if let Some(fb) = cap.settings.get("fallback").and_then(|v| v.as_array()) {
out.fallback = fb
.iter()
.filter_map(|v| v.as_str())
.map(|m| out.routing.resolve_alias(m))
.collect();
}
}
out.refusal = [Some(&out.model)]
.into_iter()
.flatten()
.chain(out.small_model.iter())
.chain(out.fallback.iter())
.filter(|m| !m.is_empty())
.find_map(|m| out.routing.refusal(m));
out
}
pub fn base_prompt_for<'a>(
prompts: &'a std::collections::BTreeMap<String, String>,
model: &str,
) -> Option<&'a str> {
prompts
.iter()
.filter(|(pattern, _)| crate::config::glob_match(pattern, model))
.max_by(|(a, _), (b, _)| a.len().cmp(&b.len()).then_with(|| b.cmp(a)))
.map(|(_, body)| body.as_str())
}
pub const EFFORT_LADDER: &[&str] = &["none", "minimal", "low", "medium", "high", "xhigh", "ultra"];
pub fn effort_rank(level: &str) -> Option<usize> {
EFFORT_LADDER.iter().position(|l| *l == level)
}
pub fn cap_effort(level: Option<&str>, cap: Option<&str>) -> Option<String> {
let level = level?;
let Some(cap) = cap else {
return Some(level.to_string());
};
match (effort_rank(level), effort_rank(cap)) {
(Some(l), Some(c)) if l > c => Some(cap.to_string()),
_ => Some(level.to_string()),
}
}
pub fn pattern_capture(pattern: &str, value: &str) -> Option<String> {
match pattern.split_once('*') {
None => (pattern == value).then(|| value.to_string()),
Some((prefix, suffix)) => {
if suffix.contains('*') {
return None;
}
if value.len() < prefix.len() + suffix.len() {
return None;
}
if !value.starts_with(prefix) || !value.ends_with(suffix) {
return None;
}
Some(value[prefix.len()..value.len() - suffix.len()].to_string())
}
}
}
fn pattern_specificity(pattern: &str) -> (usize, u8) {
let literal = pattern.chars().filter(|c| *c != '*').count();
(literal, u8::from(!pattern.contains('*')))
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ModelRules {
pub effort: Option<String>,
pub max_effort: Option<String>,
pub thinking_budget: Option<u32>,
pub service_tier: Option<String>,
pub apply_patch: Option<bool>,
pub search_tool: Option<bool>,
}
impl ModelRules {
pub fn overlay(&mut self, other: &ModelRules) {
if other.effort.is_some() {
self.effort.clone_from(&other.effort);
}
if other.max_effort.is_some() {
self.max_effort.clone_from(&other.max_effort);
}
if other.thinking_budget.is_some() {
self.thinking_budget = other.thinking_budget;
}
if other.service_tier.is_some() {
self.service_tier.clone_from(&other.service_tier);
}
if other.apply_patch.is_some() {
self.apply_patch = other.apply_patch;
}
if other.search_tool.is_some() {
self.search_tool = other.search_tool;
}
}
fn from_settings(obj: &serde_json::Map<String, serde_json::Value>) -> ModelRules {
let text = |key: &str| {
obj.get(key)
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string)
};
ModelRules {
effort: text("effort"),
max_effort: text("max_effort"),
thinking_budget: obj
.get("thinking_budget")
.and_then(|v| v.as_u64())
.filter(|n| *n > 0)
.map(|n| n as u32),
service_tier: text("service_tier"),
apply_patch: obj.get("apply_patch").and_then(|v| v.as_bool()),
search_tool: obj.get("search_tool").and_then(|v| v.as_bool()),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Routing {
pub aliases: Vec<(String, String)>,
pub models: Vec<(String, ModelRules)>,
pub defaults: ModelRules,
pub allowed: Vec<String>,
pub denied: Vec<String>,
pub provider: Option<String>,
pub account: Option<String>,
}
impl Routing {
pub fn resolve_alias(&self, model: &str) -> String {
self.resolve_alias_depth(model, 0)
}
fn resolve_alias_depth(&self, model: &str, depth: usize) -> String {
if depth > 4 {
return model.to_string();
}
for (alias, slug) in &self.aliases {
if !alias.contains('*') && alias == model {
return slug.clone();
}
}
if let Some((_, slug)) = DEFAULT_ALIASES.iter().find(|(a, _)| *a == model) {
return (*slug).to_string();
}
let mut best: Option<(usize, u8, &str, String)> = None;
for (alias, slug) in &self.aliases {
if !alias.contains('*') {
continue;
}
let Some(stem) = pattern_capture(alias, model) else {
continue;
};
let (literal, exact) = pattern_specificity(alias);
if best
.as_ref()
.is_none_or(|(l, e, _, _)| (literal, exact) > (*l, *e))
{
best = Some((literal, exact, slug.as_str(), stem));
}
}
match best {
Some((_, _, template, stem)) => {
let expanded = self.resolve_alias_depth(&stem, depth + 1);
template.replace("{}", &expanded)
}
None => model.to_string(),
}
}
pub fn rules_for(&self, model: &str) -> ModelRules {
let mut rules = self.defaults.clone();
for (pattern, r) in &self.models {
if pattern_capture(pattern, model).is_some() {
rules.overlay(r);
}
}
rules
}
pub fn effective_effort(&self, model: &str, session_effort: Option<&str>) -> Option<String> {
let rules = self.rules_for(model);
let level = rules.effort.as_deref().or(session_effort);
cap_effort(level, rules.max_effort.as_deref())
}
pub fn refusal(&self, model: &str) -> Option<String> {
if let Some(pattern) = self
.denied
.iter()
.find(|p| pattern_capture(p, model).is_some())
{
return Some(format!(
"model `{model}` matches capabilities.model_catalog.denied_models entry `{pattern}`"
));
}
if !self.allowed.is_empty()
&& !self
.allowed
.iter()
.any(|p| pattern_capture(p, model).is_some())
{
return Some(format!(
"model `{model}` is not in capabilities.model_catalog.allowed_models ({})",
self.allowed.join(", ")
));
}
None
}
pub fn restricts_models(&self) -> bool {
!self.allowed.is_empty() || !self.denied.is_empty()
}
pub fn from_capabilities(
capabilities: &std::collections::BTreeMap<String, crate::configfile::CapabilityConfig>,
) -> Routing {
let Some(cap) = capabilities.get("model_catalog") else {
return Routing::default();
};
let s = &cap.settings;
let scope = |key: &str| {
s.get(key)
.and_then(|v| v.as_str())
.filter(|p| !p.is_empty())
.map(str::to_string)
};
let provider = scope("provider");
let account = scope("account");
let mut aliases: Vec<(String, String)> = Vec::new();
let provider_table = provider
.as_deref()
.and_then(|p| s.get("providers")?.as_object()?.get(p))
.and_then(|v| v.as_object());
if let (Some(pt), Some(account)) = (provider_table, account.as_deref()) {
if let Some(at) = pt
.get("accounts")
.and_then(|v| v.as_object())
.and_then(|a| a.get(account))
.and_then(|v| v.as_object())
{
push_alias_table(at.get("aliases"), &mut aliases);
}
}
if let Some(pt) = provider_table {
push_alias_table(pt.get("aliases"), &mut aliases);
}
push_alias_table(s.get("aliases"), &mut aliases);
let mut models: Vec<(String, ModelRules)> = s
.get("models")
.and_then(|v| v.as_object())
.map(|o| {
o.iter()
.filter_map(|(pattern, v)| {
Some((pattern.clone(), ModelRules::from_settings(v.as_object()?)))
})
.collect()
})
.unwrap_or_default();
models.sort_by_key(|(pattern, _)| pattern_specificity(pattern));
Routing {
aliases,
models,
defaults: ModelRules::from_settings(s),
allowed: string_list(s.get("allowed_models")),
denied: string_list(s.get("denied_models")),
provider,
account,
}
}
}
fn push_alias_table(value: Option<&serde_json::Value>, out: &mut Vec<(String, String)>) {
let Some(obj) = value.and_then(|v| v.as_object()) else {
return;
};
let mut entries: Vec<(String, String)> = obj
.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect();
entries.sort();
out.extend(entries);
}
fn string_list(value: Option<&serde_json::Value>) -> Vec<String> {
value
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default()
}