1use crate::error::{Result, RuntimeError};
5use crate::traits::{
6 EmbeddingModel, GenerationOptions, GenerationResult, GeneratorModel, Message, RawTensorModel,
7 RerankerModel, ScoredDoc, TensorBatch, TensorSpec,
8};
9use async_trait::async_trait;
10use std::sync::{Arc, Mutex};
11use std::time::{Duration, Instant};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15enum State {
16 Closed,
17 Open,
18 HalfOpen,
19}
20
21pub struct CircuitBreakerConfig {
23 pub failure_threshold: u32,
25 pub open_wait_seconds: u64,
27}
28
29impl Default for CircuitBreakerConfig {
30 fn default() -> Self {
31 Self {
32 failure_threshold: 5,
33 open_wait_seconds: 10,
34 }
35 }
36}
37
38struct Inner {
39 state: State,
40 failures: u32,
41 last_failure: Option<Instant>,
42 config: CircuitBreakerConfig,
43 half_open_probe_in_flight: bool,
44}
45
46#[derive(Clone)]
53pub struct CircuitBreakerWrapper {
54 inner: Arc<Mutex<Inner>>,
55}
56
57impl CircuitBreakerWrapper {
58 pub fn new(config: CircuitBreakerConfig) -> Self {
60 Self {
61 inner: Arc::new(Mutex::new(Inner {
62 state: State::Closed,
63 failures: 0,
64 last_failure: None,
65 config,
66 half_open_probe_in_flight: false,
67 })),
68 }
69 }
70
71 pub async fn call<F, Fut, T>(&self, f: F) -> Result<T>
77 where
78 F: FnOnce() -> Fut,
79 Fut: std::future::Future<Output = Result<T>>,
80 {
81 let is_probe_call;
82
83 {
85 let mut inner = self.inner.lock().unwrap();
86 match inner.state {
87 State::Open => {
88 if let Some(last) = inner.last_failure {
89 if last.elapsed() >= Duration::from_secs(inner.config.open_wait_seconds) {
90 inner.state = State::HalfOpen;
91 } else {
92 return Err(RuntimeError::Unavailable);
93 }
94 }
95 }
96 State::HalfOpen => {
97 if inner.half_open_probe_in_flight {
98 return Err(RuntimeError::Unavailable);
99 }
100 }
101 State::Closed => {}
102 }
103 is_probe_call = inner.state == State::HalfOpen;
104 if is_probe_call {
105 inner.half_open_probe_in_flight = true;
106 }
107 }
108
109 let result = f().await;
111
112 let mut inner = self.inner.lock().unwrap();
114 match result {
115 Ok(val) => {
116 if is_probe_call {
117 inner.state = State::Closed;
118 inner.failures = 0;
119 inner.half_open_probe_in_flight = false;
120 } else if inner.state == State::Closed {
121 inner.failures = 0;
122 }
123 Ok(val)
124 }
125 Err(e) => {
126 if is_probe_call {
127 inner.half_open_probe_in_flight = false;
128 }
129 inner.failures += 1;
130 inner.last_failure = Some(Instant::now());
131
132 if is_probe_call
133 || (inner.state == State::Closed
134 && inner.failures >= inner.config.failure_threshold)
135 {
136 inner.state = State::Open;
137 }
138 Err(e)
139 }
140 }
141 }
142}
143
144pub struct InstrumentedEmbeddingModel {
148 pub inner: Arc<dyn EmbeddingModel>,
149 pub alias: String,
150 pub provider_id: String,
151 pub timeout: Option<Duration>,
152 pub retry: Option<crate::api::RetryConfig>,
153}
154
155#[async_trait]
156impl EmbeddingModel for InstrumentedEmbeddingModel {
157 async fn embed(&self, texts: &[&str]) -> Result<crate::traits::EmbedResult> {
158 let start = Instant::now();
159 let mut attempts = 0;
160 let max_attempts = self.retry.as_ref().map(|r| r.max_attempts).unwrap_or(1);
161
162 let res = loop {
163 attempts += 1;
164 let fut = self.inner.embed(texts);
165
166 let res = if let Some(timeout) = self.timeout {
167 match tokio::time::timeout(timeout, fut).await {
168 Ok(r) => r,
169 Err(_) => Err(RuntimeError::Timeout),
170 }
171 } else {
172 fut.await
173 };
174
175 match res {
176 Ok(val) => break Ok(val),
177 Err(e) if e.is_retryable() && attempts < max_attempts => {
178 let backoff = self.retry.as_ref().unwrap().get_backoff(attempts);
179 tracing::warn!(
180 alias = %self.alias,
181 attempt = attempts,
182 backoff_ms = backoff.as_millis(),
183 error = %e,
184 "Retrying embedding call"
185 );
186 tokio::time::sleep(backoff).await;
187 continue;
188 }
189 Err(e) => break Err(e),
190 }
191 };
192
193 let duration = start.elapsed();
194 let status = if res.is_ok() { "success" } else { "failure" };
195
196 metrics::histogram!(
197 "model_inference.duration_seconds",
198 "alias" => self.alias.clone(),
199 "task" => "embed",
200 "provider" => self.provider_id.clone()
201 )
202 .record(duration.as_secs_f64());
203
204 metrics::counter!(
205 "model_inference.total",
206 "alias" => self.alias.clone(),
207 "task" => "embed",
208 "provider" => self.provider_id.clone(),
209 "status" => status
210 )
211 .increment(1);
212
213 res
214 }
215
216 fn dimensions(&self) -> u32 {
217 self.inner.dimensions()
218 }
219
220 async fn warmup(&self) -> Result<()> {
221 self.inner.warmup().await
222 }
223}
224
225impl crate::traits::ModelInfo for InstrumentedEmbeddingModel {
226 fn model_id(&self) -> &str {
227 self.inner.model_id()
228 }
229 fn active_execution_providers(&self) -> Vec<String> {
230 self.inner.active_execution_providers()
231 }
232}
233
234pub struct InstrumentedGeneratorModel {
238 pub inner: Arc<dyn GeneratorModel>,
239 pub alias: String,
240 pub provider_id: String,
241 pub timeout: Option<Duration>,
242 pub retry: Option<crate::api::RetryConfig>,
243}
244
245#[async_trait]
246impl GeneratorModel for InstrumentedGeneratorModel {
247 async fn generate(
248 &self,
249 messages: &[Message],
250 options: GenerationOptions,
251 ) -> Result<GenerationResult> {
252 let start = Instant::now();
253 let mut attempts = 0;
254 let max_attempts = self.retry.as_ref().map(|r| r.max_attempts).unwrap_or(1);
255
256 let res = loop {
257 attempts += 1;
258 let fut = self.inner.generate(messages, options.clone());
259
260 let res = if let Some(timeout) = self.timeout {
261 match tokio::time::timeout(timeout, fut).await {
262 Ok(r) => r,
263 Err(_) => Err(RuntimeError::Timeout),
264 }
265 } else {
266 fut.await
267 };
268
269 match res {
270 Ok(val) => break Ok(val),
271 Err(e) if e.is_retryable() && attempts < max_attempts => {
272 let backoff = self.retry.as_ref().unwrap().get_backoff(attempts);
273 tracing::warn!(
274 alias = %self.alias,
275 attempt = attempts,
276 backoff_ms = backoff.as_millis(),
277 error = %e,
278 "Retrying generation call"
279 );
280 tokio::time::sleep(backoff).await;
281 continue;
282 }
283 Err(e) => break Err(e),
284 }
285 };
286
287 let duration = start.elapsed();
288 let status = if res.is_ok() { "success" } else { "failure" };
289
290 metrics::histogram!(
291 "model_inference.duration_seconds",
292 "alias" => self.alias.clone(),
293 "task" => "generate",
294 "provider" => self.provider_id.clone()
295 )
296 .record(duration.as_secs_f64());
297
298 metrics::counter!(
299 "model_inference.total",
300 "alias" => self.alias.clone(),
301 "task" => "generate",
302 "provider" => self.provider_id.clone(),
303 "status" => status
304 )
305 .increment(1);
306
307 res
308 }
309
310 async fn warmup(&self) -> Result<()> {
311 self.inner.warmup().await
312 }
313}
314
315impl crate::traits::ModelInfo for InstrumentedGeneratorModel {
316 fn model_id(&self) -> &str {
317 self.inner.model_id()
318 }
319 fn active_execution_providers(&self) -> Vec<String> {
320 self.inner.active_execution_providers()
321 }
322}
323
324pub struct InstrumentedRawTensorModel {
326 pub inner: Arc<dyn RawTensorModel>,
327 pub alias: String,
328 pub provider_id: String,
329 pub timeout: Option<Duration>,
330 pub retry: Option<crate::api::RetryConfig>,
331}
332
333#[async_trait]
334impl RawTensorModel for InstrumentedRawTensorModel {
335 async fn run(&self, inputs: &TensorBatch) -> Result<TensorBatch> {
336 let start = Instant::now();
337 let mut attempts = 0;
338 let max_attempts = self.retry.as_ref().map(|r| r.max_attempts).unwrap_or(1);
339
340 let res = loop {
341 attempts += 1;
342 let fut = self.inner.run(inputs);
343
344 let res = if let Some(timeout) = self.timeout {
345 match tokio::time::timeout(timeout, fut).await {
346 Ok(r) => r,
347 Err(_) => Err(RuntimeError::Timeout),
348 }
349 } else {
350 fut.await
351 };
352
353 match res {
354 Ok(val) => break Ok(val),
355 Err(e) if e.is_retryable() && attempts < max_attempts => {
356 let backoff = self.retry.as_ref().unwrap().get_backoff(attempts);
357 tracing::warn!(
358 alias = %self.alias,
359 attempt = attempts,
360 backoff_ms = backoff.as_millis(),
361 error = %e,
362 "Retrying ONNX run call"
363 );
364 tokio::time::sleep(backoff).await;
365 continue;
366 }
367 Err(e) => break Err(rewrite_onnx_error_alias(e, &self.alias)),
368 }
369 };
370
371 record_onnx_metrics(
372 &self.alias,
373 &self.provider_id,
374 start.elapsed(),
375 if res.is_ok() { "success" } else { "failure" },
376 );
377 res
378 }
379
380 async fn run_batch(&self, inputs: &[TensorBatch]) -> Result<Vec<TensorBatch>> {
381 let start = Instant::now();
382 let mut attempts = 0;
383 let max_attempts = self.retry.as_ref().map(|r| r.max_attempts).unwrap_or(1);
384
385 let res = loop {
386 attempts += 1;
387 let fut = self.inner.run_batch(inputs);
388
389 let res = if let Some(timeout) = self.timeout {
390 match tokio::time::timeout(timeout, fut).await {
391 Ok(r) => r,
392 Err(_) => Err(RuntimeError::Timeout),
393 }
394 } else {
395 fut.await
396 };
397
398 match res {
399 Ok(val) => break Ok(val),
400 Err(e) if e.is_retryable() && attempts < max_attempts => {
401 let backoff = self.retry.as_ref().unwrap().get_backoff(attempts);
402 tracing::warn!(
403 alias = %self.alias,
404 attempt = attempts,
405 backoff_ms = backoff.as_millis(),
406 error = %e,
407 "Retrying ONNX run_batch call"
408 );
409 tokio::time::sleep(backoff).await;
410 continue;
411 }
412 Err(e) => break Err(rewrite_onnx_error_alias(e, &self.alias)),
413 }
414 };
415
416 record_onnx_metrics(
417 &self.alias,
418 &self.provider_id,
419 start.elapsed(),
420 if res.is_ok() { "success" } else { "failure" },
421 );
422 res
423 }
424
425 fn max_batch_size(&self) -> usize {
426 self.inner.max_batch_size()
427 }
428
429 fn input_signature(&self) -> &[TensorSpec] {
430 self.inner.input_signature()
431 }
432
433 fn output_signature(&self) -> &[TensorSpec] {
434 self.inner.output_signature()
435 }
436
437 async fn warmup(&self) -> Result<()> {
438 self.inner.warmup().await
439 }
440}
441
442impl crate::traits::ModelInfo for InstrumentedRawTensorModel {
443 fn model_id(&self) -> &str {
444 self.inner.model_id()
445 }
446 fn active_execution_providers(&self) -> Vec<String> {
447 self.inner.active_execution_providers()
448 }
449}
450
451fn rewrite_onnx_error_alias(error: RuntimeError, alias: &str) -> RuntimeError {
452 match error {
453 RuntimeError::OnnxModelNotFound { path, .. } => RuntimeError::OnnxModelNotFound {
454 alias: alias.to_string(),
455 path,
456 },
457 RuntimeError::OnnxArtifactSelectionFailure { cause, .. } => {
458 RuntimeError::OnnxArtifactSelectionFailure {
459 alias: alias.to_string(),
460 cause,
461 }
462 }
463 RuntimeError::OnnxDownloadFailure { cause, .. } => RuntimeError::OnnxDownloadFailure {
464 alias: alias.to_string(),
465 cause,
466 },
467 RuntimeError::OnnxLoadFailure { path, cause, .. } => RuntimeError::OnnxLoadFailure {
468 alias: alias.to_string(),
469 path,
470 cause,
471 },
472 RuntimeError::OnnxSignatureIntrospectionFailure { cause, .. } => {
473 RuntimeError::OnnxSignatureIntrospectionFailure {
474 alias: alias.to_string(),
475 cause,
476 }
477 }
478 RuntimeError::OnnxInputMissing { required_input, .. } => RuntimeError::OnnxInputMissing {
479 alias: alias.to_string(),
480 required_input,
481 },
482 RuntimeError::OnnxInputTypeMismatch {
483 input_name,
484 expected,
485 got,
486 ..
487 } => RuntimeError::OnnxInputTypeMismatch {
488 alias: alias.to_string(),
489 input_name,
490 expected,
491 got,
492 },
493 RuntimeError::OnnxInputShapeMismatch {
494 input_name,
495 expected,
496 got,
497 ..
498 } => RuntimeError::OnnxInputShapeMismatch {
499 alias: alias.to_string(),
500 input_name,
501 expected,
502 got,
503 },
504 RuntimeError::OnnxInvocationFailure { cause, .. } => RuntimeError::OnnxInvocationFailure {
505 alias: alias.to_string(),
506 cause,
507 },
508 RuntimeError::OnnxBatchStackingFailure { cause, .. } => {
509 RuntimeError::OnnxBatchStackingFailure {
510 alias: alias.to_string(),
511 cause,
512 }
513 }
514 other => other,
515 }
516}
517
518fn record_onnx_metrics(alias: &str, provider_id: &str, duration: Duration, status: &str) {
519 metrics::histogram!(
520 "model_inference.duration_seconds",
521 "alias" => alias.to_string(),
522 "task" => "raw",
523 "provider" => provider_id.to_string()
524 )
525 .record(duration.as_secs_f64());
526
527 metrics::counter!(
528 "model_inference.total",
529 "alias" => alias.to_string(),
530 "task" => "raw",
531 "provider" => provider_id.to_string(),
532 "status" => status.to_string()
533 )
534 .increment(1);
535}
536
537pub struct InstrumentedRerankerModel {
541 pub inner: Arc<dyn RerankerModel>,
542 pub alias: String,
543 pub provider_id: String,
544 pub timeout: Option<Duration>,
545 pub retry: Option<crate::api::RetryConfig>,
546}
547
548#[async_trait]
549impl RerankerModel for InstrumentedRerankerModel {
550 async fn rerank(&self, query: &str, docs: &[&str]) -> Result<Vec<ScoredDoc>> {
551 let start = Instant::now();
552 let mut attempts = 0;
553 let max_attempts = self.retry.as_ref().map(|r| r.max_attempts).unwrap_or(1);
554
555 let res = loop {
556 attempts += 1;
557 let fut = self.inner.rerank(query, docs);
558
559 let res = if let Some(timeout) = self.timeout {
560 match tokio::time::timeout(timeout, fut).await {
561 Ok(r) => r,
562 Err(_) => Err(RuntimeError::Timeout),
563 }
564 } else {
565 fut.await
566 };
567
568 match res {
569 Ok(val) => break Ok(val),
570 Err(e) if e.is_retryable() && attempts < max_attempts => {
571 let backoff = self.retry.as_ref().unwrap().get_backoff(attempts);
572 tracing::warn!(
573 alias = %self.alias,
574 attempt = attempts,
575 backoff_ms = backoff.as_millis(),
576 error = %e,
577 "Retrying rerank call"
578 );
579 tokio::time::sleep(backoff).await;
580 continue;
581 }
582 Err(e) => break Err(e),
583 }
584 };
585
586 let duration = start.elapsed();
587 let status = if res.is_ok() { "success" } else { "failure" };
588
589 metrics::histogram!(
590 "model_inference.duration_seconds",
591 "alias" => self.alias.clone(),
592 "task" => "rerank",
593 "provider" => self.provider_id.clone()
594 )
595 .record(duration.as_secs_f64());
596
597 metrics::counter!(
598 "model_inference.total",
599 "alias" => self.alias.clone(),
600 "task" => "rerank",
601 "provider" => self.provider_id.clone(),
602 "status" => status
603 )
604 .increment(1);
605
606 res
607 }
608
609 async fn warmup(&self) -> Result<()> {
610 self.inner.warmup().await
611 }
612}
613
614impl crate::traits::ModelInfo for InstrumentedRerankerModel {
615 fn model_id(&self) -> &str {
616 self.inner.model_id()
617 }
618 fn active_execution_providers(&self) -> Vec<String> {
619 self.inner.active_execution_providers()
620 }
621}
622
623async fn run_instrumented<F, Fut, T>(
642 alias: &str,
643 provider_id: &str,
644 task: &'static str,
645 timeout: Option<Duration>,
646 retry: Option<&crate::api::RetryConfig>,
647 mut make_fut: F,
648) -> Result<T>
649where
650 F: FnMut() -> Fut,
651 Fut: std::future::Future<Output = Result<T>>,
652{
653 let start = Instant::now();
654 let mut attempts = 0u32;
655 let max_attempts = retry.as_ref().map(|r| r.max_attempts).unwrap_or(1);
656
657 let res = loop {
658 attempts += 1;
659 let fut = make_fut();
660 let attempt_res = if let Some(t) = timeout {
661 match tokio::time::timeout(t, fut).await {
662 Ok(r) => r,
663 Err(_) => Err(RuntimeError::Timeout),
664 }
665 } else {
666 fut.await
667 };
668
669 match attempt_res {
670 Ok(val) => break Ok(val),
671 Err(e) if e.is_retryable() && attempts < max_attempts => {
672 let backoff = retry.as_ref().unwrap().get_backoff(attempts);
673 tracing::warn!(
674 alias = %alias,
675 attempt = attempts,
676 backoff_ms = backoff.as_millis(),
677 error = %e,
678 "Retrying {task} call",
679 );
680 tokio::time::sleep(backoff).await;
681 continue;
682 }
683 Err(e) => break Err(e),
684 }
685 };
686
687 let duration = start.elapsed();
688 let status = if res.is_ok() { "success" } else { "failure" };
689 metrics::histogram!(
690 "model_inference.duration_seconds",
691 "alias" => alias.to_string(),
692 "task" => task,
693 "provider" => provider_id.to_string()
694 )
695 .record(duration.as_secs_f64());
696 metrics::counter!(
697 "model_inference.total",
698 "alias" => alias.to_string(),
699 "task" => task,
700 "provider" => provider_id.to_string(),
701 "status" => status
702 )
703 .increment(1);
704
705 res
706}
707
708pub struct InstrumentedImageEmbeddingModel {
711 pub inner: Arc<dyn crate::traits::ImageEmbeddingModel>,
712 pub alias: String,
713 pub provider_id: String,
714 pub timeout: Option<Duration>,
715 pub retry: Option<crate::api::RetryConfig>,
716}
717
718#[async_trait]
719impl crate::traits::ImageEmbeddingModel for InstrumentedImageEmbeddingModel {
720 async fn embed(
721 &self,
722 images: Vec<crate::traits::ImageInput>,
723 ) -> Result<crate::traits::EmbedResult> {
724 run_instrumented(
725 &self.alias,
726 &self.provider_id,
727 "embed_image",
728 self.timeout,
729 self.retry.as_ref(),
730 || self.inner.embed(images.clone()),
731 )
732 .await
733 }
734
735 fn dimensions(&self) -> u32 {
736 self.inner.dimensions()
737 }
738
739 async fn warmup(&self) -> Result<()> {
740 self.inner.warmup().await
741 }
742}
743
744impl crate::traits::ModelInfo for InstrumentedImageEmbeddingModel {
745 fn model_id(&self) -> &str {
746 self.inner.model_id()
747 }
748 fn active_execution_providers(&self) -> Vec<String> {
749 self.inner.active_execution_providers()
750 }
751}
752
753pub struct InstrumentedAudioEmbeddingModel {
756 pub inner: Arc<dyn crate::traits::AudioEmbeddingModel>,
757 pub alias: String,
758 pub provider_id: String,
759 pub timeout: Option<Duration>,
760 pub retry: Option<crate::api::RetryConfig>,
761}
762
763#[async_trait]
764impl crate::traits::AudioEmbeddingModel for InstrumentedAudioEmbeddingModel {
765 async fn embed(
766 &self,
767 audios: Vec<crate::traits::AudioInput>,
768 ) -> Result<crate::traits::EmbedResult> {
769 run_instrumented(
770 &self.alias,
771 &self.provider_id,
772 "embed_audio",
773 self.timeout,
774 self.retry.as_ref(),
775 || self.inner.embed(audios.clone()),
776 )
777 .await
778 }
779
780 fn dimensions(&self) -> u32 {
781 self.inner.dimensions()
782 }
783
784 async fn warmup(&self) -> Result<()> {
785 self.inner.warmup().await
786 }
787}
788
789impl crate::traits::ModelInfo for InstrumentedAudioEmbeddingModel {
790 fn model_id(&self) -> &str {
791 self.inner.model_id()
792 }
793 fn active_execution_providers(&self) -> Vec<String> {
794 self.inner.active_execution_providers()
795 }
796}
797
798pub struct InstrumentedMultimodalEmbeddingModel {
801 pub inner: Arc<dyn crate::traits::MultimodalEmbeddingModel>,
802 pub alias: String,
803 pub provider_id: String,
804 pub timeout: Option<Duration>,
805 pub retry: Option<crate::api::RetryConfig>,
806}
807
808#[async_trait]
809impl crate::traits::MultimodalEmbeddingModel for InstrumentedMultimodalEmbeddingModel {
810 async fn embed(
811 &self,
812 inputs: Vec<crate::traits::MultimodalInput>,
813 ) -> Result<crate::traits::EmbedResult> {
814 run_instrumented(
815 &self.alias,
816 &self.provider_id,
817 "embed_multimodal",
818 self.timeout,
819 self.retry.as_ref(),
820 || self.inner.embed(inputs.clone()),
821 )
822 .await
823 }
824
825 fn dimensions(&self) -> u32 {
826 self.inner.dimensions()
827 }
828
829 fn supported_modalities(&self) -> &[crate::traits::Modality] {
830 self.inner.supported_modalities()
831 }
832
833 async fn warmup(&self) -> Result<()> {
834 self.inner.warmup().await
835 }
836}
837
838impl crate::traits::ModelInfo for InstrumentedMultimodalEmbeddingModel {
839 fn model_id(&self) -> &str {
840 self.inner.model_id()
841 }
842 fn active_execution_providers(&self) -> Vec<String> {
843 self.inner.active_execution_providers()
844 }
845}
846
847pub struct InstrumentedSparseEmbeddingModel {
850 pub inner: Arc<dyn crate::traits::SparseEmbeddingModel>,
851 pub alias: String,
852 pub provider_id: String,
853 pub timeout: Option<Duration>,
854 pub retry: Option<crate::api::RetryConfig>,
855}
856
857#[async_trait]
858impl crate::traits::SparseEmbeddingModel for InstrumentedSparseEmbeddingModel {
859 async fn embed(&self, texts: &[&str]) -> Result<crate::traits::SparseEmbedResult> {
860 run_instrumented(
861 &self.alias,
862 &self.provider_id,
863 "embed_sparse",
864 self.timeout,
865 self.retry.as_ref(),
866 || self.inner.embed(texts),
867 )
868 .await
869 }
870
871 fn vocab_size(&self) -> u32 {
872 self.inner.vocab_size()
873 }
874
875 async fn warmup(&self) -> Result<()> {
876 self.inner.warmup().await
877 }
878}
879
880impl crate::traits::ModelInfo for InstrumentedSparseEmbeddingModel {
881 fn model_id(&self) -> &str {
882 self.inner.model_id()
883 }
884 fn active_execution_providers(&self) -> Vec<String> {
885 self.inner.active_execution_providers()
886 }
887}
888
889pub struct InstrumentedMultiVectorEmbeddingModel {
892 pub inner: Arc<dyn crate::traits::MultiVectorEmbeddingModel>,
893 pub alias: String,
894 pub provider_id: String,
895 pub timeout: Option<Duration>,
896 pub retry: Option<crate::api::RetryConfig>,
897}
898
899#[async_trait]
900impl crate::traits::MultiVectorEmbeddingModel for InstrumentedMultiVectorEmbeddingModel {
901 async fn embed(&self, texts: &[&str]) -> Result<crate::traits::MultiVectorEmbedResult> {
902 run_instrumented(
903 &self.alias,
904 &self.provider_id,
905 "embed_multi_vector",
906 self.timeout,
907 self.retry.as_ref(),
908 || self.inner.embed(texts),
909 )
910 .await
911 }
912
913 fn dimensions(&self) -> u32 {
914 self.inner.dimensions()
915 }
916
917 async fn warmup(&self) -> Result<()> {
918 self.inner.warmup().await
919 }
920}
921
922impl crate::traits::ModelInfo for InstrumentedMultiVectorEmbeddingModel {
923 fn model_id(&self) -> &str {
924 self.inner.model_id()
925 }
926 fn active_execution_providers(&self) -> Vec<String> {
927 self.inner.active_execution_providers()
928 }
929}
930
931pub struct InstrumentedHybridEmbeddingModel {
934 pub inner: Arc<dyn crate::traits::HybridEmbeddingModel>,
935 pub alias: String,
936 pub provider_id: String,
937 pub timeout: Option<Duration>,
938 pub retry: Option<crate::api::RetryConfig>,
939}
940
941#[async_trait]
942impl crate::traits::HybridEmbeddingModel for InstrumentedHybridEmbeddingModel {
943 async fn embed(
944 &self,
945 texts: &[&str],
946 heads: crate::traits::HeadSet,
947 ) -> Result<crate::traits::HybridEmbedResult> {
948 run_instrumented(
949 &self.alias,
950 &self.provider_id,
951 "embed_hybrid",
952 self.timeout,
953 self.retry.as_ref(),
954 || self.inner.embed(texts, heads),
955 )
956 .await
957 }
958
959 fn available_heads(&self) -> crate::traits::HeadSet {
960 self.inner.available_heads()
961 }
962
963 async fn warmup(&self) -> Result<()> {
964 self.inner.warmup().await
965 }
966}
967
968impl crate::traits::ModelInfo for InstrumentedHybridEmbeddingModel {
969 fn model_id(&self) -> &str {
970 self.inner.model_id()
971 }
972 fn active_execution_providers(&self) -> Vec<String> {
973 self.inner.active_execution_providers()
974 }
975}
976
977pub struct InstrumentedNlpModel {
979 pub inner: Arc<dyn crate::traits::NlpModel>,
980 pub alias: String,
981 pub provider_id: String,
982 pub timeout: Option<Duration>,
983 pub retry: Option<crate::api::RetryConfig>,
984}
985
986#[async_trait]
987impl crate::traits::NlpModel for InstrumentedNlpModel {
988 async fn analyze(
989 &self,
990 requests: Vec<crate::traits::NlpRequest<'_>>,
991 ) -> Result<Vec<crate::traits::NlpResult>> {
992 run_instrumented(
993 &self.alias,
994 &self.provider_id,
995 "nlp",
996 self.timeout,
997 self.retry.as_ref(),
998 || self.inner.analyze(requests.clone()),
999 )
1000 .await
1001 }
1002
1003 fn supported_tasks(&self) -> crate::traits::NlpTasks {
1004 self.inner.supported_tasks()
1005 }
1006
1007 fn label_maps(&self) -> Option<&crate::traits::NlpLabelMaps> {
1008 self.inner.label_maps()
1009 }
1010
1011 async fn warmup(&self) -> Result<()> {
1012 self.inner.warmup().await
1013 }
1014}
1015
1016impl crate::traits::ModelInfo for InstrumentedNlpModel {
1017 fn model_id(&self) -> &str {
1018 self.inner.model_id()
1019 }
1020 fn active_execution_providers(&self) -> Vec<String> {
1021 self.inner.active_execution_providers()
1022 }
1023}
1024
1025pub struct InstrumentedDocumentExtractionModel {
1028 pub inner: Arc<dyn crate::traits::DocumentExtractionModel>,
1029 pub alias: String,
1030 pub provider_id: String,
1031 pub timeout: Option<Duration>,
1032 pub retry: Option<crate::api::RetryConfig>,
1033}
1034
1035#[async_trait]
1036impl crate::traits::DocumentExtractionModel for InstrumentedDocumentExtractionModel {
1037 async fn extract(
1038 &self,
1039 pages: Vec<crate::traits::ImageInput>,
1040 options: crate::traits::DocExtractOptions,
1041 ) -> Result<Vec<crate::traits::DocExtractResult>> {
1042 run_instrumented(
1043 &self.alias,
1044 &self.provider_id,
1045 "document_extract",
1046 self.timeout,
1047 self.retry.as_ref(),
1048 || self.inner.extract(pages.clone(), options.clone()),
1049 )
1050 .await
1051 }
1052
1053 async fn warmup(&self) -> Result<()> {
1054 self.inner.warmup().await
1055 }
1056}
1057
1058impl crate::traits::ModelInfo for InstrumentedDocumentExtractionModel {
1059 fn model_id(&self) -> &str {
1060 self.inner.model_id()
1061 }
1062 fn active_execution_providers(&self) -> Vec<String> {
1063 self.inner.active_execution_providers()
1064 }
1065}
1066
1067pub struct InstrumentedTranscriptionModel {
1073 pub inner: Arc<dyn crate::traits::TranscriptionModel>,
1074 pub alias: String,
1075 pub provider_id: String,
1076 pub timeout: Option<Duration>,
1077 pub retry: Option<crate::api::RetryConfig>,
1078}
1079
1080#[async_trait]
1081impl crate::traits::TranscriptionModel for InstrumentedTranscriptionModel {
1082 async fn transcribe(
1083 &self,
1084 audios: Vec<crate::traits::AudioInput>,
1085 options: crate::traits::TranscribeOptions,
1086 ) -> Result<Vec<crate::traits::TranscribeResult>> {
1087 run_instrumented(
1088 &self.alias,
1089 &self.provider_id,
1090 "transcribe",
1091 self.timeout,
1092 self.retry.as_ref(),
1093 || self.inner.transcribe(audios.clone(), options.clone()),
1094 )
1095 .await
1096 }
1097
1098 fn supported_languages(&self) -> &[String] {
1099 self.inner.supported_languages()
1100 }
1101
1102 async fn warmup(&self) -> Result<()> {
1103 self.inner.warmup().await
1104 }
1105}
1106
1107impl crate::traits::ModelInfo for InstrumentedTranscriptionModel {
1108 fn model_id(&self) -> &str {
1109 self.inner.model_id()
1110 }
1111 fn active_execution_providers(&self) -> Vec<String> {
1112 self.inner.active_execution_providers()
1113 }
1114}
1115
1116pub struct InstrumentedOcrModel {
1118 pub inner: Arc<dyn crate::traits::OcrModel>,
1119 pub alias: String,
1120 pub provider_id: String,
1121 pub timeout: Option<Duration>,
1122 pub retry: Option<crate::api::RetryConfig>,
1123}
1124
1125#[async_trait]
1126impl crate::traits::OcrModel for InstrumentedOcrModel {
1127 async fn recognize(
1128 &self,
1129 images: Vec<crate::traits::ImageInput>,
1130 ) -> Result<Vec<crate::traits::OcrResult>> {
1131 run_instrumented(
1132 &self.alias,
1133 &self.provider_id,
1134 "ocr",
1135 self.timeout,
1136 self.retry.as_ref(),
1137 || self.inner.recognize(images.clone()),
1138 )
1139 .await
1140 }
1141
1142 async fn warmup(&self) -> Result<()> {
1143 self.inner.warmup().await
1144 }
1145}
1146
1147impl crate::traits::ModelInfo for InstrumentedOcrModel {
1148 fn model_id(&self) -> &str {
1149 self.inner.model_id()
1150 }
1151 fn active_execution_providers(&self) -> Vec<String> {
1152 self.inner.active_execution_providers()
1153 }
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158 use super::*;
1159 use crate::traits::{ModelInfo, NlpModel};
1160 use std::sync::atomic::{AtomicU32, Ordering};
1161
1162 #[tokio::test]
1163 async fn test_circuit_breaker_transitions() {
1164 let config = CircuitBreakerConfig {
1165 failure_threshold: 2,
1166 open_wait_seconds: 1,
1167 };
1168 let cb = CircuitBreakerWrapper::new(config);
1169 let counter = Arc::new(AtomicU32::new(0));
1170
1171 let res = cb.call(|| async { Ok::<_, RuntimeError>(()) }).await;
1173 assert!(res.is_ok());
1174
1175 let res = cb
1177 .call(|| async { Err::<(), _>(RuntimeError::InferenceError("fail".into())) })
1178 .await;
1179 assert!(res.is_err()); let res = cb
1182 .call(|| async { Err::<(), _>(RuntimeError::InferenceError("fail".into())) })
1183 .await;
1184 assert!(res.is_err()); let res = cb
1188 .call(|| async {
1189 counter.fetch_add(1, Ordering::SeqCst);
1190 Ok(())
1191 })
1192 .await;
1193 assert!(res.is_err());
1194 assert_eq!(res.err().unwrap().to_string(), "Unavailable");
1195 assert_eq!(counter.load(Ordering::SeqCst), 0); tokio::time::sleep(Duration::from_millis(1100)).await;
1199
1200 let res = cb
1203 .call(|| async { Err::<(), _>(RuntimeError::InferenceError("fail".into())) })
1204 .await;
1205 assert!(res.is_err());
1206
1207 let res = cb.call(|| async { Ok(()) }).await;
1209 assert!(res.is_err());
1210 assert_eq!(res.err().unwrap().to_string(), "Unavailable");
1211
1212 tokio::time::sleep(Duration::from_millis(1100)).await;
1214
1215 let res = cb.call(|| async { Ok(()) }).await;
1217 assert!(res.is_ok());
1218
1219 let res = cb.call(|| async { Ok(()) }).await;
1221 assert!(res.is_ok());
1222 }
1223
1224 #[tokio::test]
1225 async fn test_half_open_allows_single_probe() {
1226 let config = CircuitBreakerConfig {
1227 failure_threshold: 1,
1228 open_wait_seconds: 1,
1229 };
1230 let cb = CircuitBreakerWrapper::new(config);
1231
1232 let _ = cb
1234 .call(|| async { Err::<(), _>(RuntimeError::InferenceError("fail".into())) })
1235 .await;
1236
1237 tokio::time::sleep(Duration::from_millis(1100)).await;
1238
1239 let started = Arc::new(std::sync::atomic::AtomicU32::new(0));
1240 let finished = Arc::new(std::sync::atomic::AtomicU32::new(0));
1241
1242 let cb_probe = cb.clone();
1243 let started_probe = started.clone();
1244 let finished_probe = finished.clone();
1245 let probe = tokio::spawn(async move {
1246 cb_probe
1247 .call(|| async move {
1248 started_probe.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1249 tokio::time::sleep(Duration::from_millis(150)).await;
1250 finished_probe.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1251 Ok::<_, RuntimeError>(())
1252 })
1253 .await
1254 });
1255
1256 tokio::time::sleep(Duration::from_millis(20)).await;
1258
1259 let second = cb.call(|| async { Ok::<_, RuntimeError>(()) }).await;
1261 assert!(matches!(second, Err(RuntimeError::Unavailable)));
1262
1263 let probe_result = probe.await.unwrap();
1264 assert!(probe_result.is_ok());
1265 assert_eq!(started.load(std::sync::atomic::Ordering::SeqCst), 1);
1266 assert_eq!(finished.load(std::sync::atomic::Ordering::SeqCst), 1);
1267
1268 let res = cb.call(|| async { Ok::<_, RuntimeError>(()) }).await;
1270 assert!(res.is_ok());
1271 }
1272
1273 struct LabelMapNlpModel {
1276 labels: crate::traits::NlpLabelMaps,
1277 }
1278
1279 #[async_trait]
1280 impl crate::traits::NlpModel for LabelMapNlpModel {
1281 async fn analyze(
1282 &self,
1283 _requests: Vec<crate::traits::NlpRequest<'_>>,
1284 ) -> Result<Vec<crate::traits::NlpResult>> {
1285 Ok(Vec::new())
1286 }
1287
1288 fn supported_tasks(&self) -> crate::traits::NlpTasks {
1289 crate::traits::NlpTasks::CLS
1290 }
1291
1292 fn label_maps(&self) -> Option<&crate::traits::NlpLabelMaps> {
1293 Some(&self.labels)
1294 }
1295 }
1296
1297 impl crate::traits::ModelInfo for LabelMapNlpModel {
1298 fn model_id(&self) -> &str {
1299 "mock/labelmaps"
1300 }
1301 }
1302
1303 #[tokio::test]
1308 async fn instrumented_nlp_forwards_label_maps() {
1309 let labels = crate::traits::NlpLabelMaps {
1310 cls: vec!["statement".to_string(), "question".to_string()],
1311 ..Default::default()
1312 };
1313 let inner: Arc<dyn crate::traits::NlpModel> = Arc::new(LabelMapNlpModel { labels });
1314 let wrapped = InstrumentedNlpModel {
1315 inner,
1316 alias: "nlp/x".to_string(),
1317 provider_id: "test".to_string(),
1318 timeout: None,
1319 retry: None,
1320 };
1321
1322 let maps = wrapped.label_maps().expect("wrapper forwards label_maps");
1323 assert_eq!(maps.cls, ["statement", "question"]);
1324 assert_eq!(wrapped.model_id(), "mock/labelmaps");
1325 assert_eq!(wrapped.supported_tasks(), crate::traits::NlpTasks::CLS);
1326 }
1327
1328 #[tokio::test]
1331 async fn instrumented_nlp_label_maps_none_passes_through() {
1332 let inner: Arc<dyn crate::traits::NlpModel> = Arc::new(crate::mock::MockNlpModel::new());
1333 let wrapped = InstrumentedNlpModel {
1334 inner,
1335 alias: "nlp/y".to_string(),
1336 provider_id: "test".to_string(),
1337 timeout: None,
1338 retry: None,
1339 };
1340 assert!(wrapped.label_maps().is_none());
1341 }
1342}