1use std::collections::HashMap;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum ModelType {
27 Gpt4o,
29 O1,
31 Gpt4,
33 Gemini3,
35 ClaudeOpus45,
37 ClaudeSonnet4,
39 Other,
41}
42
43impl std::fmt::Display for ModelType {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 ModelType::Gpt4o => write!(f, "GPT-4o"),
47 ModelType::O1 => write!(f, "o1"),
48 ModelType::Gpt4 => write!(f, "GPT-4"),
49 ModelType::Gemini3 => write!(f, "Gemini 3"),
50 ModelType::ClaudeOpus45 => write!(f, "Claude Opus 4.5"),
51 ModelType::ClaudeSonnet4 => write!(f, "Claude Sonnet 4"),
52 ModelType::Other => write!(f, "Other"),
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct TokenInfo {
60 pub count: usize,
62 pub ids: Vec<u32>,
64 pub tokens: Vec<String>,
66 pub model: ModelType,
68}
69
70impl TokenInfo {
71 pub fn new(count: usize, ids: Vec<u32>, tokens: Vec<String>, model: ModelType) -> Self {
73 Self {
74 count,
75 ids,
76 tokens,
77 model,
78 }
79 }
80
81 pub fn count_only(count: usize, model: ModelType) -> Self {
83 Self {
84 count,
85 ids: Vec::new(),
86 tokens: Vec::new(),
87 model,
88 }
89 }
90}
91
92pub struct TokenCounter {
97 }
101
102impl TokenCounter {
103 pub fn new() -> Self {
105 Self {}
106 }
107
108 pub fn count(&self, text: &str, model: ModelType) -> TokenInfo {
117 match model {
118 ModelType::Gpt4o | ModelType::O1 => self.count_openai_o200k(text, model),
119 ModelType::Gpt4 => self.count_openai_cl100k(text, model),
120 ModelType::Gemini3 => self.count_gemini(text, model),
121 ModelType::ClaudeOpus45 | ModelType::ClaudeSonnet4 => self.count_claude(text, model),
122 ModelType::Other => self.count_other(text, model),
123 }
124 }
125
126 fn count_openai_o200k(&self, text: &str, model: ModelType) -> TokenInfo {
128 #[cfg(feature = "tiktoken")]
130 {
131 use tiktoken_rs::o200k_base;
132 if let Ok(bpe) = o200k_base() {
133 let tokens = bpe.encode_with_special_tokens(text);
134 let decoded: Vec<String> = tokens
135 .iter()
136 .filter_map(|&id| bpe.decode(vec![id]).ok())
137 .collect();
138 return TokenInfo::new(tokens.len(), tokens, decoded, model);
139 }
140 }
141
142 self.approximate_token_count(text, model, 4.0)
144 }
145
146 fn count_openai_cl100k(&self, text: &str, model: ModelType) -> TokenInfo {
148 #[cfg(feature = "tiktoken")]
149 {
150 use tiktoken_rs::cl100k_base;
151 if let Ok(bpe) = cl100k_base() {
152 let tokens = bpe.encode_with_special_tokens(text);
153 let decoded: Vec<String> = tokens
154 .iter()
155 .filter_map(|&id| bpe.decode(vec![id]).ok())
156 .collect();
157 return TokenInfo::new(tokens.len(), tokens, decoded, model);
158 }
159 }
160
161 self.approximate_token_count(text, model, 4.0)
163 }
164
165 fn count_gemini(&self, text: &str, model: ModelType) -> TokenInfo {
167 #[cfg(feature = "tokenizers")]
168 {
169 use tokenizers::Tokenizer;
170 let paths = [
172 "tokenizers/gemma-tokenizer.json",
173 "~/.cache/huggingface/tokenizers/google/gemma-3/tokenizer.json",
174 ];
175
176 for path in &paths {
177 if let Ok(tokenizer) = Tokenizer::from_file(path) {
178 if let Ok(encoding) = tokenizer.encode(text, false) {
179 let ids: Vec<u32> = encoding.get_ids().to_vec();
180 let tokens: Vec<String> = encoding
181 .get_tokens()
182 .iter()
183 .map(|s| s.to_string())
184 .collect();
185 return TokenInfo::new(ids.len(), ids, tokens, model);
186 }
187 }
188 }
189 }
190
191 self.approximate_token_count(text, model, 3.5)
193 }
194
195 fn count_claude(&self, text: &str, model: ModelType) -> TokenInfo {
197 #[cfg(feature = "tokenizers-hf")]
200 {
201 use tokenizers::Tokenizer;
202 let paths = [
204 "tokenizers/claude-tokenizer.json",
205 "~/.cache/huggingface/tokenizers/anthropic/claude/tokenizer.json",
206 ];
207
208 for path in &paths {
209 if let Ok(tokenizer) = Tokenizer::from_file(path) {
210 if let Ok(encoding) = tokenizer.encode(text, false) {
211 let ids: Vec<u32> = encoding.get_ids().to_vec();
212 let tokens: Vec<String> = encoding
213 .get_tokens()
214 .iter()
215 .map(|s| s.to_string())
216 .collect();
217 return TokenInfo::new(ids.len(), ids, tokens, model);
218 }
219 }
220 }
221 }
222
223 self.approximate_token_count(text, model, 3.8)
225 }
226
227 fn count_other(&self, text: &str, model: ModelType) -> TokenInfo {
229 self.approximate_token_count(text, model, 3.7)
231 }
232
233 fn approximate_token_count(
235 &self,
236 text: &str,
237 model: ModelType,
238 chars_per_token: f64,
239 ) -> TokenInfo {
240 let count = (text.len() as f64 / chars_per_token).ceil() as usize;
241 TokenInfo::count_only(count.max(1), model)
242 }
243
244 pub fn count_all(&self, text: &str) -> HashMap<ModelType, TokenInfo> {
246 let models = [
247 ModelType::Gpt4o,
248 ModelType::O1,
249 ModelType::Gpt4,
250 ModelType::Gemini3,
251 ModelType::ClaudeOpus45,
252 ModelType::ClaudeSonnet4,
253 ModelType::Other,
254 ];
255
256 models
257 .iter()
258 .map(|&model| (model, self.count(text, model)))
259 .collect()
260 }
261
262 pub fn count_primary_models(&self, text: &str) -> HashMap<ModelType, TokenInfo> {
271 let models = [
272 ModelType::Gpt4o, ModelType::ClaudeSonnet4, ModelType::Gemini3, ModelType::Other, ];
277
278 models
279 .iter()
280 .map(|&model| (model, self.count(text, model)))
281 .collect()
282 }
283
284 pub fn summary(&self, text: &str) -> String {
286 let counts = self.count_all(text);
287 let mut lines = vec![format!("Token counts for {} chars:", text.len())];
288
289 for model in [
290 ModelType::Gpt4o,
291 ModelType::Gemini3,
292 ModelType::ClaudeOpus45,
293 ] {
294 if let Some(info) = counts.get(&model) {
295 lines.push(format!(" {}: {} tokens", model, info.count));
296 }
297 }
298
299 lines.join("\n")
300 }
301}
302
303impl Default for TokenCounter {
304 fn default() -> Self {
305 Self::new()
306 }
307}
308
309pub struct TokenEfficiencyMeasurement {
311 pub original: TokenInfo,
313 pub dx_format: TokenInfo,
315 pub savings_percent: f64,
317}
318
319impl TokenEfficiencyMeasurement {
320 pub fn calculate(original: TokenInfo, dx_format: TokenInfo) -> Self {
322 let savings = if original.count > 0 {
323 ((original.count as f64 - dx_format.count as f64) / original.count as f64) * 100.0
324 } else {
325 0.0
326 };
327
328 Self {
329 original,
330 dx_format,
331 savings_percent: savings,
332 }
333 }
334}
335
336pub trait TokenCountExt {
338 fn token_count(&self, model: ModelType) -> TokenInfo;
340
341 fn token_counts(&self) -> HashMap<ModelType, TokenInfo>;
343}
344
345impl TokenCountExt for String {
346 fn token_count(&self, model: ModelType) -> TokenInfo {
347 let counter = TokenCounter::new();
348 counter.count(self, model)
349 }
350
351 fn token_counts(&self) -> HashMap<ModelType, TokenInfo> {
352 let counter = TokenCounter::new();
353 counter.count_all(self)
354 }
355}
356
357impl TokenCountExt for str {
358 fn token_count(&self, model: ModelType) -> TokenInfo {
359 let counter = TokenCounter::new();
360 counter.count(self, model)
361 }
362
363 fn token_counts(&self) -> HashMap<ModelType, TokenInfo> {
364 let counter = TokenCounter::new();
365 counter.count_all(self)
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn test_token_counter_creation() {
375 let counter = TokenCounter::new();
376 let info = counter.count("Hello, world!", ModelType::Gpt4o);
377 assert!(info.count > 0);
378 }
379
380 #[test]
381 fn test_count_all_models() {
382 let counter = TokenCounter::new();
383 let counts = counter.count_all("Hello, world!");
384
385 assert!(counts.contains_key(&ModelType::Gpt4o));
386 assert!(counts.contains_key(&ModelType::Gemini3));
387 assert!(counts.contains_key(&ModelType::ClaudeOpus45));
388 assert!(counts.contains_key(&ModelType::Other));
389 }
390
391 #[test]
392 fn test_count_primary_models() {
393 let counter = TokenCounter::new();
394 let counts = counter.count_primary_models("Hello, world!");
395
396 assert_eq!(counts.len(), 4);
398 assert!(counts.contains_key(&ModelType::Gpt4o));
399 assert!(counts.contains_key(&ModelType::ClaudeSonnet4));
400 assert!(counts.contains_key(&ModelType::Gemini3));
401 assert!(counts.contains_key(&ModelType::Other));
402
403 for info in counts.values() {
405 assert!(info.count > 0, "Token count should be non-zero");
406 }
407 }
408
409 #[test]
410 fn test_other_model() {
411 let counter = TokenCounter::new();
412 let info = counter.count("Hello, world!", ModelType::Other);
413 assert!(info.count > 0);
414 assert_eq!(info.model, ModelType::Other);
415 }
416
417 #[test]
418 fn test_token_efficiency_measurement() {
419 let original = TokenInfo::count_only(100, ModelType::Gpt4o);
420 let dx = TokenInfo::count_only(73, ModelType::Gpt4o);
421 let measurement = TokenEfficiencyMeasurement::calculate(original, dx);
422
423 assert!((measurement.savings_percent - 27.0).abs() < 0.1);
424 }
425
426 #[test]
427 fn test_empty_string() {
428 let counter = TokenCounter::new();
429 let info = counter.count("", ModelType::Gpt4o);
430 assert_eq!(info.count, 1); }
432
433 #[test]
434 fn test_model_display() {
435 assert_eq!(format!("{}", ModelType::Gpt4o), "GPT-4o");
436 assert_eq!(format!("{}", ModelType::Gemini3), "Gemini 3");
437 assert_eq!(format!("{}", ModelType::ClaudeOpus45), "Claude Opus 4.5");
438 }
439
440 #[test]
441 fn test_summary() {
442 let counter = TokenCounter::new();
443 let summary = counter.summary("Hello, world!");
444 assert!(summary.contains("Token counts"));
445 assert!(summary.contains("GPT-4o"));
446 }
447
448 #[test]
449 fn test_token_count_extension() {
450 let text = "Hello, world!";
451 let info = text.token_count(ModelType::Gpt4o);
452 assert!(info.count > 0);
453 }
454
455 #[test]
456 fn test_dx_format_token_efficiency() {
457 let json = r#"{"name":"dx-serializer","version":"0.1.0","description":"Binary-first serialization format for LLMs","workspace":["frontend/www","frontend/mobile","backend/api","backend/workers"],"dependencies":{"serde":"1.0","bincode":"2.0","tokio":"1.0"},"enabled":true,"count":42}"#;
460 let dx = "nm=dx-serializer ver=0.1.0 ds=\"Binary-first serialization format for LLMs\" ws:frontend/www,frontend/mobile,backend/api,backend/workers deps.serde=1.0 deps.bincode=2.0 deps.tokio=1.0 en=true ct=42";
461
462 let counter = TokenCounter::new();
463 let json_tokens = counter.count(json, ModelType::Gpt4o);
464 let dx_tokens = counter.count(dx, ModelType::Gpt4o);
465
466 let json_count = json_tokens.count;
468 let dx_count = dx_tokens.count;
469
470 let measurement = TokenEfficiencyMeasurement::calculate(json_tokens, dx_tokens);
472 println!(
473 "JSON: {} tokens, DX: {} tokens, Savings: {:.1}%",
474 measurement.original.count, measurement.dx_format.count, measurement.savings_percent
475 );
476
477 assert!(
480 measurement.savings_percent >= 0.0 || dx_count <= json_count,
481 "DX format should not be worse than JSON"
482 );
483 }
484}