use crate::types::message::Message;
pub struct CharApproxCounter {
chars_per_token: usize,
}
impl CharApproxCounter {
#[must_use]
pub fn new(chars_per_token: usize) -> Self {
Self {
chars_per_token: chars_per_token.max(1),
}
}
#[must_use]
pub fn count(&self, messages: &[Message]) -> usize {
messages
.iter()
.map(|m| m.content.len() / self.chars_per_token + 1)
.sum()
}
#[must_use]
pub fn count_str(&self, text: &str) -> usize {
text.len() / self.chars_per_token + 1
}
}
impl Default for CharApproxCounter {
fn default() -> Self {
Self::new(4)
}
}
pub trait TokenCounter: Send + Sync {
fn count_messages(&self, messages: &[Message]) -> usize;
fn count_str(&self, text: &str) -> usize;
}
impl TokenCounter for CharApproxCounter {
fn count_messages(&self, messages: &[Message]) -> usize {
self.count(messages)
}
fn count_str(&self, text: &str) -> usize {
CharApproxCounter::count_str(self, text)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::message::MessageRole;
fn msg(content: &str) -> Message {
Message {
role: MessageRole::User,
content: content.to_string(),
tool_call_id: None,
}
}
#[test]
fn test_char_approx_default() {
let counter = CharApproxCounter::default();
assert_eq!(counter.count_str("Hello world!"), 4);
}
#[test]
fn test_char_approx_custom_ratio() {
let counter = CharApproxCounter::new(2);
assert_eq!(counter.count_str("Hello world!"), 7);
}
#[test]
fn test_char_approx_messages() {
let counter = CharApproxCounter::default();
let messages = vec![msg("aaaa"), msg("bbbbbbbb")]; assert_eq!(counter.count(&messages), 5);
}
#[test]
fn test_char_approx_empty() {
let counter = CharApproxCounter::default();
assert_eq!(counter.count(&[]), 0);
assert_eq!(counter.count_str(""), 1);
}
#[test]
fn test_char_approx_zero_ratio_clamped() {
let counter = CharApproxCounter::new(0);
assert_eq!(counter.count_str("abcd"), 5); }
#[test]
fn test_token_counter_trait() {
let counter = CharApproxCounter::default();
let tc: &dyn TokenCounter = &counter;
assert_eq!(tc.count_str("abcdefgh"), 3); }
}