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::Thinking { thinking, .. } => Self::estimate_text(thinking),
52 ContentBlock::RedactedThinking { .. } => 10, ContentBlock::ToolUse { name, input, .. } => {
54 let input_str = serde_json::to_string(input).unwrap_or_default();
55 Self::estimate_text(name)
56 + Self::estimate_text(&input_str)
57 + Self::TOOL_USE_OVERHEAD
58 }
59 ContentBlock::ToolResult { content, .. } => {
60 Self::estimate_text(content) + Self::TOOL_RESULT_OVERHEAD
61 }
62 ContentBlock::Image { source } | ContentBlock::Document { source } => {
63 source.data.len() / 4 + Self::MESSAGE_OVERHEAD
65 }
66 }
67 }
68
69 #[must_use]
71 pub fn estimate_history(messages: &[Message]) -> usize {
72 messages.iter().map(Self::estimate_message).sum()
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::llm::Role;
80 use serde_json::json;
81
82 #[test]
83 fn test_estimate_text() {
84 assert_eq!(TokenEstimator::estimate_text(""), 0);
86
87 assert_eq!(TokenEstimator::estimate_text("hi"), 1);
89
90 assert_eq!(TokenEstimator::estimate_text("test"), 1);
92
93 assert_eq!(TokenEstimator::estimate_text("hello"), 2);
95
96 assert_eq!(TokenEstimator::estimate_text("hello world!"), 3); }
99
100 #[test]
101 fn test_estimate_text_message() {
102 let message = Message {
103 role: Role::User,
104 content: Content::Text("Hello, how are you?".to_string()), };
106
107 let estimate = TokenEstimator::estimate_message(&message);
108 assert_eq!(estimate, 9);
110 }
111
112 #[test]
113 fn test_estimate_blocks_message() {
114 let message = Message {
115 role: Role::Assistant,
116 content: Content::Blocks(vec![
117 ContentBlock::Text {
118 text: "Let me help.".to_string(), },
120 ContentBlock::ToolUse {
121 id: "tool_123".to_string(),
122 name: "read".to_string(), input: json!({"path": "/test.txt"}), thought_signature: None,
125 },
126 ]),
127 };
128
129 let estimate = TokenEstimator::estimate_message(&message);
130 assert!(estimate > 25); }
136
137 #[test]
138 fn test_estimate_tool_result() {
139 let message = Message {
140 role: Role::User,
141 content: Content::Blocks(vec![ContentBlock::ToolResult {
142 tool_use_id: "tool_123".to_string(),
143 content: "File contents here...".to_string(), is_error: None,
145 }]),
146 };
147
148 let estimate = TokenEstimator::estimate_message(&message);
149 assert_eq!(estimate, 20);
151 }
152
153 #[test]
154 fn test_estimate_history() {
155 let messages = vec![
156 Message::user("Hello"), Message::assistant("Hi there!"), Message::user("How are you?"), ];
160
161 let estimate = TokenEstimator::estimate_history(&messages);
162 assert_eq!(estimate, 20);
163 }
164
165 #[test]
166 fn test_empty_history() {
167 let messages: Vec<Message> = vec![];
168 assert_eq!(TokenEstimator::estimate_history(&messages), 0);
169 }
170}