use crate::constants::env::{ai, ai_code};
use std::sync::OnceLock;
pub type ModelShortName = String;
pub type ModelName = String;
pub type ModelSetting = Option<ModelNameOrAlias>;
pub type ModelNameOrAlias = String;
pub type ModelAlias = String;
pub fn get_small_fast_model() -> ModelName {
std::env::var(ai::SMALL_FAST_MODEL)
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(get_default_haiku_model)
}
pub fn is_non_custom_opus_model(model: &ModelName) -> bool {
model == &get_model_strings().opus_40
|| model == &get_model_strings().opus_41
|| model == &get_model_strings().opus_45
|| model == &get_model_strings().opus_46
}
pub fn get_user_specified_model_setting() -> Option<String> {
if let Some(override_model) = get_main_loop_model_override() {
if is_model_allowed(&override_model) {
return Some(override_model);
} else {
return None;
}
}
if let Ok(env_model) = std::env::var(ai::MODEL) {
if !env_model.is_empty() && is_model_allowed(&env_model) {
return Some(env_model);
}
}
None
}
pub fn get_main_loop_model() -> ModelName {
if let Some(model) = get_user_specified_model_setting() {
return parse_user_specified_model(model);
}
get_default_main_loop_model()
}
pub fn get_best_model() -> ModelName {
get_default_opus_model()
}
pub fn get_default_opus_model() -> ModelName {
std::env::var(ai::DEFAULT_OPUS_MODEL)
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| get_model_strings().opus_46.clone())
}
pub fn get_default_sonnet_model() -> ModelName {
if let Ok(model) = std::env::var(ai::DEFAULT_SONNET_MODEL) {
if !model.is_empty() {
return model;
}
}
if get_api_provider() != "firstParty" {
return get_model_strings().sonnet_45.clone();
}
get_model_strings().sonnet_46.clone()
}
pub fn get_default_haiku_model() -> ModelName {
std::env::var(ai::DEFAULT_HAIKU_MODEL)
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| get_model_strings().haiku_45.clone())
}
pub fn get_runtime_main_loop_model(
permission_mode: &str,
main_loop_model: &str,
exceeds_200k_tokens: bool,
) -> ModelName {
if get_user_specified_model_setting() == Some("opusplan".to_string())
&& permission_mode == "plan"
&& !exceeds_200k_tokens
{
return get_default_opus_model();
}
if get_user_specified_model_setting() == Some("haiku".to_string()) && permission_mode == "plan"
{
return get_default_sonnet_model();
}
main_loop_model.to_string()
}
pub fn get_default_main_loop_model_setting() -> ModelNameOrAlias {
if let Ok(user_type) = std::env::var(ai::USER_TYPE) {
if user_type == "ant" {
if let Some(ant_config) = get_ant_model_override_config() {
return ant_config.default_model;
}
return format!("{}[1m]", get_default_opus_model());
}
}
if is_max_subscriber() {
return if is_opus_1m_merge_enabled() {
format!("{}[1m]", get_default_opus_model())
} else {
get_default_opus_model()
};
}
if is_team_premium_subscriber() {
return if is_opus_1m_merge_enabled() {
format!("{}[1m]", get_default_opus_model())
} else {
get_default_opus_model()
};
}
get_default_sonnet_model()
}
pub fn get_default_main_loop_model() -> ModelName {
parse_user_specified_model(get_default_main_loop_model_setting())
}
pub fn first_party_name_to_canonical(name: &ModelName) -> ModelShortName {
let name_lower = name.to_lowercase();
if name_lower.contains("claude-opus-4-6") {
return "claude-opus-4-6".to_string();
}
if name_lower.contains("claude-opus-4-5") {
return "claude-opus-4-5".to_string();
}
if name_lower.contains("claude-opus-4-1") {
return "claude-opus-4-1".to_string();
}
if name_lower.contains("claude-opus-4") {
return "claude-opus-4".to_string();
}
if name_lower.contains("claude-sonnet-4-6") {
return "claude-sonnet-4-6".to_string();
}
if name_lower.contains("claude-sonnet-4-5") {
return "claude-sonnet-4-5".to_string();
}
if name_lower.contains("claude-sonnet-4") {
return "claude-sonnet-4".to_string();
}
if name_lower.contains("claude-haiku-4-5") {
return "claude-haiku-4-5".to_string();
}
if name_lower.contains("claude-3-7-sonnet") {
return "claude-3-7-sonnet".to_string();
}
if name_lower.contains("claude-3-5-sonnet") {
return "claude-3-5-sonnet".to_string();
}
if name_lower.contains("claude-3-5-haiku") {
return "claude-3-5-haiku".to_string();
}
if name_lower.contains("claude-3-opus") {
return "claude-3-opus".to_string();
}
if name_lower.contains("claude-3-sonnet") {
return "claude-3-sonnet".to_string();
}
if name_lower.contains("claude-3-haiku") {
return "claude-3-haiku".to_string();
}
if let Some(captures) = regex::Regex::new(r"(claude-(\d+-\d+-)?\w+)")
.ok()
.and_then(|re| re.captures(&name_lower))
{
if let Some(m) = captures.get(1) {
return m.as_str().to_string();
}
}
name.clone()
}
pub fn get_canonical_name(full_model_name: &str) -> ModelShortName {
let resolved = resolve_overridden_model(full_model_name);
first_party_name_to_canonical(&resolved)
}
pub fn get_claude_ai_user_default_model_description(fast_mode: bool) -> String {
if is_max_subscriber() || is_team_premium_subscriber() {
let base = if is_opus_1m_merge_enabled() {
"Opus 4.6 with 1M context"
} else {
"Opus 4.6"
};
let suffix = if fast_mode {
get_opus_46_pricing_suffix(true)
} else {
"".to_string()
};
format!("{} · Most capable for complex work{}", base, suffix)
} else {
"Sonnet 4.6 · Best for everyday tasks".to_string()
}
}
pub fn render_default_model_setting(setting: &ModelNameOrAlias) -> String {
if setting == "opusplan" {
return "Opus 4.6 in plan mode, else Sonnet 4.6".to_string();
}
render_model_name(&parse_user_specified_model(setting.clone()))
}
pub fn get_opus_46_pricing_suffix(fast_mode: bool) -> String {
if get_api_provider() != "firstParty" {
return "".to_string();
}
let pricing = "pricing_placeholder".to_string();
let fast_mode_indicator = if fast_mode { " (lightning)" } else { "" };
format!(" ·{} {}", fast_mode_indicator, pricing)
}
pub fn is_opus_1m_merge_enabled() -> bool {
if is_1m_context_disabled() || is_pro_subscriber() || get_api_provider() != "firstParty" {
return false;
}
if is_claude_ai_subscriber() && get_subscription_type().is_none() {
return false;
}
true
}
pub fn render_model_setting(setting: &ModelNameOrAlias) -> String {
if setting == "opusplan" {
return "Opus Plan".to_string();
}
if is_model_alias(setting) {
return capitalize(setting);
}
render_model_name(setting)
}
pub fn get_public_model_display_name(model: &ModelName) -> Option<String> {
let model_strings = get_model_strings();
if model == &model_strings.opus_46 {
return Some("Opus 4.6".to_string());
}
if model == &format!("{}[1m]", model_strings.opus_46) {
return Some("Opus 4.6 (1M context)".to_string());
}
if model == &model_strings.opus_45 {
return Some("Opus 4.5".to_string());
}
if model == &model_strings.opus_41 {
return Some("Opus 4.1".to_string());
}
if model == &model_strings.opus_40 {
return Some("Opus 4".to_string());
}
if model == &format!("{}[1m]", model_strings.sonnet_46) {
return Some("Sonnet 4.6 (1M context)".to_string());
}
if model == &model_strings.sonnet_46 {
return Some("Sonnet 4.6".to_string());
}
if model == &format!("{}[1m]", model_strings.sonnet_45) {
return Some("Sonnet 4.5 (1M context)".to_string());
}
if model == &model_strings.sonnet_45 {
return Some("Sonnet 4.5".to_string());
}
if model == &model_strings.sonnet_40 {
return Some("Sonnet 4".to_string());
}
if model == &format!("{}[1m]", model_strings.sonnet_40) {
return Some("Sonnet 4 (1M context)".to_string());
}
if model == &model_strings.sonnet_37 {
return Some("Sonnet 3.7".to_string());
}
if model == &model_strings.sonnet_35 {
return Some("Sonnet 3.5".to_string());
}
if model == &model_strings.haiku_45 {
return Some("Haiku 4.5".to_string());
}
if model == &model_strings.haiku_35 {
return Some("Haiku 3.5".to_string());
}
None
}
fn mask_model_codename(base_name: &str) -> String {
let parts: Vec<&str> = base_name.split('-').collect();
if parts.is_empty() {
return base_name.to_string();
}
let codename = parts[0];
let rest: Vec<&str> = parts[1..].to_vec();
let masked = if codename.len() > 3 {
format!("{}{}", &codename[..3], "*".repeat(codename.len() - 3))
} else {
codename.to_string()
};
let mut result = masked;
for part in rest {
result.push('-');
result.push_str(part);
}
result
}
pub fn render_model_name(model: &ModelName) -> String {
if let Some(public_name) = get_public_model_display_name(model) {
return public_name;
}
if let Ok(user_type) = std::env::var(ai::USER_TYPE) {
if user_type == "ant" {
let resolved = parse_user_specified_model(model.clone());
if let Some(ant_model) = resolve_ant_model(model) {
let base_name = ant_model.model.replace("[1m]", "");
let masked = mask_model_codename(&base_name);
let suffix = if has_1m_context(&resolved) {
"[1m]"
} else {
""
};
return format!("{}{}", masked, suffix);
}
if resolved != *model {
return format!("{} ({})", model, resolved);
}
return resolved;
}
}
model.clone()
}
pub fn get_public_model_name(model: &ModelName) -> String {
if let Some(public_name) = get_public_model_display_name(model) {
return format!("Claude {}", public_name);
}
format!("Claude ({})", model)
}
pub fn parse_user_specified_model(model_input: ModelNameOrAlias) -> ModelName {
let model_input_trimmed = model_input.trim().to_string();
let normalized_model = model_input_trimmed.to_lowercase();
let has_1m_tag = has_1m_context(&normalized_model);
let model_string = if has_1m_tag {
normalized_model.replace("[1m]", "").trim().to_string()
} else {
normalized_model.clone()
};
if is_model_alias(&model_string) {
match model_string.as_str() {
"opusplan" => {
return format!(
"{}{}",
get_default_sonnet_model(),
if has_1m_tag { "[1m]" } else { "" }
);
}
"sonnet" => {
return format!(
"{}{}",
get_default_sonnet_model(),
if has_1m_tag { "[1m]" } else { "" }
);
}
"haiku" => {
return format!(
"{}{}",
get_default_haiku_model(),
if has_1m_tag { "[1m]" } else { "" }
);
}
"opus" => {
return format!(
"{}{}",
get_default_opus_model(),
if has_1m_tag { "[1m]" } else { "" }
);
}
"best" => {
return get_best_model();
}
_ => {}
}
}
if get_api_provider() == "firstParty"
&& is_legacy_opus_first_party(&model_string)
&& is_legacy_model_remap_enabled()
{
return format!(
"{}{}",
get_default_opus_model(),
if has_1m_tag { "[1m]" } else { "" }
);
}
if let Ok(user_type) = std::env::var(ai::USER_TYPE) {
if user_type == "ant" {
let has_1m_ant_tag = has_1m_context(&normalized_model);
let base_ant_model = normalized_model.replace("[1m]", "").trim().to_string();
if let Some(ant_model) = resolve_ant_model(&base_ant_model) {
let suffix = if has_1m_ant_tag { "[1m]" } else { "" };
return format!("{}{}", ant_model.model, suffix);
}
}
}
if has_1m_tag {
return format!("{}[1m]", model_input_trimmed.replace("[1m]", "").trim());
}
model_input_trimmed
}
pub fn resolve_skill_model_override(skill_model: &str, current_model: &str) -> String {
if has_1m_context(skill_model) || !has_1m_context(current_model) {
return skill_model.to_string();
}
if model_supports_1m(&parse_user_specified_model(skill_model.to_string())) {
return format!("{}[1m]", skill_model);
}
skill_model.to_string()
}
const LEGACY_OPUS_FIRSTPARTY: &[&str] = &[
"claude-opus-4-20250514",
"claude-opus-4-1-20250805",
"claude-opus-4-0",
"claude-opus-4-1",
];
fn is_legacy_opus_first_party(model: &str) -> bool {
LEGACY_OPUS_FIRSTPARTY.contains(&model)
}
pub fn is_legacy_model_remap_enabled() -> bool {
!is_env_truthy(&std::env::var(ai_code::DISABLE_LEGACY_MODEL_REMAP).unwrap_or_default())
}
pub fn model_display_string(model: &ModelSetting) -> String {
if model.is_none() {
if let Ok(user_type) = std::env::var(ai::USER_TYPE) {
if user_type == "ant" {
return format!(
"Default for Ants ({})",
render_default_model_setting(&get_default_main_loop_model_setting())
);
}
}
if is_claude_ai_subscriber() {
return format!(
"Default ({})",
get_claude_ai_user_default_model_description(false)
);
}
return format!("Default ({})", get_default_main_loop_model());
}
let model = model.as_ref().unwrap();
let resolved_model = parse_user_specified_model(model.clone());
if model == &resolved_model {
resolved_model
} else {
format!("{} ({})", model, resolved_model)
}
}
pub fn get_marketing_name_for_model(model_id: &str) -> Option<String> {
if get_api_provider() == "foundry" {
return None;
}
let has_1m = model_id.to_lowercase().contains("[1m]");
let canonical = get_canonical_name(model_id);
if canonical.contains("claude-opus-4-6") {
return Some(if has_1m {
"Opus 4.6 (with 1M context)".to_string()
} else {
"Opus 4.6".to_string()
});
}
if canonical.contains("claude-opus-4-5") {
return Some("Opus 4.5".to_string());
}
if canonical.contains("claude-opus-4-1") {
return Some("Opus 4.1".to_string());
}
if canonical.contains("claude-opus-4") {
return Some("Opus 4".to_string());
}
if canonical.contains("claude-sonnet-4-6") {
return Some(if has_1m {
"Sonnet 4.6 (with 1M context)".to_string()
} else {
"Sonnet 4.6".to_string()
});
}
if canonical.contains("claude-sonnet-4-5") {
return Some(if has_1m {
"Sonnet 4.5 (with 1M context)".to_string()
} else {
"Sonnet 4.5".to_string()
});
}
if canonical.contains("claude-sonnet-4") {
return Some(if has_1m {
"Sonnet 4 (with 1M context)".to_string()
} else {
"Sonnet 4".to_string()
});
}
if canonical.contains("claude-3-7-sonnet") {
return Some("Claude 3.7 Sonnet".to_string());
}
if canonical.contains("claude-3-5-sonnet") {
return Some("Claude 3.5 Sonnet".to_string());
}
if canonical.contains("claude-haiku-4-5") {
return Some("Haiku 4.5".to_string());
}
if canonical.contains("claude-3-5-haiku") {
return Some("Claude 3.5 Haiku".to_string());
}
None
}
pub fn normalize_model_string_for_api(model: &str) -> String {
regex::Regex::new(r"\[(1|2)m\]")
.map(|re| re.replace_all(model, "").to_string())
.unwrap_or_else(|_| model.to_string())
}
static MODEL_STRINGS: OnceLock<ModelStrings> = OnceLock::new();
#[derive(Debug, Clone)]
struct ModelStrings {
opus_40: ModelName,
opus_41: ModelName,
opus_45: ModelName,
opus_46: ModelName,
sonnet_35: ModelName,
sonnet_37: ModelName,
sonnet_40: ModelName,
sonnet_45: ModelName,
sonnet_46: ModelName,
haiku_35: ModelName,
haiku_45: ModelName,
}
fn get_model_strings() -> &'static ModelStrings {
MODEL_STRINGS.get_or_init(|| ModelStrings {
opus_40: "claude-opus-4-0-20250514".to_string(),
opus_41: "claude-opus-4-1-20250805".to_string(),
opus_45: "claude-opus-4-5-20250514".to_string(),
opus_46: "claude-opus-4-6-20251106".to_string(),
sonnet_35: "claude-sonnet-3-5-20241022".to_string(),
sonnet_37: "claude-sonnet-3-7-20250120".to_string(),
sonnet_40: "claude-sonnet-4-0-20250514".to_string(),
sonnet_45: "claude-sonnet-4-5-20241022".to_string(),
sonnet_46: "claude-sonnet-4-6-20251106".to_string(),
haiku_35: "claude-haiku-3-5-20241022".to_string(),
haiku_45: "claude-haiku-4-5-20250513".to_string(),
})
}
fn get_api_provider() -> String {
std::env::var(ai::API_PROVIDER)
.ok()
.unwrap_or_else(|| "firstParty".to_string())
}
fn get_main_loop_model_override() -> Option<ModelName> {
None
}
fn is_model_allowed(_model: &str) -> bool {
true
}
fn is_model_alias(model: &str) -> bool {
matches!(model, "opus" | "sonnet" | "haiku" | "opusplan" | "best")
}
fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
fn is_1m_context_disabled() -> bool {
false
}
fn has_1m_context(model: &str) -> bool {
model.to_lowercase().ends_with("[1m]")
}
fn model_supports_1m(model: &ModelName) -> bool {
let canonical = get_canonical_name(model);
matches!(
canonical.as_str(),
"claude-opus-4-6" | "claude-opus-4-5" | "claude-sonnet-4-6" | "claude-sonnet-4-5"
)
}
fn resolve_overridden_model(model: &str) -> ModelName {
model.to_string()
}
fn is_max_subscriber() -> bool {
false
}
fn is_team_premium_subscriber() -> bool {
false
}
fn is_pro_subscriber() -> bool {
false
}
fn is_claude_ai_subscriber() -> bool {
false
}
fn get_subscription_type() -> Option<String> {
None
}
fn is_env_truthy(value: &str) -> bool {
let normalized = value.to_lowercase();
matches!(normalized.trim(), "1" | "true" | "yes" | "on")
}
#[derive(Debug, Clone)]
struct AntModelConfig {
default_model: String,
model: String,
}
fn get_ant_model_override_config() -> Option<AntModelConfig> {
None
}
fn resolve_ant_model(_model: &str) -> Option<AntModelConfig> {
None
}