use super::estimation::{estimate_tokens, TokenEstimator, MESSAGE_OVERHEAD_TOKENS};
use llm_types::llm::LlmRequest;
use llm_types::message::{Message, MessageContent, MessageContentValue};
pub fn estimate_message_tokens(msg: &Message) -> u32 {
let mut total = 0u32;
match &msg.content {
MessageContentValue::Text(text) => total += estimate_tokens(text) as u32,
MessageContentValue::Rich(blocks) => {
for block in blocks {
total += match block {
MessageContent::Text { text } => estimate_tokens(text) as u32,
MessageContent::Thinking { thinking, .. } => estimate_tokens(thinking) as u32,
MessageContent::ToolUse { tool_use } => {
let json = serde_json::to_string(&tool_use.input).unwrap_or_default();
estimate_tokens(&json) as u32
}
MessageContent::ToolResult { tool_result } => {
estimate_tokens(&tool_result.content) as u32
}
MessageContent::ImageUrl { image_url } => estimate_image_tokens(&image_url.url),
};
}
}
}
if let Some(thinking) = &msg.thinking {
total += estimate_tokens(thinking) as u32;
}
if let Some(tool_calls) = &msg.tool_calls {
if let Ok(json) = serde_json::to_string(tool_calls) {
total += estimate_tokens(&json) as u32;
}
}
total + MESSAGE_OVERHEAD_TOKENS
}
pub fn estimate_tool_declarations(tools: Option<&[llm_types::tool::Tool]>) -> u32 {
let mut total = 0u32;
for tool in tools.unwrap_or_default() {
total += estimate_tokens(&tool.name) as u32;
total += estimate_tokens(&tool.description) as u32;
if let Some(params) = &tool.parameters {
if let Ok(json) = serde_json::to_string(params) {
total += estimate_tokens(&json) as u32;
}
}
}
total
}
pub fn estimate_request_tokens(request: &LlmRequest) -> u32 {
let mut total = 0u32;
for msg in &request.messages {
total += estimate_message_tokens(msg);
}
total += estimate_tool_declarations(request.tools.as_deref());
total
}
pub fn estimate_image_tokens(url_or_data_uri: &str) -> u32 {
if let Some(rest) = url_or_data_uri.strip_prefix("data:") {
let b64 = if let Some(semicolon) = rest.find(';') {
rest[semicolon + 1..]
.strip_prefix("base64,")
.unwrap_or(&rest[semicolon + 1..])
} else {
rest
};
let bytes = b64.len() as f64 * 3.0 / 4.0;
let pixels = bytes / 3.0;
(pixels / 750.0).ceil() as u32 + 200
} else {
170
}
}
pub fn estimate_messages(messages: &[Message]) -> u32 {
messages.iter().map(estimate_message_tokens).sum()
}
pub fn default_estimator() -> TokenEstimator {
TokenEstimator::default()
}
#[cfg(test)]
mod tests {
use super::*;
use llm_types::message::{
ImageUrlContent, Message, MessageContent, MessageContentValue, MessageRole,
ToolResultContent,
};
fn text_message(role: MessageRole, text: &str) -> Message {
Message {
id: "id".to_string(),
role,
content: MessageContentValue::Text(text.to_string()),
timestamp: 0,
tool_call_id: None,
tool_name: None,
tool_calls: None,
thinking: None,
metadata: None,
}
}
#[test]
fn test_estimate_simple_message() {
let msg = text_message(MessageRole::User, "Hello");
assert_eq!(estimate_message_tokens(&msg), 5);
}
#[test]
fn test_estimate_cjk_message() {
let msg = text_message(MessageRole::User, "你好世界");
assert_eq!(estimate_message_tokens(&msg), 8);
}
#[test]
fn test_estimate_message_with_thinking() {
let mut msg = text_message(MessageRole::Assistant, "result");
msg.thinking = Some("思考过程".to_string());
assert_eq!(estimate_message_tokens(&msg), 10);
}
#[test]
fn test_estimate_message_with_tool_calls() {
let mut msg = text_message(MessageRole::Assistant, "");
msg.tool_calls = Some(vec![llm_types::message::LlmToolCall {
id: "call_1".to_string(),
r#type: "function".to_string(),
function: llm_types::message::LlmFunctionCall {
name: "get_weather".to_string(),
arguments: r#"{"city":"Beijing"}"#.to_string(),
},
}]);
let total = estimate_message_tokens(&msg);
assert!(total >= 4, "tokens: {}", total);
}
#[test]
fn test_estimate_rich_content() {
let msg = Message {
id: "id".to_string(),
role: MessageRole::User,
content: MessageContentValue::Rich(vec![
MessageContent::Text {
text: "look at this".to_string(),
},
MessageContent::ImageUrl {
image_url: ImageUrlContent {
url: "https://example.com/img.png".to_string(),
detail: None,
},
},
]),
timestamp: 0,
tool_call_id: None,
tool_name: None,
tool_calls: None,
thinking: None,
metadata: None,
};
let total = estimate_message_tokens(&msg);
assert!(total >= 170, "tokens: {}", total);
}
#[test]
fn test_estimate_rich_tool_result() {
let msg = Message {
id: "id".to_string(),
role: MessageRole::Tool,
content: MessageContentValue::Rich(vec![MessageContent::ToolResult {
tool_result: ToolResultContent {
tool_use_id: "call_1".to_string(),
content: "the weather is sunny".to_string(),
is_error: None,
},
}]),
timestamp: 0,
tool_call_id: None,
tool_name: None,
tool_calls: None,
thinking: None,
metadata: None,
};
let total = estimate_message_tokens(&msg);
assert!(total >= 4, "tokens: {}", total);
}
#[test]
fn test_estimate_image_tokens_url() {
assert_eq!(estimate_image_tokens("https://example.com/a.png"), 170);
}
#[test]
fn test_estimate_image_tokens_data_uri() {
let uri = format!("data:image/png;base64,{}", "A".repeat(3600));
assert_eq!(estimate_image_tokens(&uri), 202);
}
#[test]
fn test_estimate_request_tokens() {
let req = LlmRequest {
profile_id: "default".to_string(),
messages: vec![
text_message(MessageRole::System, "You are a helper"),
text_message(MessageRole::User, "你好"),
],
parameters: None,
generation: None,
tools: None,
tool_call_protocol: None,
locked_tool_call_protocol: None,
violation_policy: None,
execution_id: None,
stream: None,
dead_loop_detection: None,
protocol_auto_converted: None,
timeout_ms: None,
};
assert_eq!(estimate_request_tokens(&req), 15);
}
#[test]
fn test_estimate_messages_empty() {
assert_eq!(estimate_messages(&[]), 0);
}
}