agent_sdk/context/
estimator.rs1use crate::llm::{Content, ContentBlock, Message};
4
5pub struct TokenEstimator;
13
14impl TokenEstimator {
15 const CHARS_PER_TOKEN: usize = 4;
18
19 const MESSAGE_OVERHEAD: usize = 4;
21
22 const TOOL_USE_OVERHEAD: usize = 20;
24
25 const TOOL_RESULT_OVERHEAD: usize = 10;
27
28 #[must_use]
30 pub const fn estimate_text(text: &str) -> usize {
31 text.len().div_ceil(Self::CHARS_PER_TOKEN)
33 }
34
35 #[must_use]
37 pub fn estimate_message(message: &Message) -> usize {
38 let content_tokens = match &message.content {
39 Content::Text(text) => Self::estimate_text(text),
40 Content::Blocks(blocks) => blocks.iter().map(Self::estimate_block).sum(),
41 };
42
43 content_tokens + Self::MESSAGE_OVERHEAD
44 }
45
46 #[must_use]
48 pub fn estimate_block(block: &ContentBlock) -> usize {
49 match block {
50 ContentBlock::Text { text } => Self::estimate_text(text),
51 ContentBlock::ToolUse { name, input, .. } => {
52 let input_str = serde_json::to_string(input).unwrap_or_default();
53 Self::estimate_text(name)
54 + Self::estimate_text(&input_str)
55 + Self::TOOL_USE_OVERHEAD
56 }
57 ContentBlock::ToolResult { content, .. } => {
58 Self::estimate_text(content) + Self::TOOL_RESULT_OVERHEAD
59 }
60 }
61 }
62
63 #[must_use]
65 pub fn estimate_history(messages: &[Message]) -> usize {
66 messages.iter().map(Self::estimate_message).sum()
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use crate::llm::Role;
74 use serde_json::json;
75
76 #[test]
77 fn test_estimate_text() {
78 assert_eq!(TokenEstimator::estimate_text(""), 0);
80
81 assert_eq!(TokenEstimator::estimate_text("hi"), 1);
83
84 assert_eq!(TokenEstimator::estimate_text("test"), 1);
86
87 assert_eq!(TokenEstimator::estimate_text("hello"), 2);
89
90 assert_eq!(TokenEstimator::estimate_text("hello world!"), 3); }
93
94 #[test]
95 fn test_estimate_text_message() {
96 let message = Message {
97 role: Role::User,
98 content: Content::Text("Hello, how are you?".to_string()), };
100
101 let estimate = TokenEstimator::estimate_message(&message);
102 assert_eq!(estimate, 9);
104 }
105
106 #[test]
107 fn test_estimate_blocks_message() {
108 let message = Message {
109 role: Role::Assistant,
110 content: Content::Blocks(vec![
111 ContentBlock::Text {
112 text: "Let me help.".to_string(), },
114 ContentBlock::ToolUse {
115 id: "tool_123".to_string(),
116 name: "read".to_string(), input: json!({"path": "/test.txt"}), },
119 ]),
120 };
121
122 let estimate = TokenEstimator::estimate_message(&message);
123 assert!(estimate > 25); }
129
130 #[test]
131 fn test_estimate_tool_result() {
132 let message = Message {
133 role: Role::User,
134 content: Content::Blocks(vec![ContentBlock::ToolResult {
135 tool_use_id: "tool_123".to_string(),
136 content: "File contents here...".to_string(), is_error: None,
138 }]),
139 };
140
141 let estimate = TokenEstimator::estimate_message(&message);
142 assert_eq!(estimate, 20);
144 }
145
146 #[test]
147 fn test_estimate_history() {
148 let messages = vec![
149 Message::user("Hello"), Message::assistant("Hi there!"), Message::user("How are you?"), ];
153
154 let estimate = TokenEstimator::estimate_history(&messages);
155 assert_eq!(estimate, 20);
156 }
157
158 #[test]
159 fn test_empty_history() {
160 let messages: Vec<Message> = vec![];
161 assert_eq!(TokenEstimator::estimate_history(&messages), 0);
162 }
163}