microclaw_engine/llm/
key_pool.rs1#[derive(Clone)]
5pub struct KeyPool {
6 keys: std::sync::Arc<Vec<String>>,
7 index: std::sync::Arc<std::sync::atomic::AtomicUsize>,
8 rotations: std::sync::Arc<std::sync::atomic::AtomicU64>,
9}
10
11impl KeyPool {
12 pub fn new(primary: &str, extra: &[String]) -> Self {
13 let mut keys: Vec<String> = Vec::with_capacity(1 + extra.len());
14 keys.push(primary.to_string());
15 for key in extra {
16 let key = key.trim();
17 if !key.is_empty() && !keys.iter().any(|k| k == key) {
18 keys.push(key.to_string());
19 }
20 }
21 KeyPool {
22 keys: std::sync::Arc::new(keys),
23 index: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
24 rotations: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
25 }
26 }
27
28 pub fn current(&self) -> String {
29 let idx = self.index.load(std::sync::atomic::Ordering::Relaxed) % self.keys.len();
30 self.keys[idx].clone()
31 }
32
33 pub fn advance(&self) -> bool {
36 if self.keys.len() <= 1 {
37 return false;
38 }
39 self.index
40 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
41 self.rotations
42 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
43 true
44 }
45
46 pub fn len(&self) -> usize {
47 self.keys.len()
48 }
49
50 pub fn is_empty(&self) -> bool {
51 self.keys.is_empty()
52 }
53
54 pub fn rotations(&self) -> u64 {
55 self.rotations.load(std::sync::atomic::Ordering::Relaxed)
56 }
57}
58
59pub(crate) fn is_key_rotation_status(status: u16) -> bool {
62 matches!(status, 401 | 403 | 429)
63}
64
65#[cfg(test)]
66mod key_pool_tests {
67 use super::*;
68
69 #[test]
70 fn key_pool_rotates_round_robin_and_dedupes() {
71 let pool = KeyPool::new("k1", &["k2".into(), " ".into(), "k1".into(), "k3".into()]);
72 assert_eq!(pool.len(), 3);
73 assert_eq!(pool.current(), "k1");
74 assert!(pool.advance());
75 assert_eq!(pool.current(), "k2");
76 assert!(pool.advance());
77 assert_eq!(pool.current(), "k3");
78 assert!(pool.advance());
79 assert_eq!(pool.current(), "k1");
80 assert_eq!(pool.rotations(), 3);
81 }
82
83 #[test]
84 fn single_key_pool_never_advances() {
85 let pool = KeyPool::new("only", &[]);
86 assert!(!pool.advance());
87 assert_eq!(pool.current(), "only");
88 assert_eq!(pool.rotations(), 0);
89 }
90
91 #[test]
92 fn rotation_statuses() {
93 assert!(is_key_rotation_status(401));
94 assert!(is_key_rotation_status(403));
95 assert!(is_key_rotation_status(429));
96 assert!(!is_key_rotation_status(500));
97 }
98}