use crate::types::{AgentKind, TaskBudget};
pub(crate) fn resolve_explicit_agent_model(
agent_name: &str,
caller_model: Option<&str>,
declared_budget: Option<TaskBudget>,
) -> Option<String> {
if caller_model.is_some() {
return None;
}
let config = crate::agent_config::get_default_model(agent_name);
let catalog = catalog_model(agent_name, declared_budget);
let selected = config.clone().or(catalog);
if let Some(msg) = declared_budget_model_warning(
agent_name,
declared_budget,
selected.as_deref(),
config.is_some(),
) {
aid_warn!("{msg}");
}
selected
}
fn catalog_model(
agent_name: &str,
declared_budget: Option<TaskBudget>,
) -> Option<String> {
let budget = match declared_budget? {
budget @ (TaskBudget::Free | TaskBudget::Cheap) => budget,
TaskBudget::Standard | TaskBudget::Premium => return None,
};
AgentKind::parse_str(agent_name)
.and_then(|kind| crate::model_catalog::model_for_task_budget(kind, budget))
.map(str::to_string)
}
fn declared_budget_model_warning(
agent_name: &str,
declared_budget: Option<TaskBudget>,
chosen_model: Option<&str>,
from_config: bool,
) -> Option<String> {
let budget = declared_budget?;
if !matches!(budget, TaskBudget::Free | TaskBudget::Cheap) {
return None;
}
let kind = AgentKind::parse_str(agent_name);
let on_preference = chosen_model.is_some_and(|name| {
kind.is_some_and(|kind| crate::model_catalog::model_on_budget_preference(kind, budget, name))
});
if on_preference {
return None;
}
let catalog_has_preference = kind.is_some_and(|kind| {
crate::model_catalog::model_for_task_budget(kind, budget)
.is_some_and(|name| crate::model_catalog::model_on_budget_preference(kind, budget, name))
});
let chosen = chosen_model.unwrap_or("agent default");
if from_config && catalog_has_preference {
return Some(format!(
"[aid] Warning: configured default model '{chosen}' overrides declared budget {}",
budget.label()
));
}
Some(format!(
"[aid] Warning: agent '{agent_name}' has no model eligible for declared budget {}; using {chosen}",
budget.label()
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::paths::AidHomeGuard;
#[test]
fn config_override_warning_does_not_claim_no_eligible_model() {
let msg = declared_budget_model_warning(
"gemini",
Some(TaskBudget::Cheap),
Some("pro"),
true,
)
.expect("configured default that outranks cheap catalog rows must warn");
assert!(
!msg.contains("no model eligible"),
"catalog still has cheap rows; they were outranked: {msg}"
);
assert!(
msg.contains("configured default") && msg.contains("pro") && msg.contains("cheap"),
"{msg}"
);
}
#[test]
fn missing_catalog_tier_still_reports_no_eligible_model() {
let msg = declared_budget_model_warning(
"claude",
Some(TaskBudget::Free),
Some("claude-opus-5"),
false,
)
.expect("claude has no free catalog row");
assert!(msg.contains("no model eligible"), "{msg}");
assert!(msg.contains("free"), "{msg}");
}
#[test]
fn on_preference_model_does_not_warn() {
assert_eq!(
declared_budget_model_warning(
"gemini",
Some(TaskBudget::Cheap),
Some("flash-lite"),
false,
),
None
);
}
#[test]
fn resolve_skips_catalog_when_caller_supplied_model() {
let temp = tempfile::tempdir().expect("tempdir");
let _guard = AidHomeGuard::set(temp.path());
crate::agent_config::save_agent_default_model("gemini", Some("pro")).expect("save");
assert_eq!(
resolve_explicit_agent_model("gemini", Some("flash"), Some(TaskBudget::Cheap)),
None
);
}
#[test]
fn standard_premium_and_undeclared_defer_to_cli_default() {
let temp = tempfile::tempdir().expect("tempdir");
let _guard = AidHomeGuard::set(temp.path());
for budget in [Some(TaskBudget::Standard), Some(TaskBudget::Premium), None] {
assert_eq!(resolve_explicit_agent_model("codex", None, budget), None);
}
}
#[test]
fn cheap_uses_catalog_unless_agent_config_overrides_it() {
let temp = tempfile::tempdir().expect("tempdir");
let _guard = AidHomeGuard::set(temp.path());
let resolve = || resolve_explicit_agent_model("gemini", None, Some(TaskBudget::Cheap));
assert_eq!(resolve().as_deref(), Some("flash-lite"));
crate::agent_config::save_agent_default_model("gemini", Some("pro")).expect("save");
assert_eq!(resolve().as_deref(), Some("pro"));
}
}