const PER_MESSAGE_OVERHEAD: u64 = 4;
pub fn estimate_text(text: &str) -> u64 {
(text.len() as u64).div_ceil(4)
}
pub fn floor_text(text: &str) -> u64 {
let mut bytes: u64 = 0;
let mut prev = None;
let mut run = 0u32;
for c in text.chars() {
if Some(c) == prev {
run += 1;
} else {
prev = Some(c);
run = 1;
}
if run <= 2 {
bytes += c.len_utf8() as u64;
}
}
bytes / 8
}
pub fn estimate_prompt<'a>(system: Option<&str>, parts: impl IntoIterator<Item = &'a str>) -> u64 {
let mut total = 0;
if let Some(s) = system {
total += estimate_text(s) + PER_MESSAGE_OVERHEAD;
}
for part in parts {
total += estimate_text(part) + PER_MESSAGE_OVERHEAD;
}
total
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_is_about_four_chars_per_token() {
assert_eq!(estimate_text("abcdefghijklmnop"), 4);
}
#[test]
fn cyrillic_is_denser_per_char() {
assert_eq!(estimate_text("текс"), 2);
}
#[test]
fn non_empty_text_is_at_least_one_token() {
assert_eq!(estimate_text("a"), 1);
assert_eq!(estimate_text(""), 0);
}
#[test]
fn the_floor_is_half_the_estimate_on_ordinary_text() {
assert_eq!(floor_text("abcdefghijklmnop"), 2);
assert_eq!(floor_text("текстовый"), 2);
assert_eq!(floor_text(""), 0);
}
#[test]
fn a_run_of_one_character_counts_two() {
let rule = "=".repeat(120);
assert_eq!(floor_text(&rule), 0);
let padded = format!("item{}42", " ".repeat(40));
assert_eq!(floor_text(&padded), floor_text("item 42"));
assert_eq!(floor_text("aabbccddeeffgghh"), 2);
}
#[test]
fn the_floor_never_exceeds_the_estimate() {
for text in [
"",
"a",
"hello, world",
"== == ==",
"текст",
"日本語のテキスト",
] {
assert!(floor_text(text) <= estimate_text(text), "{text}");
}
}
#[test]
fn prompt_sums_messages_with_overhead() {
let est = estimate_prompt(Some("ab"), ["cd", "cd"]);
assert_eq!(est, 5 + 10);
}
}