use std::sync::Arc;
pub trait Tokenizer: Send + Sync {
fn count(&self, text: &str) -> usize;
fn encode(&self, _text: &str) -> Option<Vec<u32>> {
None
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CharApproxTokenizer;
impl CharApproxTokenizer {
pub fn new() -> Self {
CharApproxTokenizer
}
}
impl Tokenizer for CharApproxTokenizer {
fn count(&self, text: &str) -> usize {
if text.is_empty() {
0
} else {
text.len().div_ceil(4)
}
}
}
impl<T: Tokenizer + ?Sized> Tokenizer for Arc<T> {
fn count(&self, text: &str) -> usize {
(**self).count(text)
}
fn encode(&self, text: &str) -> Option<Vec<u32>> {
(**self).encode(text)
}
}
#[cfg(feature = "tiktoken")]
mod tiktoken_impl {
use super::Tokenizer;
use tiktoken_rs::{cl100k_base, CoreBPE};
pub struct TiktokenCl100k {
bpe: CoreBPE,
}
impl TiktokenCl100k {
pub fn new() -> Result<Self, String> {
cl100k_base()
.map(|bpe| TiktokenCl100k { bpe })
.map_err(|e| e.to_string())
}
}
impl std::fmt::Debug for TiktokenCl100k {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TiktokenCl100k").finish()
}
}
impl Tokenizer for TiktokenCl100k {
fn count(&self, text: &str) -> usize {
self.bpe.encode_with_special_tokens(text).len()
}
fn encode(&self, text: &str) -> Option<Vec<u32>> {
Some(self.bpe.encode_with_special_tokens(text))
}
}
}
#[cfg(feature = "tiktoken")]
pub use tiktoken_impl::TiktokenCl100k;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn char_approx_returns_zero_for_empty_string() {
let tok = CharApproxTokenizer::new();
assert_eq!(tok.count(""), 0);
}
#[test]
fn char_approx_uses_four_chars_per_token() {
let tok = CharApproxTokenizer;
assert_eq!(tok.count("a"), 1);
assert_eq!(tok.count("abcd"), 1);
assert_eq!(tok.count("abcde"), 2);
assert_eq!(tok.count("abcdefgh"), 2);
assert_eq!(tok.count("abcdefghi"), 3);
}
#[test]
fn char_approx_encode_returns_none() {
let tok = CharApproxTokenizer;
assert!(tok.encode("anything").is_none());
}
#[test]
fn arc_dyn_tokenizer_forwards() {
let tok: Arc<dyn Tokenizer> = Arc::new(CharApproxTokenizer);
assert_eq!(tok.count("abcd"), 1);
assert!(tok.encode("abcd").is_none());
}
#[cfg(feature = "tiktoken")]
#[test]
fn tiktoken_cl100k_counts_a_short_string() {
let tok = TiktokenCl100k::new().expect("load cl100k");
let count = tok.count("hello world");
assert!(
count > 0 && count <= 5,
"got {count} tokens for 'hello world'"
);
let ids = tok.encode("hello world").expect("ids");
assert_eq!(ids.len(), count, "encode/count agreement");
}
}