use crate::types::Message;
use std::fmt::{self, Write};
fn json_len(value: &serde_json::Value) -> usize {
struct ByteCount(usize);
impl Write for ByteCount {
fn write_str(&mut self, text: &str) -> fmt::Result {
self.0 += text.len();
Ok(())
}
}
let mut count = ByteCount(0);
write!(count, "{value}").expect("counting JSON bytes cannot fail");
count.0
}
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 += json_len(tool.input());
}
crate::types::ContentBlock::ToolResult(result) => {
total_chars += result.tool_use_id().len();
total_chars += json_len(result.content());
}
}
}
}
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[0].role == crate::types::MessageRole::System;
let start = messages.len() - keep;
if has_system {
let mut result = vec![messages[0].clone()];
result.extend_from_slice(&messages[start..]);
result
} else {
messages[start..].to_vec()
}
}
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, ImageBlock, ImageDetail, Message, MessageRole};
#[test]
fn text_estimates_include_overhead_and_round_up() {
assert_eq!(estimate_tokens(&[]), 0);
assert_eq!(estimate_tokens(&[Message::user("Hello world")]), 9);
}
#[test]
fn image_estimates_include_detail_allowance_and_message_overhead() {
for (detail, expected) in [
(ImageDetail::Low, 91),
(ImageDetail::High, 306),
(ImageDetail::Auto, 206),
] {
let image = ImageBlock::from_url("https://example.com/img.jpg")
.unwrap()
.with_detail(detail);
let message = Message::new(MessageRole::User, vec![ContentBlock::Image(image)]);
assert_eq!(estimate_tokens(&[message]), expected, "{detail:?}");
}
}
}