leviath_runtime/
providers.rs1use crate::script_provider::ScriptProviderLayer;
5use leviath_providers::Provider;
6use std::collections::HashMap;
7use std::sync::Arc;
8
9#[derive(Clone, Default)]
16pub struct ProviderRegistry {
17 providers: HashMap<String, Arc<dyn Provider>>,
18 script_layer: Option<Arc<ScriptProviderLayer>>,
21}
22
23impl ProviderRegistry {
24 pub fn new() -> Self {
26 Self::default()
27 }
28
29 pub fn with_script_layer(mut self, layer: Arc<ScriptProviderLayer>) -> Self {
31 self.script_layer = Some(layer);
32 self
33 }
34
35 pub fn register(&mut self, name: String, provider: Arc<dyn Provider>) {
37 self.providers.insert(name, provider);
38 }
39
40 pub fn get(&self, name: &str) -> Option<Arc<dyn Provider>> {
45 if let Some(p) = self.providers.get(name) {
46 return Some(p.clone());
47 }
48 self.script_layer.as_ref()?.get_or_load(name)
49 }
50
51 pub async fn prime_capabilities(&self, timeout: std::time::Duration) {
65 for (name, provider) in &self.providers {
66 match tokio::time::timeout(timeout, provider.prime_capabilities()).await {
67 Ok(Ok(())) => {}
68 Ok(Err(e)) => tracing::warn!(
69 provider = %name,
70 error = %e,
71 "could not read this provider's model list, so model sizes \
72 come from the table compiled into this build; a model it \
73 does not name gets a conservative window"
74 ),
75 Err(_) => tracing::warn!(
76 provider = %name,
77 timeout_secs = timeout.as_secs(),
78 "timed out reading this provider's model list, so model \
79 sizes come from the table compiled into this build"
80 ),
81 }
82 }
83 }
84
85 pub fn has(&self, name: &str) -> bool {
89 self.providers.contains_key(name)
90 || self
91 .script_layer
92 .as_ref()
93 .is_some_and(|l| l.get_or_load(name).is_some())
94 }
95
96 pub fn provider_names(&self) -> Vec<&str> {
99 self.providers.keys().map(|k| k.as_str()).collect()
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use leviath_providers::{
107 InferenceRequest, InferenceResponse, ModelCapabilities, ProviderError,
108 };
109
110 enum PrimeOutcome {
112 Ok,
113 Fails,
114 Hangs,
115 }
116
117 struct StubProvider {
118 primed: Arc<std::sync::atomic::AtomicUsize>,
119 outcome: PrimeOutcome,
120 }
121
122 #[async_trait::async_trait]
123 impl Provider for StubProvider {
124 async fn prime_capabilities(&self) -> Result<(), ProviderError> {
125 self.primed
126 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
127 match self.outcome {
128 PrimeOutcome::Ok => Ok(()),
129 PrimeOutcome::Fails => Err(ProviderError::ApiError("no".to_string())),
130 PrimeOutcome::Hangs => {
131 tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
134 Ok(())
135 }
136 }
137 }
138 async fn infer(
139 &self,
140 _request: &InferenceRequest,
141 ) -> Result<InferenceResponse, ProviderError> {
142 Err(ProviderError::ApiError("stub".to_string()))
143 }
144 async fn count_tokens(&self, text: &str, _model: &str) -> usize {
145 text.len()
146 }
147 fn max_context_tokens(&self, _model: &str) -> usize {
148 8192
149 }
150 fn name(&self) -> &str {
151 "stub"
152 }
153 fn capabilities(&self, _model: &str) -> ModelCapabilities {
154 ModelCapabilities::default()
155 }
156 }
157
158 fn mock() -> Arc<dyn Provider> {
159 Arc::new(StubProvider {
160 primed: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
161 outcome: PrimeOutcome::Ok,
162 })
163 }
164
165 #[test]
166 fn register_get_has_and_names() {
167 let mut reg = ProviderRegistry::new();
168 assert!(!reg.has("anthropic"));
169 assert!(reg.get("anthropic").is_none());
170 reg.register("anthropic".to_string(), mock());
171 assert!(reg.has("anthropic"));
172 assert!(reg.get("anthropic").is_some());
173 assert_eq!(reg.provider_names(), vec!["anthropic"]);
174 }
175
176 #[test]
177 fn default_is_empty() {
178 let reg = ProviderRegistry::default();
179 assert!(reg.provider_names().is_empty());
180 }
181
182 fn priming(outcome: PrimeOutcome) -> (Arc<dyn Provider>, Arc<std::sync::atomic::AtomicUsize>) {
183 let primed = Arc::new(std::sync::atomic::AtomicUsize::new(0));
184 (
185 Arc::new(StubProvider {
186 primed: primed.clone(),
187 outcome,
188 }),
189 primed,
190 )
191 }
192
193 #[tokio::test]
194 async fn priming_reaches_every_registered_provider() {
195 let mut reg = ProviderRegistry::new();
196 let (p, primed) = priming(PrimeOutcome::Ok);
197 reg.register("prime".to_string(), p);
198 reg.register("other".to_string(), mock());
199
200 reg.prime_capabilities(std::time::Duration::from_secs(5))
201 .await;
202 assert_eq!(primed.load(std::sync::atomic::Ordering::Relaxed), 1);
203 }
204
205 #[tokio::test]
208 async fn a_failing_prime_does_not_stop_the_rest() {
209 let mut reg = ProviderRegistry::new();
210 let (bad, bad_calls) = priming(PrimeOutcome::Fails);
211 reg.register("bad".to_string(), bad);
212 reg.prime_capabilities(std::time::Duration::from_secs(5))
213 .await;
214 assert_eq!(bad_calls.load(std::sync::atomic::Ordering::Relaxed), 1);
215 }
216
217 #[tokio::test(start_paused = true)]
219 async fn priming_gives_up_on_a_provider_that_hangs() {
220 let mut reg = ProviderRegistry::new();
221 let (slow, calls) = priming(PrimeOutcome::Hangs);
222 reg.register("slow".to_string(), slow);
223 reg.prime_capabilities(std::time::Duration::from_secs(10))
227 .await;
228 assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 1);
229 }
230
231 #[test]
232 fn script_layer_resolves_and_native_wins() {
233 use crate::script_provider::ScriptProviderLayer;
234 use std::collections::HashMap;
235
236 let dir = tempfile::tempdir().unwrap();
237 std::fs::write(
238 dir.path().join("groq.rhai"),
239 "fn initialize(config) { #{} }\nfn inference(state, request) { #{ content: \"ok\" } }",
240 )
241 .unwrap();
242 let layer = ScriptProviderLayer::new(
243 dir.path().to_path_buf(),
244 HashMap::new(),
245 HashMap::new(),
246 None,
247 Vec::new(),
248 );
249 let mut reg = ProviderRegistry::new().with_script_layer(Arc::new(layer));
250 reg.register("anthropic".to_string(), mock());
251
252 assert!(reg.has("anthropic"));
254 assert!(reg.get("anthropic").is_some());
255
256 assert!(reg.has("groq"));
258 let p = reg.get("groq").expect("script provider resolves");
259 assert_eq!(p.name(), "groq");
260
261 assert!(!reg.has("nope"));
263 assert!(reg.get("nope").is_none());
264 }
265
266 #[tokio::test]
267 async fn stub_provider_methods_are_exercised() {
268 let p = StubProvider {
269 primed: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
270 outcome: PrimeOutcome::Ok,
271 };
272 assert_eq!(p.name(), "stub");
273 assert_eq!(p.count_tokens("abcd", "m").await, 4);
274 assert_eq!(p.max_context_tokens("m"), 8192);
275 let _ = p.capabilities("m");
276 let request = InferenceRequest {
277 system: Vec::new(),
278 messages: Vec::new(),
279 model: "m".to_string(),
280 max_tokens: 10,
281 temperature: 0.0,
282 tools: Vec::new(),
283 extra: serde_json::Value::Null,
284 request_timeout_secs: None,
285 };
286 assert!(p.infer(&request).await.is_err());
287 }
288}