use crate::types::base_types::{
CostStatus, Messages, ModelOutput, ModelUsageCosting, UsageCosting, UsageReport,
UserModelContent,
};
#[must_use]
pub fn calculate_cost(
pricing: &ModelUsageCosting,
usage: &UsageReport,
status: CostStatus,
) -> UsageCosting {
let input = (pricing.input / 1_000_000.0) * usage.input;
let output = (pricing.output / 1_000_000.0) * usage.output;
let cache_read = (pricing.cache_read / 1_000_000.0) * usage.cache_read;
let cache_write = (pricing.cache_write / 1_000_000.0) * usage.cache_write;
UsageCosting {
currency: String::from("USD"),
input,
output,
cache_read,
cache_write,
total_tokens: usage.total_tokens,
status,
}
}
#[derive(Debug, Clone, Default)]
pub struct CostAccumulator {
input: f64,
output: f64,
cache_read: f64,
cache_write: f64,
total_tokens: f64,
call_count: u64,
}
impl CostAccumulator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, costing: &UsageCosting) {
self.input += costing.input;
self.output += costing.output;
self.cache_read += costing.cache_read;
self.cache_write += costing.cache_write;
self.total_tokens += costing.total_tokens;
self.call_count += 1;
}
#[must_use]
pub fn result(&self) -> UsageCosting {
UsageCosting {
currency: String::from("USD"),
input: self.input,
output: self.output,
cache_read: self.cache_read,
cache_write: self.cache_write,
total_tokens: self.total_tokens,
status: if self.call_count > 0 {
CostStatus::Actual
} else {
CostStatus::Unknown
},
}
}
#[must_use]
pub fn call_count(&self) -> u64 {
self.call_count
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn estimate_tokens(messages: &[Messages]) -> UsageReport {
const CHARS_PER_TOKEN: f64 = 4.0;
const TOKENS_PER_IMAGE: f64 = 1000.0;
const BASE64_CHARS_PER_TOKEN: f64 = 20.0;
const MESSAGE_OVERHEAD: f64 = 4.0;
const EMBED_VALUES_PER_TOKEN: f64 = 100.0;
let mut input: f64 = 0.0;
let mut images: f64 = 0.0;
for msg in messages {
input += MESSAGE_OVERHEAD;
match msg {
Messages::User { content, .. } | Messages::ToolResult { content, .. } => {
match content {
UserModelContent::Text(tc) => {
input += tc.content.len() as f64 / CHARS_PER_TOKEN;
}
UserModelContent::Image(img) => {
images += TOKENS_PER_IMAGE;
input += img.b64.len() as f64 / BASE64_CHARS_PER_TOKEN;
}
}
}
Messages::Assistant { content, .. } => match content {
ModelOutput::Text(tc) => {
input += tc.content.len() as f64 / CHARS_PER_TOKEN;
}
ModelOutput::ThinkingContent { thinking, .. } => {
input += thinking.len() as f64 / CHARS_PER_TOKEN;
}
ModelOutput::ToolCall { arguments, .. } => {
if let Some(args) = arguments {
let json_str = serde_json::to_string(args).unwrap_or_default();
input += json_str.len() as f64 / CHARS_PER_TOKEN;
}
}
ModelOutput::Image(img) => {
images += TOKENS_PER_IMAGE;
input += img.b64.len() as f64 / BASE64_CHARS_PER_TOKEN;
}
ModelOutput::Embedding { values, .. } => {
input += values.len() as f64 / EMBED_VALUES_PER_TOKEN;
}
},
}
}
UsageReport {
input,
output: 0.0,
cache_read: 0.0,
cache_write: input,
total_tokens: input + images,
cost: UsageCosting {
currency: String::from("USD"),
input: 0.0,
output: 0.0,
cache_read: 0.0,
cache_write: 0.0,
total_tokens: input + images,
status: CostStatus::Estimated,
},
}
}
#[must_use]
pub fn message_cost(messages: &[Messages]) -> UsageCosting {
let mut input = 0.0;
let mut output = 0.0;
let mut cache_read = 0.0;
let mut cache_write = 0.0;
for msg in messages {
if let Messages::Assistant { usage, .. } = msg {
input += usage.cost.input;
output += usage.cost.output;
cache_read += usage.cost.cache_read;
cache_write += usage.cost.cache_write;
}
}
UsageCosting {
currency: String::from("USD"),
input,
output,
cache_read,
cache_write,
total_tokens: 0.0,
status: CostStatus::Actual,
}
}