1use parking_lot::RwLock;
4use serde::{Deserialize, Serialize};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct RoutingConfig {
11 pub auto_routing: bool,
13 pub prefer_cost_efficient: bool,
15 pub fallback_models: Vec<String>,
17 pub excluded_models: Vec<String>,
19}
20
21impl Default for RoutingConfig {
22 fn default() -> Self {
23 Self {
24 auto_routing: true,
25 prefer_cost_efficient: false,
26 fallback_models: Vec::new(),
27 excluded_models: Vec::new(),
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
37pub struct RoutingControl {
38 enabled: Arc<AtomicBool>,
39 config: Arc<RwLock<RoutingConfig>>,
40}
41
42impl RoutingControl {
43 pub fn new(config: RoutingConfig) -> Self {
45 Self {
46 enabled: Arc::new(AtomicBool::new(config.auto_routing)),
47 config: Arc::new(RwLock::new(config)),
48 }
49 }
50
51 pub fn disabled() -> Self {
53 Self {
54 enabled: Arc::new(AtomicBool::new(false)),
55 config: Arc::new(RwLock::new(RoutingConfig::default())),
56 }
57 }
58
59 pub fn set_enabled(&self, enabled: bool) {
61 self.enabled.store(enabled, Ordering::SeqCst);
62 }
63
64 pub fn is_enabled(&self) -> bool {
66 self.enabled.load(Ordering::SeqCst)
67 }
68
69 pub fn update_config(&self, f: impl FnOnce(&mut RoutingConfig)) {
71 f(&mut self.config.write());
72 }
73
74 pub fn set_fallback_models(&self, models: Vec<String>) {
76 self.config.write().fallback_models = models;
77 }
78
79 pub fn exclude_model(&self, model_id: &str) {
81 let mut config = self.config.write();
82 if !config.excluded_models.contains(&model_id.to_string()) {
83 config.excluded_models.push(model_id.to_string());
84 }
85 }
86
87 pub fn unexclude_model(&self, model_id: &str) {
89 self.config
90 .write()
91 .excluded_models
92 .retain(|m| m != model_id);
93 }
94
95 pub fn config(&self) -> RoutingConfig {
97 self.config.read().clone()
98 }
99
100 pub fn fallback_models(&self) -> Vec<String> {
102 self.config.read().fallback_models.clone()
103 }
104
105 pub fn excluded_models(&self) -> Vec<String> {
107 self.config.read().excluded_models.clone()
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn routing_control_default() {
117 let rc = RoutingControl::new(RoutingConfig::default());
118 assert!(rc.is_enabled());
119 }
120
121 #[test]
122 fn routing_control_toggle() {
123 let rc = RoutingControl::new(RoutingConfig::default());
124 rc.set_enabled(false);
125 assert!(!rc.is_enabled());
126 rc.set_enabled(true);
127 assert!(rc.is_enabled());
128 }
129
130 #[test]
131 fn routing_control_disabled() {
132 let rc = RoutingControl::disabled();
133 assert!(!rc.is_enabled());
134 }
135
136 #[test]
137 fn routing_control_fallback_models() {
138 let rc = RoutingControl::new(RoutingConfig::default());
139 rc.set_fallback_models(vec!["model-a".into(), "model-b".into()]);
140 assert_eq!(rc.fallback_models().len(), 2);
141 }
142
143 #[test]
144 fn routing_control_exclude_model() {
145 let rc = RoutingControl::new(RoutingConfig::default());
146 rc.exclude_model("bad-model");
147 assert!(rc.excluded_models().contains(&"bad-model".to_string()));
148 rc.unexclude_model("bad-model");
149 assert!(!rc.excluded_models().contains(&"bad-model".to_string()));
150 }
151
152 #[test]
153 fn routing_control_update_config() {
154 let rc = RoutingControl::new(RoutingConfig::default());
155 rc.update_config(|c| {
156 c.prefer_cost_efficient = true;
157 });
158 assert!(rc.config().prefer_cost_efficient);
159 }
160
161 #[test]
162 fn routing_control_no_duplicate_exclusion() {
163 let rc = RoutingControl::new(RoutingConfig::default());
164 rc.exclude_model("model-1");
165 rc.exclude_model("model-1");
166 assert_eq!(rc.excluded_models().len(), 1);
167 }
168
169 #[test]
175 fn routing_control_live_across_clones() {
176 let rc = RoutingControl::new(RoutingConfig::default());
177 let observer = rc.clone();
178
179 rc.set_enabled(false);
181 rc.exclude_model("primary-model");
182 rc.set_fallback_models(vec!["fallback-a".into(), "fallback-b".into()]);
183
184 assert!(
186 !observer.is_enabled(),
187 "set_enabled must propagate to clones"
188 );
189 assert!(
190 observer
191 .excluded_models()
192 .contains(&"primary-model".to_string()),
193 "exclude_model must propagate to clones"
194 );
195 assert_eq!(
196 observer.fallback_models().len(),
197 2,
198 "set_fallback_models must propagate to clones"
199 );
200
201 observer.unexclude_model("primary-model");
203 assert!(
204 !rc.excluded_models().contains(&"primary-model".to_string()),
205 "unexclude_model via clone must propagate back"
206 );
207 }
208
209 #[test]
214 fn routing_control_config_snapshot_is_point_in_time() {
215 let rc = RoutingControl::new(RoutingConfig::default());
216 let snap = rc.config();
217 rc.exclude_model("later-exclusion");
218 assert!(
220 !snap
221 .excluded_models
222 .contains(&"later-exclusion".to_string())
223 );
224 assert!(
226 rc.excluded_models()
227 .contains(&"later-exclusion".to_string())
228 );
229 }
230}