1use std::collections::BTreeMap;
8use std::future::Future;
9use std::pin::Pin;
10use std::sync::{Arc, RwLock};
11
12use serde::{Deserialize, Serialize};
13use sha2::{Digest, Sha256};
14
15pub const DEFAULT_MAX_EMBEDDING_TEXTS: usize = 256;
16pub const DEFAULT_MAX_EMBEDDING_TEXT_BYTES: usize = 64 * 1024;
17pub const DEFAULT_MAX_EMBEDDING_INPUT_BYTES: usize = 1024 * 1024;
18pub const DEFAULT_MAX_EMBEDDING_OUTPUT_BYTES: usize = 16 * 1024 * 1024;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
21#[serde(rename_all = "snake_case")]
22pub enum EmbeddingNormalization {
23 #[default]
24 None,
25 L2,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
29#[serde(rename_all = "snake_case")]
30pub enum EmbeddingFailurePolicy {
31 #[default]
34 AbortWrite,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum EmbeddingGenerationStatus {
40 Ready,
41 Pending,
42 Failed,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
47pub struct EmbeddingProviderRef {
48 pub provider_id: String,
49 pub provider_version: String,
50 pub model_id: String,
51 pub model_version: String,
52 pub model_artifact_sha256: [u8; 32],
53 pub tokenizer_sha256: [u8; 32],
54 pub preprocessing_sha256: [u8; 32],
55 pub dimension: u32,
56 pub normalization: EmbeddingNormalization,
57}
58
59impl EmbeddingProviderRef {
60 pub fn fingerprint_sha256(&self) -> [u8; 32] {
61 let mut hasher = Sha256::new();
62 hasher.update(self.provider_id.as_bytes());
63 hasher.update([0]);
64 hasher.update(self.provider_version.as_bytes());
65 hasher.update([0]);
66 hasher.update(self.model_id.as_bytes());
67 hasher.update([0]);
68 hasher.update(self.model_version.as_bytes());
69 hasher.update([0]);
70 hasher.update(self.model_artifact_sha256);
71 hasher.update(self.tokenizer_sha256);
72 hasher.update(self.preprocessing_sha256);
73 hasher.update(self.dimension.to_le_bytes());
74 hasher.update([self.normalization as u8]);
75 hasher.finalize().into()
76 }
77 pub fn versioned_identity_key(&self) -> (&str, &str, &str, &str) {
78 (
79 &self.provider_id,
80 &self.provider_version,
81 &self.model_id,
82 &self.model_version,
83 )
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct GeneratedEmbeddingMetadata {
90 pub provider_id: String,
91 pub model_id: String,
92 pub model_version: String,
93 pub preprocessing_version: String,
94 pub source_fingerprint: [u8; 32],
95 pub status: EmbeddingGenerationStatus,
96 pub last_error_category: Option<mongreldb_types::errors::ErrorCategory>,
97 pub attempt_count: u32,
98 pub semantic_identity: EmbeddingProviderRef,
99 pub provider_registry_generation: u64,
100}
101
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104pub struct GeneratedEmbeddingValue {
105 pub vector: Vec<f32>,
106 pub metadata: GeneratedEmbeddingMetadata,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct GeneratedEmbeddingSpec {
112 pub provider_id: String,
113 pub model_id: String,
114 pub model_version: String,
115 pub source_columns: Vec<u16>,
116 pub input_template: String,
119 pub dimension: u32,
120 pub normalization: EmbeddingNormalization,
121 pub failure_policy: EmbeddingFailurePolicy,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
125#[serde(tag = "kind", rename_all = "snake_case")]
126pub enum EmbeddingSource {
127 #[default]
128 SuppliedByApplication,
129 LocalModel {
132 model_id: String,
133 #[serde(skip)]
134 model_path: std::path::PathBuf,
135 },
136 GeneratedColumn {
139 provider: String,
140 },
141 ConfiguredModel {
143 provider_id: String,
144 model_id: String,
145 model_version: String,
146 },
147 GeneratedColumnSpec {
148 spec: GeneratedEmbeddingSpec,
149 },
150}
151
152impl EmbeddingSource {
153 pub fn label(&self) -> &str {
154 match self {
155 Self::SuppliedByApplication => "supplied_by_application",
156 Self::LocalModel { .. } | Self::ConfiguredModel { .. } => "local_model",
157 Self::GeneratedColumn { .. } | Self::GeneratedColumnSpec { .. } => "generated_column",
158 }
159 }
160
161 pub fn provider_id(&self) -> Option<&str> {
162 match self {
163 Self::SuppliedByApplication => None,
164 Self::LocalModel { model_id, .. } => Some(model_id),
165 Self::GeneratedColumn { provider } => Some(provider),
166 Self::ConfiguredModel { provider_id, .. } => Some(provider_id),
167 Self::GeneratedColumnSpec { spec } => Some(&spec.provider_id),
168 }
169 }
170
171 pub fn model_id(&self) -> Option<&str> {
172 match self {
173 Self::SuppliedByApplication => None,
174 Self::LocalModel { model_id, .. } => Some(model_id),
175 Self::GeneratedColumn { .. } => None,
176 Self::ConfiguredModel { model_id, .. } => Some(model_id),
177 Self::GeneratedColumnSpec { spec } => Some(&spec.model_id),
178 }
179 }
180
181 pub fn model_version(&self) -> Option<&str> {
182 match self {
183 Self::SuppliedByApplication => None,
184 Self::LocalModel { .. } | Self::GeneratedColumn { .. } => None,
185 Self::ConfiguredModel { model_version, .. } => Some(model_version),
186 Self::GeneratedColumnSpec { spec } => Some(&spec.model_version),
187 }
188 }
189
190 pub fn requires_provider(&self) -> bool {
191 !matches!(self, Self::SuppliedByApplication)
192 }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
196pub enum EmbeddingError {
197 #[error("embedding provider {0:?} is not registered")]
198 ProviderNotFound(String),
199 #[error("embedding provider {0:?} is already registered")]
200 ProviderAlreadyRegistered(String),
201 #[error(
202 "embedding provider {provider:?} generation mismatch: expected {expected}, got {actual}"
203 )]
204 ProviderGenerationMismatch {
205 provider: String,
206 expected: u64,
207 actual: u64,
208 },
209 #[error("embedding provider {provider:?} is referenced by {references} schema columns")]
210 ProviderInUse { provider: String, references: usize },
211 #[error("embedding provider {provider:?}: {message}")]
212 ProviderFailed { provider: String, message: String },
213 #[error("embedding source is SuppliedByApplication")]
214 SuppliedByApplication,
215 #[error("embedding provider identity mismatch: expected {expected:?}, got {got:?}")]
216 ProviderIdentityMismatch { expected: String, got: String },
217 #[error("embedding model identity mismatch: expected {expected:?}, got {got:?}")]
218 ModelIdentityMismatch { expected: String, got: String },
219 #[error("embedding model version mismatch: expected {expected:?}, got {got:?}")]
220 ModelVersionMismatch { expected: String, got: String },
221 #[error("embedding provider {provider:?} replacement under versioned identity (provider_version={provider_version:?}, model_id={model_id:?}, model_version={model_version:?}) changed cryptographic semantic fingerprints; bump provider/model version for a new semantic space")]
222 SemanticIdentityImmutable {
223 provider: String,
224 provider_version: String,
225 model_id: String,
226 model_version: String,
227 },
228 #[error("ANN generation semantic identity mismatch for column {column_id}: expected fingerprint {expected:x?}, got {got:x?}")]
229 AnnSemanticIdentityMismatch {
230 column_id: u16,
231 expected: [u8; 32],
232 got: [u8; 32],
233 },
234 #[error("embedding output count mismatch: expected {expected}, got {got}")]
235 OutputCountMismatch { expected: usize, got: usize },
236 #[error("embedding dimension mismatch: expected {expected}, got {got}")]
237 DimensionMismatch { expected: u32, got: u32 },
238 #[error("embedding request exceeds {resource}: requested {requested}, limit {limit}")]
239 LimitExceeded {
240 resource: &'static str,
241 requested: usize,
242 limit: usize,
243 },
244 #[error("embedding output is not finite")]
245 NonFiniteOutput,
246 #[error("embedding output is not L2 normalized")]
247 NormalizationMismatch,
248 #[error("embedding execution cancelled or timed out: {0}")]
249 Execution(String),
250 #[error("no active ANN semantic identity for embedding column {0}")]
251 NoActiveAnnIdentity(u16),
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum ProviderHealth {
256 Ready,
257 Degraded,
258 Unavailable,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
263pub enum ProviderExecutionMode {
264 #[default]
266 Cooperative,
267 Blocking,
270 Remote,
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276pub struct EmbeddingLimits {
277 pub max_texts: usize,
278 pub max_text_bytes: usize,
279 pub max_total_input_bytes: usize,
280 pub max_dimension: u32,
281 pub max_output_bytes: usize,
282}
283
284impl Default for EmbeddingLimits {
285 fn default() -> Self {
286 Self {
287 max_texts: DEFAULT_MAX_EMBEDDING_TEXTS,
288 max_text_bytes: DEFAULT_MAX_EMBEDDING_TEXT_BYTES,
289 max_total_input_bytes: DEFAULT_MAX_EMBEDDING_INPUT_BYTES,
290 max_dimension: crate::schema::Schema::MAX_EMBEDDING_DIM,
291 max_output_bytes: DEFAULT_MAX_EMBEDDING_OUTPUT_BYTES,
292 }
293 }
294}
295
296pub struct EmbeddingRequest<'a> {
297 pub texts: &'a [&'a str],
298 pub control: &'a crate::ExecutionControl,
299 pub trace_id: &'a str,
300}
301
302#[derive(Debug, Clone, PartialEq)]
303pub struct EmbeddingResponse {
304 pub vectors: Vec<Vec<f32>>,
305}
306
307pub type EmbeddingFuture<'a> =
308 Pin<Box<dyn Future<Output = Result<EmbeddingResponse, EmbeddingError>> + Send + 'a>>;
309
310pub trait EmbeddingProvider: Send + Sync {
311 fn provider_id(&self) -> &str;
312 fn model_id(&self) -> &str;
313 fn model_version(&self) -> &str;
314 fn dimension(&self) -> u32;
315 fn normalization(&self) -> EmbeddingNormalization;
316 fn preprocessing_version(&self) -> &str;
317 fn provider_version(&self) -> &str {
318 "1"
319 }
320 fn model_artifact_sha256(&self) -> [u8; 32] {
321 let mut hasher = Sha256::new();
322 hasher.update(b"model-artifact:");
323 hasher.update(self.model_id().as_bytes());
324 hasher.update([0]);
325 hasher.update(self.model_version().as_bytes());
326 hasher.finalize().into()
327 }
328 fn tokenizer_sha256(&self) -> [u8; 32] {
329 let mut hasher = Sha256::new();
330 hasher.update(b"tokenizer:");
331 hasher.update(self.model_id().as_bytes());
332 hasher.update([0]);
333 hasher.update(self.preprocessing_version().as_bytes());
334 hasher.finalize().into()
335 }
336 fn preprocessing_sha256(&self) -> [u8; 32] {
337 let mut hasher = Sha256::new();
338 hasher.update(b"preprocessing:");
339 hasher.update(self.preprocessing_version().as_bytes());
340 hasher.update([0]);
341 hasher.update([self.normalization() as u8]);
342 hasher.finalize().into()
343 }
344 fn semantic_identity(&self) -> EmbeddingProviderRef {
345 EmbeddingProviderRef {
346 provider_id: self.provider_id().to_owned(),
347 provider_version: self.provider_version().to_owned(),
348 model_id: self.model_id().to_owned(),
349 model_version: self.model_version().to_owned(),
350 model_artifact_sha256: self.model_artifact_sha256(),
351 tokenizer_sha256: self.tokenizer_sha256(),
352 preprocessing_sha256: self.preprocessing_sha256(),
353 dimension: self.dimension(),
354 normalization: self.normalization(),
355 }
356 }
357 fn execution_mode(&self) -> ProviderExecutionMode {
358 ProviderExecutionMode::Cooperative
359 }
360
361 fn health(&self) -> ProviderHealth {
362 ProviderHealth::Ready
363 }
364
365 fn embed(&self, request: EmbeddingRequest<'_>) -> Result<EmbeddingResponse, EmbeddingError>;
368
369 fn embed_async<'a>(&'a self, request: EmbeddingRequest<'a>) -> EmbeddingFuture<'a> {
373 Box::pin(async move { self.embed(request) })
374 }
375}
376
377#[derive(Clone)]
378struct ProviderEntry {
379 generation: u64,
380 provider: Arc<dyn EmbeddingProvider>,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq)]
384pub struct ProviderStatus {
385 pub provider_id: String,
386 pub model_id: String,
387 pub model_version: String,
388 pub generation: u64,
389 pub health: ProviderHealth,
390 pub semantic_identity: EmbeddingProviderRef,
391}
392
393#[derive(Clone)]
394pub struct EmbeddingProviderRegistry {
395 inner: Arc<RwLock<BTreeMap<String, ProviderEntry>>>,
396 blocking_slots: Arc<tokio::sync::Semaphore>,
397}
398
399impl Default for EmbeddingProviderRegistry {
400 fn default() -> Self {
401 let concurrency = std::thread::available_parallelism()
402 .map(usize::from)
403 .unwrap_or(1)
404 .clamp(1, 32);
405 Self {
406 inner: Arc::new(RwLock::new(BTreeMap::new())),
407 blocking_slots: Arc::new(tokio::sync::Semaphore::new(concurrency)),
408 }
409 }
410}
411
412impl std::fmt::Debug for EmbeddingProviderRegistry {
413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 f.debug_struct("EmbeddingProviderRegistry")
415 .field("providers", &self.list_ids())
416 .finish()
417 }
418}
419
420impl EmbeddingProviderRegistry {
421 pub fn new() -> Self {
422 Self::default()
423 }
424
425 pub fn with_max_blocking_concurrency(max_concurrency: usize) -> Self {
427 Self {
428 inner: Arc::new(RwLock::new(BTreeMap::new())),
429 blocking_slots: Arc::new(tokio::sync::Semaphore::new(max_concurrency.max(1))),
430 }
431 }
432
433 pub fn register_new(
434 &self,
435 provider: Arc<dyn EmbeddingProvider>,
436 ) -> Result<u64, EmbeddingError> {
437 let id = provider.provider_id().to_owned();
438 let mut providers = self
439 .inner
440 .write()
441 .unwrap_or_else(|error| error.into_inner());
442 if providers.contains_key(&id) {
443 return Err(EmbeddingError::ProviderAlreadyRegistered(id));
444 }
445 providers.insert(
446 id,
447 ProviderEntry {
448 generation: 1,
449 provider,
450 },
451 );
452 Ok(1)
453 }
454
455 #[deprecated(note = "use register_new and handle duplicate provider IDs")]
458 pub fn register(&self, provider: Arc<dyn EmbeddingProvider>) {
459 let _ = self.register_new(provider);
460 }
461
462 pub fn replace(
463 &self,
464 expected_generation: u64,
465 provider: Arc<dyn EmbeddingProvider>,
466 ) -> Result<u64, EmbeddingError> {
467 let id = provider.provider_id().to_owned();
468 let new_identity = provider.semantic_identity();
469 let mut providers = self
470 .inner
471 .write()
472 .unwrap_or_else(|error| error.into_inner());
473 let entry = providers
474 .get_mut(&id)
475 .ok_or_else(|| EmbeddingError::ProviderNotFound(id.clone()))?;
476 if entry.generation != expected_generation {
477 return Err(EmbeddingError::ProviderGenerationMismatch {
478 provider: id,
479 expected: expected_generation,
480 actual: entry.generation,
481 });
482 }
483 let existing_identity = entry.provider.semantic_identity();
484 if existing_identity.versioned_identity_key() == new_identity.versioned_identity_key()
485 && existing_identity != new_identity
486 {
487 return Err(EmbeddingError::SemanticIdentityImmutable {
488 provider: id,
489 provider_version: new_identity.provider_version,
490 model_id: new_identity.model_id,
491 model_version: new_identity.model_version,
492 });
493 }
494 entry.generation = entry.generation.saturating_add(1);
495 entry.provider = provider;
496 Ok(entry.generation)
497 }
498
499 pub(crate) fn unregister_unreferenced(
500 &self,
501 provider_id: &str,
502 references: usize,
503 ) -> Result<bool, EmbeddingError> {
504 if references != 0 {
505 return Err(EmbeddingError::ProviderInUse {
506 provider: provider_id.to_owned(),
507 references,
508 });
509 }
510 Ok(self
511 .inner
512 .write()
513 .unwrap_or_else(|error| error.into_inner())
514 .remove(provider_id)
515 .is_some())
516 }
517
518 pub fn get(&self, provider_id: &str) -> Option<Arc<dyn EmbeddingProvider>> {
519 self.inner
520 .read()
521 .unwrap_or_else(|error| error.into_inner())
522 .get(provider_id)
523 .map(|entry| Arc::clone(&entry.provider))
524 }
525 pub fn get_with_generation(
526 &self,
527 provider_id: &str,
528 ) -> Option<(u64, Arc<dyn EmbeddingProvider>)> {
529 self.inner
530 .read()
531 .unwrap_or_else(|e| e.into_inner())
532 .get(provider_id)
533 .map(|e| (e.generation, Arc::clone(&e.provider)))
534 }
535 pub fn generation(&self, provider_id: &str) -> Option<u64> {
536 self.inner
537 .read()
538 .unwrap_or_else(|e| e.into_inner())
539 .get(provider_id)
540 .map(|e| e.generation)
541 }
542 pub fn list_ids(&self) -> Vec<String> {
543 self.inner
544 .read()
545 .unwrap_or_else(|error| error.into_inner())
546 .keys()
547 .cloned()
548 .collect()
549 }
550
551 pub fn status(&self, provider_id: &str) -> Option<ProviderStatus> {
552 let providers = self.inner.read().unwrap_or_else(|error| error.into_inner());
553 let entry = providers.get(provider_id)?;
554 Some(ProviderStatus {
555 provider_id: provider_id.to_owned(),
556 model_id: entry.provider.model_id().to_owned(),
557 model_version: entry.provider.model_version().to_owned(),
558 generation: entry.generation,
559 health: entry.provider.health(),
560 semantic_identity: entry.provider.semantic_identity(),
561 })
562 }
563 pub fn resolve(
564 &self,
565 source: &EmbeddingSource,
566 ) -> Result<Arc<dyn EmbeddingProvider>, EmbeddingError> {
567 Ok(self.resolve_with_generation(source)?.1)
568 }
569 pub fn resolve_with_generation(
570 &self,
571 source: &EmbeddingSource,
572 ) -> Result<(u64, Arc<dyn EmbeddingProvider>), EmbeddingError> {
573 let provider_id = source
574 .provider_id()
575 .ok_or(EmbeddingError::SuppliedByApplication)?;
576 self.get_with_generation(provider_id)
577 .ok_or_else(|| EmbeddingError::ProviderNotFound(provider_id.to_owned()))
578 }
579 pub fn resolve_by_semantic_identity(
580 &self,
581 identity: &EmbeddingProviderRef,
582 ) -> Result<(u64, Arc<dyn EmbeddingProvider>), EmbeddingError> {
583 let providers = self.inner.read().unwrap_or_else(|e| e.into_inner());
584 for entry in providers.values() {
585 if &entry.provider.semantic_identity() == identity {
586 return Ok((entry.generation, Arc::clone(&entry.provider)));
587 }
588 }
589 Err(EmbeddingError::ProviderNotFound(format!(
590 "semantic identity provider_id={} model_id={} model_version={}",
591 identity.provider_id, identity.model_id, identity.model_version
592 )))
593 }
594
595 pub fn embed(
596 &self,
597 source: &EmbeddingSource,
598 texts: &[&str],
599 expected_dim: u32,
600 ) -> Result<Vec<Vec<f32>>, EmbeddingError> {
601 self.embed_controlled(
602 source,
603 texts,
604 expected_dim,
605 &crate::ExecutionControl::new(None),
606 "embedding",
607 EmbeddingLimits::default(),
608 )
609 }
610
611 pub fn embed_controlled(
612 &self,
613 source: &EmbeddingSource,
614 texts: &[&str],
615 expected_dim: u32,
616 control: &crate::ExecutionControl,
617 trace_id: &str,
618 limits: EmbeddingLimits,
619 ) -> Result<Vec<Vec<f32>>, EmbeddingError> {
620 control
621 .checkpoint()
622 .map_err(|error| EmbeddingError::Execution(error.to_string()))?;
623 validate_request(texts, expected_dim, limits)?;
624 let provider = self.resolve(source)?;
625 validate_identity(source, provider.as_ref())?;
626 if provider.health() == ProviderHealth::Unavailable {
627 return Err(EmbeddingError::ProviderFailed {
628 provider: provider.provider_id().to_owned(),
629 message: "provider unavailable".into(),
630 });
631 }
632 if provider.dimension() != expected_dim {
633 return Err(EmbeddingError::DimensionMismatch {
634 expected: expected_dim,
635 got: provider.dimension(),
636 });
637 }
638 let response = provider.embed(EmbeddingRequest {
639 texts,
640 control,
641 trace_id,
642 })?;
643 control
644 .checkpoint()
645 .map_err(|error| EmbeddingError::Execution(error.to_string()))?;
646 validate_response(
647 provider.as_ref(),
648 texts.len(),
649 expected_dim,
650 limits,
651 &response.vectors,
652 )?;
653 Ok(response.vectors)
654 }
655
656 pub async fn embed_async_controlled(
662 &self,
663 source: &EmbeddingSource,
664 texts: &[&str],
665 expected_dim: u32,
666 control: &crate::ExecutionControl,
667 trace_id: &str,
668 limits: EmbeddingLimits,
669 ) -> Result<Vec<Vec<f32>>, EmbeddingError> {
670 control
671 .checkpoint()
672 .map_err(|error| EmbeddingError::Execution(error.to_string()))?;
673 validate_request(texts, expected_dim, limits)?;
674 let provider = self.resolve(source)?;
675 validate_identity(source, provider.as_ref())?;
676 if provider.health() == ProviderHealth::Unavailable {
677 return Err(EmbeddingError::ProviderFailed {
678 provider: provider.provider_id().to_owned(),
679 message: "provider unavailable".into(),
680 });
681 }
682 if provider.dimension() != expected_dim {
683 return Err(EmbeddingError::DimensionMismatch {
684 expected: expected_dim,
685 got: provider.dimension(),
686 });
687 }
688
689 let response = match provider.execution_mode() {
690 ProviderExecutionMode::Cooperative | ProviderExecutionMode::Remote => {
691 let request = EmbeddingRequest {
692 texts,
693 control,
694 trace_id,
695 };
696 tokio::select! {
697 result = provider.embed_async(request) => result?,
698 _ = control.cancelled() => {
699 return Err(execution_stopped(control));
700 }
701 }
702 }
703 ProviderExecutionMode::Blocking => {
704 let permit = tokio::select! {
705 result = Arc::clone(&self.blocking_slots).acquire_owned() => {
706 result.map_err(|_| EmbeddingError::Execution(
707 "embedding blocking executor closed".into()
708 ))?
709 }
710 _ = control.cancelled() => {
711 return Err(execution_stopped(control));
712 }
713 };
714 let provider = Arc::clone(&provider);
715 let owned_texts = texts
716 .iter()
717 .map(|text| (*text).to_owned())
718 .collect::<Vec<_>>();
719 let owned_control = control.clone();
720 let owned_trace_id = trace_id.to_owned();
721 let task = tokio::task::spawn_blocking(move || {
722 let _permit = permit;
723 let text_refs = owned_texts.iter().map(String::as_str).collect::<Vec<_>>();
724 provider.embed(EmbeddingRequest {
725 texts: &text_refs,
726 control: &owned_control,
727 trace_id: &owned_trace_id,
728 })
729 });
730 tokio::select! {
731 result = task => {
732 result
733 .map_err(|error| EmbeddingError::Execution(
734 format!("embedding blocking task failed: {error}")
735 ))??
736 }
737 _ = control.cancelled() => {
738 return Err(execution_stopped(control));
739 }
740 }
741 }
742 };
743
744 control
745 .checkpoint()
746 .map_err(|error| EmbeddingError::Execution(error.to_string()))?;
747 validate_response(
748 provider.as_ref(),
749 texts.len(),
750 expected_dim,
751 limits,
752 &response.vectors,
753 )?;
754 Ok(response.vectors)
755 }
756}
757
758fn execution_stopped(control: &crate::ExecutionControl) -> EmbeddingError {
759 let message = control
760 .checkpoint()
761 .expect_err("cancelled future completed without an execution error")
762 .to_string();
763 EmbeddingError::Execution(message)
764}
765
766fn validate_identity(
767 source: &EmbeddingSource,
768 provider: &dyn EmbeddingProvider,
769) -> Result<(), EmbeddingError> {
770 if let EmbeddingSource::GeneratedColumnSpec { spec } = source {
771 if spec.normalization != provider.normalization() {
772 return Err(EmbeddingError::NormalizationMismatch);
773 }
774 if spec.dimension != provider.dimension() {
775 return Err(EmbeddingError::DimensionMismatch {
776 expected: spec.dimension,
777 got: provider.dimension(),
778 });
779 }
780 }
781 if source
782 .provider_id()
783 .is_some_and(|id| id != provider.provider_id())
784 {
785 return Err(EmbeddingError::ProviderIdentityMismatch {
786 expected: source.provider_id().unwrap_or_default().to_owned(),
787 got: provider.provider_id().to_owned(),
788 });
789 }
790 if source
791 .model_id()
792 .is_some_and(|id| id != provider.model_id())
793 {
794 return Err(EmbeddingError::ModelIdentityMismatch {
795 expected: source.model_id().unwrap_or_default().to_owned(),
796 got: provider.model_id().to_owned(),
797 });
798 }
799 if source
800 .model_version()
801 .is_some_and(|version| version != provider.model_version())
802 {
803 return Err(EmbeddingError::ModelVersionMismatch {
804 expected: source.model_version().unwrap_or_default().to_owned(),
805 got: provider.model_version().to_owned(),
806 });
807 }
808 Ok(())
809}
810
811fn validate_request(
812 texts: &[&str],
813 dimension: u32,
814 limits: EmbeddingLimits,
815) -> Result<(), EmbeddingError> {
816 if texts.len() > limits.max_texts {
817 return Err(EmbeddingError::LimitExceeded {
818 resource: "text count",
819 requested: texts.len(),
820 limit: limits.max_texts,
821 });
822 }
823 if dimension > limits.max_dimension {
824 return Err(EmbeddingError::LimitExceeded {
825 resource: "dimension",
826 requested: dimension as usize,
827 limit: limits.max_dimension as usize,
828 });
829 }
830 let mut total = 0usize;
831 for text in texts {
832 if text.len() > limits.max_text_bytes {
833 return Err(EmbeddingError::LimitExceeded {
834 resource: "text bytes",
835 requested: text.len(),
836 limit: limits.max_text_bytes,
837 });
838 }
839 total = total.saturating_add(text.len());
840 }
841 if total > limits.max_total_input_bytes {
842 return Err(EmbeddingError::LimitExceeded {
843 resource: "total input bytes",
844 requested: total,
845 limit: limits.max_total_input_bytes,
846 });
847 }
848 Ok(())
849}
850
851pub fn validate_response(
852 provider: &dyn EmbeddingProvider,
853 expected_count: usize,
854 expected_dim: u32,
855 limits: EmbeddingLimits,
856 vectors: &[Vec<f32>],
857) -> Result<(), EmbeddingError> {
858 if vectors.len() != expected_count {
859 return Err(EmbeddingError::OutputCountMismatch {
860 expected: expected_count,
861 got: vectors.len(),
862 });
863 }
864 let output_bytes = vectors
865 .len()
866 .saturating_mul(expected_dim as usize)
867 .saturating_mul(std::mem::size_of::<f32>());
868 if output_bytes > limits.max_output_bytes {
869 return Err(EmbeddingError::LimitExceeded {
870 resource: "output bytes",
871 requested: output_bytes,
872 limit: limits.max_output_bytes,
873 });
874 }
875 for vector in vectors {
876 if vector.len() as u32 != expected_dim {
877 return Err(EmbeddingError::DimensionMismatch {
878 expected: expected_dim,
879 got: vector.len() as u32,
880 });
881 }
882 if vector.iter().any(|value| !value.is_finite()) {
883 return Err(EmbeddingError::NonFiniteOutput);
884 }
885 if provider.normalization() == EmbeddingNormalization::L2 {
886 let norm = vector
887 .iter()
888 .map(|value| (*value as f64) * (*value as f64))
889 .sum::<f64>()
890 .sqrt();
891 if (norm - 1.0).abs() > 1e-4 {
892 return Err(EmbeddingError::NormalizationMismatch);
893 }
894 }
895 }
896 Ok(())
897}
898
899#[derive(Debug, Clone)]
900pub struct FixedVectorProvider {
901 pub id: String,
902 pub model_id: String,
903 pub model_version: String,
904 pub normalization: EmbeddingNormalization,
905 pub vector: Vec<f32>,
906 pub provider_version: String,
907 pub model_artifact_sha256: Option<[u8; 32]>,
908 pub tokenizer_sha256: Option<[u8; 32]>,
909 pub preprocessing_sha256: Option<[u8; 32]>,
910}
911impl FixedVectorProvider {
912 pub fn new(
913 id: impl Into<String>,
914 model_id: impl Into<String>,
915 model_version: impl Into<String>,
916 normalization: EmbeddingNormalization,
917 vector: Vec<f32>,
918 ) -> Self {
919 Self {
920 id: id.into(),
921 model_id: model_id.into(),
922 model_version: model_version.into(),
923 normalization,
924 vector,
925 provider_version: "1".into(),
926 model_artifact_sha256: None,
927 tokenizer_sha256: None,
928 preprocessing_sha256: None,
929 }
930 }
931 pub fn with_artifact_sha256(mut self, sha: [u8; 32]) -> Self {
932 self.model_artifact_sha256 = Some(sha);
933 self
934 }
935}
936impl EmbeddingProvider for FixedVectorProvider {
937 fn provider_id(&self) -> &str {
938 &self.id
939 }
940 fn model_id(&self) -> &str {
941 &self.model_id
942 }
943 fn model_version(&self) -> &str {
944 &self.model_version
945 }
946 fn dimension(&self) -> u32 {
947 self.vector.len() as u32
948 }
949 fn normalization(&self) -> EmbeddingNormalization {
950 self.normalization
951 }
952 fn preprocessing_version(&self) -> &str {
953 "fixed-test-v1"
954 }
955 fn provider_version(&self) -> &str {
956 &self.provider_version
957 }
958 fn model_artifact_sha256(&self) -> [u8; 32] {
959 self.model_artifact_sha256.unwrap_or_else(|| {
960 let mut h = Sha256::new();
961 h.update(b"model-artifact:");
962 h.update(self.model_id.as_bytes());
963 h.update([0]);
964 h.update(self.model_version.as_bytes());
965 h.finalize().into()
966 })
967 }
968 fn tokenizer_sha256(&self) -> [u8; 32] {
969 self.tokenizer_sha256.unwrap_or_else(|| {
970 let mut h = Sha256::new();
971 h.update(b"tokenizer:");
972 h.update(self.model_id.as_bytes());
973 h.update([0]);
974 h.update(b"fixed-test-v1");
975 h.finalize().into()
976 })
977 }
978 fn preprocessing_sha256(&self) -> [u8; 32] {
979 self.preprocessing_sha256.unwrap_or_else(|| {
980 let mut h = Sha256::new();
981 h.update(b"preprocessing:");
982 h.update(b"fixed-test-v1");
983 h.update([0]);
984 h.update([self.normalization as u8]);
985 h.finalize().into()
986 })
987 }
988 fn embed(&self, request: EmbeddingRequest<'_>) -> Result<EmbeddingResponse, EmbeddingError> {
989 request
990 .control
991 .checkpoint()
992 .map_err(|e| EmbeddingError::Execution(e.to_string()))?;
993 Ok(EmbeddingResponse {
994 vectors: request.texts.iter().map(|_| self.vector.clone()).collect(),
995 })
996 }
997}
998
999#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1000pub struct EmbeddingModelMeta {
1001 pub provider_id: Option<String>,
1002 pub model_id: Option<String>,
1003 pub model_version: Option<String>,
1004 pub preprocessing_version: Option<String>,
1005 pub normalization: EmbeddingNormalization,
1006 pub dimension: u32,
1007 pub source_kind: Option<String>,
1008 #[serde(default)]
1009 pub semantic_identity: Option<EmbeddingProviderRef>,
1010}
1011impl EmbeddingModelMeta {
1012 pub fn from_provider(source: &EmbeddingSource, provider: &dyn EmbeddingProvider) -> Self {
1013 Self {
1014 provider_id: Some(provider.provider_id().to_owned()),
1015 model_id: Some(provider.model_id().to_owned()),
1016 model_version: Some(provider.model_version().to_owned()),
1017 preprocessing_version: Some(provider.preprocessing_version().to_owned()),
1018 normalization: provider.normalization(),
1019 dimension: provider.dimension(),
1020 source_kind: Some(source.label().to_owned()),
1021 semantic_identity: Some(provider.semantic_identity()),
1022 }
1023 }
1024}
1025#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1026pub struct TextSearchOptions {
1027 pub k: usize,
1028}
1029impl Default for TextSearchOptions {
1030 fn default() -> Self {
1031 Self { k: 10 }
1032 }
1033}
1034impl TextSearchOptions {
1035 pub fn new(k: usize) -> Self {
1036 Self { k }
1037 }
1038}
1039#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1040pub struct TextRetrieveProvenance {
1041 pub semantic_identity: EmbeddingProviderRef,
1042 pub provider_registry_generation: u64,
1043 pub query_source_fingerprint: [u8; 32],
1044 pub embedding_column: u16,
1045}
1046#[derive(Debug, Clone, PartialEq)]
1047pub struct TextRetrieveResult {
1048 pub hits: Vec<crate::query::RetrieverHit>,
1049 pub provenance: TextRetrieveProvenance,
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054 use super::*;
1055
1056 struct SlowBlockingProvider;
1057
1058 impl EmbeddingProvider for SlowBlockingProvider {
1059 fn provider_id(&self) -> &str {
1060 "slow-blocking"
1061 }
1062 fn model_id(&self) -> &str {
1063 "slow"
1064 }
1065 fn model_version(&self) -> &str {
1066 "1"
1067 }
1068 fn dimension(&self) -> u32 {
1069 2
1070 }
1071 fn normalization(&self) -> EmbeddingNormalization {
1072 EmbeddingNormalization::None
1073 }
1074 fn preprocessing_version(&self) -> &str {
1075 "slow-v1"
1076 }
1077 fn execution_mode(&self) -> ProviderExecutionMode {
1078 ProviderExecutionMode::Blocking
1079 }
1080 fn embed(
1081 &self,
1082 request: EmbeddingRequest<'_>,
1083 ) -> Result<EmbeddingResponse, EmbeddingError> {
1084 std::thread::sleep(std::time::Duration::from_millis(50));
1085 Ok(EmbeddingResponse {
1086 vectors: request.texts.iter().map(|_| vec![1.0, 2.0]).collect(),
1087 })
1088 }
1089 }
1090
1091 fn provider() -> Arc<FixedVectorProvider> {
1092 Arc::new(FixedVectorProvider::new(
1093 "local-test",
1094 "fixed",
1095 "1",
1096 EmbeddingNormalization::L2,
1097 vec![0.0, 1.0],
1098 ))
1099 }
1100
1101 fn source() -> EmbeddingSource {
1102 EmbeddingSource::GeneratedColumnSpec {
1103 spec: GeneratedEmbeddingSpec {
1104 provider_id: "local-test".into(),
1105 model_id: "fixed".into(),
1106 model_version: "1".into(),
1107 source_columns: vec![1],
1108 input_template: "{text}".into(),
1109 dimension: 2,
1110 normalization: EmbeddingNormalization::L2,
1111 failure_policy: EmbeddingFailurePolicy::AbortWrite,
1112 },
1113 }
1114 }
1115
1116 #[test]
1117 fn registration_requires_expected_generation() {
1118 let registry = EmbeddingProviderRegistry::new();
1119 assert_eq!(registry.register_new(provider()).unwrap(), 1);
1120 assert!(matches!(
1121 registry.register_new(provider()),
1122 Err(EmbeddingError::ProviderAlreadyRegistered(_))
1123 ));
1124 assert!(matches!(
1125 registry.replace(9, provider()),
1126 Err(EmbeddingError::ProviderGenerationMismatch { .. })
1127 ));
1128 assert_eq!(registry.replace(1, provider()).unwrap(), 2);
1129 assert_eq!(registry.status("local-test").unwrap().generation, 2);
1130 }
1131
1132 #[test]
1133 fn validates_count_dimension_finiteness_limits_and_cancellation() {
1134 let registry = EmbeddingProviderRegistry::new();
1135 registry.register_new(provider()).unwrap();
1136 assert_eq!(registry.embed(&source(), &["a", "b"], 2).unwrap().len(), 2);
1137 let control = crate::ExecutionControl::new(None);
1138 control.cancel(crate::CancellationReason::ClientRequest);
1139 assert!(matches!(
1140 registry.embed_controlled(
1141 &source(),
1142 &["a"],
1143 2,
1144 &control,
1145 "cancelled",
1146 EmbeddingLimits::default(),
1147 ),
1148 Err(EmbeddingError::Execution(_))
1149 ));
1150 assert!(matches!(
1151 registry.embed_controlled(
1152 &source(),
1153 &["too long"],
1154 2,
1155 &crate::ExecutionControl::new(None),
1156 "bounded",
1157 EmbeddingLimits {
1158 max_text_bytes: 2,
1159 ..EmbeddingLimits::default()
1160 },
1161 ),
1162 Err(EmbeddingError::LimitExceeded { .. })
1163 ));
1164 }
1165
1166 #[test]
1167 fn legacy_local_model_path_is_process_local_only() {
1168 let source = EmbeddingSource::LocalModel {
1169 model_id: "legacy".into(),
1170 model_path: "/node-only/model.onnx".into(),
1171 };
1172 let encoded = serde_json::to_string(&source).unwrap();
1173 assert!(!encoded.contains("node-only"));
1174 assert_eq!(
1175 serde_json::from_str::<EmbeddingSource>(&encoded).unwrap(),
1176 EmbeddingSource::LocalModel {
1177 model_id: "legacy".into(),
1178 model_path: std::path::PathBuf::new(),
1179 }
1180 );
1181 }
1182
1183 struct WrongCount;
1184 impl EmbeddingProvider for WrongCount {
1185 fn provider_id(&self) -> &str {
1186 "wrong"
1187 }
1188 fn model_id(&self) -> &str {
1189 "wrong-model"
1190 }
1191 fn model_version(&self) -> &str {
1192 "1"
1193 }
1194 fn dimension(&self) -> u32 {
1195 2
1196 }
1197 fn normalization(&self) -> EmbeddingNormalization {
1198 EmbeddingNormalization::None
1199 }
1200 fn preprocessing_version(&self) -> &str {
1201 "1"
1202 }
1203 fn embed(
1204 &self,
1205 _request: EmbeddingRequest<'_>,
1206 ) -> Result<EmbeddingResponse, EmbeddingError> {
1207 Ok(EmbeddingResponse {
1208 vectors: Vec::new(),
1209 })
1210 }
1211 }
1212
1213 #[test]
1214 fn rejects_wrong_output_count() {
1215 let registry = EmbeddingProviderRegistry::new();
1216 registry.register_new(Arc::new(WrongCount)).unwrap();
1217 let source = EmbeddingSource::ConfiguredModel {
1218 provider_id: "wrong".into(),
1219 model_id: "wrong-model".into(),
1220 model_version: "1".into(),
1221 };
1222 assert!(matches!(
1223 registry.embed(&source, &["a"], 2),
1224 Err(EmbeddingError::OutputCountMismatch {
1225 expected: 1,
1226 got: 0
1227 })
1228 ));
1229 }
1230
1231 #[tokio::test(flavor = "current_thread")]
1232 async fn async_path_bounds_blocking_provider_and_honors_deadline() {
1233 let registry = EmbeddingProviderRegistry::with_max_blocking_concurrency(1);
1234 registry
1235 .register_new(Arc::new(SlowBlockingProvider))
1236 .unwrap();
1237 let source = EmbeddingSource::ConfiguredModel {
1238 provider_id: "slow-blocking".into(),
1239 model_id: "slow".into(),
1240 model_version: "1".into(),
1241 };
1242 let control = crate::ExecutionControl::with_timeout(std::time::Duration::from_millis(5));
1243 assert!(matches!(
1244 registry
1245 .embed_async_controlled(
1246 &source,
1247 &["text"],
1248 2,
1249 &control,
1250 "deadline-test",
1251 EmbeddingLimits::default(),
1252 )
1253 .await,
1254 Err(EmbeddingError::Execution(_))
1255 ));
1256 }
1257}