use hotl_types::Item;
const CHARS_PER_TOKEN: u64 = 3;
const ITEM_OVERHEAD: u64 = 8;
pub fn estimate_text(text: &str) -> u64 {
(text.len() as u64).div_ceil(CHARS_PER_TOKEN)
}
pub fn estimate_item(item: &Item) -> u64 {
let body = match item {
Item::System { text } | Item::User { text, .. } => estimate_text(text),
Item::Assistant { blocks } => blocks.iter().map(|b| estimate_text(&b.to_string())).sum(),
Item::ToolResults { results } => {
results.iter().map(|r| estimate_text(&r.content) + 6).sum()
}
Item::Unknown => 0,
};
ITEM_OVERHEAD + body
}
pub fn estimate_items(items: &[Item]) -> u64 {
items.iter().map(estimate_item).sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn estimates_overcount_not_undercount() {
let text = "the quick brown fox jumps over the lazy dog. ".repeat(100);
let actual_ish = text.len() as u64 / 4;
assert!(estimate_text(&text) > actual_ish);
let items = vec![
Item::User {
text: text.clone(),
synthetic: None,
},
Item::Assistant {
blocks: vec![serde_json::json!({"type":"text","text":text})],
},
];
assert!(estimate_items(&items) > 2 * actual_ish);
}
}