1use serde::{Deserialize, Serialize};
6use std::time::SystemTime;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ModelProvenance {
31 pub model: EmbeddingModel,
33 pub model_id: String,
35 pub hash: String,
41 pub loaded_at: SystemTime,
43 pub loaded_at_iso: String,
45}
46
47impl ModelProvenance {
48 pub fn new(model: EmbeddingModel, model_id: String) -> Self {
50 let loaded_at = SystemTime::now();
51 let loaded_at_iso = {
52 let dt: chrono::DateTime<chrono::Utc> = loaded_at.into();
53 dt.to_rfc3339()
54 };
55
56 let hash_input = format!("{model_id}:{loaded_at_iso}:{model:?}");
58 let hash = blake3::hash(hash_input.as_bytes()).to_hex().to_string();
59
60 Self {
61 model,
62 model_id,
63 hash,
64 loaded_at,
65 loaded_at_iso,
66 }
67 }
68
69 pub fn dimensions(&self) -> usize {
71 self.model.dimensions()
72 }
73
74 pub fn matches_model(&self, expected: EmbeddingModel) -> bool {
76 self.model == expected
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
98#[serde(rename_all = "snake_case")]
99#[non_exhaustive]
100pub enum EmbeddingModel {
101 #[default]
103 #[serde(alias = "BgeSmallEnV15")]
104 BgeSmallEnV15,
105
106 #[serde(alias = "BgeBaseEnV15")]
108 BgeBaseEnV15,
109
110 #[serde(alias = "BgeLargeEnV15")]
112 BgeLargeEnV15,
113
114 #[serde(alias = "MultilingualE5Small")]
116 MultilingualE5Small,
117
118 #[serde(alias = "MultilingualE5Base")]
120 MultilingualE5Base,
121
122 #[serde(alias = "Qwen3Embedding0_6B")]
124 Qwen3Embedding0_6B,
125
126 #[serde(alias = "Qwen3Embedding4B")]
128 Qwen3Embedding4B,
129
130 #[serde(alias = "AllMiniLmL6V2")]
132 AllMiniLmL6V2,
133
134 #[serde(alias = "ParaphraseMultilingualMiniLmL12V2")]
136 ParaphraseMultilingualMiniLmL12V2,
137
138 #[serde(alias = "TextEmbedding3Small")]
140 TextEmbedding3Small,
141}
142
143impl EmbeddingModel {
144 #[inline]
149 pub const fn native_dimensions(&self) -> usize {
150 match self {
151 EmbeddingModel::BgeSmallEnV15
152 | EmbeddingModel::MultilingualE5Small
153 | EmbeddingModel::AllMiniLmL6V2
154 | EmbeddingModel::ParaphraseMultilingualMiniLmL12V2 => 384,
155 EmbeddingModel::BgeBaseEnV15 | EmbeddingModel::MultilingualE5Base => 768,
156 EmbeddingModel::BgeLargeEnV15 | EmbeddingModel::Qwen3Embedding0_6B => 1024,
157 EmbeddingModel::Qwen3Embedding4B => 2560,
158 EmbeddingModel::TextEmbedding3Small => 1536,
159 }
160 }
161
162 #[inline]
174 pub const fn dimensions(&self) -> usize {
175 self.native_dimensions()
176 }
177
178 #[inline]
180 pub const fn is_local(&self) -> bool {
181 matches!(
182 self,
183 EmbeddingModel::BgeSmallEnV15
184 | EmbeddingModel::BgeBaseEnV15
185 | EmbeddingModel::BgeLargeEnV15
186 | EmbeddingModel::MultilingualE5Small
187 | EmbeddingModel::MultilingualE5Base
188 | EmbeddingModel::AllMiniLmL6V2
189 | EmbeddingModel::ParaphraseMultilingualMiniLmL12V2
190 | EmbeddingModel::Qwen3Embedding0_6B
191 | EmbeddingModel::Qwen3Embedding4B
192 )
193 }
194
195 #[inline]
197 pub const fn is_remote(&self) -> bool {
198 matches!(self, EmbeddingModel::TextEmbedding3Small)
199 }
200
201 #[inline]
211 pub const fn max_input_tokens(&self) -> usize {
212 match self {
213 EmbeddingModel::BgeSmallEnV15 => 512,
215 EmbeddingModel::BgeBaseEnV15 => 512,
216 EmbeddingModel::BgeLargeEnV15 => 512,
217 EmbeddingModel::MultilingualE5Small => 512,
219 EmbeddingModel::MultilingualE5Base => 512,
220 EmbeddingModel::AllMiniLmL6V2 => 256,
222 EmbeddingModel::ParaphraseMultilingualMiniLmL12V2 => 128,
224 EmbeddingModel::Qwen3Embedding0_6B => 8192,
226 EmbeddingModel::Qwen3Embedding4B => 8192,
227 EmbeddingModel::TextEmbedding3Small => 8191,
229 }
230 }
231
232 #[inline]
255 pub const fn query_instruction(&self) -> Option<&'static str> {
256 match self {
257 EmbeddingModel::MultilingualE5Small | EmbeddingModel::MultilingualE5Base => {
258 Some("query: ")
261 }
262 EmbeddingModel::Qwen3Embedding0_6B | EmbeddingModel::Qwen3Embedding4B => Some(
263 "Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery: ",
264 ),
265 EmbeddingModel::BgeSmallEnV15
266 | EmbeddingModel::BgeBaseEnV15
267 | EmbeddingModel::BgeLargeEnV15 => {
268 Some("Represent this sentence for searching relevant passages: ")
272 }
273 _ => None,
274 }
275 }
276
277 #[inline]
290 pub const fn document_instruction(&self) -> Option<&'static str> {
291 match self {
292 EmbeddingModel::MultilingualE5Small | EmbeddingModel::MultilingualE5Base => {
293 Some("passage: ")
295 }
296 _ => None,
297 }
298 }
299
300 #[inline]
302 pub const fn model_id(&self) -> &'static str {
303 match self {
304 EmbeddingModel::BgeSmallEnV15 => "BAAI/bge-small-en-v1.5",
305 EmbeddingModel::BgeBaseEnV15 => "BAAI/bge-base-en-v1.5",
306 EmbeddingModel::BgeLargeEnV15 => "BAAI/bge-large-en-v1.5",
307 EmbeddingModel::MultilingualE5Small => "intfloat/multilingual-e5-small",
308 EmbeddingModel::MultilingualE5Base => "intfloat/multilingual-e5-base",
309 EmbeddingModel::AllMiniLmL6V2 => "sentence-transformers/all-MiniLM-L6-v2",
310 EmbeddingModel::ParaphraseMultilingualMiniLmL12V2 => {
311 "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
312 }
313 EmbeddingModel::Qwen3Embedding0_6B => "Qwen/Qwen3-Embedding-0.6B",
314 EmbeddingModel::Qwen3Embedding4B => "Qwen/Qwen3-Embedding-4B",
315 EmbeddingModel::TextEmbedding3Small => "text-embedding-3-small",
316 }
317 }
318
319 #[inline]
321 pub const fn supports_output_dim(&self) -> bool {
322 matches!(
323 self,
324 EmbeddingModel::Qwen3Embedding0_6B | EmbeddingModel::Qwen3Embedding4B
325 )
326 }
327
328 #[cfg(feature = "native")]
339 #[inline]
340 pub const fn bert_pooling(&self) -> Option<lattice_inference::BertPooling> {
341 match self {
342 EmbeddingModel::BgeSmallEnV15
344 | EmbeddingModel::BgeBaseEnV15
345 | EmbeddingModel::BgeLargeEnV15 => Some(lattice_inference::BertPooling::CLS),
346 EmbeddingModel::MultilingualE5Small | EmbeddingModel::MultilingualE5Base => {
348 Some(lattice_inference::BertPooling::Mean)
349 }
350 EmbeddingModel::AllMiniLmL6V2 | EmbeddingModel::ParaphraseMultilingualMiniLmL12V2 => {
352 Some(lattice_inference::BertPooling::Mean)
353 }
354 EmbeddingModel::Qwen3Embedding0_6B
356 | EmbeddingModel::Qwen3Embedding4B
357 | EmbeddingModel::TextEmbedding3Small => None,
358 }
359 }
360
361 #[inline]
363 pub const fn key_version(&self) -> &'static str {
364 match self {
365 EmbeddingModel::TextEmbedding3Small
366 | EmbeddingModel::Qwen3Embedding0_6B
367 | EmbeddingModel::Qwen3Embedding4B => "v3",
368 EmbeddingModel::AllMiniLmL6V2 | EmbeddingModel::ParaphraseMultilingualMiniLmL12V2 => {
369 "v2"
370 }
371 _ => "v1.5",
372 }
373 }
374}
375
376impl std::fmt::Display for EmbeddingModel {
377 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378 match self {
379 EmbeddingModel::BgeSmallEnV15 => write!(f, "bge-small-en-v1.5"),
380 EmbeddingModel::BgeBaseEnV15 => write!(f, "bge-base-en-v1.5"),
381 EmbeddingModel::BgeLargeEnV15 => write!(f, "bge-large-en-v1.5"),
382 EmbeddingModel::MultilingualE5Small => write!(f, "multilingual-e5-small"),
383 EmbeddingModel::MultilingualE5Base => write!(f, "multilingual-e5-base"),
384 EmbeddingModel::Qwen3Embedding0_6B => write!(f, "qwen3-embedding-0.6b"),
385 EmbeddingModel::Qwen3Embedding4B => write!(f, "qwen3-embedding-4b"),
386 EmbeddingModel::AllMiniLmL6V2 => write!(f, "all-minilm-l6-v2"),
387 EmbeddingModel::ParaphraseMultilingualMiniLmL12V2 => {
388 write!(f, "paraphrase-multilingual-minilm-l12-v2")
389 }
390 EmbeddingModel::TextEmbedding3Small => write!(f, "text-embedding-3-small"),
391 }
392 }
393}
394
395impl std::str::FromStr for EmbeddingModel {
396 type Err = String;
397
398 fn from_str(s: &str) -> Result<Self, Self::Err> {
405 let lower = s.to_lowercase();
406 let normalized = lower.trim().replace("_", "-").replace("baai/", "");
407
408 match normalized.as_str() {
409 "bge-small-en-v1.5" | "bge-small-en" | "bge-small" | "small" => {
410 Ok(EmbeddingModel::BgeSmallEnV15)
411 }
412 "bge-base-en-v1.5" | "bge-base-en" | "bge-base" | "base" => {
413 Ok(EmbeddingModel::BgeBaseEnV15)
414 }
415 "bge-large-en-v1.5" | "bge-large-en" | "bge-large" | "large" => {
416 Ok(EmbeddingModel::BgeLargeEnV15)
417 }
418 "multilingual-e5-small" | "e5-small" | "intfloat/multilingual-e5-small" => {
419 Ok(EmbeddingModel::MultilingualE5Small)
420 }
421 "multilingual-e5-base" | "e5-base" | "intfloat/multilingual-e5-base" => {
422 Ok(EmbeddingModel::MultilingualE5Base)
423 }
424 "qwen3-embedding-0.6b" | "qwen3-embedding" | "qwen3" | "qwen/qwen3-embedding-0.6b" => {
425 Ok(EmbeddingModel::Qwen3Embedding0_6B)
426 }
427 "qwen3-embedding-4b" | "qwen3-4b" | "qwen/qwen3-embedding-4b" => {
428 Ok(EmbeddingModel::Qwen3Embedding4B)
429 }
430 "all-minilm-l6-v2"
431 | "minilm"
432 | "all-minilm"
433 | "sentence-transformers/all-minilm-l6-v2" => Ok(EmbeddingModel::AllMiniLmL6V2),
434 "paraphrase-multilingual-minilm-l12-v2"
435 | "paraphrase-multilingual"
436 | "multilingual-minilm"
437 | "sentence-transformers/paraphrase-multilingual-minilm-l12-v2" => {
438 Ok(EmbeddingModel::ParaphraseMultilingualMiniLmL12V2)
439 }
440 "text-embedding-3-small" | "openai-small" | "openai" => {
441 Ok(EmbeddingModel::TextEmbedding3Small)
442 }
443 _ => Err(format!(
444 "unknown embedding model: '{s}'. Valid: bge-small-en-v1.5, bge-base-en-v1.5, bge-large-en-v1.5, multilingual-e5-small, multilingual-e5-base, text-embedding-3-small"
445 )),
446 }
447 }
448}
449
450pub const MIN_MRL_OUTPUT_DIM: usize = 32;
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
462pub struct ModelConfig {
463 pub model: EmbeddingModel,
465 #[serde(default)]
467 pub output_dim: Option<usize>,
468}
469
470impl Default for ModelConfig {
471 fn default() -> Self {
472 Self::new(EmbeddingModel::default())
473 }
474}
475
476impl ModelConfig {
477 pub const fn new(model: EmbeddingModel) -> Self {
479 Self {
480 model,
481 output_dim: None,
482 }
483 }
484
485 pub fn try_new(
487 model: EmbeddingModel,
488 output_dim: Option<usize>,
489 ) -> std::result::Result<Self, crate::error::EmbedError> {
490 let config = Self { model, output_dim };
491 config.validate()?;
492 Ok(config)
493 }
494
495 pub fn validate(&self) -> std::result::Result<(), crate::error::EmbedError> {
497 let Some(dim) = self.output_dim else {
498 return Ok(());
499 };
500 if !self.model.supports_output_dim() {
501 return Err(crate::error::EmbedError::InvalidInput(format!(
502 "{} does not support configurable embedding dimensions",
503 self.model
504 )));
505 }
506 if dim < MIN_MRL_OUTPUT_DIM {
507 return Err(crate::error::EmbedError::InvalidInput(format!(
508 "embedding output dimension {dim} is below minimum {MIN_MRL_OUTPUT_DIM}"
509 )));
510 }
511 let native = self.model.native_dimensions();
512 if dim > native {
513 return Err(crate::error::EmbedError::InvalidInput(format!(
514 "embedding output dimension {dim} exceeds native dimension {native} for {}",
515 self.model
516 )));
517 }
518 Ok(())
519 }
520
521 pub fn dimensions(&self) -> usize {
523 self.output_dim
524 .unwrap_or_else(|| self.model.native_dimensions())
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 #[test]
533 fn test_default_model() {
534 let model = EmbeddingModel::default();
535 assert_eq!(model, EmbeddingModel::BgeSmallEnV15);
536 }
537
538 #[test]
539 fn test_model_provenance_new() {
540 let provenance = ModelProvenance::new(
541 EmbeddingModel::BgeSmallEnV15,
542 "BAAI/bge-small-en-v1.5".into(),
543 );
544
545 assert_eq!(provenance.model, EmbeddingModel::BgeSmallEnV15);
546 assert_eq!(provenance.model_id, "BAAI/bge-small-en-v1.5");
547 assert!(!provenance.hash.is_empty());
548 assert_eq!(provenance.hash.len(), 64); assert!(!provenance.loaded_at_iso.is_empty());
550 }
551
552 #[test]
553 fn test_model_provenance_unique_hash() {
554 let p1 = ModelProvenance::new(EmbeddingModel::BgeSmallEnV15, "model1".into());
555 std::thread::sleep(std::time::Duration::from_millis(10)); let p2 = ModelProvenance::new(EmbeddingModel::BgeSmallEnV15, "model1".into());
557
558 assert_ne!(p1.hash, p2.hash);
560 }
561
562 #[test]
563 fn test_model_provenance_dimensions() {
564 let p1 = ModelProvenance::new(EmbeddingModel::BgeSmallEnV15, "small".into());
565 assert_eq!(p1.dimensions(), 384);
566
567 let p2 = ModelProvenance::new(EmbeddingModel::BgeBaseEnV15, "base".into());
568 assert_eq!(p2.dimensions(), 768);
569
570 let p3 = ModelProvenance::new(EmbeddingModel::BgeLargeEnV15, "large".into());
571 assert_eq!(p3.dimensions(), 1024);
572 }
573
574 #[test]
575 fn test_model_provenance_matches_model() {
576 let provenance = ModelProvenance::new(EmbeddingModel::BgeSmallEnV15, "test".into());
577
578 assert!(provenance.matches_model(EmbeddingModel::BgeSmallEnV15));
579 assert!(!provenance.matches_model(EmbeddingModel::BgeBaseEnV15));
580 assert!(!provenance.matches_model(EmbeddingModel::BgeLargeEnV15));
581 }
582
583 #[test]
584 fn test_model_provenance_serialization() {
585 let provenance = ModelProvenance::new(EmbeddingModel::BgeSmallEnV15, "test-model".into());
586
587 let json = serde_json::to_string(&provenance).unwrap();
588 assert!(json.contains("bge_small_en_v15"), "json={json}");
591 assert!(json.contains("test-model"));
592 assert!(json.contains(&provenance.hash));
593
594 let parsed: ModelProvenance = serde_json::from_str(&json).unwrap();
595 assert_eq!(parsed.model, provenance.model);
596 assert_eq!(parsed.model_id, provenance.model_id);
597 assert_eq!(parsed.hash, provenance.hash);
598 }
599
600 #[test]
601 fn test_dimensions() {
602 assert_eq!(EmbeddingModel::BgeSmallEnV15.dimensions(), 384);
603 assert_eq!(EmbeddingModel::BgeBaseEnV15.dimensions(), 768);
604 assert_eq!(EmbeddingModel::BgeLargeEnV15.dimensions(), 1024);
605 assert_eq!(EmbeddingModel::Qwen3Embedding4B.dimensions(), 2560);
606 }
607
608 #[test]
609 fn test_model_config_native_dims() {
610 assert_eq!(
611 ModelConfig::new(EmbeddingModel::Qwen3Embedding4B).dimensions(),
612 2560
613 );
614 assert_eq!(
615 ModelConfig::new(EmbeddingModel::Qwen3Embedding0_6B).dimensions(),
616 1024
617 );
618 assert_eq!(
619 ModelConfig::new(EmbeddingModel::BgeSmallEnV15).dimensions(),
620 384
621 );
622 }
623
624 #[test]
625 fn test_model_config_configured_dim() {
626 let cfg = ModelConfig::try_new(EmbeddingModel::Qwen3Embedding4B, Some(1024)).unwrap();
627 assert_eq!(cfg.dimensions(), 1024);
628
629 let cfg = ModelConfig::try_new(EmbeddingModel::Qwen3Embedding0_6B, Some(512)).unwrap();
630 assert_eq!(cfg.dimensions(), 512);
631 }
632
633 #[test]
634 fn test_model_config_validation_below_min() {
635 assert!(ModelConfig::try_new(EmbeddingModel::Qwen3Embedding4B, Some(31)).is_err());
636 assert!(ModelConfig::try_new(EmbeddingModel::Qwen3Embedding4B, Some(0)).is_err());
637 }
638
639 #[test]
640 fn test_model_config_validation_above_native() {
641 assert!(ModelConfig::try_new(EmbeddingModel::Qwen3Embedding4B, Some(2561)).is_err());
642 assert!(ModelConfig::try_new(EmbeddingModel::Qwen3Embedding0_6B, Some(1025)).is_err());
643 }
644
645 #[test]
646 fn test_model_config_validation_non_mrl_model() {
647 assert!(ModelConfig::try_new(EmbeddingModel::BgeSmallEnV15, Some(128)).is_err());
648 assert!(ModelConfig::try_new(EmbeddingModel::BgeBaseEnV15, Some(512)).is_err());
649 }
650
651 #[test]
652 fn test_model_config_none_output_dim_ok_for_any_model() {
653 assert!(ModelConfig::try_new(EmbeddingModel::BgeSmallEnV15, None).is_ok());
654 assert!(ModelConfig::try_new(EmbeddingModel::Qwen3Embedding4B, None).is_ok());
655 }
656
657 #[test]
658 fn test_is_local() {
659 assert!(EmbeddingModel::BgeSmallEnV15.is_local());
660 assert!(EmbeddingModel::BgeBaseEnV15.is_local());
661 assert!(EmbeddingModel::BgeLargeEnV15.is_local());
662 }
663
664 #[test]
665 fn test_display() {
666 assert_eq!(
667 EmbeddingModel::BgeSmallEnV15.to_string(),
668 "bge-small-en-v1.5"
669 );
670 assert_eq!(EmbeddingModel::BgeBaseEnV15.to_string(), "bge-base-en-v1.5");
671 assert_eq!(
672 EmbeddingModel::BgeLargeEnV15.to_string(),
673 "bge-large-en-v1.5"
674 );
675 }
676
677 #[test]
678 fn test_serialization_roundtrip() {
679 let model = EmbeddingModel::BgeSmallEnV15;
680 let json = serde_json::to_string(&model).unwrap();
681 let parsed: EmbeddingModel = serde_json::from_str(&json).unwrap();
682 assert_eq!(model, parsed);
683 }
684
685 #[test]
686 fn test_max_input_tokens() {
687 assert_eq!(EmbeddingModel::BgeSmallEnV15.max_input_tokens(), 512);
688 assert_eq!(EmbeddingModel::BgeBaseEnV15.max_input_tokens(), 512);
689 assert_eq!(EmbeddingModel::BgeLargeEnV15.max_input_tokens(), 512);
690 }
691
692 #[test]
693 fn test_from_str_display_names() {
694 assert_eq!(
695 "bge-small-en-v1.5".parse::<EmbeddingModel>().unwrap(),
696 EmbeddingModel::BgeSmallEnV15
697 );
698 assert_eq!(
699 "bge-base-en-v1.5".parse::<EmbeddingModel>().unwrap(),
700 EmbeddingModel::BgeBaseEnV15
701 );
702 assert_eq!(
703 "bge-large-en-v1.5".parse::<EmbeddingModel>().unwrap(),
704 EmbeddingModel::BgeLargeEnV15
705 );
706 }
707
708 #[test]
709 fn test_from_str_short_names() {
710 assert_eq!(
711 "small".parse::<EmbeddingModel>().unwrap(),
712 EmbeddingModel::BgeSmallEnV15
713 );
714 assert_eq!(
715 "bge-base".parse::<EmbeddingModel>().unwrap(),
716 EmbeddingModel::BgeBaseEnV15
717 );
718 assert_eq!(
719 "LARGE".parse::<EmbeddingModel>().unwrap(), EmbeddingModel::BgeLargeEnV15
721 );
722 }
723
724 #[test]
725 fn test_from_str_huggingface_ids() {
726 assert_eq!(
727 "BAAI/bge-small-en-v1.5".parse::<EmbeddingModel>().unwrap(),
728 EmbeddingModel::BgeSmallEnV15
729 );
730 }
731
732 #[test]
733 fn test_from_str_invalid() {
734 let result = "unknown-model".parse::<EmbeddingModel>();
735 assert!(result.is_err());
736 assert!(result.unwrap_err().contains("unknown embedding model"));
737 }
738
739 #[cfg(feature = "native")]
745 #[test]
746 fn test_bge_models_use_cls_pooling() {
747 use lattice_inference::BertPooling;
748
749 assert_eq!(
750 EmbeddingModel::BgeSmallEnV15.bert_pooling(),
751 Some(BertPooling::CLS),
752 "BgeSmallEnV15 must use CLS pooling"
753 );
754 assert_eq!(
755 EmbeddingModel::BgeBaseEnV15.bert_pooling(),
756 Some(BertPooling::CLS),
757 "BgeBaseEnV15 must use CLS pooling"
758 );
759 assert_eq!(
760 EmbeddingModel::BgeLargeEnV15.bert_pooling(),
761 Some(BertPooling::CLS),
762 "BgeLargeEnV15 must use CLS pooling"
763 );
764 }
765
766 #[cfg(feature = "native")]
768 #[test]
769 fn test_e5_models_use_mean_pooling() {
770 use lattice_inference::BertPooling;
771
772 assert_eq!(
773 EmbeddingModel::MultilingualE5Small.bert_pooling(),
774 Some(BertPooling::Mean),
775 "MultilingualE5Small must use mean pooling"
776 );
777 assert_eq!(
778 EmbeddingModel::MultilingualE5Base.bert_pooling(),
779 Some(BertPooling::Mean),
780 "MultilingualE5Base must use mean pooling"
781 );
782 }
783
784 #[cfg(feature = "native")]
786 #[test]
787 fn test_minilm_models_use_mean_pooling() {
788 use lattice_inference::BertPooling;
789
790 assert_eq!(
791 EmbeddingModel::AllMiniLmL6V2.bert_pooling(),
792 Some(BertPooling::Mean),
793 "AllMiniLmL6V2 must use mean pooling"
794 );
795 assert_eq!(
796 EmbeddingModel::ParaphraseMultilingualMiniLmL12V2.bert_pooling(),
797 Some(BertPooling::Mean),
798 "ParaphraseMultilingualMiniLmL12V2 must use mean pooling"
799 );
800 }
801
802 #[cfg(feature = "native")]
804 #[test]
805 fn test_non_bert_models_return_none_pooling() {
806 assert_eq!(
807 EmbeddingModel::Qwen3Embedding0_6B.bert_pooling(),
808 None,
809 "Qwen model must return None for bert_pooling()"
810 );
811 assert_eq!(
812 EmbeddingModel::Qwen3Embedding4B.bert_pooling(),
813 None,
814 "Qwen model must return None for bert_pooling()"
815 );
816 assert_eq!(
817 EmbeddingModel::TextEmbedding3Small.bert_pooling(),
818 None,
819 "Remote model must return None for bert_pooling()"
820 );
821 }
822
823 #[cfg(feature = "native")]
825 #[test]
826 fn test_bge_and_e5_use_different_pooling() {
827 assert_ne!(
828 EmbeddingModel::BgeSmallEnV15.bert_pooling(),
829 EmbeddingModel::MultilingualE5Small.bert_pooling(),
830 "BGE and E5 must use different pooling strategies"
831 );
832 }
833}