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, extra: &[(&str, &str)]) -> String {
extra
.iter()
.find(|(alias, _)| *alias == model)
.or_else(|| DEFAULT_ALIASES.iter().find(|(alias, _)| *alias == model))
.map(|(_, slug)| (*slug).to_string())
.unwrap_or_else(|| model.to_string())
}
pub fn resolve_fallback_chain(chain: &[String], extra: &[(&str, &str)]) -> Vec<String> {
chain.iter().map(|m| resolve_alias(m, extra)).collect()
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Resolution {
pub model: String,
pub small_model: Option<String>,
pub fallback: Vec<String>,
}
pub fn resolve(
capabilities: &std::collections::BTreeMap<String, crate::configfile::CapabilityConfig>,
base_model: &str,
) -> Resolution {
let extra = extra_aliases(capabilities);
let extra_ref: Vec<(&str, &str)> = extra
.iter()
.map(|(a, b)| (a.as_str(), b.as_str()))
.collect();
let mut out = Resolution {
model: if base_model.is_empty() {
String::new()
} else {
resolve_alias(base_model, &extra_ref)
},
small_model: None,
fallback: Vec::new(),
};
let Some(cap) = capabilities.get("model_catalog") else {
return out;
};
if let Some(sm) = cap.settings.get("small_model").and_then(|v| v.as_str()) {
if !sm.is_empty() {
out.small_model = Some(resolve_alias(sm, &extra_ref));
}
}
if let Some(fb) = cap.settings.get("fallback").and_then(|v| v.as_array()) {
let chain: Vec<String> = fb
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
if !chain.is_empty() {
out.fallback = resolve_fallback_chain(&chain, &extra_ref);
}
}
out
}
fn extra_aliases(
capabilities: &std::collections::BTreeMap<String, crate::configfile::CapabilityConfig>,
) -> Vec<(String, String)> {
capabilities
.get("model_catalog")
.and_then(|cap| cap.settings.get("aliases"))
.and_then(|v| v.as_object())
.map(|o| {
o.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_alias_with_no_extra_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");
}
#[test]
fn resolve_alias_extra_table_overrides_a_built_in() {
let extra = [("opus", "vendor/my-custom-opus")];
assert_eq!(resolve_alias("opus", &extra), "vendor/my-custom-opus");
assert_eq!(
resolve_alias("sonnet", &extra),
"anthropic/claude-sonnet-4-6"
);
}
#[test]
fn resolve_alias_extra_table_adds_a_brand_new_alias() {
let extra = [("fast", "vendor/fast-model")];
assert_eq!(resolve_alias("fast", &extra), "vendor/fast-model");
assert_eq!(resolve_alias("unknown-thing", &extra), "unknown-thing");
}
#[test]
fn resolve_fallback_chain_resolves_each_entry_and_preserves_order() {
let chain = vec!["haiku".to_string(), "gpt".to_string()];
assert_eq!(
resolve_fallback_chain(&chain, &[]),
vec![
"anthropic/claude-haiku-4-5".to_string(),
"openai/gpt-5.5".to_string(),
]
);
}
#[test]
fn resolve_fallback_chain_empty_input_is_empty_output() {
assert!(resolve_fallback_chain(&[], &[]).is_empty());
}
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"));
}
}