use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use std::str::FromStr;
const MAGIC: [u8; 4] = [0x5A, 0x4B, 0x45, 0x59]; const VERSION: u8 = 1;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ZenKeyStore {
pub magic: [u8; 4],
pub version: u8,
pub providers: Vec<Provider>,
pub models: Vec<Model>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Provider {
pub name: String,
pub base_url: String,
pub rotation: RotationStrategy,
pub keys: Vec<KeyEntry>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
pub enum RotationStrategy {
RoundRobin,
Priority,
}
impl RotationStrategy {
pub fn as_str(&self) -> &'static str {
match self {
Self::RoundRobin => "round-robin",
Self::Priority => "priority",
}
}
}
impl FromStr for RotationStrategy {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"priority" => Ok(Self::Priority),
_ => Ok(Self::RoundRobin),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct KeyEntry {
pub key: String,
pub priority: u8,
pub quota_remaining: Option<f64>,
#[serde(default)]
pub last_error: Option<String>,
#[serde(default)]
pub disabled: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Model {
pub id: String,
pub provider: Option<String>,
pub endpoint: String,
pub free: bool,
pub priority: u8,
}
impl ZenKeyStore {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
if !path.as_ref().exists() {
return Ok(Self {
magic: MAGIC,
version: VERSION,
providers: Vec::new(),
models: Vec::new(),
});
}
let bytes = fs::read(path.as_ref())?;
let store: ZenKeyStore = bincode::deserialize(&bytes)?;
if store.magic != MAGIC {
return Err("Invalid zen_keys.bin magic bytes".into());
}
if store.version != VERSION {
return Err(format!("Unsupported zen_keys.bin version: {}", store.version).into());
}
Ok(store)
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Box<dyn std::error::Error>> {
let bytes = bincode::serialize(self)?;
let tmp = path.as_ref().with_extension("bin.tmp");
fs::write(&tmp, &bytes)?;
fs::rename(&tmp, path.as_ref())?;
Ok(())
}
pub fn get_key(&self, provider: &str) -> Option<&KeyEntry> {
self.providers
.iter()
.find(|p| p.name == provider)
.and_then(|p| p.keys.iter().find(|k| !k.disabled))
}
pub fn get_all_keys(&self, provider: &str) -> Vec<&KeyEntry> {
self.providers
.iter()
.find(|p| p.name == provider)
.map(|p| {
let mut v: Vec<_> = p.keys.iter().filter(|k| !k.disabled).collect();
v.sort_by_key(|k| k.priority);
v
})
.unwrap_or_default()
}
pub fn get_model(&self, id: &str) -> Option<&Model> {
self.models.iter().find(|m| m.id == id)
}
pub fn default_model(&self) -> Option<&Model> {
self.models.iter().min_by_key(|m| m.priority)
}
pub fn free_models(&self) -> Vec<&Model> {
let mut v: Vec<_> = self.models.iter().filter(|m| m.free).collect();
v.sort_by_key(|m| m.priority);
v
}
pub fn record_error(&mut self, provider: &str, key_idx: usize, error: &str) {
if let Some(p) = self.providers.iter_mut().find(|p| p.name == provider) {
if let Some(entry) = p.keys.get_mut(key_idx) {
entry.last_error = Some(error.to_string());
if error.contains("429") || error.contains("401") || error.contains("403") {
entry.disabled = true;
}
}
}
}
pub fn record_success(&mut self, provider: &str, key_idx: usize) {
if let Some(p) = self.providers.iter_mut().find(|p| p.name == provider) {
if let Some(entry) = p.keys.get_mut(key_idx) {
entry.last_error = None;
entry.disabled = false;
}
}
}
pub fn import_json(json_str: &str) -> Result<Self, Box<dyn std::error::Error>> {
let json: serde_json::Value = serde_json::from_str(json_str)?;
let mut providers = Vec::new();
if let Some(base_url) = json["base_url"].as_str() {
let rotation = json["rotation"]
.as_str()
.unwrap_or("round-robin")
.parse()
.unwrap_or(RotationStrategy::RoundRobin);
let keys: Vec<KeyEntry> = json["keys"]
.as_array()
.map(|arr| {
arr.iter()
.enumerate()
.map(|(i, k)| KeyEntry {
key: k.as_str().unwrap_or("").to_string(),
priority: i as u8,
quota_remaining: None,
last_error: None,
disabled: false,
})
.collect()
})
.unwrap_or_default();
providers.push(Provider {
name: "openai".to_string(),
base_url: base_url.to_string(),
rotation,
keys,
});
}
if let Some(groq) = json["groq"].as_object() {
let base_url = groq["base_url"]
.as_str()
.unwrap_or("https://api.groq.com/openai/v1")
.to_string();
let rotation = groq["rotation"]
.as_str()
.unwrap_or("round-robin")
.parse()
.unwrap_or(RotationStrategy::RoundRobin);
let keys: Vec<KeyEntry> = groq["keys"]
.as_array()
.map(|arr| {
arr.iter()
.enumerate()
.map(|(i, k)| KeyEntry {
key: k.as_str().unwrap_or("").to_string(),
priority: i as u8,
quota_remaining: None,
last_error: None,
disabled: false,
})
.collect()
})
.unwrap_or_default();
providers.push(Provider {
name: "groq".to_string(),
base_url,
rotation,
keys,
});
}
if let Some(sn) = json["sambanova"].as_object() {
let base_url = sn["base_url"]
.as_str()
.unwrap_or("https://api.sambanova.ai/v1")
.to_string();
let rotation = sn["rotation"]
.as_str()
.unwrap_or("round-robin")
.parse()
.unwrap_or(RotationStrategy::RoundRobin);
let keys: Vec<KeyEntry> = sn["keys"]
.as_array()
.map(|arr| {
arr.iter()
.enumerate()
.map(|(i, k)| KeyEntry {
key: k.as_str().unwrap_or("").to_string(),
priority: i as u8,
quota_remaining: None,
last_error: None,
disabled: false,
})
.collect()
})
.unwrap_or_default();
providers.push(Provider {
name: "sambanova".to_string(),
base_url,
rotation,
keys,
});
}
let models: Vec<Model> = json["models"]
.as_array()
.map(|arr| {
arr.iter()
.map(|m| Model {
id: m["id"].as_str().unwrap_or("unknown").to_string(),
provider: m["provider"].as_str().map(|s| s.to_string()),
endpoint: m["endpoint"]
.as_str()
.unwrap_or("chat/completions")
.to_string(),
free: m["free"].as_bool().unwrap_or(false),
priority: m["priority"].as_u64().unwrap_or(99) as u8,
})
.collect()
})
.unwrap_or_default();
Ok(Self {
magic: MAGIC,
version: VERSION,
providers,
models,
})
}
pub fn stats(&self) -> String {
let mut s = String::new();
s.push_str(&format!("Providers: {}\n", self.providers.len()));
for p in &self.providers {
let active = p.keys.iter().filter(|k| !k.disabled).count();
let total = p.keys.len();
s.push_str(&format!(
" {}: {}/{} keys active, rotation={}\n",
p.name,
active,
total,
p.rotation.as_str()
));
}
s.push_str(&format!("Models: {}\n", self.models.len()));
for m in &self.models {
let prov = m.provider.as_deref().unwrap_or("openai");
s.push_str(&format!(
" #{} {} [{}] {} {}\n",
m.priority,
m.id,
prov,
m.endpoint,
if m.free { "FREE" } else { "PAID" }
));
}
s
}
}
pub fn default_zen_keys_path(output_dir: &str) -> String {
format!("{}/zen_keys.bin", output_dir)
}