#![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 Some(bpe) = bpe() else {
let end = text.floor_char_boundary(max_tokens.saturating_mul(CHARS_PER_TOKEN));
return text[..end].to_string();
};
let tokens = bpe.encode_with_special_tokens(text);
if tokens.len() <= max_tokens {
return text.to_string();
}
for end in (1..=max_tokens).rev() {
if let Ok(decoded) = bpe.decode_to_string(&tokens[..end]) {
return decoded;
}
}
String::new()
}
#[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 truncate_split_unicode_token_stays_within_budget() {
let text = "🙂🙂 more text";
let bpe = bpe().expect("builtin encoding is available");
let tokens = bpe.encode_with_special_tokens(text);
assert!(bpe.decode_to_string(&tokens[..1]).is_err());
for limit in 1..=4 {
let truncated = truncate_to_tokens(text, limit);
assert!(text.starts_with(&truncated), "limit={limit} output={truncated:?}");
assert!(estimate_tokens(&truncated) <= limit, "limit={limit} output={truncated:?}");
}
assert_eq!(truncate_to_tokens("short", 20), "short");
}
#[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");
}
}