#![expect(
clippy::indexing_slicing,
clippy::string_slice,
reason = "Token and UTF-8 boundaries come from tokenizer output and the byte truncation helper."
)]
use std::sync::OnceLock;
use tiktoken::CoreBpe;
fn bpe() -> Option<&'static CoreBpe> {
static BPE: OnceLock<Option<&'static CoreBpe>> = OnceLock::new();
*BPE.get_or_init(|| tiktoken::get_encoding("cl100k_base"))
}
pub const CHARS_PER_TOKEN: usize = 4;
#[must_use]
pub fn heuristic_tokens_from_byte_len(byte_len: usize) -> usize {
byte_len.div_ceil(CHARS_PER_TOKEN)
}
#[must_use]
pub fn heuristic_tokens_from_byte_len_u64(byte_len: u64) -> u64 {
byte_len.div_ceil(CHARS_PER_TOKEN as u64)
}
fn heuristic_token_count(text: &str) -> usize {
heuristic_tokens_from_byte_len(text.len())
}
pub fn estimate_tokens(text: &str) -> usize {
if text.is_empty() {
return 0;
}
match bpe() {
Some(bpe) => bpe.count(text),
None => heuristic_token_count(text),
}
}
pub fn format_token_count(tokens: usize) -> String {
let (divisor, suffix) = match tokens {
999_500_000.. => (1_000_000_000, "B"),
999_500.. => (1_000_000, "M"),
1_000.. => (1_000, "k"),
_ => return tokens.to_string(),
};
let whole = tokens / divisor;
let decimal_step = divisor / 10;
let tenths = (tokens % divisor + decimal_step / 2) / decimal_step;
let (whole, tenths) = if tenths >= 10 {
(whole.saturating_add(1), 0)
} else {
(whole, tenths)
};
if tenths == 0 {
format!("{whole}{suffix}")
} else {
format!("{whole}.{tenths}{suffix}")
}
}
pub fn truncate_to_tokens(text: &str, max_tokens: usize) -> String {
if max_tokens == 0 || text.is_empty() {
return String::new();
}
let byte_truncate = || {
let end = text.floor_char_boundary(max_tokens.saturating_mul(CHARS_PER_TOKEN));
let mut result = text[..end].to_string();
result.push_str("...");
result
};
let Some(bpe) = bpe() else {
return byte_truncate();
};
let tokens = bpe.encode_with_special_tokens(text);
if tokens.len() <= max_tokens {
return text.to_string();
}
bpe.decode_to_string(&tokens[..max_tokens]).unwrap_or_else(|_| byte_truncate())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_string_returns_zero() {
assert_eq!(estimate_tokens(""), 0);
assert_eq!(truncate_to_tokens("", 10), "");
}
#[test]
fn count_is_reasonable() {
let count = estimate_tokens("Hello, how are you today?");
assert!((4..=12).contains(&count), "count={count}");
}
#[test]
fn heuristic_byte_len_rounds_up_at_boundaries() {
assert_eq!(heuristic_tokens_from_byte_len(0), 0);
assert_eq!(heuristic_tokens_from_byte_len(1), 1);
assert_eq!(heuristic_tokens_from_byte_len(3), 1);
assert_eq!(heuristic_tokens_from_byte_len(4), 1);
assert_eq!(heuristic_tokens_from_byte_len(5), 2);
assert_eq!(heuristic_tokens_from_byte_len(8), 2);
assert_eq!(heuristic_tokens_from_byte_len(9), 3);
}
#[test]
fn heuristic_u64_twin_matches_usize_flavor() {
for len in [0usize, 1, 3, 4, 5, 1_000, 1_000_003] {
assert_eq!(
heuristic_tokens_from_byte_len_u64(len as u64),
heuristic_tokens_from_byte_len(len) as u64,
"len={len}"
);
}
assert_eq!(heuristic_tokens_from_byte_len_u64(u64::MAX), u64::MAX.div_ceil(4));
}
#[test]
fn bpe_fallback_heuristic_delegates_to_byte_len_helper() {
for text in ["", "a", "abcd", "abcde", "Hello, world!", "\u{1f44b}\u{1f30d}"] {
assert_eq!(heuristic_token_count(text), heuristic_tokens_from_byte_len(text.len()), "text={text:?}");
}
}
#[test]
fn truncate_respects_limit() {
let text = "the quick brown fox jumps over the lazy dog";
let truncated = truncate_to_tokens(text, 5);
let count = estimate_tokens(&truncated);
assert!(count <= 5 + 1, "count={count} should be <= 6");
}
#[test]
fn truncate_zero_returns_empty() {
assert_eq!(truncate_to_tokens("hello", 0), "");
}
#[test]
fn code_and_prose_tokenize() {
let code = "fn main() { println!(\"hello\"); }";
let prose = "the main function prints hello to console";
assert!(estimate_tokens(code) > 0);
assert!(estimate_tokens(prose) > 0);
}
#[test]
fn json_tokenizes() {
let json = r#"{"name":"test","value":123,"nested":{"key":"value"}}"#;
let count = estimate_tokens(json);
assert!((10..=40).contains(&count), "json count={count}");
}
#[test]
fn token_count_format_is_compact_and_rounds_across_suffix_boundaries() {
assert_eq!(format_token_count(999), "999");
assert_eq!(format_token_count(1_000), "1k");
assert_eq!(format_token_count(1_050), "1.1k");
assert_eq!(format_token_count(6_100), "6.1k");
assert_eq!(format_token_count(999_500), "1M");
assert_eq!(format_token_count(15_600_000), "15.6M");
assert_eq!(format_token_count(999_500_000), "1B");
assert_eq!(format_token_count(1_550_000_000), "1.6B");
assert_eq!(format_token_count(1_549_999_999), "1.5B");
}
}