use crate::message::{ContentBlock, Message};
pub fn estimate_tokens(text: &str) -> u32 {
let chars = text.len() as u32;
chars.div_ceil(4)
}
pub fn estimate_message_tokens(message: &Message) -> u32 {
let overhead = 3u32;
let content: u32 = message
.content
.iter()
.map(|block| match block {
ContentBlock::Text { text } => estimate_tokens(text),
ContentBlock::ToolUse { name, input, .. } => {
estimate_tokens(name) + estimate_tokens(&input.to_string())
}
ContentBlock::ToolResult { content, .. } => estimate_tokens(content),
})
.sum();
overhead + content
}
pub fn estimate_conversation_tokens(messages: &[Message], system: Option<&str>) -> u32 {
let system_tokens = system.map_or(0, estimate_tokens);
let message_tokens: u32 = messages.iter().map(estimate_message_tokens).sum();
system_tokens + message_tokens
}
pub fn context_window(model: &str) -> Option<u32> {
crate::models::get_context_window(model)
}
pub fn fits_in_context(
messages: &[Message],
system: Option<&str>,
model: &str,
max_output_tokens: u32,
) -> bool {
let window = match context_window(model) {
Some(w) => w,
None => return true, };
let estimated = estimate_conversation_tokens(messages, system);
estimated + max_output_tokens <= window
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::Message;
#[test]
fn estimate_tokens_basic() {
let t = estimate_tokens("hello");
assert!((1..=3).contains(&t));
}
#[test]
fn estimate_tokens_empty() {
assert_eq!(estimate_tokens(""), 0);
}
#[test]
fn estimate_tokens_long_text() {
let text = "a".repeat(1000);
let t = estimate_tokens(&text);
assert!((200..=300).contains(&t));
}
#[test]
fn estimate_message_tokens_text() {
let msg = Message::user("Hello, how are you?");
let t = estimate_message_tokens(&msg);
assert!(t >= 5);
}
#[test]
fn estimate_conversation_basic() {
let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
let t = estimate_conversation_tokens(&messages, Some("Be helpful"));
assert!(t > 0);
}
#[test]
fn context_window_known_models() {
assert_eq!(context_window("claude-sonnet-4"), Some(200_000));
assert_eq!(context_window("claude-opus-4"), Some(200_000));
assert_eq!(context_window("gpt-4o"), Some(128_000));
assert_eq!(context_window("unknown-model"), None);
}
#[test]
fn fits_in_context_check() {
let messages = vec![Message::user("Hello")];
assert!(fits_in_context(&messages, None, "claude-sonnet-4", 4096));
}
#[test]
fn fits_in_context_unknown_model() {
let messages = vec![Message::user("Hello")];
assert!(fits_in_context(&messages, None, "unknown-model", 4096));
}
}