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 {
75 let mut s = TokenSavings::default();
76 for m in metrics {
77 s.openai_before += m.tokens_before.openai_tokens;
78 s.openai_after += m.tokens_after.openai_tokens;
79 s.anthropic_before += m.tokens_before.anthropic_tokens;
80 s.anthropic_after += m.tokens_after.anthropic_tokens;
81 }
82 s
83 }
84}
85
86pub fn openai_tokens(width: u32, height: u32) -> u64 {
103 if width == 0 || height == 0 {
104 return 0;
105 }
106
107 let (w, h) = openai_scale_to_fit(width, height);
109 let tiles_w = (w as f64 / 512.0).ceil() as u64;
110 let tiles_h = (h as f64 / 512.0).ceil() as u64;
111 let tiles = tiles_w * tiles_h;
112
113 170 * tiles + 85
114}
115
116fn openai_scale_to_fit(width: u32, height: u32) -> (u32, u32) {
119 let mut w = width as f64;
120 let mut h = height as f64;
121
122 let max_dim = w.max(h);
124 if max_dim > 2048.0 {
125 let scale = 2048.0 / max_dim;
126 w *= scale;
127 h *= scale;
128 }
129
130 let min_side = w.min(h);
132 if min_side > 768.0 {
133 let scale = 768.0 / min_side;
134 w *= scale;
135 h *= scale;
136 }
137
138 (w.ceil() as u32, h.ceil() as u32)
139}
140
141pub fn openai_tokens_low() -> u64 {
143 85
144}
145
146pub fn anthropic_tokens(width: u32, height: u32) -> u64 {
162 if width == 0 || height == 0 {
163 return 0;
164 }
165
166 let (w, h) = anthropic_scale_to_fit(width, height);
167
168 let pw = next_multiple_of_28(w);
170 let ph = next_multiple_of_28(h);
171
172 let tokens = (pw as u64 * ph as u64) / 750;
173 tokens.min(1568)
175}
176
177fn anthropic_scale_to_fit(width: u32, height: u32) -> (u32, u32) {
179 let max_edge = 1568.0_f64;
180 let w = width as f64;
181 let h = height as f64;
182 let long_edge = w.max(h);
183
184 if long_edge <= max_edge {
185 return (width, height);
186 }
187
188 let scale = max_edge / long_edge;
189 ((w * scale).ceil() as u32, (h * scale).ceil() as u32)
190}
191
192fn next_multiple_of_28(val: u32) -> u32 {
193 val.div_ceil(28) * 28
194}
195
196pub fn estimate_tokens(width: u32, height: u32) -> TokenEstimate {
198 TokenEstimate {
199 openai_tokens: openai_tokens(width, height),
200 anthropic_tokens: anthropic_tokens(width, height),
201 }
202}
203
204#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
213 fn test_openai_small_image() {
214 assert_eq!(openai_tokens(512, 512), 255);
216 }
217
218 #[test]
219 fn test_openai_768_image() {
220 assert_eq!(openai_tokens(768, 768), 765);
223 }
224
225 #[test]
226 fn test_openai_large_landscape() {
227 assert_eq!(openai_tokens(4000, 3000), 765);
235 }
236
237 #[test]
238 fn test_openai_tall_portrait() {
239 assert_eq!(openai_tokens(1000, 4000), 765);
246 }
247
248 #[test]
249 fn test_openai_zero() {
250 assert_eq!(openai_tokens(0, 0), 0);
251 }
252
253 #[test]
254 fn test_openai_low_detail() {
255 assert_eq!(openai_tokens_low(), 85);
256 }
257
258 #[test]
259 fn test_openai_very_small() {
260 assert_eq!(openai_tokens(100, 100), 255);
262 }
263
264 #[test]
267 fn test_anthropic_small_image() {
268 assert_eq!(anthropic_tokens(200, 200), 66);
271 }
272
273 #[test]
274 fn test_anthropic_1000x1000() {
275 assert_eq!(anthropic_tokens(1000, 1000), 1354);
279 }
280
281 #[test]
282 fn test_anthropic_large_downscaled() {
283 assert_eq!(anthropic_tokens(3000, 2000), 1568);
288 }
289
290 #[test]
291 fn test_anthropic_zero() {
292 assert_eq!(anthropic_tokens(0, 0), 0);
293 }
294
295 #[test]
296 fn test_anthropic_exact_max() {
297 assert_eq!(anthropic_tokens(1568, 1568), 1568);
301 }
302
303 #[test]
306 fn test_savings_calculation() {
307 let s = TokenSavings {
308 openai_before: 1000,
309 openai_after: 300,
310 anthropic_before: 2000,
311 anthropic_after: 500,
312 };
313 assert_eq!(s.openai_saved(), 700);
314 assert_eq!(s.anthropic_saved(), 1500);
315 assert!((s.openai_pct() - 70.0).abs() < 0.1);
316 assert!((s.anthropic_pct() - 75.0).abs() < 0.1);
317 }
318
319 #[test]
320 fn test_savings_zero_before() {
321 let s = TokenSavings::default();
322 assert_eq!(s.openai_pct(), 0.0);
323 assert_eq!(s.anthropic_pct(), 0.0);
324 }
325
326 #[test]
329 fn test_estimate_tokens_both() {
330 let est = estimate_tokens(1000, 1000);
331 assert!(est.openai_tokens > 0);
332 assert!(est.anthropic_tokens > 0);
333 }
334
335 #[test]
338 fn test_next_multiple_of_28() {
339 assert_eq!(next_multiple_of_28(28), 28);
340 assert_eq!(next_multiple_of_28(29), 56);
341 assert_eq!(next_multiple_of_28(1), 28);
342 assert_eq!(next_multiple_of_28(200), 224);
343 assert_eq!(next_multiple_of_28(1568), 1568);
344 }
345}