1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct TokenEstimate {
12 pub openai_tokens: u64,
13 pub anthropic_tokens: u64,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ImageMetrics {
19 pub image_index: usize,
21 pub original_width: u32,
23 pub original_height: u32,
24 pub transformed_width: u32,
26 pub transformed_height: u32,
27 pub original_bytes: usize,
29 pub transformed_bytes: usize,
31 pub format_before: String,
33 pub format_after: String,
35 pub tokens_before: TokenEstimate,
37 pub tokens_after: TokenEstimate,
39}
40
41#[derive(Debug, Clone, Default, Serialize, Deserialize)]
43pub struct TokenSavings {
44 pub openai_before: u64,
45 pub openai_after: u64,
46 pub anthropic_before: u64,
47 pub anthropic_after: u64,
48}
49
50impl TokenSavings {
51 pub fn openai_saved(&self) -> u64 {
52 self.openai_before.saturating_sub(self.openai_after)
53 }
54
55 pub fn anthropic_saved(&self) -> u64 {
56 self.anthropic_before.saturating_sub(self.anthropic_after)
57 }
58
59 pub fn openai_pct(&self) -> f64 {
60 if self.openai_before == 0 {
61 return 0.0;
62 }
63 (self.openai_saved() as f64 / self.openai_before as f64) * 100.0
64 }
65
66 pub fn anthropic_pct(&self) -> f64 {
67 if self.anthropic_before == 0 {
68 return 0.0;
69 }
70 (self.anthropic_saved() as f64 / self.anthropic_before as f64) * 100.0
71 }
72
73 pub fn from_metrics(metrics: &[ImageMetrics]) -> Self {
79 let mut s = TokenSavings::default();
80 for m in metrics {
81 let was_dropped = (m.original_width > 0 || m.original_height > 0)
83 && m.transformed_width == 0
84 && m.transformed_height == 0;
85 if was_dropped {
86 continue;
87 }
88 s.openai_before += m.tokens_before.openai_tokens;
89 s.openai_after += m.tokens_after.openai_tokens;
90 s.anthropic_before += m.tokens_before.anthropic_tokens;
91 s.anthropic_after += m.tokens_after.anthropic_tokens;
92 }
93 s
94 }
95
96 pub fn from_metrics_all(metrics: &[ImageMetrics]) -> Self {
98 let mut s = TokenSavings::default();
99 for m in metrics {
100 s.openai_before += m.tokens_before.openai_tokens;
101 s.openai_after += m.tokens_after.openai_tokens;
102 s.anthropic_before += m.tokens_before.anthropic_tokens;
103 s.anthropic_after += m.tokens_after.anthropic_tokens;
104 }
105 s
106 }
107}
108
109pub fn openai_tokens(width: u32, height: u32) -> u64 {
133 if width == 0 || height == 0 {
134 return 0;
135 }
136
137 let (w, h) = openai_scale_to_fit(width, height);
139 let tiles_w = (w as f64 / 512.0).ceil() as u64;
140 let tiles_h = (h as f64 / 512.0).ceil() as u64;
141 let tiles = tiles_w * tiles_h;
142
143 170 * tiles + 85
144}
145
146fn openai_scale_to_fit(width: u32, height: u32) -> (u32, u32) {
149 let mut w = width as f64;
150 let mut h = height as f64;
151
152 let max_dim = w.max(h);
154 if max_dim > 2048.0 {
155 let scale = 2048.0 / max_dim;
156 w *= scale;
157 h *= scale;
158 }
159
160 let min_side = w.min(h);
162 if min_side > 768.0 {
163 let scale = 768.0 / min_side;
164 w *= scale;
165 h *= scale;
166 }
167
168 (w.ceil() as u32, h.ceil() as u32)
169}
170
171pub fn openai_tokens_low() -> u64 {
173 85
174}
175
176pub fn anthropic_tokens(width: u32, height: u32) -> u64 {
192 if width == 0 || height == 0 {
193 return 0;
194 }
195
196 let (w, h) = anthropic_scale_to_fit(width, height);
197
198 let pw = next_multiple_of_28(w);
200 let ph = next_multiple_of_28(h);
201
202 let tokens = (pw as u64 * ph as u64) / 750;
203 tokens.min(1568)
205}
206
207fn anthropic_scale_to_fit(width: u32, height: u32) -> (u32, u32) {
209 let max_edge = 1568.0_f64;
210 let w = width as f64;
211 let h = height as f64;
212 let long_edge = w.max(h);
213
214 if long_edge <= max_edge {
215 return (width, height);
216 }
217
218 let scale = max_edge / long_edge;
219 ((w * scale).ceil() as u32, (h * scale).ceil() as u32)
220}
221
222fn next_multiple_of_28(val: u32) -> u32 {
223 val.div_ceil(28) * 28
224}
225
226pub fn estimate_tokens(width: u32, height: u32) -> TokenEstimate {
228 TokenEstimate {
229 openai_tokens: openai_tokens(width, height),
230 anthropic_tokens: anthropic_tokens(width, height),
231 }
232}
233
234#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
243 fn test_openai_small_image() {
244 assert_eq!(openai_tokens(512, 512), 255);
246 }
247
248 #[test]
249 fn test_openai_768_image() {
250 assert_eq!(openai_tokens(768, 768), 765);
253 }
254
255 #[test]
256 fn test_openai_large_landscape() {
257 assert_eq!(openai_tokens(4000, 3000), 765);
265 }
266
267 #[test]
268 fn test_openai_tall_portrait() {
269 assert_eq!(openai_tokens(1000, 4000), 765);
276 }
277
278 #[test]
279 fn test_openai_zero() {
280 assert_eq!(openai_tokens(0, 0), 0);
281 }
282
283 #[test]
284 fn test_openai_low_detail() {
285 assert_eq!(openai_tokens_low(), 85);
286 }
287
288 #[test]
289 fn test_openai_very_small() {
290 assert_eq!(openai_tokens(100, 100), 255);
292 }
293
294 #[test]
297 fn test_anthropic_small_image() {
298 assert_eq!(anthropic_tokens(200, 200), 66);
301 }
302
303 #[test]
304 fn test_anthropic_1000x1000() {
305 assert_eq!(anthropic_tokens(1000, 1000), 1354);
309 }
310
311 #[test]
312 fn test_anthropic_large_downscaled() {
313 assert_eq!(anthropic_tokens(3000, 2000), 1568);
318 }
319
320 #[test]
321 fn test_anthropic_zero() {
322 assert_eq!(anthropic_tokens(0, 0), 0);
323 }
324
325 #[test]
326 fn test_anthropic_exact_max() {
327 assert_eq!(anthropic_tokens(1568, 1568), 1568);
331 }
332
333 #[test]
336 fn test_savings_calculation() {
337 let s = TokenSavings {
338 openai_before: 1000,
339 openai_after: 300,
340 anthropic_before: 2000,
341 anthropic_after: 500,
342 };
343 assert_eq!(s.openai_saved(), 700);
344 assert_eq!(s.anthropic_saved(), 1500);
345 assert!((s.openai_pct() - 70.0).abs() < 0.1);
346 assert!((s.anthropic_pct() - 75.0).abs() < 0.1);
347 }
348
349 #[test]
350 fn test_savings_zero_before() {
351 let s = TokenSavings::default();
352 assert_eq!(s.openai_pct(), 0.0);
353 assert_eq!(s.anthropic_pct(), 0.0);
354 }
355
356 #[test]
359 fn test_estimate_tokens_both() {
360 let est = estimate_tokens(1000, 1000);
361 assert!(est.openai_tokens > 0);
362 assert!(est.anthropic_tokens > 0);
363 }
364
365 #[test]
368 fn test_estimate_tokens_varies_by_size() {
369 let small = estimate_tokens(100, 100);
370 let large = estimate_tokens(4000, 3000);
371 assert_ne!(
374 small.openai_tokens, large.openai_tokens,
375 "different dimensions should produce different OpenAI estimates"
376 );
377 }
378
379 #[test]
382 fn test_openai_extreme_tall() {
383 assert_eq!(openai_tokens(1, 10000), 765);
388 }
389
390 #[test]
391 fn test_openai_extreme_wide() {
392 assert_eq!(openai_tokens(10000, 1), 765);
394 }
395
396 #[test]
397 fn test_anthropic_extreme_tall() {
398 let tokens = anthropic_tokens(1, 10000);
403 assert!(tokens > 0 && tokens < 100, "got {}", tokens);
404 }
405
406 #[test]
407 fn test_openai_1x1() {
408 assert_eq!(openai_tokens(1, 1), 255);
410 }
411
412 #[test]
413 fn test_anthropic_1x1() {
414 assert_eq!(anthropic_tokens(1, 1), 1);
416 }
417
418 #[test]
421 fn test_next_multiple_of_28() {
422 assert_eq!(next_multiple_of_28(28), 28);
423 assert_eq!(next_multiple_of_28(29), 56);
424 assert_eq!(next_multiple_of_28(1), 28);
425 assert_eq!(next_multiple_of_28(200), 224);
426 assert_eq!(next_multiple_of_28(1568), 1568);
427 }
428
429 #[test]
432 fn test_savings_excludes_dropped() {
433 use crate::cost::ImageMetrics;
434
435 let metrics = vec![
436 ImageMetrics {
438 image_index: 0,
439 original_width: 4000,
440 original_height: 3000,
441 transformed_width: 2048,
442 transformed_height: 1536,
443 original_bytes: 5_000_000,
444 transformed_bytes: 500_000,
445 format_before: "png".to_string(),
446 format_after: "png".to_string(),
447 tokens_before: estimate_tokens(4000, 3000),
448 tokens_after: estimate_tokens(2048, 1536),
449 },
450 ImageMetrics {
452 image_index: 1,
453 original_width: 1000,
454 original_height: 1000,
455 transformed_width: 0,
456 transformed_height: 0,
457 original_bytes: 100_000,
458 transformed_bytes: 0,
459 format_before: "png".to_string(),
460 format_after: "png".to_string(),
461 tokens_before: estimate_tokens(1000, 1000),
462 tokens_after: estimate_tokens(0, 0),
463 },
464 ];
465
466 let savings = TokenSavings::from_metrics(&metrics);
467 let savings_all = TokenSavings::from_metrics_all(&metrics);
468
469 assert_eq!(
471 savings.openai_before,
472 estimate_tokens(4000, 3000).openai_tokens
473 );
474 assert_eq!(
475 savings.openai_after,
476 estimate_tokens(2048, 1536).openai_tokens
477 );
478
479 assert_eq!(
481 savings_all.openai_before,
482 estimate_tokens(4000, 3000).openai_tokens + estimate_tokens(1000, 1000).openai_tokens
483 );
484 }
485}