use crate::constants::env::{ai, anthropic};
use crate::env::EnvConfig;
use once_cell::sync::Lazy;
use std::sync::Mutex;
pub type ModelName = String;
pub type ModelShortName = String;
pub type ModelAlias = String;
pub type ModelSetting = Option<String>;
#[derive(Debug, Clone, Default)]
pub struct ModelStrings {
pub opus46: String,
pub opus45: String,
pub opus41: String,
pub opus40: String,
pub sonnet46: String,
pub sonnet45: String,
pub sonnet40: String,
pub sonnet37: String,
pub sonnet35: String,
pub haiku45: String,
pub haiku35: String,
}
impl ModelStrings {
pub fn default_first_party() -> Self {
Self {
opus46: "claude-opus-4-6-20250514".to_string(),
opus45: "claude-opus-4-5-20250514".to_string(),
opus41: "claude-opus-4-1-20250514".to_string(),
opus40: "claude-opus-4-0-20250514".to_string(),
sonnet46: "claude-sonnet-4-6-20250514".to_string(),
sonnet45: "claude-sonnet-4-5-20250514".to_string(),
sonnet40: "claude-sonnet-4-0-20250514".to_string(),
sonnet37: "claude-sonnet-3-7-20250514".to_string(),
sonnet35: "claude-sonnet-3-5-20250603".to_string(),
haiku45: "claude-haiku-4-5-20250514".to_string(),
haiku35: "claude-haiku-3-5-20250514".to_string(),
}
}
pub fn get() -> Self {
MODEL_STRINGS.lock().unwrap().clone()
}
}
static MODEL_STRINGS: Lazy<Mutex<ModelStrings>> = Lazy::new(|| {
let env_config = EnvConfig::load();
Mutex::new(ModelStrings::default_first_party())
});
pub const MODEL_ALIASES: &[&str] = &["opus", "sonnet", "haiku", "opusplan", "best"];
pub fn is_model_alias(model: &str) -> bool {
MODEL_ALIASES.contains(&model.to_lowercase().as_str())
}
pub fn get_small_fast_model() -> ModelName {
std::env::var(ai::SMALL_FAST_MODEL)
.ok()
.unwrap_or_else(get_default_haiku_model)
}
pub fn is_non_custom_opus_model(model: &ModelName) -> bool {
let strings = ModelStrings::get();
model == strings.opus40
|| model == strings.opus41
|| model == strings.opus45
|| model == strings.opus46
}
pub fn get_user_specified_model_setting() -> ModelSetting {
let env_config = EnvConfig::load();
if let Some(model) = &env_config.model {
if is_model_allowed(model) {
return Some(model.clone());
}
return None;
}
None
}
pub fn is_model_allowed(model: &str) -> bool {
let lower = model.to_lowercase();
if is_model_alias(&lower) {
return true;
}
let strings = ModelStrings::get();
let known_models = [
strings.opus46.as_str(),
strings.opus45.as_str(),
strings.opus41.as_str(),
strings.opus40.as_str(),
strings.sonnet46.as_str(),
strings.sonnet45.as_str(),
strings.sonnet40.as_str(),
strings.sonnet37.as_str(),
strings.sonnet35.as_str(),
strings.haiku45.as_str(),
strings.haiku35.as_str(),
];
if known_models.contains(&lower.as_str()) {
return true;
}
if known_models.contains(&lower.replace("[1m]", "").as_str()) {
return true;
}
let prefixes = ["claude-opus-", "claude-sonnet-", "claude-haiku-"];
for prefix in prefixes {
if lower.starts_with(prefix) {
return true;
}
}
false
}
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 {
if let Ok(model) = std::env::var(anthropic::DEFAULT_OPUS_MODEL) {
return model;
}
ModelStrings::get().opus46
}
pub fn get_default_sonnet_model() -> ModelName {
if let Ok(model) = std::env::var(anthropic::DEFAULT_SONNET_MODEL) {
return model;
}
ModelStrings::get().sonnet46
}
pub fn get_default_haiku_model() -> ModelName {
if let Ok(model) = std::env::var(anthropic::DEFAULT_HAIKU_MODEL) {
return model;
}
ModelStrings::get().haiku45
}
pub fn get_default_main_loop_model() -> ModelName {
parse_user_specified_model(&get_default_main_loop_model_setting())
}
pub fn get_default_main_loop_model_setting() -> String {
get_default_sonnet_model()
}
pub fn parse_user_specified_model(model_input: &str) -> ModelName {
let model_input = model_input.trim();
let normalized = model_input.to_lowercase();
let has_1m = normalized.contains("[1m]");
let model_base = if has_1m {
normalized.replace("[1m]", "").trim().to_string()
} else {
normalized.clone()
};
if is_model_alias(&model_base) {
match model_base.as_str() {
"opusplan" => {
let base = get_default_sonnet_model();
return if has_1m {
format!("{}[1m]", base)
} else {
base
};
}
"sonnet" => {
let base = get_default_sonnet_model();
return if has_1m {
format!("{}[1m]", base)
} else {
base
};
}
"haiku" => {
let base = get_default_haiku_model();
return if has_1m {
format!("{}[1m]", base)
} else {
base
};
}
"opus" => {
let base = get_default_opus_model();
return if has_1m {
format!("{}[1m]", base)
} else {
base
};
}
"best" => {
return get_best_model();
}
_ => {}
}
}
let legacy_opus = [
"claude-opus-4-20250514",
"claude-opus-4-1-20250805",
"claude-opus-4-0",
"claude-opus-4-1",
];
if legacy_opus.contains(&model_base.as_str()) {
let base = get_default_opus_model();
return if has_1m {
format!("{}[1m]", base)
} else {
base
};
}
if has_1m {
let base = model_input.replace("[1m]", "").trim();
return format!("{}[1m]", base);
}
model_input.to_string()
}
pub fn first_party_name_to_canonical(name: &str) -> 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(cap) = regex::Regex::new(r"(claude-(\d+-\d+-)?\w+)")
.ok()
.and_then(|re| re.captures(&name_lower))
{
if let Some(m) = cap.get(1) {
return m.as_str().to_string();
}
}
name.to_string()
}
pub fn get_canonical_name(full_model_name: &str) -> ModelShortName {
first_party_name_to_canonical(full_model_name)
}
pub fn get_public_model_display_name(model: &str) -> Option<String> {
let strings = ModelStrings::get();
match model {
s if s == strings.opus46 => Some("Opus 4.6".to_string()),
s if s == format!("{}[1m]", strings.opus46) => Some("Opus 4.6 (1M context)".to_string()),
s if s == strings.opus45 => Some("Opus 4.5".to_string()),
s if s == strings.opus41 => Some("Opus 4.1".to_string()),
s if s == strings.opus40 => Some("Opus 4".to_string()),
s if s == format!("{}[1m]", strings.sonnet46) => {
Some("Sonnet 4.6 (1M context)".to_string())
}
s if s == strings.sonnet46 => Some("Sonnet 4.6".to_string()),
s if s == format!("{1}[1m]", strings.sonnet45) => {
Some("Sonnet 4.5 (1M context)".to_string())
}
s if s == strings.sonnet45 => Some("Sonnet 4.5".to_string()),
s if s == strings.sonnet40 => Some("Sonnet 4".to_string()),
s if s == format!("{}[1m]", strings.sonnet40) => Some("Sonnet 4 (1M context)".to_string()),
s if s == strings.sonnet37 => Some("Sonnet 3.7".to_string()),
s if s == strings.sonnet35 => Some("Sonnet 3.5".to_string()),
s if s == strings.haiku45 => Some("Haiku 4.5".to_string()),
s if s == strings.haiku35 => Some("Haiku 3.5".to_string()),
_ => None,
}
}
pub fn render_model_name(model: &ModelName) -> String {
if let Some(public_name) = get_public_model_display_name(model) {
return public_name;
}
model.clone()
}
pub fn normalize_model_string_for_api(model: &str) -> String {
let re = regex::Regex::new(r"\[(1|2)m\]").unwrap();
re.replace_all(model, "").to_string()
}
pub fn model_supports_1m(model: &str) -> bool {
let strings = ModelStrings::get();
let canonical = get_canonical_name(model);
canonical.contains("claude-opus-4") || canonical.contains("claude-sonnet-4")
}
pub fn has_1m_context(model: &str) -> bool {
model.to_lowercase().contains("[1m]")
}
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();
}
let parsed = parse_user_specified_model(skill_model);
if model_supports_1m(&parsed) {
return format!("{}[1m]", skill_model);
}
skill_model.to_string()
}