use crate::types::AgentKind;
const PREMIUM_GROUP: &str = "premium";
const AUTO_GROUP: &str = "auto";
pub(crate) fn has_grouped_quota(agent: AgentKind) -> bool {
if !groups_for_agent(agent).is_empty() {
return true;
}
matches!(
crate::types::provider_for_cli(agent).1,
crate::types::MeteringShape::PerModelFamily
)
}
pub(crate) fn model_group(agent: AgentKind, model: Option<&str>) -> Option<&'static str> {
if !has_grouped_quota(agent) {
return None;
}
let model = model?.to_ascii_lowercase();
if agent == AgentKind::Cursor {
return Some(if model.starts_with("auto") { AUTO_GROUP } else { PREMIUM_GROUP });
}
Some(family_of(&model))
}
pub(crate) fn group_from_refusal(agent: AgentKind, message: &str) -> Option<&'static str> {
if agent != AgentKind::Cursor {
return None;
}
message
.to_ascii_lowercase()
.contains("you're out of usage")
.then_some(PREMIUM_GROUP)
}
fn family_of(model: &str) -> &'static str {
crate::types::model_family(model)
}
pub(crate) fn groups_for_agent(agent: AgentKind) -> &'static [(&'static str, &'static [&'static str])] {
match agent {
AgentKind::Antigravity => &[
("gemini", &["gemini-3.1-pro-high", "gemini-3.6-flash-high", "gemini-3.6-flash-low"]),
("claude", &["claude-opus-4-6-thinking", "claude-sonnet-4-6"]),
("gpt-oss", &["gpt-oss-120b-medium"]),
],
AgentKind::Cursor => &[
(PREMIUM_GROUP, &["composer-2.5", "gpt-5.4-high"]),
(AUTO_GROUP, &["auto"]),
],
_ => &[],
}
}
pub(crate) fn healthy_model_for(
agent: AgentKind,
current: Option<&str>,
is_group_limited: impl Fn(&str) -> bool,
) -> Option<&'static str> {
let groups = groups_for_agent(agent);
if groups.is_empty() {
return None;
}
let current_group = model_group(agent, current);
if let Some(group) = current_group
&& !is_group_limited(group)
{
return None; }
groups
.iter()
.find(|(name, models)| !is_group_limited(name) && !models.is_empty())
.and_then(|(_, models)| models.first().copied())
}
#[cfg(test)]
#[path = "model_group_tests.rs"]
mod tests;