language_barrier_core/token.rs
1/// A simple token counter for tracking token usage in conversations
2///
3/// This is a very basic implementation that provides a minimal token counting
4/// utility. In a real-world scenario, you would want to use a proper tokenizer
5/// like `tiktoken-rs` for accurate token counting specific to the model being used.
6///
7/// # Examples
8///
9/// ```
10/// use language_barrier_core::TokenCounter;
11///
12/// let mut counter = TokenCounter::default();
13/// counter.observe("Hello, world!");
14/// assert_eq!(counter.total(), 2); // "Hello," and "world!" as two tokens
15///
16/// counter.subtract("Hello,");
17/// assert_eq!(counter.total(), 1); // "world!" as one token
18///
19/// assert!(counter.under_budget(10)); // 1 < 10
20/// ```
21#[derive(Default, Clone, Debug)]
22pub struct TokenCounter {
23 total: usize,
24}
25
26impl TokenCounter {
27 /// Creates a new `TokenCounter` with zero tokens
28 ///
29 /// # Examples
30 ///
31 /// ```
32 /// use language_barrier_core::TokenCounter;
33 ///
34 /// let counter = TokenCounter::new();
35 /// assert_eq!(counter.total(), 0);
36 /// ```
37 #[must_use]
38 pub fn new() -> Self {
39 Self { total: 0 }
40 }
41
42 /// Counts the number of tokens in a string (naive implementation)
43 ///
44 /// This is a very simple whitespace-based tokenization. In a real-world
45 /// implementation, you would want to use a proper tokenizer like `tiktoken-rs`.
46 ///
47 /// # Examples
48 ///
49 /// ```
50 /// use language_barrier_core::TokenCounter;
51 ///
52 /// assert_eq!(TokenCounter::count_tokens("Hello, world!"), 2);
53 /// assert_eq!(TokenCounter::count_tokens("one two three four"), 4);
54 /// ```
55 #[must_use]
56 pub fn count_tokens(text: &str) -> usize {
57 text.split_whitespace().count()
58 }
59
60 /// Adds the token count of the given text to the total
61 ///
62 /// # Examples
63 ///
64 /// ```
65 /// use language_barrier_core::TokenCounter;
66 ///
67 /// let mut counter = TokenCounter::default();
68 /// counter.observe("Hello, world!");
69 /// assert_eq!(counter.total(), 2);
70 /// ```
71 pub fn observe(&mut self, text: &str) {
72 self.total += Self::count_tokens(text);
73 }
74
75 /// Subtracts the token count of the given text from the total
76 ///
77 /// Will not go below zero (saturates at zero).
78 ///
79 /// # Examples
80 ///
81 /// ```
82 /// use language_barrier_core::TokenCounter;
83 ///
84 /// let mut counter = TokenCounter::default();
85 /// counter.observe("Hello, world!");
86 /// counter.subtract("Hello,");
87 /// assert_eq!(counter.total(), 1);
88 ///
89 /// // Won't go below zero
90 /// counter.subtract("world! and more");
91 /// assert_eq!(counter.total(), 0);
92 /// ```
93 pub fn subtract(&mut self, text: &str) {
94 self.total = self.total.saturating_sub(Self::count_tokens(text));
95 }
96
97 /// Returns the current total token count
98 ///
99 /// # Examples
100 ///
101 /// ```
102 /// use language_barrier_core::TokenCounter;
103 ///
104 /// let mut counter = TokenCounter::default();
105 /// counter.observe("Hello, world!");
106 /// assert_eq!(counter.total(), 2);
107 /// ```
108 #[must_use]
109 pub fn total(&self) -> usize {
110 self.total
111 }
112
113 /// Checks if the total token count is under the specified budget
114 ///
115 /// # Examples
116 ///
117 /// ```
118 /// use language_barrier_core::TokenCounter;
119 ///
120 /// let mut counter = TokenCounter::default();
121 /// counter.observe("Hello, world!");
122 /// assert!(counter.under_budget(5));
123 /// assert!(!counter.under_budget(1));
124 /// ```
125 #[must_use]
126 pub fn under_budget(&self, max: usize) -> bool {
127 self.total <= max
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_token_counter_new() {
137 let counter = TokenCounter::new();
138 assert_eq!(counter.total(), 0);
139 }
140
141 #[test]
142 fn test_token_counting() {
143 assert_eq!(TokenCounter::count_tokens("Hello world"), 2);
144 assert_eq!(TokenCounter::count_tokens(""), 0);
145 assert_eq!(TokenCounter::count_tokens(" "), 0);
146 assert_eq!(TokenCounter::count_tokens("one two three"), 3);
147 }
148
149 #[test]
150 fn test_observe_and_total() {
151 let mut counter = TokenCounter::default();
152 counter.observe("Hello world");
153 assert_eq!(counter.total(), 2);
154
155 counter.observe("another message");
156 assert_eq!(counter.total(), 4); // 2 + 2 tokens
157 }
158
159 #[test]
160 fn test_subtract() {
161 let mut counter = TokenCounter::default();
162 counter.observe("Hello world another message");
163 assert_eq!(counter.total(), 4);
164
165 counter.subtract("Hello world");
166 assert_eq!(counter.total(), 2);
167
168 // Test saturation (won't go below zero)
169 counter.subtract("way more tokens than we have now");
170 assert_eq!(counter.total(), 0);
171 }
172
173 #[test]
174 fn test_under_budget() {
175 let mut counter = TokenCounter::default();
176 counter.observe("Hello world");
177
178 assert!(counter.under_budget(2));
179 assert!(counter.under_budget(3));
180 assert!(!counter.under_budget(1));
181 }
182}