1use std::collections::BTreeMap;
27use std::path::PathBuf;
28use std::sync::{Arc, RwLock};
29
30use serde::{Deserialize, Serialize};
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
37#[serde(tag = "kind", rename_all = "snake_case")]
38pub enum EmbeddingSource {
39 #[default]
42 SuppliedByApplication,
43 LocalModel {
46 model_path: PathBuf,
48 model_id: String,
50 },
51 GeneratedColumn {
55 provider: String,
57 },
58}
59
60impl EmbeddingSource {
61 pub fn label(&self) -> &str {
63 match self {
64 Self::SuppliedByApplication => "supplied_by_application",
65 Self::LocalModel { .. } => "local_model",
66 Self::GeneratedColumn { .. } => "generated_column",
67 }
68 }
69
70 pub fn model_id(&self) -> Option<&str> {
73 match self {
74 Self::SuppliedByApplication => None,
75 Self::LocalModel { model_id, .. } => Some(model_id.as_str()),
76 Self::GeneratedColumn { provider } => Some(provider.as_str()),
77 }
78 }
79
80 pub fn requires_provider(&self) -> bool {
83 !matches!(self, Self::SuppliedByApplication)
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
89pub enum EmbeddingError {
90 #[error("embedding provider {0:?} is not registered")]
92 ProviderNotFound(String),
93 #[error("embedding provider {provider:?}: {message}")]
96 ProviderFailed {
97 provider: String,
99 message: String,
101 },
102 #[error("embedding source is SuppliedByApplication; pass vectors from the client")]
104 SuppliedByApplication,
105 #[error("no embedding source configured for generation")]
108 NoSource,
109 #[error("embedding dimension mismatch: expected {expected}, got {got}")]
111 DimensionMismatch {
112 expected: u32,
114 got: u32,
116 },
117}
118
119pub trait EmbeddingProvider: Send + Sync {
123 fn provider_id(&self) -> &str;
126
127 fn model_id(&self) -> &str;
129
130 fn dimension(&self) -> u32;
132
133 fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, EmbeddingError>;
139}
140
141#[derive(Clone, Default)]
146pub struct EmbeddingProviderRegistry {
147 inner: Arc<RwLock<BTreeMap<String, Arc<dyn EmbeddingProvider>>>>,
148}
149
150impl std::fmt::Debug for EmbeddingProviderRegistry {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 let guard = self.inner.read().unwrap_or_else(|e| e.into_inner());
153 f.debug_struct("EmbeddingProviderRegistry")
154 .field("providers", &guard.keys().cloned().collect::<Vec<_>>())
155 .finish()
156 }
157}
158
159impl EmbeddingProviderRegistry {
160 pub fn new() -> Self {
162 Self::default()
163 }
164
165 pub fn register(&self, provider: Arc<dyn EmbeddingProvider>) {
167 let id = provider.provider_id().to_owned();
168 let mut guard = self.inner.write().unwrap_or_else(|e| e.into_inner());
169 guard.insert(id, provider);
170 }
171
172 pub fn unregister(&self, provider_id: &str) -> bool {
174 let mut guard = self.inner.write().unwrap_or_else(|e| e.into_inner());
175 guard.remove(provider_id).is_some()
176 }
177
178 pub fn get(&self, provider_id: &str) -> Option<Arc<dyn EmbeddingProvider>> {
180 let guard = self.inner.read().unwrap_or_else(|e| e.into_inner());
181 guard.get(provider_id).cloned()
182 }
183
184 pub fn list_ids(&self) -> Vec<String> {
186 let guard = self.inner.read().unwrap_or_else(|e| e.into_inner());
187 guard.keys().cloned().collect()
188 }
189
190 pub fn resolve(
193 &self,
194 source: &EmbeddingSource,
195 ) -> Result<Arc<dyn EmbeddingProvider>, EmbeddingError> {
196 match source {
197 EmbeddingSource::SuppliedByApplication => Err(EmbeddingError::SuppliedByApplication),
198 EmbeddingSource::LocalModel { model_id, .. } => self
199 .get(model_id)
200 .ok_or_else(|| EmbeddingError::ProviderNotFound(model_id.clone())),
201 EmbeddingSource::GeneratedColumn { provider } => self
202 .get(provider)
203 .ok_or_else(|| EmbeddingError::ProviderNotFound(provider.clone())),
204 }
205 }
206
207 pub fn embed(
212 &self,
213 source: &EmbeddingSource,
214 texts: &[&str],
215 expected_dim: u32,
216 ) -> Result<Vec<Vec<f32>>, EmbeddingError> {
217 let provider = self.resolve(source)?;
218 if provider.dimension() != expected_dim {
219 return Err(EmbeddingError::DimensionMismatch {
220 expected: expected_dim,
221 got: provider.dimension(),
222 });
223 }
224 let vectors = provider.embed(texts)?;
225 for v in &vectors {
226 if v.len() as u32 != expected_dim {
227 return Err(EmbeddingError::DimensionMismatch {
228 expected: expected_dim,
229 got: v.len() as u32,
230 });
231 }
232 if v.iter().any(|x| !x.is_finite()) {
233 return Err(EmbeddingError::ProviderFailed {
234 provider: provider.provider_id().to_owned(),
235 message: "provider returned non-finite embedding values".into(),
236 });
237 }
238 }
239 Ok(vectors)
240 }
241}
242
243#[derive(Debug, Clone)]
247pub struct FixedVectorProvider {
248 pub id: String,
250 pub model_id: String,
252 pub vector: Vec<f32>,
254}
255
256impl EmbeddingProvider for FixedVectorProvider {
257 fn provider_id(&self) -> &str {
258 &self.id
259 }
260
261 fn model_id(&self) -> &str {
262 &self.model_id
263 }
264
265 fn dimension(&self) -> u32 {
266 self.vector.len() as u32
267 }
268
269 fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
270 Ok(texts.iter().map(|_| self.vector.clone()).collect())
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
277pub struct EmbeddingModelMeta {
278 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub model_id: Option<String>,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub source_kind: Option<String>,
284 pub dim: u32,
286}
287
288impl EmbeddingModelMeta {
289 pub fn from_source(source: &EmbeddingSource, dim: u32) -> Self {
291 Self {
292 model_id: source.model_id().map(str::to_owned),
293 source_kind: Some(source.label().to_owned()),
294 dim,
295 }
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn default_source_is_application_supplied() {
305 assert_eq!(
306 EmbeddingSource::default(),
307 EmbeddingSource::SuppliedByApplication
308 );
309 assert!(!EmbeddingSource::SuppliedByApplication.requires_provider());
310 }
311
312 #[test]
313 fn registry_is_empty_by_default_no_vendor() {
314 let reg = EmbeddingProviderRegistry::new();
315 assert!(reg.list_ids().is_empty());
316 assert!(matches!(
317 reg.resolve(&EmbeddingSource::GeneratedColumn {
318 provider: "openai".into()
319 }),
320 Err(EmbeddingError::ProviderNotFound(_))
321 ));
322 }
323
324 #[test]
325 fn supplied_by_application_refuses_generation() {
326 let reg = EmbeddingProviderRegistry::new();
327 let err = reg
328 .embed(&EmbeddingSource::SuppliedByApplication, &["hello"], 4)
329 .unwrap_err();
330 assert!(matches!(err, EmbeddingError::SuppliedByApplication));
331 }
332
333 #[test]
334 fn registered_provider_embeds_with_dim_check() {
335 let reg = EmbeddingProviderRegistry::new();
336 reg.register(Arc::new(FixedVectorProvider {
337 id: "local-test".into(),
338 model_id: "fixed-v1".into(),
339 vector: vec![0.0, 1.0, 0.0, 0.0],
340 }));
341 let source = EmbeddingSource::GeneratedColumn {
342 provider: "local-test".into(),
343 };
344 let out = reg.embed(&source, &["a", "b"], 4).unwrap();
345 assert_eq!(out.len(), 2);
346 assert_eq!(out[0], vec![0.0, 1.0, 0.0, 0.0]);
347 let dim_err = reg.embed(&source, &["a"], 8).unwrap_err();
348 assert!(matches!(
349 dim_err,
350 EmbeddingError::DimensionMismatch {
351 expected: 8,
352 got: 4
353 }
354 ));
355 }
356
357 #[test]
358 fn local_model_source_resolves_by_model_id() {
359 let reg = EmbeddingProviderRegistry::new();
360 reg.register(Arc::new(FixedVectorProvider {
361 id: "kit-mini".into(),
362 model_id: "kit-mini".into(),
363 vector: vec![1.0, 0.0],
364 }));
365 let source = EmbeddingSource::LocalModel {
366 model_path: PathBuf::from("/models/kit-mini"),
367 model_id: "kit-mini".into(),
368 };
369 assert_eq!(source.model_id(), Some("kit-mini"));
370 let out = reg.embed(&source, &["q"], 2).unwrap();
371 assert_eq!(out[0].len(), 2);
372 }
373
374 #[test]
375 fn model_meta_from_source() {
376 let meta = EmbeddingModelMeta::from_source(&EmbeddingSource::SuppliedByApplication, 768);
377 assert_eq!(meta.dim, 768);
378 assert_eq!(meta.model_id, None);
379 assert_eq!(meta.source_kind.as_deref(), Some("supplied_by_application"));
380 }
381
382 #[test]
383 fn sparse_path_needs_no_provider() {
384 let reg = EmbeddingProviderRegistry::new();
387 assert!(reg.list_ids().is_empty());
388 }
389}