use std::hash::{DefaultHasher, Hash, Hasher};
use std::io;
use std::sync::Arc;
use rig_core::completion::{Message, ToolDefinition, Usage};
use rig_core::conversation::{Conversation, MessageId};
use rig_core::wasm_compat::{WasmCompatSend, WasmCompatSync};
use serde::{Deserialize, Serialize};
pub trait TokenCounter: WasmCompatSend + WasmCompatSync {
fn count(&self, message: &Message) -> usize;
}
impl<F> TokenCounter for F
where
F: Fn(&Message) -> usize + WasmCompatSend + WasmCompatSync,
{
fn count(&self, message: &Message) -> usize {
(self)(message)
}
}
impl<C: TokenCounter + ?Sized> TokenCounter for Arc<C> {
fn count(&self, message: &Message) -> usize {
(**self).count(message)
}
}
impl TokenCounter for Box<dyn TokenCounter> {
fn count(&self, message: &Message) -> usize {
(**self).count(message)
}
}
impl TokenCounter for &dyn TokenCounter {
fn count(&self, message: &Message) -> usize {
(**self).count(message)
}
}
#[derive(Debug, Clone, Copy)]
pub struct HeuristicTokenCounter {
bytes_per_token: f32,
per_message_overhead: usize,
per_attachment_tokens: usize,
}
impl HeuristicTokenCounter {
pub fn new(
bytes_per_token: f32,
per_message_overhead: usize,
per_attachment_tokens: usize,
) -> Self {
let bytes_per_token = if bytes_per_token.is_finite() && bytes_per_token >= 1.0 {
bytes_per_token
} else {
1.0
};
Self {
bytes_per_token,
per_message_overhead,
per_attachment_tokens,
}
}
pub fn anthropic() -> Self {
Self::new(3.5, 4, ATTACHMENT_TOKENS)
}
fn bytes_to_tokens(&self, bytes: usize) -> usize {
let tokens = (bytes as f32) / self.bytes_per_token;
tokens.ceil() as usize
}
pub fn count_tool(&self, tool: &ToolDefinition) -> usize {
let bytes = tool.name.len().saturating_add(tool.description.len());
self.bytes_to_tokens(bytes.saturating_add(json_len(&tool.parameters)))
.saturating_add(self.per_message_overhead)
}
pub(crate) fn count_user(&self, content: &rig_core::message::UserContent) -> usize {
use rig_core::message::UserContent;
match content {
UserContent::Text(text) => self.bytes_to_tokens(text.text.len()),
UserContent::ToolResult(result) => result
.content
.iter()
.map(|c| match c {
rig_core::message::ToolResultContent::Text(t) => {
self.bytes_to_tokens(t.text.len())
}
rig_core::message::ToolResultContent::Json { value } => {
self.bytes_to_tokens(json_len(value))
}
rig_core::message::ToolResultContent::Image(_) => self.per_attachment_tokens,
})
.fold(0, usize::saturating_add),
UserContent::Image(_)
| UserContent::Audio(_)
| UserContent::Video(_)
| UserContent::Document(_) => self.per_attachment_tokens,
}
}
fn count_assistant(&self, content: &rig_core::message::AssistantContent) -> usize {
use rig_core::message::AssistantContent;
match content {
AssistantContent::Text(text) => self.bytes_to_tokens(text.text.len()),
AssistantContent::Reasoning(reasoning) => self.bytes_to_tokens(reasoning.text.len()),
AssistantContent::ToolCall(call) => {
let name_bytes = call.function.name.len();
let args_bytes = json_len(&call.function.arguments);
self.bytes_to_tokens(name_bytes + args_bytes)
}
AssistantContent::Image(_) => self.per_attachment_tokens,
AssistantContent::Opaque(opaque) => self.bytes_to_tokens(json_len(&opaque.item)),
other => self.bytes_to_tokens(json_len(other)),
}
}
}
fn json_len(value: &impl serde::Serialize) -> usize {
struct Len(usize);
impl io::Write for Len {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
self.0 += bytes.len();
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
let mut len = Len(0);
serde_json::to_writer(&mut len, value).map_or(0, |()| len.0)
}
pub const ATTACHMENT_TOKENS: usize = 1_200;
impl Default for HeuristicTokenCounter {
fn default() -> Self {
Self::new(4.0, 4, ATTACHMENT_TOKENS)
}
}
impl TokenCounter for HeuristicTokenCounter {
fn count(&self, message: &Message) -> usize {
let content_tokens: usize = match message {
Message::User { content } => content
.iter()
.map(|c| self.count_user(c))
.fold(0, usize::saturating_add),
Message::Assistant(turn) => turn
.content
.iter()
.map(|c| self.count_assistant(c))
.fold(0, usize::saturating_add),
Message::System { content } => self.bytes_to_tokens(content.len()),
};
content_tokens.saturating_add(self.per_message_overhead)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Anchor {
pub usage: Usage,
pub through: MessageId,
}
#[derive(Clone, Copy, Debug)]
pub struct Prepared<'a> {
pub model: &'a str,
pub prompt: Option<&'a str>,
pub tools: &'a [Arc<ToolDefinition>],
pub history: &'a Conversation,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RequestStart {
fingerprint: u64,
messages: usize,
}
impl Prepared<'_> {
pub fn start(&self) -> RequestStart {
let messages = self.history.messages().len();
RequestStart {
fingerprint: self.fingerprint(messages),
messages,
}
}
fn fingerprint(&self, messages: usize) -> u64 {
let mut hasher = DefaultHasher::new();
(self.model, self.prompt).hash(&mut hasher);
for tool in self.tools {
let mut bytes = HashWriter(&mut hasher);
let _ = serde_json::to_writer(&mut bytes, &**tool);
}
for entry in self.history.entries().take(messages) {
(entry.key(), std::ptr::from_ref(entry.message).addr()).hash(&mut hasher);
}
hasher.finish()
}
pub fn whole_tokens(&self) -> u64 {
let counter = HeuristicTokenCounter::default();
let prompt = self
.prompt
.map_or(0, |prompt| counter.count(&Message::system(prompt)));
let tools = self.tools.iter().map(|tool| counter.count_tool(tool));
let tools = tools.fold(0, usize::saturating_add);
let messages = estimate_messages(self.history.messages());
(prompt.saturating_add(tools) as u64).saturating_add(messages)
}
}
struct HashWriter<'h>(&'h mut DefaultHasher);
impl io::Write for HashWriter<'_> {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
self.0.write(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn estimate_messages(messages: &[Arc<Message>]) -> u64 {
let counter = HeuristicTokenCounter::default();
messages
.iter()
.map(|message| counter.count(message) as u64)
.fold(0, u64::saturating_add)
}
pub fn next_request_tokens(
anchor: Option<&Anchor>,
sent: Option<RequestStart>,
request: &Prepared<'_>,
) -> u64 {
let anchored = anchor.zip(sent).and_then(|(anchor, sent)| {
let at = request.history.index_of(anchor.through)?;
let follows = at == sent.messages || at.saturating_add(1) == sent.messages;
let same = follows && request.fingerprint(sent.messages) == sent.fingerprint;
let reported = anchor.usage.context_tokens().filter(|_| same)?;
let after = request.history.messages().get(at.saturating_add(1)..);
Some(reported.saturating_add(estimate_messages(after.unwrap_or_default())))
});
anchored.unwrap_or_else(|| request.whole_tokens())
}
pub const OUTPUT_MARGIN: u64 = 4_096;
pub fn output_room(window: Option<u64>, most: Option<u64>, estimate: u64) -> Option<u64> {
let room = window.map(|window| window.saturating_sub(estimate.saturating_add(OUTPUT_MARGIN)));
match (room, most) {
(Some(room), Some(most)) => Some(room.min(most)),
(room, most) => room.or(most),
}
}
#[cfg(test)]
mod tests;