use std::sync::OnceLock;
use tiktoken_rs::{cl100k_base, CoreBPE};
static TOKENIZER: OnceLock<CoreBPE> = OnceLock::new();
fn get_tokenizer() -> &'static CoreBPE {
TOKENIZER.get_or_init(|| {
cl100k_base().unwrap_or_else(|_| {
panic!("Failed to initialize tokenizer")
})
})
}
pub fn estimate_tokens(text: &str) -> usize {
let tokenizer = get_tokenizer();
let tokens = tokenizer.encode_ordinary(text);
tokens.len()
}
pub fn truncate_to_tokens(text: &str, max_tokens: usize) -> String {
let tokenizer = get_tokenizer();
let mut tokens = tokenizer.encode_ordinary(text);
if tokens.len() <= max_tokens {
return text.to_string();
}
tokens.truncate(max_tokens);
tokenizer.decode(&tokens).unwrap_or_else(|_| {
let boundary = crate::utils::truncation::floor_char_boundary(text, text.len() / 2);
text[..boundary].to_string()
})
}
pub fn estimate_message_tokens(message: &crate::session::Message) -> usize {
let mut tokens = 0;
tokens += 3;
tokens += estimate_tokens(&message.role);
if !message.content.is_empty() {
tokens += estimate_tokens(&message.content);
}
if let Some(tool_calls) = &message.tool_calls {
if let Ok(json_str) = serde_json::to_string(tool_calls) {
tokens += estimate_tokens(&json_str);
}
}
if let Some(thinking) = &message.thinking {
if let Ok(json_str) = serde_json::to_string(thinking) {
tokens += estimate_tokens(&json_str);
}
}
if let Some(name) = &message.name {
tokens += estimate_tokens(name);
tokens += 1;
}
if let Some(images) = &message.images {
tokens += images.len() * 85;
}
tokens
}
pub fn estimate_session_tokens(messages: &[crate::session::Message]) -> usize {
let mut total = 0;
for msg in messages {
total += estimate_message_tokens(msg);
}
if !messages.is_empty() {
total += 3;
}
total
}
pub fn estimate_full_context_tokens(
messages: &[crate::session::Message],
tools: Option<&[crate::mcp::McpFunction]>,
) -> usize {
let mut total = estimate_session_tokens(messages);
if let Some(tool_list) = tools {
for tool in tool_list {
let tool_json = serde_json::json!({
"name": tool.name,
"description": tool.description,
"input_schema": tool.parameters
});
let tool_str = serde_json::to_string(&tool_json).unwrap_or_default();
total += estimate_tokens(&tool_str);
}
total += tool_list.len() * 5;
total += 10;
}
total
}
pub async fn calculate_minimum_session_tokens(
config: &crate::config::Config,
role: &str,
current_dir: &std::path::Path,
) -> anyhow::Result<usize> {
let (_, _, _, _, system_prompt) = config.get_role_config(role);
let system_tokens = estimate_tokens(system_prompt);
let tool_tokens = if !config.mcp.servers.is_empty() {
let tools = crate::mcp::get_available_functions(config).await;
let mut total = 0;
for tool in &tools {
let tool_json = serde_json::json!({
"name": tool.name,
"description": tool.description,
"input_schema": tool.parameters
});
let tool_str = serde_json::to_string(&tool_json).unwrap_or_default();
total += estimate_tokens(&tool_str);
}
total + (tools.len() * 5) + 10 } else {
0
};
let initial_messages_tokens = match crate::session::chat::session::get_initial_messages(
config,
role,
current_dir,
)
.await
{
Ok(messages) => {
let mut total = 0;
for message in &messages {
total += estimate_tokens(&message.content);
total += 20; }
total
}
Err(_) => {
320
}
};
let request_overhead = 50;
Ok(system_tokens + tool_tokens + initial_messages_tokens + request_overhead)
}
pub async fn validate_session_token_threshold(
config: &crate::config::Config,
role: &str,
current_dir: &std::path::Path,
) -> anyhow::Result<()> {
if config.max_session_tokens_threshold == 0 {
return Ok(()); }
let minimum_tokens = calculate_minimum_session_tokens(config, role, current_dir).await?;
let threshold = config.max_session_tokens_threshold;
let (_, _, _, _, system_prompt) = config.get_role_config(role);
let system_tokens = estimate_tokens(system_prompt);
let tool_tokens = if !config.mcp.servers.is_empty() {
let tools = crate::mcp::get_available_functions(config).await;
let mut total = 0;
for tool in &tools {
let tool_json = serde_json::json!({
"name": tool.name,
"description": tool.description,
"input_schema": tool.parameters
});
let tool_str = serde_json::to_string(&tool_json).unwrap_or_default();
total += estimate_tokens(&tool_str);
}
total + (tools.len() * 5) + 10
} else {
0
};
let initial_messages_tokens = minimum_tokens - system_tokens - tool_tokens;
if minimum_tokens * 2 > threshold {
return Err(anyhow::anyhow!(
"max_session_tokens_threshold ({}) is too low for role '{}'
Minimum required: {} tokens (system prompt + tools + initial messages)
Recommended minimum: {} tokens (2x safety margin)
Breakdown:
- System prompt: {} tokens
- Tool definitions: {} tokens
- Initial messages: {} tokens
- Safety margin: 2x multiplier
Please increase max_session_tokens_threshold to at least {}",
threshold,
role,
minimum_tokens,
minimum_tokens * 2,
system_tokens,
tool_tokens,
initial_messages_tokens,
minimum_tokens * 2
));
}
if minimum_tokens * 3 > threshold {
crate::log_info!(
"⚠️ max_session_tokens_threshold ({}) is close to minimum requirements ({} tokens).
Consider increasing for better session continuity.",
threshold,
minimum_tokens
);
}
Ok(())
}