use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TokenEstimate {
pub openai_tokens: u64,
pub anthropic_tokens: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageMetrics {
pub image_index: usize,
pub original_width: u32,
pub original_height: u32,
pub transformed_width: u32,
pub transformed_height: u32,
pub original_bytes: usize,
pub transformed_bytes: usize,
pub format_before: String,
pub format_after: String,
pub tokens_before: TokenEstimate,
pub tokens_after: TokenEstimate,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TokenSavings {
pub openai_before: u64,
pub openai_after: u64,
pub anthropic_before: u64,
pub anthropic_after: u64,
}
impl TokenSavings {
pub fn openai_saved(&self) -> u64 {
self.openai_before.saturating_sub(self.openai_after)
}
pub fn anthropic_saved(&self) -> u64 {
self.anthropic_before.saturating_sub(self.anthropic_after)
}
pub fn openai_pct(&self) -> f64 {
if self.openai_before == 0 {
return 0.0;
}
(self.openai_saved() as f64 / self.openai_before as f64) * 100.0
}
pub fn anthropic_pct(&self) -> f64 {
if self.anthropic_before == 0 {
return 0.0;
}
(self.anthropic_saved() as f64 / self.anthropic_before as f64) * 100.0
}
pub fn from_metrics(metrics: &[ImageMetrics]) -> Self {
let mut s = TokenSavings::default();
for m in metrics {
s.openai_before += m.tokens_before.openai_tokens;
s.openai_after += m.tokens_after.openai_tokens;
s.anthropic_before += m.tokens_before.anthropic_tokens;
s.anthropic_after += m.tokens_after.anthropic_tokens;
}
s
}
}
pub fn openai_tokens(width: u32, height: u32) -> u64 {
if width == 0 || height == 0 {
return 0;
}
let (w, h) = openai_scale_to_fit(width, height);
let tiles_w = (w as f64 / 512.0).ceil() as u64;
let tiles_h = (h as f64 / 512.0).ceil() as u64;
let tiles = tiles_w * tiles_h;
170 * tiles + 85
}
fn openai_scale_to_fit(width: u32, height: u32) -> (u32, u32) {
let mut w = width as f64;
let mut h = height as f64;
let max_dim = w.max(h);
if max_dim > 2048.0 {
let scale = 2048.0 / max_dim;
w *= scale;
h *= scale;
}
let min_side = w.min(h);
if min_side > 768.0 {
let scale = 768.0 / min_side;
w *= scale;
h *= scale;
}
(w.ceil() as u32, h.ceil() as u32)
}
pub fn openai_tokens_low() -> u64 {
85
}
pub fn anthropic_tokens(width: u32, height: u32) -> u64 {
if width == 0 || height == 0 {
return 0;
}
let (w, h) = anthropic_scale_to_fit(width, height);
let pw = next_multiple_of_28(w);
let ph = next_multiple_of_28(h);
let tokens = (pw as u64 * ph as u64) / 750;
tokens.min(1568)
}
fn anthropic_scale_to_fit(width: u32, height: u32) -> (u32, u32) {
let max_edge = 1568.0_f64;
let w = width as f64;
let h = height as f64;
let long_edge = w.max(h);
if long_edge <= max_edge {
return (width, height);
}
let scale = max_edge / long_edge;
((w * scale).ceil() as u32, (h * scale).ceil() as u32)
}
fn next_multiple_of_28(val: u32) -> u32 {
val.div_ceil(28) * 28
}
pub fn estimate_tokens(width: u32, height: u32) -> TokenEstimate {
TokenEstimate {
openai_tokens: openai_tokens(width, height),
anthropic_tokens: anthropic_tokens(width, height),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_openai_small_image() {
assert_eq!(openai_tokens(512, 512), 255);
}
#[test]
fn test_openai_768_image() {
assert_eq!(openai_tokens(768, 768), 765);
}
#[test]
fn test_openai_large_landscape() {
assert_eq!(openai_tokens(4000, 3000), 765);
}
#[test]
fn test_openai_tall_portrait() {
assert_eq!(openai_tokens(1000, 4000), 765);
}
#[test]
fn test_openai_zero() {
assert_eq!(openai_tokens(0, 0), 0);
}
#[test]
fn test_openai_low_detail() {
assert_eq!(openai_tokens_low(), 85);
}
#[test]
fn test_openai_very_small() {
assert_eq!(openai_tokens(100, 100), 255);
}
#[test]
fn test_anthropic_small_image() {
assert_eq!(anthropic_tokens(200, 200), 66);
}
#[test]
fn test_anthropic_1000x1000() {
assert_eq!(anthropic_tokens(1000, 1000), 1354);
}
#[test]
fn test_anthropic_large_downscaled() {
assert_eq!(anthropic_tokens(3000, 2000), 1568);
}
#[test]
fn test_anthropic_zero() {
assert_eq!(anthropic_tokens(0, 0), 0);
}
#[test]
fn test_anthropic_exact_max() {
assert_eq!(anthropic_tokens(1568, 1568), 1568);
}
#[test]
fn test_savings_calculation() {
let s = TokenSavings {
openai_before: 1000,
openai_after: 300,
anthropic_before: 2000,
anthropic_after: 500,
};
assert_eq!(s.openai_saved(), 700);
assert_eq!(s.anthropic_saved(), 1500);
assert!((s.openai_pct() - 70.0).abs() < 0.1);
assert!((s.anthropic_pct() - 75.0).abs() < 0.1);
}
#[test]
fn test_savings_zero_before() {
let s = TokenSavings::default();
assert_eq!(s.openai_pct(), 0.0);
assert_eq!(s.anthropic_pct(), 0.0);
}
#[test]
fn test_estimate_tokens_both() {
let est = estimate_tokens(1000, 1000);
assert!(est.openai_tokens > 0);
assert!(est.anthropic_tokens > 0);
}
#[test]
fn test_next_multiple_of_28() {
assert_eq!(next_multiple_of_28(28), 28);
assert_eq!(next_multiple_of_28(29), 56);
assert_eq!(next_multiple_of_28(1), 28);
assert_eq!(next_multiple_of_28(200), 224);
assert_eq!(next_multiple_of_28(1568), 1568);
}
}