use std::path::Path;
use std::str::FromStr;
use tiktoken_rs::{cl100k_base, o200k_base, p50k_base, CoreBPE};
#[derive(Debug, Clone)]
pub struct TokenCount {
pub count: usize,
#[allow(dead_code)] pub encoding: String,
#[allow(dead_code)] pub char_count: Option<usize>,
}
impl TokenCount {
#[allow(dead_code)] pub fn new(count: usize, encoding: &str) -> Self {
Self {
count,
encoding: encoding.to_string(),
char_count: None,
}
}
pub fn with_char_count(count: usize, encoding: &str, char_count: usize) -> Self {
Self {
count,
encoding: encoding.to_string(),
char_count: Some(char_count),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Encoding {
#[default]
Cl100kBase,
O200kBase,
P50kBase,
}
impl Encoding {
pub fn as_str(&self) -> &'static str {
match self {
Self::Cl100kBase => "cl100k_base",
Self::O200kBase => "o200k_base",
Self::P50kBase => "p50k_base",
}
}
}
impl FromStr for Encoding {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"cl100k_base" | "cl100k" => Ok(Encoding::Cl100kBase),
"o200k_base" | "o200k" => Ok(Encoding::O200kBase),
"p50k_base" | "p50k" => Ok(Encoding::P50kBase),
_ => Err(()),
}
}
}
pub fn get_bpe(encoding: Encoding) -> Result<CoreBPE, String> {
match encoding {
Encoding::Cl100kBase => cl100k_base().map_err(|e| e.to_string()),
Encoding::O200kBase => o200k_base().map_err(|e| e.to_string()),
Encoding::P50kBase => p50k_base().map_err(|e| e.to_string()),
}
}
#[allow(dead_code)] pub fn count_tokens(text: &str) -> Result<usize, String> {
count_tokens_with_encoding(text, Encoding::default())
}
pub fn count_tokens_with_encoding(text: &str, encoding: Encoding) -> Result<usize, String> {
let bpe = get_bpe(encoding)?;
Ok(bpe.encode_with_special_tokens(text).len())
}
pub fn count_tokens_detailed(text: &str, encoding: Encoding) -> Result<TokenCount, String> {
let bpe = get_bpe(encoding)?;
let count = bpe.encode_with_special_tokens(text).len();
Ok(TokenCount::with_char_count(
count,
encoding.as_str(),
text.len(),
))
}
pub fn count_file_tokens(path: &Path, encoding: Encoding) -> Result<TokenCount, String> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?;
count_tokens_detailed(&content, encoding)
}
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct FileTokens {
pub path: String,
pub tokens: usize,
pub size_bytes: usize,
}
pub trait HasTokenCount {
fn token_count(&self) -> usize;
}
impl HasTokenCount for FileTokens {
fn token_count(&self) -> usize {
self.tokens
}
}
pub fn select_by_token_budget<T: HasTokenCount>(
items: Vec<T>,
max_tokens: usize,
) -> (Vec<T>, usize, usize) {
let mut selected = Vec::new();
let mut total_tokens = 0;
let mut omitted = 0;
for item in items {
if total_tokens + item.token_count() <= max_tokens {
total_tokens += item.token_count();
selected.push(item);
} else {
omitted += 1;
}
}
(selected, total_tokens, omitted)
}
#[allow(dead_code)] pub fn select_files_by_tokens(
files: &[FileTokens],
max_tokens: usize,
) -> (Vec<FileTokens>, usize, usize) {
let mut selected = Vec::new();
let mut total_tokens = 0;
let mut omitted = 0;
for file in files {
if total_tokens + file.tokens <= max_tokens {
total_tokens += file.tokens;
selected.push(file.clone());
} else {
omitted += 1;
}
}
(selected, total_tokens, omitted)
}
#[allow(dead_code)] pub fn estimate_tokens(text: &str) -> usize {
text.len().div_ceil(4)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_count_tokens_basic() {
let text = "Hello, world!";
let count = count_tokens(text).unwrap();
assert!(count > 0);
assert!(count < text.len()); }
#[test]
fn test_count_tokens_encoding() {
let text = "fn main() { println!(\"Hello\"); }";
let cl100k = count_tokens_with_encoding(text, Encoding::Cl100kBase).unwrap();
let o200k = count_tokens_with_encoding(text, Encoding::O200kBase).unwrap();
assert!(cl100k > 0 && cl100k < 50);
assert!(o200k > 0 && o200k < 50);
}
#[test]
fn test_count_tokens_detailed() {
let text = "Hello, world!";
let result = count_tokens_detailed(text, Encoding::Cl100kBase).unwrap();
assert!(result.count > 0);
assert_eq!(result.encoding, "cl100k_base");
assert_eq!(result.char_count, Some(text.len()));
}
#[test]
fn test_select_files_by_tokens() {
let files = vec![
FileTokens {
path: "a.rs".to_string(),
tokens: 100,
size_bytes: 400,
},
FileTokens {
path: "b.rs".to_string(),
tokens: 200,
size_bytes: 800,
},
FileTokens {
path: "c.rs".to_string(),
tokens: 150,
size_bytes: 600,
},
];
let (selected, total, omitted) = select_files_by_tokens(&files, 500);
assert_eq!(selected.len(), 3);
assert_eq!(total, 450);
assert_eq!(omitted, 0);
let (selected, total, omitted) = select_files_by_tokens(&files, 300);
assert_eq!(selected.len(), 2);
assert_eq!(total, 300);
assert_eq!(omitted, 1);
let (selected, total, omitted) = select_files_by_tokens(&files, 150);
assert_eq!(selected.len(), 1);
assert_eq!(total, 100);
assert_eq!(omitted, 2);
}
#[test]
fn test_encoding_from_str() {
use std::str::FromStr;
assert_eq!(
Encoding::from_str("cl100k_base").ok(),
Some(Encoding::Cl100kBase)
);
assert_eq!(
Encoding::from_str("o200k_base").ok(),
Some(Encoding::O200kBase)
);
assert_eq!(
Encoding::from_str("p50k_base").ok(),
Some(Encoding::P50kBase)
);
assert_eq!(Encoding::from_str("unknown").ok(), None);
}
#[test]
fn test_estimate_tokens() {
let text = "Hello, world!"; let estimate = estimate_tokens(text);
assert!(estimate >= 3 && estimate <= 5);
}
}