1use serde::{Deserialize, Serialize};
9use std::fs;
10use std::path::Path;
11use std::str::FromStr;
12
13const MAGIC: [u8; 4] = [0x5A, 0x4B, 0x45, 0x59]; const VERSION: u8 = 1;
16
17#[derive(Debug, Serialize, Deserialize, Clone)]
19pub struct ZenKeyStore {
20 pub magic: [u8; 4],
21 pub version: u8,
22 pub providers: Vec<Provider>,
23 pub models: Vec<Model>,
24}
25
26#[derive(Debug, Serialize, Deserialize, Clone)]
28pub struct Provider {
29 pub name: String,
31 pub base_url: String,
33 pub rotation: RotationStrategy,
35 pub keys: Vec<KeyEntry>,
37}
38
39#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
41pub enum RotationStrategy {
42 RoundRobin,
43 Priority,
44}
45
46impl RotationStrategy {
47 pub fn as_str(&self) -> &'static str {
48 match self {
49 Self::RoundRobin => "round-robin",
50 Self::Priority => "priority",
51 }
52 }
53}
54
55impl FromStr for RotationStrategy {
56 type Err = ();
57
58 fn from_str(s: &str) -> Result<Self, Self::Err> {
59 match s.to_lowercase().as_str() {
60 "priority" => Ok(Self::Priority),
61 _ => Ok(Self::RoundRobin),
62 }
63 }
64}
65
66#[derive(Debug, Serialize, Deserialize, Clone)]
68pub struct KeyEntry {
69 pub key: String,
71 pub priority: u8,
73 pub quota_remaining: Option<f64>,
75 #[serde(default)]
77 pub last_error: Option<String>,
78 #[serde(default)]
80 pub disabled: bool,
81}
82
83#[derive(Debug, Serialize, Deserialize, Clone)]
85pub struct Model {
86 pub id: String,
88 pub provider: Option<String>,
90 pub endpoint: String,
92 pub free: bool,
94 pub priority: u8,
96}
97
98impl ZenKeyStore {
99 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
101 if !path.as_ref().exists() {
102 return Ok(Self {
103 magic: MAGIC,
104 version: VERSION,
105 providers: Vec::new(),
106 models: Vec::new(),
107 });
108 }
109 let bytes = fs::read(path.as_ref())?;
110 let store: ZenKeyStore = bincode::deserialize(&bytes)?;
111 if store.magic != MAGIC {
112 return Err("Invalid zen_keys.bin magic bytes".into());
113 }
114 if store.version != VERSION {
115 return Err(format!("Unsupported zen_keys.bin version: {}", store.version).into());
116 }
117 Ok(store)
118 }
119
120 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Box<dyn std::error::Error>> {
122 let bytes = bincode::serialize(self)?;
123 let tmp = path.as_ref().with_extension("bin.tmp");
124 fs::write(&tmp, &bytes)?;
125 fs::rename(&tmp, path.as_ref())?;
126 Ok(())
127 }
128
129 pub fn get_key(&self, provider: &str) -> Option<&KeyEntry> {
131 self.providers
132 .iter()
133 .find(|p| p.name == provider)
134 .and_then(|p| p.keys.iter().find(|k| !k.disabled))
135 }
136
137 pub fn get_all_keys(&self, provider: &str) -> Vec<&KeyEntry> {
139 self.providers
140 .iter()
141 .find(|p| p.name == provider)
142 .map(|p| {
143 let mut v: Vec<_> = p.keys.iter().filter(|k| !k.disabled).collect();
144 v.sort_by_key(|k| k.priority);
145 v
146 })
147 .unwrap_or_default()
148 }
149
150 pub fn get_model(&self, id: &str) -> Option<&Model> {
152 self.models.iter().find(|m| m.id == id)
153 }
154
155 pub fn default_model(&self) -> Option<&Model> {
157 self.models.iter().min_by_key(|m| m.priority)
158 }
159
160 pub fn free_models(&self) -> Vec<&Model> {
162 let mut v: Vec<_> = self.models.iter().filter(|m| m.free).collect();
163 v.sort_by_key(|m| m.priority);
164 v
165 }
166
167 pub fn record_error(&mut self, provider: &str, key_idx: usize, error: &str) {
169 if let Some(p) = self.providers.iter_mut().find(|p| p.name == provider) {
170 if let Some(entry) = p.keys.get_mut(key_idx) {
171 entry.last_error = Some(error.to_string());
172 if error.contains("429") || error.contains("401") || error.contains("403") {
173 entry.disabled = true;
174 }
175 }
176 }
177 }
178
179 pub fn record_success(&mut self, provider: &str, key_idx: usize) {
181 if let Some(p) = self.providers.iter_mut().find(|p| p.name == provider) {
182 if let Some(entry) = p.keys.get_mut(key_idx) {
183 entry.last_error = None;
184 entry.disabled = false;
185 }
186 }
187 }
188
189 pub fn import_json(json_str: &str) -> Result<Self, Box<dyn std::error::Error>> {
191 let json: serde_json::Value = serde_json::from_str(json_str)?;
192
193 let mut providers = Vec::new();
194
195 if let Some(base_url) = json["base_url"].as_str() {
197 let rotation = json["rotation"]
198 .as_str()
199 .unwrap_or("round-robin")
200 .parse()
201 .unwrap_or(RotationStrategy::RoundRobin);
202 let keys: Vec<KeyEntry> = json["keys"]
203 .as_array()
204 .map(|arr| {
205 arr.iter()
206 .enumerate()
207 .map(|(i, k)| KeyEntry {
208 key: k.as_str().unwrap_or("").to_string(),
209 priority: i as u8,
210 quota_remaining: None,
211 last_error: None,
212 disabled: false,
213 })
214 .collect()
215 })
216 .unwrap_or_default();
217 providers.push(Provider {
218 name: "openai".to_string(),
219 base_url: base_url.to_string(),
220 rotation,
221 keys,
222 });
223 }
224
225 if let Some(groq) = json["groq"].as_object() {
227 let base_url = groq["base_url"]
228 .as_str()
229 .unwrap_or("https://api.groq.com/openai/v1")
230 .to_string();
231 let rotation = groq["rotation"]
232 .as_str()
233 .unwrap_or("round-robin")
234 .parse()
235 .unwrap_or(RotationStrategy::RoundRobin);
236 let keys: Vec<KeyEntry> = groq["keys"]
237 .as_array()
238 .map(|arr| {
239 arr.iter()
240 .enumerate()
241 .map(|(i, k)| KeyEntry {
242 key: k.as_str().unwrap_or("").to_string(),
243 priority: i as u8,
244 quota_remaining: None,
245 last_error: None,
246 disabled: false,
247 })
248 .collect()
249 })
250 .unwrap_or_default();
251 providers.push(Provider {
252 name: "groq".to_string(),
253 base_url,
254 rotation,
255 keys,
256 });
257 }
258
259 if let Some(sn) = json["sambanova"].as_object() {
261 let base_url = sn["base_url"]
262 .as_str()
263 .unwrap_or("https://api.sambanova.ai/v1")
264 .to_string();
265 let rotation = sn["rotation"]
266 .as_str()
267 .unwrap_or("round-robin")
268 .parse()
269 .unwrap_or(RotationStrategy::RoundRobin);
270 let keys: Vec<KeyEntry> = sn["keys"]
271 .as_array()
272 .map(|arr| {
273 arr.iter()
274 .enumerate()
275 .map(|(i, k)| KeyEntry {
276 key: k.as_str().unwrap_or("").to_string(),
277 priority: i as u8,
278 quota_remaining: None,
279 last_error: None,
280 disabled: false,
281 })
282 .collect()
283 })
284 .unwrap_or_default();
285 providers.push(Provider {
286 name: "sambanova".to_string(),
287 base_url,
288 rotation,
289 keys,
290 });
291 }
292
293 let models: Vec<Model> = json["models"]
295 .as_array()
296 .map(|arr| {
297 arr.iter()
298 .map(|m| Model {
299 id: m["id"].as_str().unwrap_or("unknown").to_string(),
300 provider: m["provider"].as_str().map(|s| s.to_string()),
301 endpoint: m["endpoint"]
302 .as_str()
303 .unwrap_or("chat/completions")
304 .to_string(),
305 free: m["free"].as_bool().unwrap_or(false),
306 priority: m["priority"].as_u64().unwrap_or(99) as u8,
307 })
308 .collect()
309 })
310 .unwrap_or_default();
311
312 Ok(Self {
313 magic: MAGIC,
314 version: VERSION,
315 providers,
316 models,
317 })
318 }
319
320 pub fn stats(&self) -> String {
322 let mut s = String::new();
323 s.push_str(&format!("Providers: {}\n", self.providers.len()));
324 for p in &self.providers {
325 let active = p.keys.iter().filter(|k| !k.disabled).count();
326 let total = p.keys.len();
327 s.push_str(&format!(
328 " {}: {}/{} keys active, rotation={}\n",
329 p.name,
330 active,
331 total,
332 p.rotation.as_str()
333 ));
334 }
335 s.push_str(&format!("Models: {}\n", self.models.len()));
336 for m in &self.models {
337 let prov = m.provider.as_deref().unwrap_or("openai");
338 s.push_str(&format!(
339 " #{} {} [{}] {} {}\n",
340 m.priority,
341 m.id,
342 prov,
343 m.endpoint,
344 if m.free { "FREE" } else { "PAID" }
345 ));
346 }
347 s
348 }
349}
350
351pub fn default_zen_keys_path(output_dir: &str) -> String {
353 format!("{}/zen_keys.bin", output_dir)
354}