#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum AnthropicModelFamily {
Opus,
Sonnet,
Haiku,
Fable,
Mythos,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct AnthropicModel<'a> {
pub normalized_name: &'a str,
pub family: AnthropicModelFamily,
pub major_version: Option<u32>,
pub minor_version: Option<u32>,
pub date_label: Option<&'a str>,
pub remaining_suffix: Vec<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct AnthropicModelCapabilities {
pub supports_effort: bool,
pub supports_max_effort: bool,
pub supports_xhigh_effort: bool,
pub supports_adaptive_thinking: bool,
pub thinking_enabled_by_default: bool,
pub supports_legacy_budget_thinking: bool,
pub max_tokens: AnthropicMaxTokens,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum AnthropicMaxTokens {
Tokens4K,
Tokens8K,
Tokens32K,
Tokens64K,
Tokens128K,
}
impl<'a> AnthropicModel<'a> {
pub fn parse(normalized_name: &'a str) -> Self {
let segments = normalized_name.split('-').collect::<Vec<_>>();
if segments.first() != Some(&"claude") {
return Self::unknown(normalized_name);
}
let Some(family_name) = segments.get(1) else {
return Self::unknown(normalized_name);
};
let family = AnthropicModelFamily::from_segment(family_name);
if family == AnthropicModelFamily::Unknown {
return Self::unknown(normalized_name);
}
let model_segments = segments.get(2..).unwrap_or_default();
let mut segment_index = 0;
let mut major_version = None;
let mut minor_version = None;
let mut date_label = None;
if let Some(segment) = model_segments.get(segment_index) {
if is_version_segment(segment) {
major_version = segment.parse().ok();
segment_index += 1;
} else if is_date_label(segment) {
date_label = Some(*segment);
segment_index += 1;
}
}
if major_version.is_some()
&& let Some(segment) = model_segments.get(segment_index)
&& is_version_segment(segment)
{
minor_version = segment.parse().ok();
segment_index += 1;
}
if date_label.is_none()
&& let Some(segment) = model_segments.get(segment_index)
&& is_date_label(segment)
{
date_label = Some(*segment);
segment_index += 1;
}
Self {
normalized_name,
family,
major_version,
minor_version,
date_label,
remaining_suffix: model_segments.get(segment_index..).unwrap_or_default().to_vec(),
}
}
pub fn version(&self) -> Option<(u32, u32)> {
self.major_version.map(|major| (major, self.minor_version.unwrap_or(0)))
}
pub fn capabilities(&self) -> AnthropicModelCapabilities {
let supports_effort = self.supports_effort();
let supports_max_effort = self.supports_max_effort();
let supports_xhigh_effort = self.supports_xhigh_effort();
let supports_adaptive_thinking = self.supports_adaptive_thinking();
AnthropicModelCapabilities {
supports_effort,
supports_max_effort,
supports_xhigh_effort,
supports_adaptive_thinking,
thinking_enabled_by_default: self.thinking_enabled_by_default(),
supports_legacy_budget_thinking: !supports_adaptive_thinking,
max_tokens: self.max_tokens(),
}
}
fn max_tokens(&self) -> AnthropicMaxTokens {
if self.is_haiku_5_or_later() {
AnthropicMaxTokens::Tokens128K
} else {
let model_name = self.normalized_name;
if legacy_is_fable_or_mythos(model_name) {
AnthropicMaxTokens::Tokens128K
} else if model_name.contains("claude-sonnet")
|| model_name.contains("claude-haiku")
|| model_name.contains("claude-3-7-sonnet")
|| model_name.contains("claude-opus-4-5")
{
AnthropicMaxTokens::Tokens64K
} else if model_name.contains("claude-opus-4") {
AnthropicMaxTokens::Tokens32K
} else if model_name.contains("claude-3-5") {
AnthropicMaxTokens::Tokens8K
} else if model_name.contains("3-opus") || model_name.contains("3-haiku") {
AnthropicMaxTokens::Tokens4K
} else {
AnthropicMaxTokens::Tokens64K
}
}
}
fn unknown(normalized_name: &'a str) -> Self {
Self {
normalized_name,
family: AnthropicModelFamily::Unknown,
major_version: None,
minor_version: None,
date_label: None,
remaining_suffix: Vec::new(),
}
}
fn is_haiku_5_or_later(&self) -> bool {
matches!(self.family, AnthropicModelFamily::Haiku) && self.version().is_some_and(|version| version >= (5, 0))
}
fn supports_effort(&self) -> bool {
match self.family {
AnthropicModelFamily::Opus => self.version().is_some_and(|version| version >= (4, 5)),
AnthropicModelFamily::Sonnet => matches!(self.version(), Some((4, 6) | (5, _))),
AnthropicModelFamily::Fable | AnthropicModelFamily::Mythos => true,
AnthropicModelFamily::Haiku => self.is_haiku_5_or_later(),
AnthropicModelFamily::Unknown => legacy_supports_effort(self.normalized_name),
}
}
fn supports_max_effort(&self) -> bool {
match self.family {
AnthropicModelFamily::Opus => self.version().is_some_and(|version| version >= (4, 6)),
AnthropicModelFamily::Sonnet => matches!(self.version(), Some((4, 6) | (5, _))),
AnthropicModelFamily::Fable | AnthropicModelFamily::Mythos => true,
AnthropicModelFamily::Haiku => self.is_haiku_5_or_later(),
AnthropicModelFamily::Unknown => legacy_supports_max_effort(self.normalized_name),
}
}
fn supports_xhigh_effort(&self) -> bool {
match self.family {
AnthropicModelFamily::Opus => self.version().is_some_and(|version| version >= (4, 7)),
AnthropicModelFamily::Sonnet => matches!(self.version(), Some((5, _))),
AnthropicModelFamily::Fable | AnthropicModelFamily::Mythos => true,
AnthropicModelFamily::Haiku => self.is_haiku_5_or_later(),
AnthropicModelFamily::Unknown => legacy_supports_xhigh_effort(self.normalized_name),
}
}
fn supports_adaptive_thinking(&self) -> bool {
match self.family {
AnthropicModelFamily::Opus => self.version().is_some_and(|version| version >= (4, 6)),
AnthropicModelFamily::Sonnet => matches!(self.version(), Some((4, 6) | (5, _))),
AnthropicModelFamily::Fable | AnthropicModelFamily::Mythos => true,
AnthropicModelFamily::Haiku => self.is_haiku_5_or_later(),
AnthropicModelFamily::Unknown => legacy_supports_adaptive_thinking(self.normalized_name),
}
}
fn thinking_enabled_by_default(&self) -> bool {
match self.family {
AnthropicModelFamily::Opus | AnthropicModelFamily::Sonnet => {
matches!(self.version(), Some((5, _)))
}
AnthropicModelFamily::Haiku => self.is_haiku_5_or_later(),
AnthropicModelFamily::Fable | AnthropicModelFamily::Mythos => false,
AnthropicModelFamily::Unknown => legacy_thinking_enabled_by_default(self.normalized_name),
}
}
}
impl AnthropicModelFamily {
fn from_segment(segment: &str) -> Self {
match segment {
"opus" => Self::Opus,
"sonnet" => Self::Sonnet,
"haiku" => Self::Haiku,
"fable" => Self::Fable,
"mythos" => Self::Mythos,
_ => Self::Unknown,
}
}
}
fn is_version_segment(segment: &str) -> bool {
(1..=2).contains(&segment.len()) && segment.bytes().all(|byte| byte.is_ascii_digit())
}
fn is_date_label(segment: &str) -> bool {
segment.len() == 8 && segment.bytes().all(|byte| byte.is_ascii_digit())
}
fn legacy_opus_version(model_name: &str) -> Option<(u32, u32)> {
let version = model_name.split_once("claude-opus-")?.1;
let major_len = version.bytes().take_while(u8::is_ascii_digit).count();
let major = version.get(..major_len)?.parse().ok()?;
let remaining = version.get(major_len..)?;
let minor_digits = remaining
.strip_prefix('-')
.map(|minor| minor.bytes().take_while(u8::is_ascii_digit).count())
.unwrap_or(0);
let minor = if minor_digits == 0 || minor_digits > 2 {
0
} else {
remaining.get(1..=minor_digits)?.parse().ok()?
};
Some((major, minor))
}
fn legacy_is_fable_or_mythos(model_name: &str) -> bool {
model_name.contains("fable") || model_name.contains("mythos")
}
fn legacy_supports_effort(model_name: &str) -> bool {
const SUPPORT_EFFORT_MODELS: &[&str] =
&["claude-opus-4-6", "claude-sonnet-4-6", "claude-opus-4-5", "claude-sonnet-5"];
SUPPORT_EFFORT_MODELS.iter().any(|name| model_name.contains(name))
|| legacy_is_fable_or_mythos(model_name)
|| legacy_opus_version(model_name).is_some_and(|version| version >= (4, 7))
}
fn legacy_supports_max_effort(model_name: &str) -> bool {
const SUPPORT_MAX_MODELS: &[&str] = &["claude-opus-4-6", "claude-sonnet-5"];
SUPPORT_MAX_MODELS.iter().any(|name| model_name.contains(name))
|| legacy_is_fable_or_mythos(model_name)
|| legacy_opus_version(model_name).is_some_and(|version| version >= (4, 7))
}
fn legacy_supports_xhigh_effort(model_name: &str) -> bool {
legacy_opus_version(model_name).is_some_and(|version| version >= (4, 7))
|| legacy_is_fable_or_mythos(model_name)
|| model_name.contains("claude-sonnet-5")
}
fn legacy_supports_adaptive_thinking(model_name: &str) -> bool {
const SUPPORT_ADAPTIVE_MODELS: &[&str] = &["claude-opus-4-6", "claude-sonnet-4-6", "claude-sonnet-5"];
SUPPORT_ADAPTIVE_MODELS.iter().any(|name| model_name.contains(name))
|| legacy_is_fable_or_mythos(model_name)
|| legacy_opus_version(model_name).is_some_and(|version| version >= (4, 7))
}
fn legacy_thinking_enabled_by_default(model_name: &str) -> bool {
model_name.contains("claude-sonnet-5") || model_name.contains("claude-opus-5")
}
#[cfg(test)]
#[path = "ant_model_tests.rs"]
mod tests;