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 }
86 }
87
88 #[must_use]
90 pub fn estimate_history(messages: &[Message]) -> usize {
91 messages.iter().map(Self::estimate_message).sum()
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::llm::Role;
99 use serde_json::json;
100
101 #[test]
102 fn test_estimate_text() {
103 assert_eq!(TokenEstimator::estimate_text(""), 0);
105
106 assert_eq!(TokenEstimator::estimate_text("hi"), 1);
108
109 assert_eq!(TokenEstimator::estimate_text("test"), 1);
111
112 assert_eq!(TokenEstimator::estimate_text("hello"), 2);
114
115 assert_eq!(TokenEstimator::estimate_text("hello world!"), 3); }
118
119 #[test]
120 fn test_estimate_text_message() {
121 let message = Message {
122 role: Role::User,
123 content: Content::Text("Hello, how are you?".to_string()), };
125
126 let estimate = TokenEstimator::estimate_message(&message);
127 assert_eq!(estimate, 9);
129 }
130
131 #[test]
132 fn test_estimate_blocks_message() {
133 let message = Message {
134 role: Role::Assistant,
135 content: Content::Blocks(vec![
136 ContentBlock::Text {
137 text: "Let me help.".to_string(), },
139 ContentBlock::ToolUse {
140 id: "tool_123".to_string(),
141 name: "read".to_string(), input: json!({"path": "/test.txt"}), thought_signature: None,
144 },
145 ]),
146 };
147
148 let estimate = TokenEstimator::estimate_message(&message);
149 assert!(estimate > 25); }
155
156 #[test]
157 fn test_estimate_tool_result() {
158 let message = Message {
159 role: Role::User,
160 content: Content::Blocks(vec![ContentBlock::ToolResult {
161 tool_use_id: "tool_123".to_string(),
162 content: "File contents here...".to_string(), is_error: None,
164 }]),
165 };
166
167 let estimate = TokenEstimator::estimate_message(&message);
168 assert_eq!(estimate, 20);
170 }
171
172 #[test]
173 fn test_estimate_history() {
174 let messages = vec![
175 Message::user("Hello"), Message::assistant("Hi there!"), Message::user("How are you?"), ];
179
180 let estimate = TokenEstimator::estimate_history(&messages);
181 assert_eq!(estimate, 20);
182 }
183
184 #[test]
185 fn test_empty_history() {
186 let messages: Vec<Message> = vec![];
187 assert_eq!(TokenEstimator::estimate_history(&messages), 0);
188 }
189
190 #[test]
191 fn test_estimate_redacted_thinking_uses_data_length() {
192 let data = "A".repeat(8192);
195 let block = ContentBlock::RedactedThinking { data };
196
197 let estimate = TokenEstimator::estimate_block(&block);
198 assert_eq!(estimate, 1536);
199 }
200
201 #[test]
202 fn test_estimate_redacted_thinking_respects_minimum() {
203 let data = "A".repeat(100);
206 let block = ContentBlock::RedactedThinking { data };
207
208 let estimate = TokenEstimator::estimate_block(&block);
209 assert_eq!(estimate, TokenEstimator::REDACTED_THINKING_MIN_TOKENS);
210 }
211
212 #[test]
213 fn test_estimate_redacted_thinking_empty_data() {
214 let block = ContentBlock::RedactedThinking {
216 data: String::new(),
217 };
218
219 let estimate = TokenEstimator::estimate_block(&block);
220 assert_eq!(estimate, TokenEstimator::REDACTED_THINKING_MIN_TOKENS);
221 }
222
223 #[test]
224 fn test_redacted_thinking_accumulates_in_history() {
225 let blocks: Vec<ContentBlock> = (0..5)
228 .map(|_| ContentBlock::RedactedThinking {
229 data: "B".repeat(10_000), })
231 .collect();
232 let message = Message {
233 role: Role::Assistant,
234 content: Content::Blocks(blocks),
235 };
236
237 let estimate = TokenEstimator::estimate_message(&message);
238 assert_eq!(estimate, 9379);
240 }
241}