use std::collections::HashMap;
use crate::types::Model;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolveError {
UnknownAlias {
name: String,
target: String,
scope: &'static str,
},
UnknownModel { name: String },
}
impl std::fmt::Display for ResolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResolveError::UnknownAlias {
name,
target,
scope,
} => write!(
f,
"{} alias '{}' points at unknown model '{}'",
scope, name, target
),
ResolveError::UnknownModel { name } => {
write!(f, "unknown model or alias: {}", name)
}
}
}
}
impl std::error::Error for ResolveError {}
pub fn resolve_model<'a>(
raw: &str,
provider_filter: Option<&str>,
project_aliases: Option<&HashMap<String, String>>,
global_aliases: &HashMap<String, String>,
all_models: &'a [Model],
) -> Result<&'a Model, ResolveError> {
let (target, alias_scope): (&str, Option<&'static str>) = if let Some(map) =
project_aliases.and_then(|m| if m.is_empty() { None } else { Some(m) })
&& let Some(t) = map.get(raw)
{
(t.as_str(), Some("project"))
} else if let Some(t) = global_aliases.get(raw) {
(t.as_str(), Some("global"))
} else {
(raw, None)
};
let (target_provider, target_id) = match target.split_once('/') {
Some((p, i)) if !p.is_empty() && !i.is_empty() => (Some(p), i),
_ => (None, target),
};
let effective_provider: Option<&str> = provider_filter.or(target_provider);
let found = all_models.iter().find(|m| {
m.id == target_id
&& match effective_provider {
Some(p) => m.provider == p,
None => true,
}
});
match found {
Some(m) => Ok(m),
None => match alias_scope {
Some(scope) => Err(ResolveError::UnknownAlias {
name: raw.to_string(),
target: target.to_string(),
scope,
}),
None => Err(ResolveError::UnknownModel {
name: raw.to_string(),
}),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Model, ModelCost, ThinkingStyle};
fn mk_model(id: &str, provider: &str) -> Model {
Model {
id: id.into(),
name: id.into(),
api: "mock".into(),
provider: provider.into(),
base_url: "http://mock".into(),
thinking: ThinkingStyle::None,
cost: ModelCost::default(),
context_window: 100_000,
max_tokens: 4_096,
headers: std::collections::HashMap::new(),
}
}
fn make_models() -> Vec<Model> {
vec![
mk_model("opus-4", "anthropic"),
mk_model("haiku-4", "anthropic"),
mk_model("gpt-4", "openai"),
mk_model("dual", "anthropic"),
mk_model("dual", "openai"),
]
}
#[test]
fn plain_id_pass_through() {
let models = make_models();
let aliases = HashMap::new();
let m = resolve_model("opus-4", None, None, &aliases, &models).unwrap();
assert_eq!(m.id, "opus-4");
assert_eq!(m.provider, "anthropic");
}
#[test]
fn unknown_plain_id_returns_unknown_model() {
let models = make_models();
let aliases = HashMap::new();
let err = resolve_model("nope", None, None, &aliases, &models).unwrap_err();
assert_eq!(
err,
ResolveError::UnknownModel {
name: "nope".into()
}
);
}
#[test]
fn global_alias_hit() {
let models = make_models();
let mut aliases = HashMap::new();
aliases.insert("smart".into(), "opus-4".into());
let m = resolve_model("smart", None, None, &aliases, &models).unwrap();
assert_eq!(m.id, "opus-4");
}
#[test]
fn project_alias_hit() {
let models = make_models();
let global = HashMap::new();
let mut project = HashMap::new();
project.insert("smart".into(), "haiku-4".into());
let m = resolve_model("smart", None, Some(&project), &global, &models).unwrap();
assert_eq!(m.id, "haiku-4");
}
#[test]
fn project_overrides_global() {
let models = make_models();
let mut global = HashMap::new();
global.insert("smart".into(), "opus-4".into());
let mut project = HashMap::new();
project.insert("smart".into(), "haiku-4".into());
let m = resolve_model("smart", None, Some(&project), &global, &models).unwrap();
assert_eq!(m.id, "haiku-4");
}
#[test]
fn unknown_alias_target_is_error() {
let models = make_models();
let mut global = HashMap::new();
global.insert("smart".into(), "ghost".into());
let err = resolve_model("smart", None, None, &global, &models).unwrap_err();
assert_eq!(
err,
ResolveError::UnknownAlias {
name: "smart".into(),
target: "ghost".into(),
scope: "global",
}
);
}
#[test]
fn unknown_project_alias_target_reports_project_scope() {
let models = make_models();
let global = HashMap::new();
let mut project = HashMap::new();
project.insert("planner".into(), "ghost".into());
let err = resolve_model("planner", None, Some(&project), &global, &models).unwrap_err();
assert!(matches!(
err,
ResolveError::UnknownAlias {
ref name,
ref target,
scope: "project",
} if name == "planner" && target == "ghost"
));
}
#[test]
fn provider_prefixed_alias_target() {
let models = make_models();
let mut global = HashMap::new();
global.insert("dual_a".into(), "anthropic/dual".into());
global.insert("dual_o".into(), "openai/dual".into());
let a = resolve_model("dual_a", None, None, &global, &models).unwrap();
assert_eq!(a.provider, "anthropic");
let o = resolve_model("dual_o", None, None, &global, &models).unwrap();
assert_eq!(o.provider, "openai");
}
#[test]
fn explicit_provider_filter_overrides_alias_prefix() {
let models = make_models();
let mut global = HashMap::new();
global.insert("d".into(), "anthropic/dual".into());
let m = resolve_model("d", Some("openai"), None, &global, &models).unwrap();
assert_eq!(m.provider, "openai");
assert_eq!(m.id, "dual");
}
#[test]
fn explicit_provider_filter_on_plain_id() {
let models = make_models();
let global = HashMap::new();
let m = resolve_model("dual", Some("openai"), None, &global, &models).unwrap();
assert_eq!(m.provider, "openai");
}
#[test]
fn explicit_provider_filter_no_match_is_error() {
let models = make_models();
let global = HashMap::new();
let err = resolve_model("gpt-4", Some("anthropic"), None, &global, &models).unwrap_err();
assert_eq!(
err,
ResolveError::UnknownModel {
name: "gpt-4".into()
}
);
}
#[test]
fn alias_name_matches_real_model_id_alias_wins() {
let models = make_models();
let mut global = HashMap::new();
global.insert("opus-4".into(), "haiku-4".into());
let m = resolve_model("opus-4", None, None, &global, &models).unwrap();
assert_eq!(m.id, "haiku-4");
}
#[test]
fn split_on_first_slash_only() {
let mut models = make_models();
models.push(mk_model("bar/baz", "foo"));
let mut global = HashMap::new();
global.insert("weird".into(), "foo/bar/baz".into());
let m = resolve_model("weird", None, None, &global, &models).unwrap();
assert_eq!(m.id, "bar/baz");
assert_eq!(m.provider, "foo");
}
#[test]
fn empty_alias_maps_match_plain_lookup() {
let models = make_models();
let global = HashMap::new();
let project = HashMap::new();
let m = resolve_model("opus-4", None, Some(&project), &global, &models).unwrap();
assert_eq!(m.id, "opus-4");
let m = resolve_model("dual", Some("openai"), Some(&project), &global, &models).unwrap();
assert_eq!(m.provider, "openai");
let err = resolve_model("ghost", None, Some(&project), &global, &models).unwrap_err();
assert_eq!(
err,
ResolveError::UnknownModel {
name: "ghost".into()
}
);
}
#[test]
fn empty_project_map_falls_through_to_global() {
let models = make_models();
let mut global = HashMap::new();
global.insert("smart".into(), "opus-4".into());
let project = HashMap::new();
let m = resolve_model("smart", None, Some(&project), &global, &models).unwrap();
assert_eq!(m.id, "opus-4");
}
}