1use std::path::Path;
7use std::str::FromStr;
8use tiktoken_rs::{cl100k_base, o200k_base, p50k_base, CoreBPE};
9
10#[derive(Debug, Clone)]
12pub struct TokenCount {
13 pub count: usize,
15 #[allow(dead_code)] pub encoding: String,
18 #[allow(dead_code)] pub char_count: Option<usize>,
21}
22
23impl TokenCount {
24 #[allow(dead_code)] pub fn new(count: usize, encoding: &str) -> Self {
27 Self {
28 count,
29 encoding: encoding.to_string(),
30 char_count: None,
31 }
32 }
33
34 pub fn with_char_count(count: usize, encoding: &str, char_count: usize) -> Self {
36 Self {
37 count,
38 encoding: encoding.to_string(),
39 char_count: Some(char_count),
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum Encoding {
47 #[default]
49 Cl100kBase,
50 O200kBase,
52 P50kBase,
54}
55
56impl Encoding {
57 pub fn as_str(&self) -> &'static str {
59 match self {
60 Self::Cl100kBase => "cl100k_base",
61 Self::O200kBase => "o200k_base",
62 Self::P50kBase => "p50k_base",
63 }
64 }
65}
66
67impl FromStr for Encoding {
68 type Err = ();
69 fn from_str(s: &str) -> Result<Self, Self::Err> {
70 match s.to_lowercase().as_str() {
71 "cl100k_base" | "cl100k" => Ok(Encoding::Cl100kBase),
72 "o200k_base" | "o200k" => Ok(Encoding::O200kBase),
73 "p50k_base" | "p50k" => Ok(Encoding::P50kBase),
74 _ => Err(()),
75 }
76 }
77}
78
79pub fn get_bpe(encoding: Encoding) -> Result<CoreBPE, String> {
81 match encoding {
82 Encoding::Cl100kBase => cl100k_base().map_err(|e| e.to_string()),
83 Encoding::O200kBase => o200k_base().map_err(|e| e.to_string()),
84 Encoding::P50kBase => p50k_base().map_err(|e| e.to_string()),
85 }
86}
87
88#[allow(dead_code)] pub fn count_tokens(text: &str) -> Result<usize, String> {
91 count_tokens_with_encoding(text, Encoding::default())
92}
93
94pub fn count_tokens_with_encoding(text: &str, encoding: Encoding) -> Result<usize, String> {
96 let bpe = get_bpe(encoding)?;
97 Ok(bpe.encode_with_special_tokens(text).len())
98}
99
100pub fn count_tokens_detailed(text: &str, encoding: Encoding) -> Result<TokenCount, String> {
102 let bpe = get_bpe(encoding)?;
103 let count = bpe.encode_with_special_tokens(text).len();
104 Ok(TokenCount::with_char_count(
105 count,
106 encoding.as_str(),
107 text.len(),
108 ))
109}
110
111pub fn count_file_tokens(path: &Path, encoding: Encoding) -> Result<TokenCount, String> {
113 let content = std::fs::read_to_string(path)
114 .map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?;
115 count_tokens_detailed(&content, encoding)
116}
117
118#[derive(Debug, Clone)]
120#[allow(dead_code)] pub struct FileTokens {
122 pub path: String,
124 pub tokens: usize,
126 pub size_bytes: usize,
128}
129
130pub trait HasTokenCount {
135 fn token_count(&self) -> usize;
137}
138
139impl HasTokenCount for FileTokens {
140 fn token_count(&self) -> usize {
141 self.tokens
142 }
143}
144
145pub fn select_by_token_budget<T: HasTokenCount>(
158 items: Vec<T>,
159 max_tokens: usize,
160) -> (Vec<T>, usize, usize) {
161 let mut selected = Vec::new();
162 let mut total_tokens = 0;
163 let mut omitted = 0;
164
165 for item in items {
166 if total_tokens + item.token_count() <= max_tokens {
167 total_tokens += item.token_count();
168 selected.push(item);
169 } else {
170 omitted += 1;
171 }
172 }
173
174 (selected, total_tokens, omitted)
175}
176
177#[allow(dead_code)] pub fn select_files_by_tokens(
191 files: &[FileTokens],
192 max_tokens: usize,
193) -> (Vec<FileTokens>, usize, usize) {
194 let mut selected = Vec::new();
195 let mut total_tokens = 0;
196 let mut omitted = 0;
197
198 for file in files {
199 if total_tokens + file.tokens <= max_tokens {
200 total_tokens += file.tokens;
201 selected.push(file.clone());
202 } else {
203 omitted += 1;
204 }
205 }
206
207 (selected, total_tokens, omitted)
208}
209
210#[allow(dead_code)] pub fn estimate_tokens(text: &str) -> usize {
216 text.len().div_ceil(4)
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn test_count_tokens_basic() {
227 let text = "Hello, world!";
228 let count = count_tokens(text).unwrap();
229 assert!(count > 0);
230 assert!(count < text.len()); }
232
233 #[test]
234 fn test_count_tokens_encoding() {
235 let text = "fn main() { println!(\"Hello\"); }";
236
237 let cl100k = count_tokens_with_encoding(text, Encoding::Cl100kBase).unwrap();
238 let o200k = count_tokens_with_encoding(text, Encoding::O200kBase).unwrap();
239
240 assert!(cl100k > 0 && cl100k < 50);
242 assert!(o200k > 0 && o200k < 50);
243 }
244
245 #[test]
246 fn test_count_tokens_detailed() {
247 let text = "Hello, world!";
248 let result = count_tokens_detailed(text, Encoding::Cl100kBase).unwrap();
249
250 assert!(result.count > 0);
251 assert_eq!(result.encoding, "cl100k_base");
252 assert_eq!(result.char_count, Some(text.len()));
253 }
254
255 #[test]
256 fn test_select_files_by_tokens() {
257 let files = vec![
258 FileTokens {
259 path: "a.rs".to_string(),
260 tokens: 100,
261 size_bytes: 400,
262 },
263 FileTokens {
264 path: "b.rs".to_string(),
265 tokens: 200,
266 size_bytes: 800,
267 },
268 FileTokens {
269 path: "c.rs".to_string(),
270 tokens: 150,
271 size_bytes: 600,
272 },
273 ];
274
275 let (selected, total, omitted) = select_files_by_tokens(&files, 500);
277 assert_eq!(selected.len(), 3);
278 assert_eq!(total, 450);
279 assert_eq!(omitted, 0);
280
281 let (selected, total, omitted) = select_files_by_tokens(&files, 300);
283 assert_eq!(selected.len(), 2);
284 assert_eq!(total, 300);
285 assert_eq!(omitted, 1);
286
287 let (selected, total, omitted) = select_files_by_tokens(&files, 150);
289 assert_eq!(selected.len(), 1);
290 assert_eq!(total, 100);
291 assert_eq!(omitted, 2);
292 }
293
294 #[test]
295 fn test_encoding_from_str() {
296 use std::str::FromStr;
297 assert_eq!(
298 Encoding::from_str("cl100k_base").ok(),
299 Some(Encoding::Cl100kBase)
300 );
301 assert_eq!(
302 Encoding::from_str("o200k_base").ok(),
303 Some(Encoding::O200kBase)
304 );
305 assert_eq!(
306 Encoding::from_str("p50k_base").ok(),
307 Some(Encoding::P50kBase)
308 );
309 assert_eq!(Encoding::from_str("unknown").ok(), None);
310 }
311
312 #[test]
313 fn test_estimate_tokens() {
314 let text = "Hello, world!"; let estimate = estimate_tokens(text);
316 assert!((3..=5).contains(&estimate));
318 }
319}