fn bare_model(model: &str) -> &str {
let mut name = model.trim();
if let Some((_, rest)) = name.rsplit_once('/') {
name = rest;
}
if let Some(idx) = name.find("anthropic.") {
name = &name[idx + "anthropic.".len()..];
}
if let Some((head, _)) = name.split_once('@') {
name = head;
}
if let Some(idx) = name.rfind("-v") {
let suffix = &name[idx + 2..];
if suffix.starts_with(|c: char| c.is_ascii_digit()) && suffix.contains(':') {
name = &name[..idx];
}
}
name
}
pub(crate) fn is_openai_reasoning_model(model: &str) -> bool {
let name = bare_model(model);
if let Some(rest) = name.strip_prefix("gpt-") {
let major: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
return major.parse::<u32>().is_ok_and(|major| major >= 5);
}
if let Some(rest) = name.strip_prefix('o') {
let digits = rest.chars().take_while(|c| c.is_ascii_digit()).count();
return digits > 0 && matches!(rest[digits..].chars().next(), None | Some('-'));
}
false
}
pub(crate) fn claude_rejects_sampling_params(model: &str) -> bool {
let name = bare_model(model);
let Some(rest) = name.strip_prefix("claude-") else {
return false;
};
let mut version: Vec<u32> = Vec::new();
let mut family: Option<&str> = None;
for token in rest.split(['-', '.']) {
let is_version_part =
!token.is_empty() && token.len() <= 2 && token.chars().all(|c| c.is_ascii_digit());
if is_version_part {
version.push(token.parse().unwrap_or(0));
continue;
}
let known_family = matches!(token, "opus" | "sonnet" | "haiku" | "instant");
if version.is_empty() && !known_family {
return true;
}
if !version.is_empty() {
break;
}
family = Some(token);
}
let last_accepting = match family {
Some("haiku") => (4, 5),
_ => (4, 6),
};
match version.as_slice() {
[] => true,
[major] => (*major, 0) > last_accepting,
[major, minor, ..] => (*major, *minor) > last_accepting,
}
}
pub(crate) fn claude_default_max_tokens(model: &str) -> u32 {
if claude_rejects_sampling_params(model) {
16_384
} else {
4_096
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn openai_reasoning_models() {
for model in [
"gpt-5",
"gpt-5-mini",
"gpt-5.5",
"gpt-6",
"gpt-6-luna",
"openai/gpt-6-luna",
"gpt-7",
"o1",
"o1-preview",
"o3-mini",
"o4-mini",
] {
assert!(
is_openai_reasoning_model(model),
"{model} should be reasoning"
);
}
for model in [
"gpt-4o",
"gpt-4o-mini",
"gpt-4.1",
"gpt-4-turbo",
"gpt-3.5-turbo",
"gpt-oss-120b",
"openai/gpt-oss-20b",
"omni-moderation-latest",
"claude-opus-5",
] {
assert!(
!is_openai_reasoning_model(model),
"{model} should not be reasoning"
);
}
}
#[test]
fn claude_models_that_reject_sampling() {
for model in [
"claude-opus-4-7",
"claude-opus-4-8",
"anthropic/claude-opus-5",
"claude-opus-5-5",
"claude-sonnet-5",
"claude-sonnet-5-5",
"claude-fable-5",
"claude-fable-5-1",
"claude-opus-4-7-20260115",
"anthropic.claude-opus-4-7-v1:0",
"us.anthropic.claude-sonnet-5-20260301-v1:0",
"claude-opus-4-7@20260115",
"claude-newfamily-1",
"claude-haiku-4-6",
"claude-haiku-5",
] {
assert!(
claude_rejects_sampling_params(model),
"{model} should reject sampling"
);
}
}
#[test]
fn claude_models_that_accept_sampling() {
for model in [
"claude-haiku-4-5",
"claude-haiku-4-5-20251001",
"claude-sonnet-4-6",
"claude-opus-4-6",
"claude-sonnet-4-5-20250929",
"claude-opus-4-1",
"claude-opus-4-20250514",
"claude-sonnet-4-20250514",
"claude-3-7-sonnet-20250219",
"claude-3-5-haiku-20241022",
"claude-3-opus-20240229",
"claude-2.1",
"claude-instant-1.2",
"anthropic/claude-sonnet-4-6",
"anthropic.claude-3-5-sonnet-20240620-v1:0",
"us.anthropic.claude-haiku-4-5-20251001-v1:0",
"claude-sonnet-4-5@20250929",
] {
assert!(
!claude_rejects_sampling_params(model),
"{model} should accept sampling"
);
}
assert!(!claude_rejects_sampling_params("gpt-6-luna"));
}
#[test]
fn claude_max_tokens_default_depends_on_generation() {
assert_eq!(claude_default_max_tokens("anthropic/claude-opus-5"), 16_384);
assert_eq!(claude_default_max_tokens("claude-haiku-4-5"), 4_096);
}
}