use std::collections::BTreeMap;
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Tiers {
pub task_tier: BTreeMap<String, String>,
pub tiers: BTreeMap<String, BTreeMap<String, String>>,
}
fn m(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
}
impl Default for Tiers {
fn default() -> Self {
Tiers {
task_tier: m(&[
("auto_name", "low"),
("name_session", "low"),
("mission", "low"),
("watch", "mid"),
("translate", "mid"),
("ask", "high"),
]),
tiers: [
("groq", m(&[
("low", "llama-3.1-8b-instant"),
("mid", "qwen/qwen3-32b"),
("high", "llama-3.3-70b-versatile"),
])),
("anthropic", m(&[
("low", "claude-haiku-4-5"),
("mid", "claude-sonnet-5"),
("high", "claude-opus-4-8"),
])),
("openai", m(&[
("low", "gpt-4o-mini"),
("mid", "gpt-4o-mini"),
("high", "gpt-4o"),
])),
("gemini", m(&[
("low", "gemini-3.1-flash-lite"),
("mid", "gemini-3.1-flash-lite"),
("high", "gemini-3.1-flash"),
])),
]
.into_iter()
.map(|(p, t)| (p.to_string(), t))
.collect(),
}
}
}
fn tiers_path() -> Option<std::path::PathBuf> {
crate::config::state_path().map(|p| p.with_file_name("tiers.json"))
}
pub fn load() -> Tiers {
let Some(path) = tiers_path() else { return Tiers::default() };
if let Ok(s) = std::fs::read_to_string(&path) {
if let Ok(t) = serde_json::from_str::<Tiers>(&s) {
return t;
}
}
let def = Tiers::default();
if !path.exists() {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(json) = serde_json::to_string_pretty(&def) {
let _ = std::fs::write(&path, json);
}
}
def
}
pub fn model_for(provider: &str, task: &str, default_model: &str) -> String {
if std::env::var("MARS_LLM_MODEL").is_ok() || std::env::var("ARES_LLM_MODEL").is_ok() {
return default_model.to_string();
}
let t = load();
let Some(tier) = t.task_tier.get(task) else { return default_model.to_string() };
t.tiers
.get(provider)
.and_then(|m| m.get(tier))
.cloned()
.unwrap_or_else(|| default_model.to_string())
}
const TIER_ORDER: [&str; 3] = ["low", "mid", "high"];
fn model_above_in(t: &Tiers, provider: &str, task: &str) -> Option<String> {
let tier = t.task_tier.get(task)?;
let models = t.tiers.get(provider)?;
let current = models.get(tier)?;
let start = TIER_ORDER.iter().position(|x| *x == tier.as_str())?;
for next in &TIER_ORDER[start + 1..] {
if let Some(m) = models.get(*next) {
if m != current {
return Some(m.clone());
}
}
}
None
}
pub fn model_above(provider: &str, task: &str) -> Option<String> {
if std::env::var("MARS_LLM_MODEL").is_ok() || std::env::var("ARES_LLM_MODEL").is_ok() {
return None;
}
model_above_in(&load(), provider, task)
}
#[cfg_attr(not(feature = "memory"), allow(dead_code))] pub fn tier_descriptions() -> Vec<String> {
let t = Tiers::default();
let mut out = vec![
"The model tier ring routes each agent task to a tier (low/mid/high); edit \
~/.config/mars/tiers.json to move a task between tiers or re-point a tier to \
a different model. An explicit MARS_LLM_MODEL overrides the ring."
.to_string(),
];
for (task, tier) in &t.task_tier {
out.push(format!(
"The `{task}` task runs on the `{tier}` tier by default; change it under \
`task_tier` in ~/.config/mars/tiers.json."
));
}
out.push(
"On a provider rate limit (HTTP 429) the agent rotates the call to another \
configured provider's model for the same tier — set more than one API key \
(e.g. GROQ_API_KEY and GEMINI_API_KEY) to enable rotation; an explicit \
MARS_LLM_MODEL disables it."
.to_string(),
);
out.push(
"If an ask reply proposes an action that fails the registry check, the \
question is retried once on the model one tier up (logged as task \
`ask_escalated`); an explicit MARS_LLM_MODEL disables escalation."
.to_string(),
);
out
}