1use shift_preflight::{DriveMode, ShiftConfig, SvgMode};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::{Arc, Mutex};
6use std::time::Instant;
7
8#[derive(Debug, Clone)]
10pub struct ProxyConfig {
11 pub port: u16,
12 pub mode: DriveMode,
13 pub verbose: bool,
14 pub providers: ProviderUrls,
16}
17
18#[derive(Debug, Clone)]
19pub struct ProviderUrls {
20 pub anthropic: String,
21 pub openai: String,
22 pub google: String,
23}
24
25impl Default for ProviderUrls {
26 fn default() -> Self {
27 Self {
28 anthropic: "https://api.anthropic.com".to_string(),
29 openai: "https://api.openai.com".to_string(),
30 google: "https://generativelanguage.googleapis.com".to_string(),
31 }
32 }
33}
34
35impl Default for ProxyConfig {
36 fn default() -> Self {
37 Self {
38 port: 8787,
39 mode: DriveMode::Balanced,
40 verbose: false,
41 providers: ProviderUrls::default(),
42 }
43 }
44}
45
46impl ProxyConfig {
47 pub fn shift_config(&self, provider: &str) -> ShiftConfig {
49 ShiftConfig {
50 mode: self.mode,
51 svg_mode: SvgMode::Raster,
52 provider: provider.to_string(),
53 model: None,
54 dry_run: false,
55 verbose: self.verbose,
56 profile_path: None,
57 limits: shift_preflight::SafetyLimits::default(),
58 }
59 }
60}
61
62#[derive(Clone)]
64pub struct ProxyState {
65 pub config: ProxyConfig,
66 pub http_client: reqwest::Client,
67 pub session: Arc<SessionStats>,
68}
69
70impl ProxyState {
71 pub fn new(config: ProxyConfig) -> Self {
72 let http_client = reqwest::Client::builder()
73 .connect_timeout(std::time::Duration::from_secs(30))
76 .redirect(reqwest::redirect::Policy::none())
79 .build()
80 .expect("failed to build HTTP client");
81
82 Self {
83 config,
84 http_client,
85 session: Arc::new(SessionStats::new()),
86 }
87 }
88}
89
90pub struct SessionStats {
92 pub started_at: Instant,
93 pub total_requests: AtomicU64,
94 pub total_images: AtomicU64,
95 pub total_images_modified: AtomicU64,
96 pub total_bytes_saved: AtomicU64,
97 pub token_savings: Mutex<TokenSavingsAccum>,
98}
99
100#[derive(Debug, Clone, Default, serde::Serialize)]
101pub struct TokenSavingsAccum {
102 pub openai_before: u64,
103 pub openai_after: u64,
104 pub anthropic_before: u64,
105 pub anthropic_after: u64,
106}
107
108impl Default for SessionStats {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114impl SessionStats {
115 pub fn new() -> Self {
116 Self {
117 started_at: Instant::now(),
118 total_requests: AtomicU64::new(0),
119 total_images: AtomicU64::new(0),
120 total_images_modified: AtomicU64::new(0),
121 total_bytes_saved: AtomicU64::new(0),
122 token_savings: Mutex::new(TokenSavingsAccum::default()),
123 }
124 }
125
126 pub fn record(&self, report: &shift_preflight::Report) {
132 self.total_requests.fetch_add(1, Ordering::Relaxed);
133 self.total_images
134 .fetch_add(report.images_found as u64, Ordering::Relaxed);
135 self.total_images_modified
136 .fetch_add(report.images_modified as u64, Ordering::Relaxed);
137 let saved = report.original_size.saturating_sub(report.transformed_size) as u64;
138 self.total_bytes_saved.fetch_add(saved, Ordering::Relaxed);
139
140 let mut ts = self.token_savings.lock().unwrap_or_else(|e| e.into_inner());
143 ts.openai_before += report.token_savings.openai_before;
144 ts.openai_after += report.token_savings.openai_after;
145 ts.anthropic_before += report.token_savings.anthropic_before;
146 ts.anthropic_after += report.token_savings.anthropic_after;
147 }
148
149 pub fn to_json(&self) -> serde_json::Value {
151 let ts = self
152 .token_savings
153 .lock()
154 .unwrap_or_else(|e| e.into_inner())
155 .clone();
156 serde_json::json!({
157 "startedAt": format!("{:.0?}", self.started_at.elapsed()),
158 "totalRequests": self.total_requests.load(Ordering::Relaxed),
159 "totalImages": self.total_images.load(Ordering::Relaxed),
160 "totalImagesModified": self.total_images_modified.load(Ordering::Relaxed),
161 "totalBytesSaved": self.total_bytes_saved.load(Ordering::Relaxed),
162 "tokenSavings": {
163 "openai_before": ts.openai_before,
164 "openai_after": ts.openai_after,
165 "anthropic_before": ts.anthropic_before,
166 "anthropic_after": ts.anthropic_after,
167 }
168 })
169 }
170}