#![deny(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Family {
Gpt,
Claude,
Gemini,
Llama,
Cohere,
}
impl Family {
pub fn chars_per_token(self) -> f64 {
match self {
Family::Gpt => 4.0,
Family::Claude => 3.5,
Family::Gemini => 4.0,
Family::Llama => 3.7,
Family::Cohere => 3.8,
}
}
pub fn guess_from_model_id(id: &str) -> Self {
let s = id.to_ascii_lowercase();
if s.contains("claude") {
Family::Claude
} else if s.contains("gemini") {
Family::Gemini
} else if s.contains("llama") {
Family::Llama
} else if s.contains("cohere") || s.contains("command-r") {
Family::Cohere
} else {
Family::Gpt
}
}
}
pub fn estimate(text: &str, family: Family) -> u64 {
estimate_with_ratio(text, family.chars_per_token())
}
pub fn estimate_with_ratio(text: &str, chars_per_token: f64) -> u64 {
if text.is_empty() {
return 0;
}
let chars = text.chars().count() as f64;
let est = (chars / chars_per_token).ceil() as u64;
est.max(1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_string_is_zero() {
assert_eq!(estimate("", Family::Gpt), 0);
}
#[test]
fn ratio_picks_floor_of_one() {
assert_eq!(estimate("a", Family::Gpt), 1);
}
#[test]
fn family_guess_works() {
assert_eq!(
Family::guess_from_model_id("claude-sonnet-4-5"),
Family::Claude
);
assert_eq!(
Family::guess_from_model_id("meta.llama3-70b"),
Family::Llama
);
assert_eq!(Family::guess_from_model_id("gemini-2.5-pro"), Family::Gemini);
assert_eq!(
Family::guess_from_model_id("cohere.command-r-plus"),
Family::Cohere
);
assert_eq!(Family::guess_from_model_id("gpt-5"), Family::Gpt);
assert_eq!(
Family::guess_from_model_id("something-else"),
Family::Gpt
);
}
}