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 const REDACTED_THINKING_MIN_TOKENS: usize = 512;
33
34 #[must_use]
36 pub const fn estimate_text(text: &str) -> usize {
37 text.len().div_ceil(Self::CHARS_PER_TOKEN)
39 }
40
41 #[must_use]
43 pub fn estimate_message(message: &Message) -> usize {
44 let content_tokens = match &message.content {
45 Content::Text(text) => Self::estimate_text(text),
46 Content::Blocks(blocks) => blocks.iter().map(Self::estimate_block).sum(),
47 };
48
49 content_tokens + Self::MESSAGE_OVERHEAD
50 }
51
52 #[must_use]
54 pub fn estimate_block(block: &ContentBlock) -> usize {
55 match block {
56 ContentBlock::Text { text } => Self::estimate_text(text),
57 ContentBlock::Thinking { thinking, .. } => Self::estimate_text(thinking),
58 ContentBlock::RedactedThinking { data } => {
59 let raw_bytes = data.len() * 3 / 4;
69 let estimated = raw_bytes.div_ceil(Self::CHARS_PER_TOKEN);
70 estimated.max(Self::REDACTED_THINKING_MIN_TOKENS)
71 }
72 ContentBlock::ToolUse { name, input, .. } => {
73 let input_str = serde_json::to_string(input).unwrap_or_default();
74 Self::estimate_text(name)
75 + Self::estimate_text(&input_str)
76 + Self::TOOL_USE_OVERHEAD
77 }
78 ContentBlock::ToolResult { content, .. } => {
79 Self::estimate_text(content) + Self::TOOL_RESULT_OVERHEAD
80 }
81 ContentBlock::Image { source } | ContentBlock::Document { source } => {
82 source.data.len() / 4 + Self::MESSAGE_OVERHEAD
84 }
85 _ => Self::MESSAGE_OVERHEAD,
88 }
89 }
90
91 #[must_use]
93 pub fn estimate_history(messages: &[Message]) -> usize {
94 messages.iter().map(Self::estimate_message).sum()
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::llm::Role;
102 use serde_json::json;
103
104 #[test]
105 fn test_estimate_text() {
106 assert_eq!(TokenEstimator::estimate_text(""), 0);
108
109 assert_eq!(TokenEstimator::estimate_text("hi"), 1);
111
112 assert_eq!(TokenEstimator::estimate_text("test"), 1);
114
115 assert_eq!(TokenEstimator::estimate_text("hello"), 2);
117
118 assert_eq!(TokenEstimator::estimate_text("hello world!"), 3); }
121
122 #[test]
123 fn test_estimate_text_message() {
124 let message = Message {
125 role: Role::User,
126 content: Content::Text("Hello, how are you?".to_string()), };
128
129 let estimate = TokenEstimator::estimate_message(&message);
130 assert_eq!(estimate, 9);
132 }
133
134 #[test]
135 fn test_estimate_blocks_message() {
136 let message = Message {
137 role: Role::Assistant,
138 content: Content::Blocks(vec![
139 ContentBlock::Text {
140 text: "Let me help.".to_string(), },
142 ContentBlock::ToolUse {
143 id: "tool_123".to_string(),
144 name: "read".to_string(), input: json!({"path": "/test.txt"}), thought_signature: None,
147 },
148 ]),
149 };
150
151 let estimate = TokenEstimator::estimate_message(&message);
152 assert!(estimate > 25); }
158
159 #[test]
160 fn test_estimate_tool_result() {
161 let message = Message {
162 role: Role::User,
163 content: Content::Blocks(vec![ContentBlock::ToolResult {
164 tool_use_id: "tool_123".to_string(),
165 content: "File contents here...".to_string(), is_error: None,
167 }]),
168 };
169
170 let estimate = TokenEstimator::estimate_message(&message);
171 assert_eq!(estimate, 20);
173 }
174
175 #[test]
176 fn test_estimate_history() {
177 let messages = vec![
178 Message::user("Hello"), Message::assistant("Hi there!"), Message::user("How are you?"), ];
182
183 let estimate = TokenEstimator::estimate_history(&messages);
184 assert_eq!(estimate, 20);
185 }
186
187 #[test]
188 fn test_empty_history() {
189 let messages: Vec<Message> = vec![];
190 assert_eq!(TokenEstimator::estimate_history(&messages), 0);
191 }
192
193 #[test]
194 fn test_estimate_redacted_thinking_uses_data_length() {
195 let data = "A".repeat(8192);
198 let block = ContentBlock::RedactedThinking { data };
199
200 let estimate = TokenEstimator::estimate_block(&block);
201 assert_eq!(estimate, 1536);
202 }
203
204 #[test]
205 fn test_estimate_redacted_thinking_respects_minimum() {
206 let data = "A".repeat(100);
209 let block = ContentBlock::RedactedThinking { data };
210
211 let estimate = TokenEstimator::estimate_block(&block);
212 assert_eq!(estimate, TokenEstimator::REDACTED_THINKING_MIN_TOKENS);
213 }
214
215 #[test]
216 fn test_estimate_redacted_thinking_empty_data() {
217 let block = ContentBlock::RedactedThinking {
219 data: String::new(),
220 };
221
222 let estimate = TokenEstimator::estimate_block(&block);
223 assert_eq!(estimate, TokenEstimator::REDACTED_THINKING_MIN_TOKENS);
224 }
225
226 #[test]
227 fn test_redacted_thinking_accumulates_in_history() {
228 let blocks: Vec<ContentBlock> = (0..5)
231 .map(|_| ContentBlock::RedactedThinking {
232 data: "B".repeat(10_000), })
234 .collect();
235 let message = Message {
236 role: Role::Assistant,
237 content: Content::Blocks(blocks),
238 };
239
240 let estimate = TokenEstimator::estimate_message(&message);
241 assert_eq!(estimate, 9379);
243 }
244}