use crate::types::Message;
pub fn estimate_tokens(messages: &[Message]) -> usize {
if messages.is_empty() {
return 0;
}
let mut total_chars = 0;
for message in messages {
total_chars += 8;
for block in &message.content {
match block {
crate::types::ContentBlock::Text(text) => {
total_chars += text.text.len();
}
crate::types::ContentBlock::Image(image) => {
use crate::types::ImageDetail;
let token_estimate = match image.detail() {
ImageDetail::Low => 85 * 4, ImageDetail::High => 300 * 4, ImageDetail::Auto => 200 * 4, };
total_chars += token_estimate;
}
crate::types::ContentBlock::ToolUse(tool) => {
total_chars += tool.name().len();
total_chars += tool.id().len();
total_chars += tool.input().to_string().len();
}
crate::types::ContentBlock::ToolResult(result) => {
total_chars += result.tool_use_id().len();
total_chars += result.content().to_string().len();
}
}
}
}
total_chars += 16;
total_chars.div_ceil(4)
}
pub fn truncate_messages(messages: &[Message], keep: usize, preserve_system: bool) -> Vec<Message> {
if messages.is_empty() {
return Vec::new();
}
if messages.len() <= keep {
return messages.to_vec();
}
let has_system = preserve_system
&& !messages.is_empty()
&& messages[0].role == crate::types::MessageRole::System;
if has_system {
let mut result = vec![messages[0].clone()];
if keep > 0 && messages.len() > 1 {
let start = messages.len().saturating_sub(keep);
result.extend_from_slice(&messages[start..]);
}
result
} else {
if keep > 0 {
let start = messages.len().saturating_sub(keep);
messages[start..].to_vec()
} else {
Vec::new()
}
}
}
pub fn is_approaching_limit(messages: &[Message], limit: usize, margin: f32) -> bool {
let estimated = estimate_tokens(messages);
let threshold = (limit as f32 * margin) as usize;
estimated > threshold
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{ContentBlock, Message, MessageRole, TextBlock};
#[test]
fn test_estimate_tokens_empty() {
let messages: Vec<Message> = vec![];
assert_eq!(estimate_tokens(&messages), 0);
}
#[test]
fn test_estimate_tokens_simple() {
let messages = vec![Message::new(
MessageRole::User,
vec![ContentBlock::Text(TextBlock::new("Hello world"))],
)];
let tokens = estimate_tokens(&messages);
assert!((3..=10).contains(&tokens));
}
#[test]
fn test_truncate_messages_empty() {
let messages: Vec<Message> = vec![];
let truncated = truncate_messages(&messages, 10, true);
assert_eq!(truncated.len(), 0);
}
#[test]
fn test_truncate_messages_preserve_system() {
let messages = vec![
Message::system("System prompt"),
Message::user("Message 1"),
Message::user("Message 2"),
Message::user("Message 3"),
Message::user("Message 4"),
];
let truncated = truncate_messages(&messages, 2, true);
assert_eq!(truncated.len(), 3);
assert_eq!(truncated[0].role, MessageRole::System);
}
#[test]
fn test_truncate_messages_no_preserve() {
let messages = vec![
Message::system("System prompt"),
Message::user("Message 1"),
Message::user("Message 2"),
Message::user("Message 3"),
];
let truncated = truncate_messages(&messages, 2, false);
assert_eq!(truncated.len(), 2);
assert_eq!(truncated[0].role, MessageRole::User);
}
#[test]
fn test_truncate_messages_keep_all() {
let messages = vec![Message::user("Message 1"), Message::user("Message 2")];
let truncated = truncate_messages(&messages, 10, true);
assert_eq!(truncated.len(), 2);
}
#[test]
fn test_is_approaching_limit() {
let messages = vec![Message::user("x".repeat(1000))];
assert!(!is_approaching_limit(&messages, 1000, 0.9));
assert!(is_approaching_limit(&messages, 200, 0.9));
}
#[test]
fn test_estimate_tokens_image_detail_low() {
use crate::types::{ImageBlock, ImageDetail};
let img = ImageBlock::from_url("https://example.com/img.jpg")
.unwrap()
.with_detail(ImageDetail::Low);
let msg = Message::new(MessageRole::User, vec![ContentBlock::Image(img)]);
let token_count = estimate_tokens(&[msg]);
assert!(
(75..=95).contains(&token_count),
"Low detail should be ~85 tokens, got {}",
token_count
);
}
#[test]
fn test_estimate_tokens_image_detail_high() {
use crate::types::{ImageBlock, ImageDetail};
let img = ImageBlock::from_url("https://example.com/img.jpg")
.unwrap()
.with_detail(ImageDetail::High);
let msg = Message::new(MessageRole::User, vec![ContentBlock::Image(img)]);
let token_count = estimate_tokens(&[msg]);
assert!(
token_count >= 250,
"High detail should be ~300+ tokens, got {}",
token_count
);
}
#[test]
fn test_estimate_tokens_image_detail_auto() {
use crate::types::{ImageBlock, ImageDetail};
let img = ImageBlock::from_url("https://example.com/img.jpg")
.unwrap()
.with_detail(ImageDetail::Auto);
let msg = Message::new(MessageRole::User, vec![ContentBlock::Image(img)]);
let token_count = estimate_tokens(&[msg]);
assert!(
(150..=250).contains(&token_count),
"Auto detail should be ~200 tokens, got {}",
token_count
);
}
}