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()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_alias_with_no_config_matches_every_built_in() {
for (alias, slug) in DEFAULT_ALIASES {
assert_eq!(resolve_alias(alias), *slug, "alias {alias}");
}
assert_eq!(resolve_alias("vendor/some-model"), "vendor/some-model");
}
fn routing(settings: serde_json::Value) -> Routing {
let mut capabilities = std::collections::BTreeMap::new();
capabilities.insert("model_catalog".to_string(), cap(settings));
Routing::from_capabilities(&capabilities)
}
#[test]
fn config_alias_table_overrides_a_built_in_and_adds_new_names() {
let r = routing(serde_json::json!({
"aliases": {"opus": "vendor/my-custom-opus", "fast": "vendor/fast-model"},
}));
assert_eq!(r.resolve_alias("opus"), "vendor/my-custom-opus");
assert_eq!(r.resolve_alias("fast"), "vendor/fast-model");
assert_eq!(r.resolve_alias("sonnet"), "anthropic/claude-sonnet-4-6");
assert_eq!(r.resolve_alias("unknown-thing"), "unknown-thing");
}
#[test]
fn a_pattern_alias_resolves_its_captured_stem_before_substituting() {
let r = routing(serde_json::json!({"aliases": {"*[1m]": "{}[1m]"}}));
assert_eq!(
r.resolve_alias("sonnet[1m]"),
"anthropic/claude-sonnet-4-6[1m]"
);
assert_eq!(r.resolve_alias("vendor/m[1m]"), "vendor/m[1m]");
let r = routing(serde_json::json!({
"aliases": {"*[1m]": "{}[1m]", "sonnet[1m]": "vendor/pinned"},
}));
assert_eq!(r.resolve_alias("sonnet[1m]"), "vendor/pinned");
}
#[test]
fn aliases_resolve_per_provider_and_per_account() {
let table = |provider: &str, account: &str| {
serde_json::json!({
"provider": provider,
"account": account,
"aliases": {"default": "vendor/generic"},
"providers": {
"anthropic": {
"aliases": {"default": "anthropic/claude-sonnet-4-6"},
"accounts": {
"max": {"aliases": {"default": "anthropic/claude-opus-4-8"}},
},
},
"openai": {"aliases": {"default": "openai/gpt-5.5"}},
},
})
};
assert_eq!(
routing(table("anthropic", "max")).resolve_alias("default"),
"anthropic/claude-opus-4-8"
);
assert_eq!(
routing(table("anthropic", "pro")).resolve_alias("default"),
"anthropic/claude-sonnet-4-6"
);
assert_eq!(
routing(table("openai", "max")).resolve_alias("default"),
"openai/gpt-5.5"
);
assert_eq!(
routing(serde_json::json!({"aliases": {"default": "vendor/generic"}}))
.resolve_alias("default"),
"vendor/generic"
);
}
#[test]
fn pattern_capture_is_exact_or_single_wildcard() {
assert_eq!(pattern_capture("a/b", "a/b").as_deref(), Some("a/b"));
assert_eq!(pattern_capture("a/b", "a/c"), None);
assert_eq!(
pattern_capture("openai/*", "openai/gpt-5").as_deref(),
Some("gpt-5")
);
assert_eq!(
pattern_capture("*[1m]", "opus[1m]").as_deref(),
Some("opus")
);
assert_eq!(
pattern_capture("*", "anything").as_deref(),
Some("anything")
);
assert_eq!(pattern_capture("a*b*c", "axbyc"), None);
assert_eq!(pattern_capture("opus*", "opus").as_deref(), Some(""));
}
#[test]
fn per_model_rules_overlay_defaults_most_specific_last() {
let r = routing(serde_json::json!({
"effort": "medium",
"thinking_budget": 4096,
"models": {
"anthropic/*": {"thinking_budget": 16384},
"anthropic/claude-opus-4-8": {"effort": "high", "apply_patch": false},
"openai/*": {"apply_patch": true},
},
}));
let opus = r.rules_for("anthropic/claude-opus-4-8");
assert_eq!(opus.effort.as_deref(), Some("high"));
assert_eq!(opus.thinking_budget, Some(16384));
assert_eq!(opus.apply_patch, Some(false));
let haiku = r.rules_for("anthropic/claude-haiku-4-5");
assert_eq!(haiku.effort.as_deref(), Some("medium"));
assert_eq!(haiku.thinking_budget, Some(16384));
assert_eq!(haiku.apply_patch, None);
let gpt = r.rules_for("openai/gpt-5.5");
assert_eq!(gpt.thinking_budget, Some(4096));
assert_eq!(gpt.apply_patch, Some(true));
}
#[test]
fn effective_effort_prefers_the_model_rule_then_the_session_then_clamps() {
let r = routing(serde_json::json!({
"models": {
"anthropic/*": {"effort": "high"},
"cheap/*": {"max_effort": "low"},
},
}));
assert_eq!(
r.effective_effort("anthropic/claude-opus-4-8", Some("medium"))
.as_deref(),
Some("high")
);
assert_eq!(
r.effective_effort("vendor/m", Some("medium")).as_deref(),
Some("medium")
);
assert_eq!(
r.effective_effort("cheap/m", Some("ultra")).as_deref(),
Some("low")
);
assert_eq!(
r.effective_effort("cheap/m", Some("none")).as_deref(),
Some("none")
);
assert_eq!(
r.effective_effort("cheap/m", Some("turbo")).as_deref(),
Some("turbo")
);
assert_eq!(r.effective_effort("vendor/m", None), None);
}
#[test]
fn allow_and_deny_lists_refuse_by_pattern() {
let r = routing(serde_json::json!({
"allowed_models": ["anthropic/*"],
"denied_models": ["anthropic/claude-opus-*"],
}));
assert!(r.restricts_models());
assert!(r.refusal("anthropic/claude-sonnet-4-6").is_none());
assert!(r
.refusal("anthropic/claude-opus-4-8")
.is_some_and(|d| d.contains("denied_models")));
assert!(r
.refusal("openai/gpt-5.5")
.is_some_and(|d| d.contains("allowed_models")));
assert!(!routing(serde_json::json!({})).restricts_models());
assert!(routing(serde_json::json!({})).refusal("anything").is_none());
}
fn cap(settings: serde_json::Value) -> crate::configfile::CapabilityConfig {
crate::configfile::CapabilityConfig {
enabled: None,
settings: settings.as_object().cloned().unwrap_or_default(),
}
}
#[test]
fn resolve_with_no_model_catalog_table_is_alias_only() {
let capabilities = std::collections::BTreeMap::new();
let r = resolve(&capabilities, "opus");
assert_eq!(r.model, "anthropic/claude-opus-4-8");
assert_eq!(r.small_model, None);
assert!(r.fallback.is_empty());
}
#[test]
fn resolve_happy_path_reads_small_model_fallback_and_aliases_regardless_of_enabled() {
let mut capabilities = std::collections::BTreeMap::new();
capabilities.insert(
"model_catalog".to_string(),
cap(serde_json::json!({
"small_model": "haiku",
"fallback": ["sonnet", "vendor/already-a-slug"],
"aliases": {"cheap": "vendor/cheap-model"},
})),
);
let r = resolve(&capabilities, "cheap");
assert_eq!(r.model, "vendor/cheap-model");
assert_eq!(r.small_model.as_deref(), Some("anthropic/claude-haiku-4-5"));
assert_eq!(
r.fallback,
vec![
"anthropic/claude-sonnet-4-6".to_string(),
"vendor/already-a-slug".to_string(),
]
);
}
#[test]
fn resolve_small_model_already_a_slug_passes_through() {
let mut capabilities = std::collections::BTreeMap::new();
capabilities.insert(
"model_catalog".to_string(),
cap(serde_json::json!({"small_model": "anthropic/claude-haiku-4-5"})),
);
let r = resolve(&capabilities, "anthropic/claude-opus-4-8");
assert_eq!(r.model, "anthropic/claude-opus-4-8");
assert_eq!(r.small_model.as_deref(), Some("anthropic/claude-haiku-4-5"));
}
}