use super::{AgentMessage, ContentBlock, Usage};
use crate::utils::tokens;
use serde_json::Value;
use tiktoken_rs::CoreBPE;
const ESTIMATED_IMAGE_TOKENS: u32 = 1200;
fn estimate_content_tokens(enc: &CoreBPE, content: &[ContentBlock]) -> u32 {
let mut tokens = 0u32;
for block in content {
match block {
ContentBlock::Text { text, .. } => tokens += tokens::count(enc, text) as u32,
ContentBlock::Image { .. } => tokens += ESTIMATED_IMAGE_TOKENS,
_ => {}
}
}
tokens
}
pub fn estimate_tokens(message: &AgentMessage) -> u32 {
let enc = tokens::encoding_for(None);
match message.role.as_str() {
"user" | "toolResult" => estimate_content_tokens(enc, &message.content),
"assistant" => {
let mut tokens = 0u32;
for block in &message.content {
match block {
ContentBlock::Text { text, .. } => tokens += tokens::count(enc, text) as u32,
ContentBlock::Thinking { thinking, .. } => {
tokens += tokens::count(enc, thinking) as u32
}
ContentBlock::ToolCall {
name, arguments, ..
} => {
tokens += tokens::count(enc, name) as u32;
tokens += tokens::count(
enc,
&serde_json::to_string(arguments).unwrap_or_default(),
) as u32;
}
_ => {}
}
}
tokens
}
_ => 0,
}
}
pub fn estimate_request_tokens(
messages: &[AgentMessage],
system_prompt: &str,
tools: &[(String, String, Value)],
) -> u32 {
let enc = tokens::encoding_for(None);
let mut tokens = tokens::count(enc, system_prompt) as u32;
if !tools.is_empty() {
let declared: Vec<Value> = tools
.iter()
.map(|(name, description, schema)| {
serde_json::json!({
"name": name,
"description": description,
"parameters": schema,
})
})
.collect();
tokens += tokens::count(enc, &serde_json::to_string(&declared).unwrap_or_default()) as u32;
}
for message in messages {
tokens = tokens.saturating_add(estimate_tokens(message));
}
tokens
}
pub fn calculate_context_tokens(usage: &Usage) -> u32 {
if usage.total_tokens > 0 {
usage.total_tokens
} else {
usage.input + usage.output + usage.cache_read + usage.cache_write
}
}
pub fn usable_usage(msg: &AgentMessage) -> Option<&Usage> {
if msg.role != "assistant" {
return None;
}
if msg.stop_reason.as_deref() == Some("aborted") || msg.stop_reason.as_deref() == Some("error")
{
return None;
}
let usage = msg.usage.as_ref()?;
(calculate_context_tokens(usage) > 0).then_some(usage)
}
fn get_last_assistant_usage_info(messages: &[AgentMessage]) -> Option<(Usage, usize)> {
let mut latest_prefix_timestamp: u64 = 0;
let mut anchor: Option<(Usage, usize)> = None;
for (i, msg) in messages.iter().enumerate() {
let usage_applies_to_prefix = msg.timestamp >= latest_prefix_timestamp;
if msg.role == "assistant"
&& usage_applies_to_prefix
&& let Some(usage) = usable_usage(msg)
{
anchor = Some((usage.clone(), i));
}
latest_prefix_timestamp = latest_prefix_timestamp.max(msg.timestamp);
}
anchor
}
pub struct ContextUsageEstimate {
pub tokens: u32,
pub usage_tokens: u32,
pub trailing_tokens: u32,
pub last_usage_index: Option<usize>,
}
pub fn estimate_context_tokens(messages: &[AgentMessage]) -> ContextUsageEstimate {
if let Some((usage, index)) = get_last_assistant_usage_info(messages) {
let usage_tokens = calculate_context_tokens(&usage);
let mut trailing_tokens = 0;
for msg in &messages[index + 1..] {
trailing_tokens += estimate_tokens(msg);
}
ContextUsageEstimate {
tokens: usage_tokens + trailing_tokens,
usage_tokens,
trailing_tokens,
last_usage_index: Some(index),
}
} else {
let mut estimated = 0;
for msg in messages {
estimated += estimate_tokens(msg);
}
ContextUsageEstimate {
tokens: estimated,
usage_tokens: 0,
trailing_tokens: estimated,
last_usage_index: None,
}
}
}