use lellm_core::{ContentBlock, Message};
pub fn estimate_tokens(messages: &[Message]) -> usize {
messages.iter().map(estimate_message).sum()
}
pub fn estimate_message(msg: &Message) -> usize {
let mut total: usize = 0;
total += 4;
match msg {
Message::System { content }
| Message::User { content }
| Message::Assistant { content } => {
for block in content {
total += estimate_block(block);
}
}
Message::ToolResult {
tool_call_id,
is_error: _,
content,
} => {
total += estimate_text(tool_call_id);
for block in content {
total += estimate_block(block);
}
}
}
total
}
pub fn estimate_reasoning_block(th: &lellm_core::ThinkingBlock) -> usize {
estimate_text(&th.thinking)
+ th.redacted.as_ref().map(|r| estimate_text(r)).unwrap_or(0)
}
fn estimate_block(block: &ContentBlock) -> usize {
match block {
ContentBlock::Text(t) => estimate_text(&t.text),
ContentBlock::Thinking(th) => estimate_reasoning_block(th),
ContentBlock::Image { .. } => 1000,
ContentBlock::ToolCall(tc) => {
6 + estimate_text(&tc.id) + estimate_text(&tc.name) + estimate_json_value(&tc.arguments)
}
}
}
fn estimate_json_value(value: &serde_json::Value) -> usize {
estimate_text(&serde_json::to_string(value).unwrap_or_default())
}
pub fn estimate_text(s: &str) -> usize {
let mut ascii_count: usize = 0;
let mut cjk_count: usize = 0;
let mut other_count: usize = 0;
for ch in s.chars() {
if ch.is_ascii() {
ascii_count += 1;
} else if ch.is_alphabetic() || ch.is_numeric() {
cjk_count += 5;
} else {
other_count += 1;
}
}
let raw = (ascii_count.saturating_div(4)) + (cjk_count.saturating_div(2)) + other_count;
(raw as f32 * 1.1).ceil() as usize
}