use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::sync::RwLock;
use tiktoken_rs::{cl100k_base, CoreBPE};
use tokenizers::Tokenizer;
use tracing::{debug, warn};
const MAX_CACHE_ENTRIES: usize = 1_000;
static CONFIGURED_MODEL: RwLock<Option<String>> = RwLock::new(None);
pub fn set_configured_model(model: &str) {
if let Ok(mut slot) = CONFIGURED_MODEL.write() {
*slot = Some(model.to_string());
}
}
fn configured_model_name() -> Option<String> {
std::env::var("SELFWARE_MODEL")
.ok()
.or_else(|| CONFIGURED_MODEL.read().ok().and_then(|slot| slot.clone()))
}
static TOKENIZER: Lazy<TokenizerState> =
Lazy::new(|| TokenizerState::for_model(configured_model_name().as_deref()));
static TOKEN_CACHE: Lazy<RwLock<HashMap<u64, usize>>> =
Lazy::new(|| RwLock::new(HashMap::with_capacity(256)));
enum TokenizerState {
Hf(Box<Tokenizer>),
Tiktoken(CoreBPE),
Heuristic,
}
impl TokenizerState {
fn for_model(model: Option<&str>) -> Self {
if let Some(model) = model {
if let Some(repo) = hf_tokenizer_repo(model) {
match Tokenizer::from_pretrained(repo, None) {
Ok(tokenizer) => {
debug!("Loaded HF tokenizer '{}' for model '{}'", repo, model);
return TokenizerState::Hf(Box::new(tokenizer));
}
Err(e) => {
debug!(
"Could not load HF tokenizer '{}' for model '{}': {}, \
falling back to tiktoken cl100k",
repo, model, e
);
}
}
} else {
debug!(
"No HF tokenizer repo mapped for model '{}', using cl100k fallback",
model
);
}
}
match cl100k_base() {
Ok(bpe) => {
debug!("Using tiktoken cl100k_base tokenizer as fallback");
TokenizerState::Tiktoken(bpe)
}
Err(e) => {
warn!(
"Failed to initialize tiktoken cl100k_base: {}. \
Using heuristic token estimate.",
e
);
TokenizerState::Heuristic
}
}
}
fn count(&self, content: &str) -> usize {
match self {
TokenizerState::Hf(t) => t
.encode(content, false)
.map(|e| e.get_tokens().len())
.unwrap_or_else(|_| heuristic_estimate(content)),
TokenizerState::Tiktoken(bpe) => bpe.encode_with_special_tokens(content).len(),
TokenizerState::Heuristic => heuristic_estimate(content),
}
}
}
fn hf_tokenizer_repo(model: &str) -> Option<&'static str> {
let lower = model.to_ascii_lowercase();
if lower.contains("qwen") {
Some("Qwen/Qwen2.5-Coder-32B")
} else if lower.contains("gpt-4") || lower.contains("gpt4") {
Some("Xenova/gpt-4")
} else if lower.contains("gpt-3.5") || lower.contains("gpt-3") {
Some("Xenova/gpt-3.5-turbo")
} else if lower.contains("llama") {
Some("hf-internal-testing/llama-tokenizer")
} else if lower.contains("glm") {
Some("THUDM/glm-4-9b-chat")
} else if lower.contains("mistral") || lower.contains("mixtral") {
Some("mistralai/Mistral-7B-v0.1")
} else if lower.contains("deepseek") {
Some("deepseek-ai/deepseek-coder-7b-instruct-v1.5")
} else {
None
}
}
#[inline]
pub fn estimate_tokens_with_overhead(content: &str, message_overhead: usize) -> usize {
estimate_content_tokens(content) + message_overhead
}
#[inline]
pub fn estimate_content_tokens(content: &str) -> usize {
let key = hash_content(content);
if let Ok(cache) = TOKEN_CACHE.read() {
if let Some(&count) = cache.get(&key) {
return count;
}
}
let count = TOKENIZER.count(content);
if let Ok(mut cache) = TOKEN_CACHE.write() {
if cache.len() >= MAX_CACHE_ENTRIES {
cache.clear();
}
cache.insert(key, count);
}
count
}
fn hash_content(content: &str) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
content.hash(&mut hasher);
hasher.finish()
}
fn heuristic_estimate(content: &str) -> usize {
let factor = if content.contains('{') || content.contains(';') {
3
} else {
4
};
(content.len() / factor).max(1)
}
#[cfg(test)]
#[path = "../tests/unit/token_count/token_count_test.rs"]
mod tests;