mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
* feat(rust): count tiktoken cl100k_base admission tokens in Rust The Rust admission token counter only had the Anthropic tokenizer, so every other model (OpenAI gpt-4 family, Azure, Gemini, Bedrock non-Claude, Mistral) tokenized with tiktoken on the Python inference worker. Add an exact cl100k_base counter to litellm-token-counter: the vendored rank file (base64 token / rank lines, the bytes Python's tiktoken uses) is parsed into a byte-level BPE model and the cl100k split pattern is a handwritten scanner over the shared Unicode classes, so no regex engine runs per request. Both tokenizers share the message, tool and reply-priming accounting. The PyO3 TokenCounter gains a from_cl100k_ranks constructor; Python reads the rank file and passes it in, the way claude_json_str already works. The bridge selects the counter through the same predicates litellm.token_counter uses (huggingface_tokenizer_kind, openai_tokenizer_encoding), declines o200k_base, downloaded HuggingFace and custom tokenizers to Python, and budget reservation counts once per distinct tokenizer a request names. The legacy gpt-3.5-turbo-0301 message accounting (4 per message, -1 per name) stays in Python: the selector declines it through the predicate token_counter itself uses. * feat(rust): count tiktoken o200k_base admission tokens in Rust (#40794) Add a handwritten o200k_base split scanner and TokenCounter::from_o200k_ranks next to the cl100k_base counter, sharing MergeRanks and the request accounting. The Python bridge selects it when openai_tokenizer_encoding names o200k_base, so gpt-4o, gpt-4.1, gpt-5, o1/o3/o4 and chatgpt-4o requests stop tokenizing on the Python worker under LITELLM_RUST=true Co-authored-by: yassin <yassin@berri.ai> --------- Co-authored-by: yassin <yassin@berri.ai> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
164 lines
4.7 KiB
Rust
164 lines
4.7 KiB
Rust
use std::cmp::Ordering;
|
|
use std::sync::LazyLock;
|
|
|
|
use tokenizers::utils::SysRegex;
|
|
|
|
struct Ranges(Box<[(u32, u32)]>);
|
|
|
|
pub(super) struct UnicodeClasses {
|
|
letters: Ranges,
|
|
numbers: Ranges,
|
|
spaces: Ranges,
|
|
uppers: Ranges,
|
|
lowers: Ranges,
|
|
}
|
|
|
|
static CLASSES: LazyLock<Option<UnicodeClasses>> = LazyLock::new(|| {
|
|
let scalars: String = (0..=u32::from(char::MAX))
|
|
.filter_map(char::from_u32)
|
|
.collect();
|
|
Some(UnicodeClasses {
|
|
letters: Ranges::load(r"\p{L}+", &scalars)?,
|
|
numbers: Ranges::load(r"\p{N}+", &scalars)?,
|
|
spaces: Ranges::load(r"\s+", &scalars)?,
|
|
uppers: Ranges::load(r"[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]+", &scalars)?,
|
|
lowers: Ranges::load(r"[\p{Ll}\p{Lm}\p{Lo}\p{M}]+", &scalars)?,
|
|
})
|
|
});
|
|
|
|
impl Ranges {
|
|
fn load(pattern: &str, scalars: &str) -> Option<Self> {
|
|
let regex = SysRegex::new(pattern).ok()?;
|
|
let ranges = regex
|
|
.find_iter(scalars)
|
|
.map(|(start, end)| {
|
|
let matched = scalars.get(start..end)?;
|
|
Some((
|
|
u32::from(matched.chars().next()?),
|
|
u32::from(matched.chars().next_back()?),
|
|
))
|
|
})
|
|
.collect::<Option<Box<[_]>>>()?;
|
|
Some(Self(ranges))
|
|
}
|
|
|
|
fn contains(&self, character: char) -> bool {
|
|
let code = u32::from(character);
|
|
self.0
|
|
.binary_search_by(|(low, high)| {
|
|
if *high < code {
|
|
Ordering::Less
|
|
} else if *low > code {
|
|
Ordering::Greater
|
|
} else {
|
|
Ordering::Equal
|
|
}
|
|
})
|
|
.is_ok()
|
|
}
|
|
}
|
|
|
|
impl UnicodeClasses {
|
|
pub(super) fn get() -> Option<&'static Self> {
|
|
CLASSES.as_ref()
|
|
}
|
|
|
|
fn is_letter(&self, character: char) -> bool {
|
|
self.letters.contains(character)
|
|
}
|
|
|
|
fn is_number(&self, character: char) -> bool {
|
|
self.numbers.contains(character)
|
|
}
|
|
|
|
fn is_space(&self, character: char) -> bool {
|
|
self.spaces.contains(character)
|
|
}
|
|
|
|
fn is_upper(&self, character: char) -> bool {
|
|
self.uppers.contains(character)
|
|
}
|
|
|
|
fn is_lower(&self, character: char) -> bool {
|
|
self.lowers.contains(character)
|
|
}
|
|
}
|
|
|
|
/// `\p{L}`, `\p{N}`, `\s` and everything else, the character classes the
|
|
/// split regexes are written in.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(super) enum Class {
|
|
Letter,
|
|
Number,
|
|
Space,
|
|
Other,
|
|
}
|
|
|
|
pub(super) fn class(character: char, unicode_classes: &UnicodeClasses) -> Class {
|
|
match character {
|
|
'A'..='Z' | 'a'..='z' => Class::Letter,
|
|
'0'..='9' => Class::Number,
|
|
'\t'..='\r' | ' ' => Class::Space,
|
|
_ if character.is_ascii() => Class::Other,
|
|
_ if unicode_classes.is_letter(character) => Class::Letter,
|
|
_ if unicode_classes.is_number(character) => Class::Number,
|
|
_ if unicode_classes.is_space(character) => Class::Space,
|
|
_ => Class::Other,
|
|
}
|
|
}
|
|
|
|
/// Byte length of the leading run of `run_class` characters.
|
|
pub(super) fn run_len(text: &str, run_class: Class, unicode_classes: &UnicodeClasses) -> usize {
|
|
text.char_indices()
|
|
.find(|(_, character)| class(*character, unicode_classes) != run_class)
|
|
.map_or(text.len(), |(index, _)| index)
|
|
}
|
|
|
|
/// Membership in the two letter classes of the o200k split regex,
|
|
/// `[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]` and `[\p{Ll}\p{Lm}\p{Lo}\p{M}]`; `Lm`,
|
|
/// `Lo` and `M` are in both.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(super) enum Case {
|
|
Upper,
|
|
Lower,
|
|
Both,
|
|
Neither,
|
|
}
|
|
|
|
impl Case {
|
|
pub(super) fn is_upper(self) -> bool {
|
|
matches!(self, Case::Upper | Case::Both)
|
|
}
|
|
|
|
pub(super) fn is_lower(self) -> bool {
|
|
matches!(self, Case::Lower | Case::Both)
|
|
}
|
|
}
|
|
|
|
pub(super) fn case(character: char, unicode_classes: &UnicodeClasses) -> Case {
|
|
match character {
|
|
'A'..='Z' => Case::Upper,
|
|
'a'..='z' => Case::Lower,
|
|
_ if character.is_ascii() => Case::Neither,
|
|
_ => match (
|
|
unicode_classes.is_upper(character),
|
|
unicode_classes.is_lower(character),
|
|
) {
|
|
(true, true) => Case::Both,
|
|
(true, false) => Case::Upper,
|
|
(false, true) => Case::Lower,
|
|
(false, false) => Case::Neither,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Byte length of the leading run of characters whose case passes `in_class`.
|
|
pub(super) fn case_run_len(
|
|
text: &str,
|
|
in_class: fn(Case) -> bool,
|
|
unicode_classes: &UnicodeClasses,
|
|
) -> usize {
|
|
text.char_indices()
|
|
.find(|(_, character)| !in_class(case(*character, unicode_classes)))
|
|
.map_or(text.len(), |(index, _)| index)
|
|
}
|