pub trait TokenCounter: Send + Sync {
fn count(&self, text: &str) -> usize;
fn name(&self) -> &'static str;
}
#[derive(Debug, Default, Clone)]
pub struct WordProxyCounter;
impl TokenCounter for WordProxyCounter {
fn count(&self, text: &str) -> usize {
text.split_whitespace().count()
}
fn name(&self) -> &'static str {
"word-proxy"
}
}
#[cfg(feature = "tiktoken")]
pub struct TiktokenCounter {
bpe: tiktoken_rs::CoreBPE,
}
#[cfg(feature = "tiktoken")]
impl TiktokenCounter {
pub fn cl100k_base() -> Self {
Self {
bpe: tiktoken_rs::cl100k_base().expect("cl100k_base tokenizer tables"),
}
}
}
#[cfg(feature = "tiktoken")]
impl TokenCounter for TiktokenCounter {
fn count(&self, text: &str) -> usize {
self.bpe.encode_ordinary(text).len()
}
fn name(&self) -> &'static str {
"cl100k_base"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn word_proxy_matches_split_whitespace() {
let c = WordProxyCounter;
assert_eq!(c.count(""), 0);
assert_eq!(c.count("hello world"), 2);
assert_eq!(c.count(" multiple spaces "), 2);
assert_eq!(c.count("punct, and! more?"), 3);
assert_eq!(c.name(), "word-proxy");
for s in ["", "a b c", " x y ", "line1\nline2\tthree", "único café"] {
assert_eq!(c.count(s), s.split_whitespace().count());
}
}
}
#[cfg(all(test, feature = "tiktoken"))]
mod tiktoken_tests {
use super::*;
#[test]
fn cl100k_exact_reference_counts() {
let c = TiktokenCounter::cl100k_base();
assert_eq!(c.count(""), 0);
assert_eq!(c.count("hello world"), 2);
assert_eq!(c.count("The quick brown fox"), 4);
assert_eq!(c.name(), "cl100k_base");
}
#[test]
fn cl100k_diverges_from_word_proxy_on_subword() {
let tk = TiktokenCounter::cl100k_base();
let wp = WordProxyCounter;
let url = "https://example.com/verify";
assert_eq!(wp.count(url), 1);
assert!(
tk.count(url) > 1,
"tiktoken should split the URL: {}",
tk.count(url)
);
}
}