use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TokenAttribution {
pub prompt_tokens: u32,
pub tool_tokens: u32,
pub memory_tokens: u32,
pub response_tokens: u32,
}
impl TokenAttribution {
pub fn total_tokens(&self) -> u32 {
self.prompt_tokens + self.tool_tokens + self.memory_tokens + self.response_tokens
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProviderUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct TokenUsage {
pub uncached_input_tokens: u32,
pub cached_read_tokens: u32,
pub cache_write_tokens: u32,
pub output_tokens: u32,
}
impl TokenUsage {
pub fn total_input(&self) -> u32 {
self.uncached_input_tokens + self.cached_read_tokens + self.cache_write_tokens
}
pub fn total_tokens(&self) -> u32 {
self.total_input() + self.output_tokens
}
pub fn as_provider_usage(&self) -> ProviderUsage {
ProviderUsage {
prompt_tokens: self.total_input(),
completion_tokens: self.output_tokens,
total_tokens: self.total_tokens(),
}
}
}
pub fn parse_usage(usage: &serde_json::Value) -> Option<TokenUsage> {
if let Some(prompt) = usage.get("prompt_tokens").and_then(|v| v.as_u64()) {
let completion = usage
.get("completion_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let cached = usage
.get("prompt_tokens_details")
.and_then(|d| d.get("cached_tokens"))
.and_then(|v| v.as_u64())
.unwrap_or(0)
.min(prompt);
return Some(TokenUsage {
uncached_input_tokens: (prompt - cached) as u32,
cached_read_tokens: cached as u32,
cache_write_tokens: 0,
output_tokens: completion as u32,
});
}
if let Some(input) = usage.get("input_tokens").and_then(|v| v.as_u64()) {
let output = usage
.get("output_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let cache_read = usage
.get("cache_read_input_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let cache_write = usage
.get("cache_creation_input_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0);
return Some(TokenUsage {
uncached_input_tokens: input as u32,
cached_read_tokens: cache_read as u32,
cache_write_tokens: cache_write as u32,
output_tokens: output as u32,
});
}
None
}
pub fn estimate_token_layers(body: &[u8]) -> (u32, u32) {
match serde_json::from_slice::<serde_json::Value>(body) {
Ok(json) => estimate_token_layers_value(&json),
Err(_) => (0, 0),
}
}
pub fn estimate_token_layers_value(json: &serde_json::Value) -> (u32, u32) {
let tool_chars = json
.get("tools")
.or_else(|| json.get("functions"))
.map(|v| v.to_string().len())
.unwrap_or(0);
let memory_chars = json
.get("messages")
.and_then(|m| m.as_array())
.map(|msgs| {
msgs.iter()
.filter(|m| m.get("role").and_then(|r| r.as_str()) == Some("system"))
.map(|m| m.get("content").map(|c| c.to_string().len()).unwrap_or(0))
.sum::<usize>()
})
.unwrap_or(0);
((tool_chars / 4) as u32, (memory_chars / 4) as u32)
}
pub fn calculate_attribution(
usage: &ProviderUsage,
estimated_tool_tokens: u32,
estimated_memory_tokens: u32,
) -> TokenAttribution {
let tool = estimated_tool_tokens.min(usage.prompt_tokens);
let memory = estimated_memory_tokens.min(usage.prompt_tokens.saturating_sub(tool));
let prompt = usage
.prompt_tokens
.saturating_sub(tool)
.saturating_sub(memory);
TokenAttribution {
prompt_tokens: prompt,
tool_tokens: tool,
memory_tokens: memory,
response_tokens: usage.completion_tokens,
}
}
struct ModelPricing {
input_per_1m: u64,
output_per_1m: u64,
cached_read_per_1m: u64,
cache_write_per_1m: u64,
}
impl ModelPricing {
fn openai(input_per_1m: u64, output_per_1m: u64) -> Self {
Self {
input_per_1m,
output_per_1m,
cached_read_per_1m: input_per_1m / 2,
cache_write_per_1m: input_per_1m,
}
}
fn anthropic(input_per_1m: u64, output_per_1m: u64) -> Self {
Self {
input_per_1m,
output_per_1m,
cached_read_per_1m: input_per_1m / 10,
cache_write_per_1m: input_per_1m * 5 / 4,
}
}
}
fn get_pricing(model: &str) -> ModelPricing {
match model {
m if m.contains("gpt-4o-mini") => ModelPricing::openai(150_000, 600_000),
m if m.contains("gpt-4o") => ModelPricing::openai(2_500_000, 10_000_000),
m if m.contains("gpt-4-turbo") => ModelPricing::openai(10_000_000, 30_000_000),
m if m.contains("gpt-4") => ModelPricing::openai(30_000_000, 60_000_000),
m if m.contains("gpt-3.5") => ModelPricing::openai(500_000, 1_500_000),
m if m.contains("claude-3-5-sonnet") => ModelPricing::anthropic(3_000_000, 15_000_000),
m if m.contains("claude-3-opus") => ModelPricing::anthropic(15_000_000, 75_000_000),
m if m.contains("claude-3-haiku") => ModelPricing::anthropic(250_000, 1_250_000),
m if m.contains("claude") => ModelPricing::anthropic(3_000_000, 15_000_000),
_ => ModelPricing::openai(2_500_000, 10_000_000), }
}
pub fn calculate_cost(model: &str, prompt_tokens: u32, completion_tokens: u32) -> u64 {
calculate_cost_detailed(
model,
&TokenUsage {
uncached_input_tokens: prompt_tokens,
output_tokens: completion_tokens,
..Default::default()
},
)
}
pub fn calculate_cost_detailed(model: &str, usage: &TokenUsage) -> u64 {
let p = get_pricing(model);
(usage.uncached_input_tokens as u64 * p.input_per_1m
+ usage.cached_read_tokens as u64 * p.cached_read_per_1m
+ usage.cache_write_tokens as u64 * p.cache_write_per_1m
+ usage.output_tokens as u64 * p.output_per_1m)
/ 1_000_000
}
pub fn calculate_cost_naive(model: &str, usage: &TokenUsage) -> u64 {
let p = get_pricing(model);
(usage.total_input() as u64 * p.input_per_1m + usage.output_tokens as u64 * p.output_per_1m)
/ 1_000_000
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_estimate_token_layers_with_tools_and_system() {
let body = serde_json::json!({
"model": "gpt-4o",
"messages": [
{"role": "system", "content": "You are a helpful assistant with access to company docs."},
{"role": "user", "content": "What is our refund policy?"}
],
"tools": [
{"type": "function", "function": {"name": "search_docs", "parameters": {"type": "object"}}}
]
});
let bytes = serde_json::to_vec(&body).unwrap();
let (tool, memory) = estimate_token_layers(&bytes);
assert!(tool > 0, "tool tokens should be > 0");
assert!(memory > 0, "memory tokens should be > 0");
}
#[test]
fn test_estimate_no_tools_no_memory() {
let body = serde_json::json!({
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Hello"}]
});
let bytes = serde_json::to_vec(&body).unwrap();
let (tool, memory) = estimate_token_layers(&bytes);
assert_eq!(tool, 0);
assert_eq!(memory, 0);
}
#[test]
fn test_calculate_attribution_with_layers() {
let usage = ProviderUsage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
let attr = calculate_attribution(&usage, 20, 30);
assert_eq!(attr.tool_tokens, 20);
assert_eq!(attr.memory_tokens, 30);
assert_eq!(attr.prompt_tokens, 50);
assert_eq!(attr.response_tokens, 50);
assert_eq!(attr.total_tokens(), 150);
}
#[test]
fn test_parse_usage_openai_with_cache() {
let usage = serde_json::json!({
"prompt_tokens": 1000,
"completion_tokens": 100,
"total_tokens": 1100,
"prompt_tokens_details": { "cached_tokens": 200 }
});
let u = parse_usage(&usage).unwrap();
assert_eq!(u.uncached_input_tokens, 800);
assert_eq!(u.cached_read_tokens, 200);
assert_eq!(u.cache_write_tokens, 0);
assert_eq!(u.output_tokens, 100);
assert_eq!(u.total_input(), 1000);
assert_eq!(u.total_tokens(), 1100);
}
#[test]
fn test_parse_usage_openai_no_cache() {
let usage = serde_json::json!({ "prompt_tokens": 500, "completion_tokens": 50, "total_tokens": 550 });
let u = parse_usage(&usage).unwrap();
assert_eq!(u.uncached_input_tokens, 500);
assert_eq!(u.cached_read_tokens, 0);
assert_eq!(u.output_tokens, 50);
}
#[test]
fn test_parse_usage_anthropic() {
let usage = serde_json::json!({
"input_tokens": 1000,
"output_tokens": 200,
"cache_read_input_tokens": 500,
"cache_creation_input_tokens": 400
});
let u = parse_usage(&usage).unwrap();
assert_eq!(u.uncached_input_tokens, 1000);
assert_eq!(u.cached_read_tokens, 500);
assert_eq!(u.cache_write_tokens, 400);
assert_eq!(u.output_tokens, 200);
assert_eq!(u.total_input(), 1900);
}
#[test]
fn test_parse_usage_empty() {
assert!(parse_usage(&serde_json::json!({})).is_none());
}
}