1use crate::api::{ModelAliasSpec, ModelRuntimeKey};
4use crate::error::{Result, RuntimeError};
5use crate::options_validation::validate_provider_options;
6use crate::reliability::{
7 InstrumentedAudioEmbeddingModel, InstrumentedDocumentExtractionModel,
8 InstrumentedEmbeddingModel, InstrumentedGeneratorModel, InstrumentedHybridEmbeddingModel,
9 InstrumentedImageEmbeddingModel, InstrumentedMultiVectorEmbeddingModel,
10 InstrumentedMultimodalEmbeddingModel, InstrumentedNlpModel, InstrumentedOcrModel,
11 InstrumentedRawTensorModel, InstrumentedRerankerModel, InstrumentedSparseEmbeddingModel,
12 InstrumentedTranscriptionModel,
13};
14use crate::traits::{
15 AudioEmbeddingModel, DocumentExtractionModel, EmbeddingModel, GeneratorModel,
16 HybridEmbeddingModel, ImageEmbeddingModel, LoadedModelHandle, ModelProvider,
17 MultiVectorEmbeddingModel, MultimodalEmbeddingModel, NlpModel, OcrModel, RawTensorModel,
18 RerankerModel, SparseEmbeddingModel, TranscriptionModel,
19};
20use dashmap::DashMap;
21use std::any::Any;
22use std::collections::HashMap;
23use std::sync::Arc;
24use tokio::sync::{Mutex, RwLock};
25
26#[derive(Default)]
38struct HandleCache {
39 embeddings: DashMap<String, Arc<dyn EmbeddingModel>>,
40 rerankers: DashMap<String, Arc<dyn RerankerModel>>,
41 generators: DashMap<String, Arc<dyn GeneratorModel>>,
42 raw_tensor_models: DashMap<String, Arc<dyn RawTensorModel>>,
43 image_embedders: DashMap<String, Arc<dyn ImageEmbeddingModel>>,
46 audio_embedders: DashMap<String, Arc<dyn AudioEmbeddingModel>>,
47 multimodal_embedders: DashMap<String, Arc<dyn MultimodalEmbeddingModel>>,
48 sparse_embedders: DashMap<String, Arc<dyn SparseEmbeddingModel>>,
49 multi_vector_embedders: DashMap<String, Arc<dyn MultiVectorEmbeddingModel>>,
50 hybrid_embedders: DashMap<String, Arc<dyn HybridEmbeddingModel>>,
51 nlp_models: DashMap<String, Arc<dyn NlpModel>>,
52 document_extractors: DashMap<String, Arc<dyn DocumentExtractionModel>>,
53 transcribers: DashMap<String, Arc<dyn TranscriptionModel>>,
54 ocr_models: DashMap<String, Arc<dyn OcrModel>>,
55}
56
57const DEFAULT_LOAD_TIMEOUT_SECS: u64 = 600;
59
60pub struct ModelRuntime {
72 providers: HashMap<String, Box<dyn ModelProvider>>,
73 registry: Arc<ModelRegistry>,
74 catalog: RwLock<HashMap<String, ModelAliasSpec>>,
75 handle_cache: HandleCache,
76}
77
78#[derive(Default)]
81pub struct ModelRegistry {
82 instances: RwLock<HashMap<ModelRuntimeKey, LoadedModelHandle>>,
83 loader_locks: Mutex<HashMap<ModelRuntimeKey, Arc<Mutex<()>>>>,
85}
86
87impl ModelRuntime {
88 pub fn builder() -> ModelRuntimeBuilder {
91 ModelRuntimeBuilder::default()
92 }
93
94 pub async fn register(&self, spec: ModelAliasSpec) -> Result<()> {
96 spec.validate()?;
97 if !self.providers.contains_key(&spec.provider_id) {
98 return Err(RuntimeError::Config(format!(
99 "Unknown provider '{}' for alias '{}'",
100 spec.provider_id, spec.alias
101 )));
102 }
103 validate_provider_options(&spec.provider_id, spec.task, &spec.options)?;
104 let mut catalog = self.catalog.write().await;
105 if catalog.contains_key(&spec.alias) {
106 return Err(RuntimeError::Config(format!(
107 "Alias '{}' already exists",
108 spec.alias
109 )));
110 }
111 catalog.insert(spec.alias.clone(), spec);
112 Ok(())
113 }
114
115 pub async fn contains_alias(&self, alias: &str) -> bool {
117 let catalog = self.catalog.read().await;
118 catalog.contains_key(alias)
119 }
120
121 async fn lookup_spec(&self, alias: &str) -> Result<ModelAliasSpec> {
123 let catalog = self.catalog.read().await;
124 catalog
125 .get(alias)
126 .cloned()
127 .ok_or_else(|| RuntimeError::AliasNotFound {
128 alias: alias.to_string(),
129 })
130 }
131
132 pub async fn prefetch_all(&self) -> Result<()> {
138 let specs: Vec<ModelAliasSpec> = {
139 let catalog = self.catalog.read().await;
140 catalog.values().cloned().collect()
141 };
142 for spec in specs {
143 tracing::info!(alias = %spec.alias, "Prefetching model");
144 self.resolve_and_load_internal(&spec).await?;
145 }
146 Ok(())
147 }
148
149 pub async fn prefetch(&self, aliases: &[&str]) -> Result<()> {
154 for alias in aliases {
155 let spec = self.lookup_spec(alias).await?;
156 tracing::info!(alias = %alias, "Prefetching model");
157 self.resolve_and_load_internal(&spec).await?;
158 }
159 Ok(())
160 }
161
162 pub async fn embedding(&self, alias: &str) -> Result<Arc<dyn EmbeddingModel>> {
168 if let Some(cached) = self.handle_cache.embeddings.get(alias) {
169 return Ok(cached.clone());
170 }
171
172 let spec = self.lookup_spec(alias).await?;
173 let handle = self.resolve_and_load_internal(&spec).await?;
174 if let Some(model) = handle.downcast_ref::<Arc<dyn EmbeddingModel>>() {
175 let cached = self
176 .handle_cache
177 .embeddings
178 .entry(alias.to_string())
179 .or_insert_with(|| {
180 let wrapper: Arc<dyn EmbeddingModel> = Arc::new(InstrumentedEmbeddingModel {
181 inner: model.clone(),
182 alias: alias.to_string(),
183 provider_id: spec.provider_id.clone(),
184 timeout: spec.timeout.map(std::time::Duration::from_secs),
185 retry: spec.retry.clone(),
186 });
187 wrapper
188 })
189 .clone();
190 return Ok(cached);
191 }
192
193 Err(RuntimeError::CapabilityMismatch(format!(
194 "Model for alias '{}' does not implement EmbeddingModel",
195 alias
196 )))
197 }
198
199 pub async fn embedder(&self, alias: &str) -> Result<Arc<dyn EmbeddingModel>> {
204 self.embedding(alias).await
205 }
206
207 pub async fn reranker(&self, alias: &str) -> Result<Arc<dyn RerankerModel>> {
213 if let Some(cached) = self.handle_cache.rerankers.get(alias) {
214 return Ok(cached.clone());
215 }
216
217 let spec = self.lookup_spec(alias).await?;
218 let handle = self.resolve_and_load_internal(&spec).await?;
219 if let Some(model) = handle.downcast_ref::<Arc<dyn RerankerModel>>() {
220 let cached = self
221 .handle_cache
222 .rerankers
223 .entry(alias.to_string())
224 .or_insert_with(|| {
225 let wrapper: Arc<dyn RerankerModel> = Arc::new(InstrumentedRerankerModel {
226 inner: model.clone(),
227 alias: alias.to_string(),
228 provider_id: spec.provider_id.clone(),
229 timeout: spec.timeout.map(std::time::Duration::from_secs),
230 retry: spec.retry.clone(),
231 });
232 wrapper
233 })
234 .clone();
235 return Ok(cached);
236 }
237 Err(RuntimeError::CapabilityMismatch(format!(
238 "Model for alias '{}' does not implement RerankerModel",
239 alias
240 )))
241 }
242
243 pub async fn generator(&self, alias: &str) -> Result<Arc<dyn GeneratorModel>> {
249 if let Some(cached) = self.handle_cache.generators.get(alias) {
250 return Ok(cached.clone());
251 }
252
253 let spec = self.lookup_spec(alias).await?;
254 let handle = self.resolve_and_load_internal(&spec).await?;
255 if let Some(model) = handle.downcast_ref::<Arc<dyn GeneratorModel>>() {
256 let cached = self
257 .handle_cache
258 .generators
259 .entry(alias.to_string())
260 .or_insert_with(|| {
261 let wrapper: Arc<dyn GeneratorModel> = Arc::new(InstrumentedGeneratorModel {
262 inner: model.clone(),
263 alias: alias.to_string(),
264 provider_id: spec.provider_id.clone(),
265 timeout: spec.timeout.map(std::time::Duration::from_secs),
266 retry: spec.retry.clone(),
267 });
268 wrapper
269 })
270 .clone();
271 return Ok(cached);
272 }
273 Err(RuntimeError::CapabilityMismatch(format!(
274 "Model for alias '{}' does not implement GeneratorModel",
275 alias
276 )))
277 }
278
279 pub async fn raw_tensor_model(&self, alias: &str) -> Result<Arc<dyn RawTensorModel>> {
285 if let Some(cached) = self.handle_cache.raw_tensor_models.get(alias) {
286 return Ok(cached.clone());
287 }
288
289 let spec = self.lookup_spec(alias).await?;
290 let handle = self.resolve_and_load_internal(&spec).await?;
291 if let Some(model) = handle.downcast_ref::<Arc<dyn RawTensorModel>>() {
292 let cached = self
293 .handle_cache
294 .raw_tensor_models
295 .entry(alias.to_string())
296 .or_insert_with(|| {
297 let wrapper: Arc<dyn RawTensorModel> = Arc::new(InstrumentedRawTensorModel {
298 inner: model.clone(),
299 alias: alias.to_string(),
300 provider_id: spec.provider_id.clone(),
301 timeout: spec.timeout.map(std::time::Duration::from_secs),
302 retry: spec.retry.clone(),
303 });
304 wrapper
305 })
306 .clone();
307 return Ok(cached);
308 }
309
310 Err(RuntimeError::ProviderCapabilityMissing {
311 alias: alias.to_string(),
312 provider_id: spec.provider_id,
313 capability: "RawTensorModel".to_string(),
314 })
315 }
316
317 pub async fn image_embedder(&self, alias: &str) -> Result<Arc<dyn ImageEmbeddingModel>> {
341 if let Some(cached) = self.handle_cache.image_embedders.get(alias) {
342 return Ok(cached.clone());
343 }
344 let spec = self.lookup_spec(alias).await?;
345 let handle = self.resolve_and_load_internal(&spec).await?;
346 if let Some(model) = handle.downcast_ref::<Arc<dyn ImageEmbeddingModel>>() {
347 let cached = self
348 .handle_cache
349 .image_embedders
350 .entry(alias.to_string())
351 .or_insert_with(|| {
352 let wrapper: Arc<dyn ImageEmbeddingModel> =
353 Arc::new(InstrumentedImageEmbeddingModel {
354 inner: model.clone(),
355 alias: alias.to_string(),
356 provider_id: spec.provider_id.clone(),
357 timeout: spec.timeout.map(std::time::Duration::from_secs),
358 retry: spec.retry.clone(),
359 });
360 wrapper
361 })
362 .clone();
363 return Ok(cached);
364 }
365 Err(RuntimeError::ProviderCapabilityMissing {
366 alias: alias.to_string(),
367 provider_id: spec.provider_id,
368 capability: "ImageEmbeddingModel".to_string(),
369 })
370 }
371
372 pub async fn audio_embedder(&self, alias: &str) -> Result<Arc<dyn AudioEmbeddingModel>> {
375 if let Some(cached) = self.handle_cache.audio_embedders.get(alias) {
376 return Ok(cached.clone());
377 }
378 let spec = self.lookup_spec(alias).await?;
379 let handle = self.resolve_and_load_internal(&spec).await?;
380 if let Some(model) = handle.downcast_ref::<Arc<dyn AudioEmbeddingModel>>() {
381 let cached = self
382 .handle_cache
383 .audio_embedders
384 .entry(alias.to_string())
385 .or_insert_with(|| {
386 let wrapper: Arc<dyn AudioEmbeddingModel> =
387 Arc::new(InstrumentedAudioEmbeddingModel {
388 inner: model.clone(),
389 alias: alias.to_string(),
390 provider_id: spec.provider_id.clone(),
391 timeout: spec.timeout.map(std::time::Duration::from_secs),
392 retry: spec.retry.clone(),
393 });
394 wrapper
395 })
396 .clone();
397 return Ok(cached);
398 }
399 Err(RuntimeError::ProviderCapabilityMissing {
400 alias: alias.to_string(),
401 provider_id: spec.provider_id,
402 capability: "AudioEmbeddingModel".to_string(),
403 })
404 }
405
406 pub async fn multimodal_embedder(
409 &self,
410 alias: &str,
411 ) -> Result<Arc<dyn MultimodalEmbeddingModel>> {
412 if let Some(cached) = self.handle_cache.multimodal_embedders.get(alias) {
413 return Ok(cached.clone());
414 }
415 let spec = self.lookup_spec(alias).await?;
416 let handle = self.resolve_and_load_internal(&spec).await?;
417 if let Some(model) = handle.downcast_ref::<Arc<dyn MultimodalEmbeddingModel>>() {
418 let cached = self
419 .handle_cache
420 .multimodal_embedders
421 .entry(alias.to_string())
422 .or_insert_with(|| {
423 let wrapper: Arc<dyn MultimodalEmbeddingModel> =
424 Arc::new(InstrumentedMultimodalEmbeddingModel {
425 inner: model.clone(),
426 alias: alias.to_string(),
427 provider_id: spec.provider_id.clone(),
428 timeout: spec.timeout.map(std::time::Duration::from_secs),
429 retry: spec.retry.clone(),
430 });
431 wrapper
432 })
433 .clone();
434 return Ok(cached);
435 }
436 Err(RuntimeError::ProviderCapabilityMissing {
437 alias: alias.to_string(),
438 provider_id: spec.provider_id,
439 capability: "MultimodalEmbeddingModel".to_string(),
440 })
441 }
442
443 pub async fn sparse_embedder(&self, alias: &str) -> Result<Arc<dyn SparseEmbeddingModel>> {
450 if let Some(cached) = self.handle_cache.sparse_embedders.get(alias) {
451 return Ok(cached.clone());
452 }
453 let spec = self.lookup_spec(alias).await?;
454 let handle = self.resolve_and_load_internal(&spec).await?;
455 if let Some(model) = handle.downcast_ref::<Arc<dyn SparseEmbeddingModel>>() {
456 let cached = self
457 .handle_cache
458 .sparse_embedders
459 .entry(alias.to_string())
460 .or_insert_with(|| {
461 let wrapper: Arc<dyn SparseEmbeddingModel> =
462 Arc::new(InstrumentedSparseEmbeddingModel {
463 inner: model.clone(),
464 alias: alias.to_string(),
465 provider_id: spec.provider_id.clone(),
466 timeout: spec.timeout.map(std::time::Duration::from_secs),
467 retry: spec.retry.clone(),
468 });
469 wrapper
470 })
471 .clone();
472 return Ok(cached);
473 }
474 Err(RuntimeError::ProviderCapabilityMissing {
475 alias: alias.to_string(),
476 provider_id: spec.provider_id,
477 capability: "SparseEmbeddingModel".to_string(),
478 })
479 }
480
481 pub async fn multi_vector_embedder(
488 &self,
489 alias: &str,
490 ) -> Result<Arc<dyn MultiVectorEmbeddingModel>> {
491 if let Some(cached) = self.handle_cache.multi_vector_embedders.get(alias) {
492 return Ok(cached.clone());
493 }
494 let spec = self.lookup_spec(alias).await?;
495 let handle = self.resolve_and_load_internal(&spec).await?;
496 if let Some(model) = handle.downcast_ref::<Arc<dyn MultiVectorEmbeddingModel>>() {
497 let cached = self
498 .handle_cache
499 .multi_vector_embedders
500 .entry(alias.to_string())
501 .or_insert_with(|| {
502 let wrapper: Arc<dyn MultiVectorEmbeddingModel> =
503 Arc::new(InstrumentedMultiVectorEmbeddingModel {
504 inner: model.clone(),
505 alias: alias.to_string(),
506 provider_id: spec.provider_id.clone(),
507 timeout: spec.timeout.map(std::time::Duration::from_secs),
508 retry: spec.retry.clone(),
509 });
510 wrapper
511 })
512 .clone();
513 return Ok(cached);
514 }
515 Err(RuntimeError::ProviderCapabilityMissing {
516 alias: alias.to_string(),
517 provider_id: spec.provider_id,
518 capability: "MultiVectorEmbeddingModel".to_string(),
519 })
520 }
521
522 pub async fn hybrid_embedder(&self, alias: &str) -> Result<Arc<dyn HybridEmbeddingModel>> {
535 if let Some(cached) = self.handle_cache.hybrid_embedders.get(alias) {
536 return Ok(cached.clone());
537 }
538 let spec = self.lookup_spec(alias).await?;
539 let handle = self.resolve_and_load_internal(&spec).await?;
540 if let Some(model) = handle.downcast_ref::<Arc<dyn HybridEmbeddingModel>>() {
541 let cached = self
542 .handle_cache
543 .hybrid_embedders
544 .entry(alias.to_string())
545 .or_insert_with(|| {
546 let wrapper: Arc<dyn HybridEmbeddingModel> =
547 Arc::new(InstrumentedHybridEmbeddingModel {
548 inner: model.clone(),
549 alias: alias.to_string(),
550 provider_id: spec.provider_id.clone(),
551 timeout: spec.timeout.map(std::time::Duration::from_secs),
552 retry: spec.retry.clone(),
553 });
554 wrapper
555 })
556 .clone();
557 return Ok(cached);
558 }
559 Err(RuntimeError::ProviderCapabilityMissing {
560 alias: alias.to_string(),
561 provider_id: spec.provider_id,
562 capability: "HybridEmbeddingModel".to_string(),
563 })
564 }
565
566 pub async fn nlp_model(&self, alias: &str) -> Result<Arc<dyn NlpModel>> {
569 if let Some(cached) = self.handle_cache.nlp_models.get(alias) {
570 return Ok(cached.clone());
571 }
572 let spec = self.lookup_spec(alias).await?;
573 let handle = self.resolve_and_load_internal(&spec).await?;
574 if let Some(model) = handle.downcast_ref::<Arc<dyn NlpModel>>() {
575 let cached = self
576 .handle_cache
577 .nlp_models
578 .entry(alias.to_string())
579 .or_insert_with(|| {
580 let wrapper: Arc<dyn NlpModel> = Arc::new(InstrumentedNlpModel {
581 inner: model.clone(),
582 alias: alias.to_string(),
583 provider_id: spec.provider_id.clone(),
584 timeout: spec.timeout.map(std::time::Duration::from_secs),
585 retry: spec.retry.clone(),
586 });
587 wrapper
588 })
589 .clone();
590 return Ok(cached);
591 }
592 Err(RuntimeError::ProviderCapabilityMissing {
593 alias: alias.to_string(),
594 provider_id: spec.provider_id,
595 capability: "NlpModel".to_string(),
596 })
597 }
598
599 pub async fn document_extractor(
629 &self,
630 alias: &str,
631 ) -> Result<Arc<dyn DocumentExtractionModel>> {
632 if let Some(cached) = self.handle_cache.document_extractors.get(alias) {
633 return Ok(cached.clone());
634 }
635 let spec = self.lookup_spec(alias).await?;
636 let handle = self.resolve_and_load_internal(&spec).await?;
637 if let Some(model) = handle.downcast_ref::<Arc<dyn DocumentExtractionModel>>() {
638 let cached = self
639 .handle_cache
640 .document_extractors
641 .entry(alias.to_string())
642 .or_insert_with(|| {
643 let wrapper: Arc<dyn DocumentExtractionModel> =
644 Arc::new(InstrumentedDocumentExtractionModel {
645 inner: model.clone(),
646 alias: alias.to_string(),
647 provider_id: spec.provider_id.clone(),
648 timeout: spec.timeout.map(std::time::Duration::from_secs),
649 retry: spec.retry.clone(),
650 });
651 wrapper
652 })
653 .clone();
654 return Ok(cached);
655 }
656 Err(RuntimeError::ProviderCapabilityMissing {
657 alias: alias.to_string(),
658 provider_id: spec.provider_id,
659 capability: "DocumentExtractionModel".to_string(),
660 })
661 }
662
663 pub async fn transcriber(&self, alias: &str) -> Result<Arc<dyn TranscriptionModel>> {
666 if let Some(cached) = self.handle_cache.transcribers.get(alias) {
667 return Ok(cached.clone());
668 }
669 let spec = self.lookup_spec(alias).await?;
670 let handle = self.resolve_and_load_internal(&spec).await?;
671 if let Some(model) = handle.downcast_ref::<Arc<dyn TranscriptionModel>>() {
672 let cached = self
673 .handle_cache
674 .transcribers
675 .entry(alias.to_string())
676 .or_insert_with(|| {
677 let wrapper: Arc<dyn TranscriptionModel> =
678 Arc::new(InstrumentedTranscriptionModel {
679 inner: model.clone(),
680 alias: alias.to_string(),
681 provider_id: spec.provider_id.clone(),
682 timeout: spec.timeout.map(std::time::Duration::from_secs),
683 retry: spec.retry.clone(),
684 });
685 wrapper
686 })
687 .clone();
688 return Ok(cached);
689 }
690 Err(RuntimeError::ProviderCapabilityMissing {
691 alias: alias.to_string(),
692 provider_id: spec.provider_id,
693 capability: "TranscriptionModel".to_string(),
694 })
695 }
696
697 pub async fn ocr_model(&self, alias: &str) -> Result<Arc<dyn OcrModel>> {
721 if let Some(cached) = self.handle_cache.ocr_models.get(alias) {
722 return Ok(cached.clone());
723 }
724 let spec = self.lookup_spec(alias).await?;
725 let handle = self.resolve_and_load_internal(&spec).await?;
726 if let Some(model) = handle.downcast_ref::<Arc<dyn OcrModel>>() {
727 let cached = self
728 .handle_cache
729 .ocr_models
730 .entry(alias.to_string())
731 .or_insert_with(|| {
732 let wrapper: Arc<dyn OcrModel> = Arc::new(InstrumentedOcrModel {
733 inner: model.clone(),
734 alias: alias.to_string(),
735 provider_id: spec.provider_id.clone(),
736 timeout: spec.timeout.map(std::time::Duration::from_secs),
737 retry: spec.retry.clone(),
738 });
739 wrapper
740 })
741 .clone();
742 return Ok(cached);
743 }
744 Err(RuntimeError::ProviderCapabilityMissing {
745 alias: alias.to_string(),
746 provider_id: spec.provider_id,
747 capability: "OcrModel".to_string(),
748 })
749 }
750
751 #[tracing::instrument(skip(self, spec), fields(provider, model))]
752 async fn resolve_and_load_internal(
753 &self,
754 spec: &ModelAliasSpec,
755 ) -> Result<Arc<dyn Any + Send + Sync>> {
756 let key = ModelRuntimeKey::new(spec);
757
758 {
760 let registry = self.registry.instances.read().await;
761 if let Some(handle) = registry.get(&key) {
762 return Ok(handle.clone());
763 }
764 }
765
766 let lock = {
768 let mut locks = self.registry.loader_locks.lock().await;
769 locks
770 .entry(key.clone())
771 .or_insert_with(|| Arc::new(Mutex::new(())))
772 .clone()
773 };
774
775 let _guard = lock.lock().await;
777
778 {
780 let registry = self.registry.instances.read().await;
781 if let Some(handle) = registry.get(&key) {
782 let result = Ok(handle.clone());
783 let mut locks = self.registry.loader_locks.lock().await;
784 locks.remove(&key);
785 return result;
786 }
787 }
788
789 let load_timeout =
790 std::time::Duration::from_secs(spec.load_timeout.unwrap_or(DEFAULT_LOAD_TIMEOUT_SECS));
791
792 let result = match tokio::time::timeout(load_timeout, async {
793 let provider = self.providers.get(&spec.provider_id).ok_or_else(|| {
794 RuntimeError::ProviderNotFound(format!("Provider '{}' not found", spec.provider_id))
795 })?;
796
797 tracing::info!(alias = %spec.alias, provider = %spec.provider_id, "Loading model instance");
798 let start = std::time::Instant::now();
799 let handle_result = provider.load(spec).await;
800 let duration = start.elapsed().as_secs_f64();
801
802 metrics::histogram!("model_load.duration_seconds").record(duration);
803
804 let handle = match handle_result {
805 Ok(h) => {
806 metrics::counter!("model_load.total", "status" => "success").increment(1);
807 h
808 }
809 Err(e) => {
810 metrics::counter!("model_load.total", "status" => "failure").increment(1);
811 tracing::error!(alias = %spec.alias, error = %e, "Model load failed");
812 return Err(e);
813 }
814 };
815
816 if let Some(model) = handle.downcast_ref::<Arc<dyn EmbeddingModel>>() {
821 model.warmup().await?;
822 } else if let Some(model) = handle.downcast_ref::<Arc<dyn RerankerModel>>() {
823 model.warmup().await?;
824 } else if let Some(model) = handle.downcast_ref::<Arc<dyn GeneratorModel>>() {
825 model.warmup().await?;
826 } else if let Some(model) = handle.downcast_ref::<Arc<dyn RawTensorModel>>() {
827 model.warmup().await?;
828 } else if let Some(model) = handle.downcast_ref::<Arc<dyn ImageEmbeddingModel>>() {
829 model.warmup().await?;
830 } else if let Some(model) = handle.downcast_ref::<Arc<dyn AudioEmbeddingModel>>() {
831 model.warmup().await?;
832 } else if let Some(model) = handle.downcast_ref::<Arc<dyn MultimodalEmbeddingModel>>() {
833 model.warmup().await?;
834 } else if let Some(model) = handle.downcast_ref::<Arc<dyn SparseEmbeddingModel>>() {
835 model.warmup().await?;
836 } else if let Some(model) = handle.downcast_ref::<Arc<dyn MultiVectorEmbeddingModel>>() {
837 model.warmup().await?;
838 } else if let Some(model) = handle.downcast_ref::<Arc<dyn HybridEmbeddingModel>>() {
839 model.warmup().await?;
840 } else if let Some(model) = handle.downcast_ref::<Arc<dyn NlpModel>>() {
841 model.warmup().await?;
842 } else if let Some(model) = handle.downcast_ref::<Arc<dyn DocumentExtractionModel>>() {
843 model.warmup().await?;
844 } else if let Some(model) = handle.downcast_ref::<Arc<dyn TranscriptionModel>>() {
845 model.warmup().await?;
846 } else if let Some(model) = handle.downcast_ref::<Arc<dyn OcrModel>>() {
847 model.warmup().await?;
848 }
849
850 {
851 let mut registry = self.registry.instances.write().await;
852 registry.insert(key.clone(), handle.clone());
853 }
854
855 Ok(handle)
856 })
857 .await
858 {
859 Ok(res) => res,
860 Err(_) => {
861 metrics::counter!("model_load.total", "status" => "failure").increment(1);
862 tracing::error!(
863 alias = %spec.alias,
864 provider = %spec.provider_id,
865 timeout_secs = load_timeout.as_secs(),
866 "Model load timed out"
867 );
868 Err(RuntimeError::Timeout)
869 }
870 };
871
872 {
875 let mut locks = self.registry.loader_locks.lock().await;
876 locks.remove(&key);
877 }
878
879 result
880 }
881}
882
883#[derive(Default)]
898pub struct ModelRuntimeBuilder {
899 providers: HashMap<String, Box<dyn ModelProvider>>,
900 catalog: Vec<ModelAliasSpec>,
901 warmup_policy: crate::api::WarmupPolicy,
902}
903
904impl ModelRuntimeBuilder {
905 pub fn register_provider<P: ModelProvider + 'static>(mut self, provider: P) -> Self {
910 self.providers
911 .insert(provider.provider_id().to_string(), Box::new(provider));
912 self
913 }
914
915 pub fn catalog(mut self, catalog: Vec<ModelAliasSpec>) -> Self {
917 self.catalog = catalog;
918 self
919 }
920
921 pub fn catalog_from_str(mut self, s: &str) -> Result<Self> {
923 self.catalog = crate::api::catalog_from_str(s)?;
924 Ok(self)
925 }
926
927 pub fn catalog_from_file(mut self, path: impl AsRef<std::path::Path>) -> Result<Self> {
929 self.catalog = crate::api::catalog_from_file(path)?;
930 Ok(self)
931 }
932
933 pub fn warmup_policy(mut self, policy: crate::api::WarmupPolicy) -> Self {
936 self.warmup_policy = policy;
937 self
938 }
939
940 pub async fn build(self) -> Result<Arc<ModelRuntime>> {
946 let mut catalog_map = HashMap::new();
947 for spec in self.catalog {
948 spec.validate()?;
949 if !self.providers.contains_key(&spec.provider_id) {
950 return Err(RuntimeError::Config(format!(
951 "Unknown provider '{}' for alias '{}'",
952 spec.provider_id, spec.alias
953 )));
954 }
955 validate_provider_options(&spec.provider_id, spec.task, &spec.options)?;
956 if catalog_map.insert(spec.alias.clone(), spec).is_some() {
957 return Err(RuntimeError::Config(
958 "Duplicate alias in catalog".to_string(),
959 ));
960 }
961 }
962
963 let runtime = Arc::new(ModelRuntime {
964 providers: self.providers,
965 registry: Arc::new(ModelRegistry::default()),
966 catalog: RwLock::new(catalog_map),
967 handle_cache: HandleCache::default(),
968 });
969
970 match self.warmup_policy {
972 crate::api::WarmupPolicy::Eager => {
973 for (id, provider) in &runtime.providers {
974 tracing::info!(provider = %id, "Eagerly warming up provider");
975 provider.warmup().await.map_err(|e| {
976 RuntimeError::Load(format!("Failed to warmup provider {}: {}", id, e))
977 })?;
978 }
979 }
980 crate::api::WarmupPolicy::Background => {
981 for id in runtime.providers.keys() {
982 tracing::info!(provider = %id, "Scheduling background provider warmup");
983 let rt = runtime.clone();
985 let provider_id = id.clone();
986 tokio::spawn(async move {
987 if let Some(provider) = rt.providers.get(&provider_id)
988 && let Err(e) = provider.warmup().await
989 {
990 tracing::error!(provider = %provider_id, error = %e, "Background provider warmup failed");
991 }
992 });
993 }
994 }
995 crate::api::WarmupPolicy::Lazy => {
996 tracing::debug!("Lazy provider warmup (no-op)");
997 }
998 }
999
1000 let mut warmup_tasks = Vec::new();
1002
1003 let specs: Vec<ModelAliasSpec> = {
1004 let catalog = runtime.catalog.read().await;
1005 catalog.values().cloned().collect()
1006 };
1007
1008 for spec in specs {
1009 match spec.warmup {
1010 crate::api::WarmupPolicy::Eager => {
1011 tracing::info!(alias = %spec.alias, "Eagerly warming up model");
1012 if let Err(e) = runtime.resolve_and_load_internal(&spec).await {
1013 if spec.required {
1014 return Err(e);
1015 }
1016 tracing::error!(
1017 alias = %spec.alias,
1018 provider = %spec.provider_id,
1019 error = %e,
1020 "Optional eager model warmup failed; continuing startup"
1021 );
1022 }
1023 }
1024 crate::api::WarmupPolicy::Background => {
1025 tracing::info!(alias = %spec.alias, "Scheduling background warmup");
1026 let rt = runtime.clone();
1027 let spec_clone = spec.clone();
1028 warmup_tasks.push(tokio::spawn(async move {
1030 if let Err(e) = rt.resolve_and_load_internal(&spec_clone).await {
1031 tracing::error!(alias = %spec_clone.alias, error = %e, "Background warmup failed");
1032 }
1033 }));
1034 }
1035 crate::api::WarmupPolicy::Lazy => {
1036 tracing::debug!(alias = %spec.alias, "Lazy warmup (no-op)");
1037 }
1038 }
1039 }
1040
1041 Ok(runtime)
1045 }
1046}
1047
1048#[cfg(test)]
1049mod tests {
1050 use super::*;
1051 use crate::api::ModelTask;
1052 use crate::mock::{MockProvider, make_spec};
1053
1054 #[tokio::test]
1055 async fn loader_lock_entries_cleaned_after_successful_load() {
1056 let spec = make_spec("embed/test", ModelTask::Embed, "mock/embed", "test-model");
1057 let runtime = ModelRuntime::builder()
1058 .register_provider(MockProvider::embed_only())
1059 .catalog(vec![spec])
1060 .build()
1061 .await
1062 .unwrap();
1063
1064 let _ = runtime.embedding("embed/test").await.unwrap();
1065
1066 let locks = runtime.registry.loader_locks.lock().await;
1067 assert!(
1068 locks.is_empty(),
1069 "loader lock map should be empty after load"
1070 );
1071 }
1072
1073 #[tokio::test]
1074 async fn loader_lock_entries_cleaned_after_failed_load() {
1075 let mut spec = make_spec("embed/test", ModelTask::Embed, "mock/failing", "test-model");
1076 spec.warmup = crate::api::WarmupPolicy::Lazy;
1077 let runtime = ModelRuntime::builder()
1078 .register_provider(MockProvider::failing())
1079 .catalog(vec![spec])
1080 .build()
1081 .await
1082 .unwrap();
1083
1084 let err = runtime.embedding("embed/test").await;
1085 assert!(err.is_err());
1086
1087 let locks = runtime.registry.loader_locks.lock().await;
1088 assert!(
1089 locks.is_empty(),
1090 "loader lock map should be empty after failure"
1091 );
1092 }
1093
1094 #[tokio::test]
1095 async fn loader_lock_entries_cleaned_after_load_timeout() {
1096 let mut spec = make_spec("embed/test", ModelTask::Embed, "mock/embed", "test-model");
1097 spec.warmup = crate::api::WarmupPolicy::Lazy;
1098 spec.load_timeout = Some(1);
1099
1100 let runtime = ModelRuntime::builder()
1101 .register_provider(MockProvider::embed_only().with_load_delay(2_000))
1102 .catalog(vec![spec])
1103 .build()
1104 .await
1105 .unwrap();
1106
1107 let err = runtime.embedding("embed/test").await;
1108 assert!(matches!(err, Err(RuntimeError::Timeout)));
1109
1110 let locks = runtime.registry.loader_locks.lock().await;
1111 assert!(
1112 locks.is_empty(),
1113 "loader lock map should be empty after load timeout"
1114 );
1115 }
1116}