pub trait TokenEstimator {
fn estimate(&self, text: &str) -> u64;
fn bytes_per_token_hint(&self) -> u64 {
3
}
}
pub type DynTokenEstimator = Box<dyn TokenEstimator + Send + Sync>;
impl<T: TokenEstimator + ?Sized> TokenEstimator for Box<T> {
fn estimate(&self, text: &str) -> u64 {
(**self).estimate(text)
}
fn bytes_per_token_hint(&self) -> u64 {
(**self).bytes_per_token_hint()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct HeuristicEstimator;
const CJK_THIRTIETHS: u64 = 25;
const DIGIT_THIRTIETHS: u64 = 30;
const OTHER_THIRTIETHS: u64 = 9;
impl TokenEstimator for HeuristicEstimator {
fn estimate(&self, text: &str) -> u64 {
let words = text
.split_whitespace()
.map(word_cost)
.fold(0, u64::saturating_add);
let newlines =
u64::try_from(text.bytes().filter(|&b| b == b'\n').count()).unwrap_or(u64::MAX);
words.saturating_add(newlines.saturating_mul(NEWLINE_THIRTIETHS).div_ceil(30))
}
}
const NEWLINE_THIRTIETHS: u64 = 15;
fn word_cost(word: &str) -> u64 {
let thirtieths = word
.chars()
.map(|ch| {
if is_cjk(ch) {
CJK_THIRTIETHS
} else if ch.is_ascii_digit() {
DIGIT_THIRTIETHS
} else {
OTHER_THIRTIETHS
}
})
.fold(0, u64::saturating_add);
thirtieths.div_ceil(30)
}
fn is_cjk(ch: char) -> bool {
matches!(
u32::from(ch),
0x3040..=0x30FF | 0x3400..=0x9FFF | 0xAC00..=0xD7AF | 0xF900..=0xFAFF
)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ImageTokenEstimator;
const CLAUDE_PIXELS_PER_TOKEN: u64 = 750;
impl ImageTokenEstimator {
#[must_use]
pub fn estimate(mime: &str, bytes: &[u8], bytes_b64: &str) -> u64 {
match image_dimensions(mime, bytes) {
Some((width, height)) => {
let pixels = u64::from(width).saturating_mul(u64::from(height));
pixels.div_ceil(CLAUDE_PIXELS_PER_TOKEN)
}
None => HeuristicEstimator.estimate(bytes_b64),
}
}
}
fn image_dimensions(mime: &str, bytes: &[u8]) -> Option<(u32, u32)> {
match mime {
"image/png" => png_dimensions(bytes),
"image/jpeg" | "image/jpg" => jpeg_dimensions(bytes),
_ => None,
}
}
fn png_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
const SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
if bytes.get(0..8)? != SIGNATURE {
return None;
}
if bytes.get(12..16)? != b"IHDR" {
return None;
}
let width = u32::from_be_bytes(bytes.get(16..20)?.try_into().ok()?);
let height = u32::from_be_bytes(bytes.get(20..24)?.try_into().ok()?);
if width == 0 || height == 0 {
return None;
}
Some((width, height))
}
fn jpeg_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
const SOF0: u8 = 0xC0;
const SOF2: u8 = 0xC2;
if bytes.get(0..2)? != [0xFF, 0xD8] {
return None;
}
let mut pos = 2_usize;
while let Some(&marker_byte) = bytes.get(pos) {
if marker_byte != 0xFF {
return None;
}
let marker = *bytes.get(pos + 1)?;
if (0xD0..=0xD9).contains(&marker) {
pos += 2;
continue;
}
let seg_len = usize::from(u16::from_be_bytes(
bytes.get(pos + 2..pos + 4)?.try_into().ok()?,
));
if seg_len < 2 {
return None;
}
if marker == SOF0 || marker == SOF2 {
let payload = bytes.get(pos + 4..pos + 9)?;
let height = u16::from_be_bytes([payload[1], payload[2]]);
let width = u16::from_be_bytes([payload[3], payload[4]]);
if width == 0 || height == 0 {
return None;
}
return Some((u32::from(width), u32::from(height)));
}
pos += 2 + seg_len;
}
None
}
#[cfg(test)]
#[path = "estimator_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "image_estimator_tests.rs"]
mod image_estimator_tests;