use std::cmp::Ordering;
use std::fs;
use std::path::PathBuf;
use std::sync::OnceLock;
use crate::types::{AgentKind, TaskBudget};
#[path = "model_catalog_data.rs"]
mod model_catalog_data;
#[path = "model_catalog_resolved.rs"]
pub(crate) mod model_catalog_resolved;
#[cfg(test)]
#[path = "model_catalog_test_support.rs"]
mod test_support;
pub use model_catalog_data::{AGENT_MODELS, AGENT_PROFILES, AgentModel};
pub(crate) use model_catalog_resolved::{
load_pricing_overrides, merged_agent_models, models_for_agent, PricingResponse,
};
pub(crate) use model_catalog_resolved::is_unpriced_discovered_model;
#[cfg(test)]
pub(crate) use test_support::set_test_qwen_home;
static QWEN_MODELS_CACHE: OnceLock<Vec<AgentModel>> = OnceLock::new();
fn qwen_home() -> Option<PathBuf> {
#[cfg(test)]
if let Some(home) = test_support::qwen_home_override() {
return Some(home);
}
std::env::var_os("HOME").map(PathBuf::from)
}
fn load_qwen_models() -> Vec<String> {
let Some(home) = qwen_home() else {
return vec!["coder-model".to_string()];
};
let path = home.join(".qwen").join("settings.json");
if !path.exists() {
return vec!["coder-model".to_string()];
}
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => return vec!["coder-model".to_string()],
};
let settings: serde_json::Value = match serde_json::from_str(&content) {
Ok(s) => s,
Err(_) => return vec!["coder-model".to_string()],
};
let mut models = std::collections::BTreeSet::new();
let mut has_providers = false;
if let Some(providers) = settings.get("modelProviders") {
if let Some(openai) = providers.get("openai").and_then(|v| v.as_array()) {
for item in openai {
if let Some(id) = item.get("id").and_then(|v| v.as_str()) {
models.insert(id.to_string());
has_providers = true;
}
}
}
}
let selected_model = settings
.get("model")
.and_then(|model| model.get("name"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(ref name) = selected_model {
models.insert(name.clone());
} else if !has_providers {
}
if models.is_empty() {
return vec!["coder-model".to_string()];
}
models.into_iter().collect()
}
pub fn get_qwen_selected_model() -> Option<String> {
let home = qwen_home()?;
let path = home.join(".qwen").join("settings.json");
if !path.exists() {
return None;
}
let content = fs::read_to_string(&path).ok()?;
let settings: serde_json::Value = serde_json::from_str(&content).ok()?;
settings
.get("model")
.and_then(|model| model.get("name"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
fn get_qwen_models() -> &'static [AgentModel] {
QWEN_MODELS_CACHE.get_or_init(|| {
let models = load_qwen_models();
models
.into_iter()
.map(|m| AgentModel {
agent: AgentKind::Qwen,
model: Box::leak(m.into_boxed_str()),
input_per_m: 0.0,
output_per_m: 0.0,
tier: "free",
description: "Default Qwen Code model",
capability: 7.4,
})
.collect()
})
}
pub(crate) fn static_models_for_agent(agent: &AgentKind) -> Vec<&'static AgentModel> {
if *agent == AgentKind::Qwen {
return get_qwen_models().iter().collect();
}
AGENT_MODELS
.iter()
.filter(|model| model.agent == *agent)
.collect()
}
fn budget_preferred_tiers(budget: TaskBudget) -> &'static [&'static str] {
match budget {
TaskBudget::Free => &["free"],
TaskBudget::Cheap => &["cheap", "free"],
TaskBudget::Standard => &["standard", "cheap", "free"],
TaskBudget::Premium => &["premium", "standard", "cheap", "free"],
}
}
pub fn model_on_budget_preference(kind: AgentKind, budget: TaskBudget, model: &str) -> bool {
AGENT_MODELS
.iter()
.find(|entry| entry.agent == kind && entry.model == model)
.is_some_and(|entry| budget_preferred_tiers(budget).contains(&entry.tier))
}
fn total_price(model: &AgentModel) -> f64 {
model.input_per_m + model.output_per_m
}
fn better_budget_candidate(budget: TaskBudget, left: &AgentModel, right: &AgentModel) -> Ordering {
match budget {
TaskBudget::Free | TaskBudget::Cheap => total_price(left)
.partial_cmp(&total_price(right))
.unwrap_or(Ordering::Equal)
.then_with(|| {
right
.capability
.partial_cmp(&left.capability)
.unwrap_or(Ordering::Equal)
}),
TaskBudget::Standard | TaskBudget::Premium => right
.capability
.partial_cmp(&left.capability)
.unwrap_or(Ordering::Equal),
}
}
fn pick_in_tier(kind: AgentKind, budget: TaskBudget, tier: &str) -> Option<&'static AgentModel> {
AGENT_MODELS
.iter()
.filter(|m| m.agent == kind && m.tier == tier)
.min_by(|a, b| better_budget_candidate(budget, a, b))
}
pub fn model_for_task_budget(kind: AgentKind, budget: TaskBudget) -> Option<&'static str> {
let preferred = budget_preferred_tiers(budget);
match budget {
TaskBudget::Free | TaskBudget::Cheap => AGENT_MODELS
.iter()
.filter(|m| m.agent == kind && preferred.contains(&m.tier))
.min_by(|a, b| better_budget_candidate(budget, a, b))
.or_else(|| pick_in_tier(kind, budget, "unknown"))
.map(|m| m.model),
TaskBudget::Standard | TaskBudget::Premium => preferred
.iter()
.copied()
.chain(std::iter::once("unknown"))
.find_map(|tier| pick_in_tier(kind, budget, tier))
.map(|m| m.model),
}
}
pub fn budget_model(agent: &AgentKind) -> Option<&'static str> {
if *agent == AgentKind::Qwen {
let models = static_models_for_agent(agent);
if models.is_empty() {
return None;
}
return get_qwen_selected_model()
.and_then(|selected| {
models
.iter()
.find(|model| model.model == selected)
.map(|model| model.model)
})
.or_else(|| models.first().map(|model| model.model));
}
model_for_task_budget(*agent, TaskBudget::Cheap)
}
#[cfg(test)]
#[path = "model_catalog_tests.rs"]
mod tests;