khive_runtime/
embedder_registry.rs1use std::collections::HashMap;
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use lattice_embed::{
12 CachedEmbeddingService, EmbeddingModel, EmbeddingService, NativeEmbeddingService,
13};
14use tokio::sync::OnceCell;
15
16use crate::error::{RuntimeError, RuntimeResult};
17
18#[async_trait]
28pub trait EmbedderProvider: Send + Sync {
29 fn name(&self) -> &str;
36
37 fn dimensions(&self) -> usize;
42
43 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>>;
49}
50
51pub(crate) struct EmbedderEntry {
54 provider: Arc<dyn EmbedderProvider>,
55 cell: Arc<OnceCell<Arc<dyn EmbeddingService>>>,
56}
57
58impl Clone for EmbedderEntry {
59 fn clone(&self) -> Self {
60 Self {
61 provider: Arc::clone(&self.provider),
62 cell: Arc::clone(&self.cell),
63 }
64 }
65}
66
67#[derive(Clone, Default)]
74pub struct EmbedderRegistry {
75 entries: HashMap<String, EmbedderEntry>,
76}
77
78impl EmbedderRegistry {
79 pub fn new() -> Self {
81 Self {
82 entries: HashMap::new(),
83 }
84 }
85
86 pub fn register<P: EmbedderProvider + 'static>(&mut self, provider: P) {
95 let name = provider.name().to_owned();
96 self.entries.insert(
97 name,
98 EmbedderEntry {
99 provider: Arc::new(provider),
100 cell: Arc::new(OnceCell::new()),
101 },
102 );
103 }
104
105 pub fn get_provider(&self, name: &str) -> Option<&dyn EmbedderProvider> {
107 self.entries.get(name).map(|e| e.provider.as_ref())
108 }
109
110 pub fn contains(&self, name: &str) -> bool {
112 self.entries.contains_key(name)
113 }
114
115 pub fn names(&self) -> Vec<String> {
117 self.entries.keys().cloned().collect()
118 }
119
120 pub(crate) fn get_entry(&self, name: &str) -> Option<EmbedderEntry> {
126 self.entries.get(name).cloned()
127 }
128
129 pub async fn get_service(&self, name: &str) -> RuntimeResult<Arc<dyn EmbeddingService>> {
138 let entry = self
139 .entries
140 .get(name)
141 .ok_or_else(|| RuntimeError::UnknownModel(name.to_string()))?
142 .clone();
143
144 entry.resolve().await
145 }
146}
147
148impl EmbedderEntry {
149 pub(crate) async fn resolve(self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
156 if let Some(svc) = self.cell.get() {
159 return Ok(Arc::clone(svc));
160 }
161 let svc = self.provider.build().await.map_err(|e| {
162 crate::error::RuntimeError::Internal(format!(
163 "EmbedderProvider '{}' build() failed: {e}",
164 self.provider.name()
165 ))
166 })?;
167 let _ = self.cell.set(Arc::clone(&svc));
169 Ok(svc)
170 }
171}
172
173pub struct LatticeEmbedderProvider {
183 model: EmbeddingModel,
184 name: String,
186}
187
188impl LatticeEmbedderProvider {
189 pub fn new(model: EmbeddingModel) -> Self {
191 let name = model.to_string();
192 Self { model, name }
193 }
194}
195
196#[async_trait]
197impl EmbedderProvider for LatticeEmbedderProvider {
198 fn name(&self) -> &str {
199 &self.name
200 }
201
202 fn dimensions(&self) -> usize {
203 self.model.dimensions()
204 }
205
206 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
207 let native = Arc::new(NativeEmbeddingService::with_model(self.model));
208 let cached = CachedEmbeddingService::with_default_cache(native);
209 Ok(Arc::new(cached) as Arc<dyn EmbeddingService>)
210 }
211}
212
213#[cfg(test)]
216mod tests {
217 use super::*;
218 use std::sync::atomic::{AtomicUsize, Ordering};
219
220 struct ConstVecProvider {
221 name: String,
222 dims: usize,
223 build_calls: Arc<AtomicUsize>,
224 }
225
226 impl ConstVecProvider {
227 fn new(name: &str, dims: usize) -> Self {
228 Self {
229 name: name.to_owned(),
230 dims,
231 build_calls: Arc::new(AtomicUsize::new(0)),
232 }
233 }
234 }
235
236 struct ConstVecService {
240 dims: usize,
241 }
242
243 #[async_trait]
244 impl EmbeddingService for ConstVecService {
245 async fn embed(
246 &self,
247 texts: &[String],
248 _model: EmbeddingModel,
249 ) -> std::result::Result<Vec<Vec<f32>>, lattice_embed::EmbedError> {
250 Ok(texts.iter().map(|_| vec![1.0_f32; self.dims]).collect())
251 }
252
253 fn supports_model(&self, _model: EmbeddingModel) -> bool {
254 true
255 }
256
257 fn name(&self) -> &'static str {
258 "const-vec-service"
259 }
260 }
261
262 #[async_trait]
263 impl EmbedderProvider for ConstVecProvider {
264 fn name(&self) -> &str {
265 &self.name
266 }
267
268 fn dimensions(&self) -> usize {
269 self.dims
270 }
271
272 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
273 self.build_calls.fetch_add(1, Ordering::SeqCst);
274 Ok(Arc::new(ConstVecService { dims: self.dims }))
275 }
276 }
277
278 #[test]
279 fn register_and_get_provider_round_trip() {
280 let mut reg = EmbedderRegistry::new();
281 reg.register(ConstVecProvider::new("mock-384", 384));
282
283 assert!(reg.contains("mock-384"), "registered name must be present");
284 let provider = reg.get_provider("mock-384").expect("provider must exist");
285 assert_eq!(provider.name(), "mock-384");
286 assert_eq!(provider.dimensions(), 384);
287 }
288
289 #[test]
290 fn duplicate_name_last_wins() {
291 let mut reg = EmbedderRegistry::new();
292 reg.register(ConstVecProvider::new("shared", 128));
293 reg.register(ConstVecProvider::new("shared", 256));
294
295 let provider = reg.get_provider("shared").expect("provider must exist");
296 assert_eq!(
297 provider.dimensions(),
298 256,
299 "last registration must win; expected dims=256"
300 );
301 }
302
303 #[test]
304 fn names_returns_all_registered() {
305 let mut reg = EmbedderRegistry::new();
306 reg.register(ConstVecProvider::new("model-a", 64));
307 reg.register(ConstVecProvider::new("model-b", 128));
308 reg.register(ConstVecProvider::new("model-c", 256));
309
310 let mut names = reg.names();
311 names.sort();
312 assert_eq!(names, vec!["model-a", "model-b", "model-c"]);
313 }
314
315 #[tokio::test]
316 async fn get_service_unknown_name_returns_error() {
317 let reg = EmbedderRegistry::new();
318 let result = reg.get_service("does-not-exist").await;
319 let err = result.err().expect("expected Err for unknown name, got Ok");
320 assert!(
321 matches!(err, RuntimeError::UnknownModel(ref n) if n == "does-not-exist"),
322 "expected UnknownModel, got {err:?}"
323 );
324 }
325
326 #[tokio::test]
327 async fn get_service_calls_build_once() {
328 let counter = Arc::new(AtomicUsize::new(0));
329 let provider = ConstVecProvider {
330 name: "cached-model".to_owned(),
331 dims: 32,
332 build_calls: Arc::clone(&counter),
333 };
334 let mut reg = EmbedderRegistry::new();
335 reg.register(provider);
336
337 let _ = reg.get_service("cached-model").await.unwrap();
338 let _ = reg.get_service("cached-model").await.unwrap();
339 let _ = reg.get_service("cached-model").await.unwrap();
340
341 assert_eq!(
342 counter.load(Ordering::SeqCst),
343 1,
344 "build must be called exactly once regardless of get_service call count"
345 );
346 }
347}