1use crate::api::{ModelAliasSpec, ModelTask};
2use crate::error::{Result, RuntimeError};
3use crate::traits::{
4 ContentBlock, DocExtractOptions, DocExtractResult, DocumentExtractionModel, EmbedResult,
5 EmbeddingModel, GenerationOptions, GenerationResult, GeneratorModel, ImageInput,
6 LoadedModelHandle, Message, MessageRole, ModelInfo, ModelProvider, ProviderCapabilities,
7 ProviderHealth, TokenUsage,
8};
9use async_trait::async_trait;
10use mistralrs::{
11 AutoDeviceMapParams, DeviceMapSetting, EmbeddingModelBuilder, EmbeddingRequestBuilder,
12 GgufModelBuilder, IsqType, Model, ModelDType, PagedAttentionMetaBuilder, RequestBuilder,
13 TextMessageRole, TextModelBuilder, UqffEmbeddingModelBuilder, UqffMultimodalModelBuilder,
14 UqffTextModelBuilder,
15};
16use serde::Deserialize;
17use std::path::PathBuf;
18use std::sync::Arc;
19
20pub struct LocalMistralRsProvider;
25
26impl LocalMistralRsProvider {
27 pub fn new() -> Self {
28 Self
29 }
30
31 fn init_hf_cache() {
43 let cache_root = crate::cache::resolve_provider_cache_root("mistralrs");
44 unsafe {
47 std::env::set_var("HF_HOME", &cache_root);
48 }
49 }
50}
51
52impl Default for LocalMistralRsProvider {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58#[async_trait]
59impl ModelProvider for LocalMistralRsProvider {
60 fn provider_id(&self) -> &'static str {
61 "local/mistralrs"
62 }
63
64 fn capabilities(&self) -> ProviderCapabilities {
65 ProviderCapabilities {
66 supported_tasks: vec![
67 ModelTask::Embed,
68 ModelTask::Generate,
69 ModelTask::DocumentExtract,
70 ],
71 }
72 }
73
74 async fn warmup(&self) -> Result<()> {
75 Self::init_hf_cache();
76 Ok(())
77 }
78
79 async fn load(&self, spec: &ModelAliasSpec) -> Result<LoadedModelHandle> {
80 Self::init_hf_cache();
83
84 let has_options = match &spec.options {
85 serde_json::Value::Null => false,
86 serde_json::Value::Object(map) => !map.is_empty(),
87 _ => true,
88 };
89
90 let opts: MistralRsOptions = if has_options {
91 serde_json::from_value(spec.options.clone())
92 .map_err(|e| RuntimeError::Config(format!("Invalid mistralrs options: {}", e)))?
93 } else {
94 MistralRsOptions::default()
95 };
96
97 match spec.task {
98 ModelTask::Embed => self.load_embedding(spec, &opts).await,
99 ModelTask::Generate => self.load_generator(spec, &opts).await,
100 ModelTask::DocumentExtract => self.load_document_extractor(spec, &opts).await,
101 ModelTask::Raw => Err(RuntimeError::CapabilityMismatch(
102 "mistralrs provider does not support task Raw".to_string(),
103 )),
104 _ => Err(RuntimeError::CapabilityMismatch(format!(
105 "mistralrs provider does not support task {:?}",
106 spec.task
107 ))),
108 }
109 }
110
111 async fn health(&self) -> ProviderHealth {
112 ProviderHealth::Healthy
113 }
114}
115
116impl LocalMistralRsProvider {
117 async fn load_embedding(
118 &self,
119 spec: &ModelAliasSpec,
120 opts: &MistralRsOptions,
121 ) -> Result<LoadedModelHandle> {
122 tracing::info!(model_id = %spec.model_id, "Loading mistralrs embedding model");
123
124 let model = if let Some(files) = &opts.gguf_files {
126 if opts.dtype.is_some() {
127 tracing::debug!("dtype option ignored for GGUF models");
128 }
129 let mut builder = GgufModelBuilder::new(spec.model_id.clone(), files.clone());
130
131 if let Some(ref chat_tmpl) = opts.chat_template {
132 builder = builder.with_chat_template(chat_tmpl.clone());
133 }
134 if let Some(ref tok_json) = opts.tokenizer_json {
135 builder = builder.with_tokenizer_json(tok_json.clone());
136 }
137 builder = builder.with_logging();
138
139 builder.build().await.map_err(|e| {
140 RuntimeError::Load(format!(
141 "Failed to build mistralrs GGUF embedding model: {}",
142 e
143 ))
144 })?
145 } else {
146 let mut builder = if let Some(files) = &opts.uqff_files {
147 let paths: Vec<PathBuf> = files.iter().map(PathBuf::from).collect();
148 UqffEmbeddingModelBuilder::new(&spec.model_id, paths).into_inner()
149 } else {
150 EmbeddingModelBuilder::new(&spec.model_id)
151 };
152
153 let dtype = resolve_model_dtype(opts)?;
154 builder = builder.with_dtype(dtype);
155
156 if opts.uqff_files.is_none()
157 && let Some(ref isq_str) = opts.isq
158 {
159 let isq = parse_isq_type(isq_str)?;
160 builder = builder.with_isq(isq);
161 }
162
163 if opts.force_cpu {
164 builder = builder.with_force_cpu();
165 }
166
167 if let Some(ref rev) = spec.revision {
168 builder = builder.with_hf_revision(rev);
169 }
170
171 if let Some(max_seqs) = opts.max_num_seqs {
172 builder = builder.with_max_num_seqs(max_seqs);
173 }
174
175 if let Some(setting) = build_device_map_setting_text(opts) {
176 builder = builder.with_device_mapping(setting);
177 }
178
179 if let Some(ref tok_json) = opts.tokenizer_json {
180 builder = builder.with_tokenizer_json(tok_json);
181 }
182
183 builder = builder.with_logging();
184
185 builder.build().await.map_err(|e| {
186 RuntimeError::Load(format!("Failed to build mistralrs embedding model: {}", e))
187 })?
188 };
189
190 let dimensions = match opts.embedding_dimensions {
191 Some(d) => d,
192 None => {
193 tracing::info!("Probing embedding dimensions with test input");
194 let probe = model.generate_embedding("probe").await.map_err(|e| {
195 RuntimeError::Load(format!("Failed to probe embedding dimensions: {}", e))
196 })?;
197 validate_embeddings(std::slice::from_ref(&probe)).map_err(|e| {
198 RuntimeError::Load(format!(
199 "Probe returned invalid values: {e}. Try dtype: \"f32\""
200 ))
201 })?;
202 probe.len() as u32
203 }
204 };
205
206 tracing::info!(
207 model_id = %spec.model_id,
208 dimensions,
209 "mistralrs embedding model loaded"
210 );
211
212 let service = MistralRsEmbeddingService {
213 model,
214 model_id: spec.model_id.clone(),
215 dimensions,
216 };
217
218 let handle: Arc<dyn EmbeddingModel> = Arc::new(service);
219 Ok(Arc::new(handle) as LoadedModelHandle)
220 }
221
222 async fn load_generator(
223 &self,
224 spec: &ModelAliasSpec,
225 opts: &MistralRsOptions,
226 ) -> Result<LoadedModelHandle> {
227 let pipeline = opts.pipeline.as_deref().unwrap_or("text");
228 match pipeline {
229 "text" => self.load_text_generator(spec, opts).await,
230 "vision" => self.load_vision_generator(spec, opts).await,
231 "diffusion" => self.load_diffusion_generator(spec, opts).await,
232 "speech" => self.load_speech_generator(spec, opts).await,
233 _ => Err(RuntimeError::Config(format!(
234 "Unknown pipeline '{}'. Valid: text, vision, diffusion, speech",
235 pipeline
236 ))),
237 }
238 }
239
240 async fn load_text_generator(
241 &self,
242 spec: &ModelAliasSpec,
243 opts: &MistralRsOptions,
244 ) -> Result<LoadedModelHandle> {
245 tracing::info!(model_id = %spec.model_id, "Loading mistralrs text generator model");
246
247 let model = if let Some(files) = &opts.gguf_files {
248 if opts.dtype.is_some() {
249 tracing::debug!("dtype option ignored for GGUF models");
250 }
251 let mut builder = GgufModelBuilder::new(spec.model_id.clone(), files.clone());
252
253 if let Some(ref chat_tmpl) = opts.chat_template {
254 builder = builder.with_chat_template(chat_tmpl.clone());
255 }
256 if let Some(ref tok_json) = opts.tokenizer_json {
257 builder = builder.with_tokenizer_json(tok_json.clone());
258 }
259 if opts.paged_attention {
260 let paged_cfg = PagedAttentionMetaBuilder::default().build().map_err(|e| {
261 RuntimeError::Load(format!("Failed to configure paged attention: {}", e))
262 })?;
263 builder = builder.with_paged_attn(paged_cfg);
264 }
265 builder = builder.with_logging();
266
267 builder.build().await.map_err(|e| {
268 RuntimeError::Load(format!(
269 "Failed to build mistralrs GGUF generator model: {}",
270 e
271 ))
272 })?
273 } else {
274 let mut builder = if let Some(files) = &opts.uqff_files {
275 let paths: Vec<PathBuf> = files.iter().map(PathBuf::from).collect();
276 UqffTextModelBuilder::new(&spec.model_id, paths).into_inner()
277 } else {
278 TextModelBuilder::new(&spec.model_id)
279 };
280
281 let dtype = resolve_model_dtype(opts)?;
282 builder = builder.with_dtype(dtype);
283
284 if opts.uqff_files.is_none()
285 && let Some(ref isq_str) = opts.isq
286 {
287 let isq = parse_isq_type(isq_str)?;
288 builder = builder.with_isq(isq);
289 }
290
291 if opts.force_cpu {
292 builder = builder.with_force_cpu();
293 }
294
295 if let Some(ref rev) = spec.revision {
296 builder = builder.with_hf_revision(rev);
297 }
298
299 if opts.paged_attention {
300 let paged_cfg = PagedAttentionMetaBuilder::default().build().map_err(|e| {
301 RuntimeError::Load(format!("Failed to configure paged attention: {}", e))
302 })?;
303 builder = builder.with_paged_attn(paged_cfg);
304 }
305
306 if let Some(ref chat_tmpl) = opts.chat_template {
307 builder = builder.with_chat_template(chat_tmpl);
308 }
309
310 if let Some(ref tok_json) = opts.tokenizer_json {
311 builder = builder.with_tokenizer_json(tok_json);
312 }
313
314 if let Some(max_seqs) = opts.max_num_seqs {
315 builder = builder.with_max_num_seqs(max_seqs);
316 }
317
318 if let Some(setting) = build_device_map_setting_text(opts) {
319 builder = builder.with_device_mapping(setting);
320 }
321
322 builder = builder.with_logging();
323
324 builder.build().await.map_err(|e| {
325 RuntimeError::Load(format!("Failed to build mistralrs generator model: {}", e))
326 })?
327 };
328
329 tracing::info!(model_id = %spec.model_id, "mistralrs generator model loaded");
330
331 let service = MistralRsGeneratorService {
332 model,
333 model_id: spec.model_id.clone(),
334 };
335
336 let handle: Arc<dyn GeneratorModel> = Arc::new(service);
337 Ok(Arc::new(handle) as LoadedModelHandle)
338 }
339
340 async fn build_vision_service(
341 &self,
342 spec: &ModelAliasSpec,
343 opts: &MistralRsOptions,
344 ) -> Result<Arc<dyn GeneratorModel>> {
345 use mistralrs::MultimodalModelBuilder;
346
347 if opts.gguf_files.is_some() {
348 return Err(RuntimeError::Config(
349 "GGUF is not supported for the vision pipeline".to_string(),
350 ));
351 }
352
353 tracing::info!(model_id = %spec.model_id, "Loading mistralrs vision generator model");
354
355 let mut builder = if let Some(files) = &opts.uqff_files {
356 let paths: Vec<PathBuf> = files.iter().map(PathBuf::from).collect();
357 UqffMultimodalModelBuilder::new(&spec.model_id, paths).into_inner()
358 } else {
359 MultimodalModelBuilder::new(&spec.model_id)
360 };
361 let dtype = resolve_model_dtype(opts)?;
362 builder = builder.with_dtype(dtype);
363
364 if opts.uqff_files.is_none()
365 && let Some(ref isq_str) = opts.isq
366 {
367 let isq = parse_isq_type(isq_str)?;
368 builder = builder.with_isq(isq);
369 }
370 if opts.force_cpu {
371 builder = builder.with_force_cpu();
372 }
373 if let Some(ref rev) = spec.revision {
374 builder = builder.with_hf_revision(rev);
375 }
376 if opts.paged_attention {
377 let paged_cfg = PagedAttentionMetaBuilder::default().build().map_err(|e| {
378 RuntimeError::Load(format!("Failed to configure paged attention: {}", e))
379 })?;
380 builder = builder.with_paged_attn(paged_cfg);
381 }
382 if let Some(ref chat_tmpl) = opts.chat_template {
383 builder = builder.with_chat_template(chat_tmpl);
384 }
385 if let Some(ref tok_json) = opts.tokenizer_json {
386 builder = builder.with_tokenizer_json(tok_json);
387 }
388 if let Some(max_seqs) = opts.max_num_seqs {
389 builder = builder.with_max_num_seqs(max_seqs);
390 }
391 if let Some(setting) = build_device_map_setting_multimodal(opts) {
392 builder = builder.with_device_mapping(setting);
393 }
394 builder = builder.with_logging();
395
396 let model = builder.build().await.map_err(|e| {
397 RuntimeError::Load(format!("Failed to build mistralrs vision model: {}", e))
398 })?;
399
400 tracing::info!(model_id = %spec.model_id, "mistralrs vision model loaded");
401
402 let service = MistralRsVisionService {
403 model,
404 model_id: spec.model_id.clone(),
405 };
406 Ok(Arc::new(service) as Arc<dyn GeneratorModel>)
407 }
408
409 async fn load_vision_generator(
410 &self,
411 spec: &ModelAliasSpec,
412 opts: &MistralRsOptions,
413 ) -> Result<LoadedModelHandle> {
414 let handle = self.build_vision_service(spec, opts).await?;
415 Ok(Arc::new(handle) as LoadedModelHandle)
416 }
417
418 async fn load_document_extractor(
419 &self,
420 spec: &ModelAliasSpec,
421 opts: &MistralRsOptions,
422 ) -> Result<LoadedModelHandle> {
423 let generator = self.build_vision_service(spec, opts).await?;
426 let style_str = spec
427 .options
428 .get("style")
429 .and_then(|v| v.as_str())
430 .unwrap_or("olmocr");
431 let style = crate::doc_parse::style_from_str(style_str).ok_or_else(|| {
432 RuntimeError::Config(format!(
433 "Document extractor '{}' has unknown `style` value '{style_str}'; \
434 expected one of: granite-docling, mineru, olmocr",
435 spec.alias
436 ))
437 })?;
438 let extractor = MistralRsDocumentExtractor {
439 generator,
440 model_id: spec.model_id.clone(),
441 style,
442 };
443 let handle: Arc<dyn DocumentExtractionModel> = Arc::new(extractor);
444 Ok(Arc::new(handle) as LoadedModelHandle)
445 }
446
447 async fn load_diffusion_generator(
448 &self,
449 spec: &ModelAliasSpec,
450 opts: &MistralRsOptions,
451 ) -> Result<LoadedModelHandle> {
452 use mistralrs::{DiffusionLoaderType, DiffusionModelBuilder};
453
454 let loader_type = match opts.diffusion_loader_type.as_deref().unwrap_or("flux") {
455 "flux" => DiffusionLoaderType::Flux,
456 "flux_offloaded" => DiffusionLoaderType::FluxOffloaded,
457 other => {
458 return Err(RuntimeError::Config(format!(
459 "Unknown diffusion_loader_type '{}'. Valid: flux, flux_offloaded",
460 other
461 )));
462 }
463 };
464
465 tracing::info!(model_id = %spec.model_id, "Loading mistralrs diffusion model");
466
467 let mut builder = DiffusionModelBuilder::new(&spec.model_id, loader_type);
468 if opts.force_cpu {
469 builder = builder.with_force_cpu();
470 }
471 let dtype = resolve_model_dtype(opts)?;
472 builder = builder.with_dtype(dtype);
473 builder = builder.with_logging();
474
475 let model = builder.build().await.map_err(|e| {
476 RuntimeError::Load(format!("Failed to build mistralrs diffusion model: {}", e))
477 })?;
478
479 tracing::info!(model_id = %spec.model_id, "mistralrs diffusion model loaded");
480
481 let service = MistralRsDiffusionService {
482 model,
483 model_id: spec.model_id.clone(),
484 };
485 let handle: Arc<dyn GeneratorModel> = Arc::new(service);
486 Ok(Arc::new(handle) as LoadedModelHandle)
487 }
488
489 async fn load_speech_generator(
490 &self,
491 spec: &ModelAliasSpec,
492 opts: &MistralRsOptions,
493 ) -> Result<LoadedModelHandle> {
494 use mistralrs::{SpeechLoaderType, SpeechModelBuilder};
495
496 let loader_type = match opts.speech_loader_type.as_deref().unwrap_or("dia") {
497 "dia" => SpeechLoaderType::Dia,
498 other => {
499 return Err(RuntimeError::Config(format!(
500 "Unknown speech_loader_type '{}'. Valid: dia",
501 other
502 )));
503 }
504 };
505
506 tracing::info!(model_id = %spec.model_id, "Loading mistralrs speech model");
507
508 let mut builder = SpeechModelBuilder::new(&spec.model_id, loader_type);
509 if opts.force_cpu {
510 builder = builder.with_force_cpu();
511 }
512 let dtype = resolve_model_dtype(opts)?;
513 builder = builder.with_dtype(dtype);
514 builder = builder.with_logging();
515
516 let model = builder.build().await.map_err(|e| {
517 RuntimeError::Load(format!("Failed to build mistralrs speech model: {}", e))
518 })?;
519
520 tracing::info!(model_id = %spec.model_id, "mistralrs speech model loaded");
521
522 let service = MistralRsSpeechService {
523 model,
524 model_id: spec.model_id.clone(),
525 };
526 let handle: Arc<dyn GeneratorModel> = Arc::new(service);
527 Ok(Arc::new(handle) as LoadedModelHandle)
528 }
529}
530
531#[derive(Deserialize, Default)]
536#[serde(deny_unknown_fields)]
537struct MistralRsOptions {
538 isq: Option<String>,
540 #[serde(default)]
542 force_cpu: bool,
543 #[serde(default)]
545 paged_attention: bool,
546 max_num_seqs: Option<usize>,
548 chat_template: Option<String>,
550 tokenizer_json: Option<String>,
552 embedding_dimensions: Option<u32>,
554 gguf_files: Option<Vec<String>>,
556 uqff_files: Option<Vec<String>>,
567 dtype: Option<String>,
569 pipeline: Option<String>,
571 diffusion_loader_type: Option<String>,
573 speech_loader_type: Option<String>,
575 max_seq_len: Option<usize>,
580 max_batch_size: Option<usize>,
583 max_image_shape: Option<[usize; 2]>,
587 max_num_images: Option<usize>,
590}
591
592fn parse_isq_type(s: &str) -> Result<IsqType> {
597 match s.to_uppercase().as_str() {
598 "Q4_0" => Ok(IsqType::Q4_0),
599 "Q4_1" => Ok(IsqType::Q4_1),
600 "Q5_0" => Ok(IsqType::Q5_0),
601 "Q5_1" => Ok(IsqType::Q5_1),
602 "Q8_0" => Ok(IsqType::Q8_0),
603 "Q8_1" => Ok(IsqType::Q8_1),
604 "Q2K" => Ok(IsqType::Q2K),
605 "Q3K" => Ok(IsqType::Q3K),
606 "Q4K" => Ok(IsqType::Q4K),
607 "Q5K" => Ok(IsqType::Q5K),
608 "Q6K" => Ok(IsqType::Q6K),
609 "Q8K" => Ok(IsqType::Q8K),
610 "HQQ4" => Ok(IsqType::HQQ4),
611 "HQQ8" => Ok(IsqType::HQQ8),
612 "F8E4M3" => Ok(IsqType::F8E4M3),
613 "AFQ8" => Ok(IsqType::AFQ8),
614 "AFQ6" => Ok(IsqType::AFQ6),
615 "AFQ4" => Ok(IsqType::AFQ4),
616 "AFQ3" => Ok(IsqType::AFQ3),
617 "AFQ2" => Ok(IsqType::AFQ2),
618 other => Err(RuntimeError::Config(format!(
619 "Unknown ISQ type '{}'. Valid types: Q4_0, Q4_1, Q5_0, Q5_1, Q8_0, Q8_1, \
620 Q2K, Q3K, Q4K, Q5K, Q6K, Q8K, HQQ4, HQQ8, F8E4M3, AFQ2-AFQ8",
621 other
622 ))),
623 }
624}
625
626fn parse_model_dtype(s: &str) -> Result<ModelDType> {
631 match s.to_lowercase().as_str() {
632 "auto" => Ok(ModelDType::Auto),
633 "f16" => Ok(ModelDType::F16),
634 "bf16" => Ok(ModelDType::BF16),
635 "f32" => Ok(ModelDType::F32),
636 other => Err(RuntimeError::Config(format!(
637 "Unknown dtype '{}'. Valid values: auto, f16, bf16, f32",
638 other
639 ))),
640 }
641}
642
643fn resolve_model_dtype(opts: &MistralRsOptions) -> Result<ModelDType> {
644 if let Some(ref s) = opts.dtype {
645 return parse_model_dtype(s);
646 }
647 if opts.force_cpu {
648 tracing::info!("force_cpu=true; defaulting dtype to F32");
649 Ok(ModelDType::F32)
650 } else if !has_gpu_support() {
651 tracing::info!("GPU feature not enabled (gpu-cuda/gpu-metal); defaulting dtype to F32");
652 Ok(ModelDType::F32)
653 } else {
654 Ok(ModelDType::Auto)
655 }
656}
657
658#[allow(unexpected_cfgs)]
659fn has_gpu_support() -> bool {
660 cfg!(any(feature = "gpu-cuda", feature = "gpu-metal"))
661}
662
663fn build_device_map_setting_text(opts: &MistralRsOptions) -> Option<DeviceMapSetting> {
668 if opts.max_seq_len.is_none() && opts.max_batch_size.is_none() {
669 return None;
670 }
671 Some(DeviceMapSetting::Auto(AutoDeviceMapParams::Text {
672 max_seq_len: opts
673 .max_seq_len
674 .unwrap_or(AutoDeviceMapParams::DEFAULT_MAX_SEQ_LEN),
675 max_batch_size: opts
676 .max_batch_size
677 .unwrap_or(AutoDeviceMapParams::DEFAULT_MAX_BATCH_SIZE),
678 }))
679}
680
681fn build_device_map_setting_multimodal(opts: &MistralRsOptions) -> Option<DeviceMapSetting> {
684 if opts.max_seq_len.is_none()
685 && opts.max_batch_size.is_none()
686 && opts.max_image_shape.is_none()
687 && opts.max_num_images.is_none()
688 {
689 return None;
690 }
691 let max_image_shape = opts.max_image_shape.map(|[h, w]| (h, w)).unwrap_or((
692 AutoDeviceMapParams::DEFAULT_MAX_IMAGE_LENGTH,
693 AutoDeviceMapParams::DEFAULT_MAX_IMAGE_LENGTH,
694 ));
695 Some(DeviceMapSetting::Auto(AutoDeviceMapParams::Multimodal {
696 max_seq_len: opts
697 .max_seq_len
698 .unwrap_or(AutoDeviceMapParams::DEFAULT_MAX_SEQ_LEN),
699 max_batch_size: opts
700 .max_batch_size
701 .unwrap_or(AutoDeviceMapParams::DEFAULT_MAX_BATCH_SIZE),
702 max_image_shape,
703 max_num_images: opts
704 .max_num_images
705 .unwrap_or(AutoDeviceMapParams::DEFAULT_MAX_NUM_IMAGES),
706 }))
707}
708
709fn extract_last_user_prompt(messages: &[Message]) -> String {
712 messages
713 .iter()
714 .rev()
715 .filter(|m| m.role == MessageRole::User)
716 .flat_map(|m| m.content.iter())
717 .find_map(|b| match b {
718 ContentBlock::Text(t) => Some(t.clone()),
719 _ => None,
720 })
721 .unwrap_or_default()
722}
723
724fn validate_embeddings(embeddings: &[Vec<f32>]) -> Result<()> {
729 for (i, vec) in embeddings.iter().enumerate() {
730 let nan_count = vec.iter().filter(|v| v.is_nan()).count();
731 let inf_count = vec.iter().filter(|v| v.is_infinite()).count();
732 if nan_count > 0 || inf_count > 0 {
733 return Err(RuntimeError::InferenceError(format!(
734 "Embedding vector {} contains invalid values ({} NaN, {} Inf out of {} dims). \
735 This typically happens with F16 on CPU. Set options: {{\"dtype\": \"f32\"}}.",
736 i,
737 nan_count,
738 inf_count,
739 vec.len()
740 )));
741 }
742 }
743 Ok(())
744}
745
746struct MistralRsEmbeddingService {
751 model: Model,
752 model_id: String,
753 dimensions: u32,
754}
755
756#[async_trait]
757impl EmbeddingModel for MistralRsEmbeddingService {
758 async fn embed(&self, texts: &[&str]) -> Result<EmbedResult> {
759 if texts.is_empty() {
760 return Ok(EmbedResult {
761 vectors: vec![],
762 usage: None,
763 });
764 }
765
766 let request =
767 EmbeddingRequestBuilder::new().add_prompts(texts.iter().map(|s| s.to_string()));
768
769 let embeddings = self.model.generate_embeddings(request).await.map_err(|e| {
770 RuntimeError::InferenceError(format!("Embedding inference failed: {}", e))
771 })?;
772
773 validate_embeddings(&embeddings)?;
774
775 Ok(EmbedResult {
776 vectors: embeddings,
777 usage: None,
778 })
779 }
780
781 fn dimensions(&self) -> u32 {
782 self.dimensions
783 }
784}
785
786impl ModelInfo for MistralRsEmbeddingService {
787 fn model_id(&self) -> &str {
788 &self.model_id
789 }
790}
791
792struct MistralRsGeneratorService {
797 model: Model,
798 model_id: String,
799}
800
801impl ModelInfo for MistralRsGeneratorService {
802 fn model_id(&self) -> &str {
803 &self.model_id
804 }
805}
806
807#[async_trait]
808impl GeneratorModel for MistralRsGeneratorService {
809 async fn generate(
810 &self,
811 messages: &[Message],
812 options: GenerationOptions,
813 ) -> Result<GenerationResult> {
814 let mut request = RequestBuilder::new();
815
816 for msg in messages {
817 let role = match msg.role {
818 MessageRole::System => TextMessageRole::System,
819 MessageRole::User => TextMessageRole::User,
820 MessageRole::Assistant => TextMessageRole::Assistant,
821 };
822 request = request.add_message(role, msg.text());
823 }
824
825 let has_sampling = options.temperature.is_some()
827 || options.top_p.is_some()
828 || options.max_tokens.is_some();
829
830 if has_sampling {
831 if let Some(temp) = options.temperature {
832 request = request.set_sampler_temperature(temp as f64);
833 }
834 if let Some(top_p) = options.top_p {
835 request = request.set_sampler_topp(top_p as f64);
836 }
837 if let Some(max_tokens) = options.max_tokens {
838 request = request.set_sampler_max_len(max_tokens);
839 }
840 } else {
841 request = request.set_deterministic_sampler();
842 }
843
844 let response = self.model.send_chat_request(request).await.map_err(|e| {
845 RuntimeError::InferenceError(format!("Generation inference failed: {}", e))
846 })?;
847
848 let text = response
849 .choices
850 .first()
851 .and_then(|c| c.message.content.as_deref())
852 .unwrap_or("")
853 .to_string();
854
855 let usage = TokenUsage {
856 prompt_tokens: response.usage.prompt_tokens,
857 completion_tokens: response.usage.completion_tokens,
858 total_tokens: response.usage.total_tokens,
859 };
860
861 Ok(GenerationResult {
862 text,
863 usage: Some(usage),
864 images: vec![],
865 audio: None,
866 })
867 }
868}
869
870struct MistralRsVisionService {
875 model: Model,
876 model_id: String,
877}
878
879impl ModelInfo for MistralRsVisionService {
880 fn model_id(&self) -> &str {
881 &self.model_id
882 }
883}
884
885#[async_trait]
886impl GeneratorModel for MistralRsVisionService {
887 async fn generate(
888 &self,
889 messages: &[Message],
890 options: GenerationOptions,
891 ) -> Result<GenerationResult> {
892 let mut request = RequestBuilder::new();
893
894 for msg in messages {
895 let role = match msg.role {
896 MessageRole::System => TextMessageRole::System,
897 MessageRole::User => TextMessageRole::User,
898 MessageRole::Assistant => TextMessageRole::Assistant,
899 };
900
901 let mut images: Vec<image::DynamicImage> = Vec::new();
903 for block in &msg.content {
904 if let ContentBlock::Image(img_input) = block {
905 let bytes = match img_input {
906 crate::traits::ImageInput::Bytes { data, .. } => data.clone(),
907 crate::traits::ImageInput::Url(_url) => {
908 return Err(RuntimeError::Config(
909 "URL-based image input not yet supported in vision pipeline"
910 .to_string(),
911 ));
912 }
913 };
914 let img = image::load_from_memory(&bytes).map_err(|e| {
915 RuntimeError::InferenceError(format!("Failed to decode image: {}", e))
916 })?;
917 images.push(img);
918 }
919 }
920
921 let text = msg.text();
922
923 if images.is_empty() {
924 request = request.add_message(role, text);
925 } else {
926 request = request.add_image_message(role, text, images);
927 }
928 }
929
930 let has_sampling = options.temperature.is_some()
932 || options.top_p.is_some()
933 || options.max_tokens.is_some();
934
935 if has_sampling {
936 if let Some(temp) = options.temperature {
937 request = request.set_sampler_temperature(temp as f64);
938 }
939 if let Some(top_p) = options.top_p {
940 request = request.set_sampler_topp(top_p as f64);
941 }
942 if let Some(max_tokens) = options.max_tokens {
943 request = request.set_sampler_max_len(max_tokens);
944 }
945 } else {
946 request = request.set_deterministic_sampler();
947 }
948
949 let response =
950 self.model.send_chat_request(request).await.map_err(|e| {
951 RuntimeError::InferenceError(format!("Vision inference failed: {}", e))
952 })?;
953
954 let text = response
955 .choices
956 .first()
957 .and_then(|c| c.message.content.as_deref())
958 .unwrap_or("")
959 .to_string();
960
961 let usage = TokenUsage {
962 prompt_tokens: response.usage.prompt_tokens,
963 completion_tokens: response.usage.completion_tokens,
964 total_tokens: response.usage.total_tokens,
965 };
966
967 Ok(GenerationResult {
968 text,
969 usage: Some(usage),
970 images: vec![],
971 audio: None,
972 })
973 }
974}
975
976const OLMOCR_PROMPT: &str = "Attached is one page of a document that you must process. \
986Just return the plain text representation of this document as if you were reading it naturally. \
987Convert equations to LateX and tables to HTML. \
988If there are any figures or charts, label them with the following markdown syntax \
989``. \
990Return your output as markdown, with a front matter section on top specifying values for the \
991primary_language, is_rotation_valid, rotation_correction, is_table, and is_diagram parameters.";
992
993const DOC_EXTRACT_TEMPERATURE: f32 = 0.1;
999
1000const DOC_EXTRACT_MAX_TOKENS: usize = 8000;
1002
1003struct MistralRsDocumentExtractor {
1005 generator: Arc<dyn GeneratorModel>,
1006 model_id: String,
1007 style: crate::doc_parse::DocStyle,
1008}
1009
1010impl ModelInfo for MistralRsDocumentExtractor {
1011 fn model_id(&self) -> &str {
1012 &self.model_id
1013 }
1014}
1015
1016#[async_trait]
1017impl DocumentExtractionModel for MistralRsDocumentExtractor {
1018 async fn extract(
1019 &self,
1020 pages: Vec<ImageInput>,
1021 _options: DocExtractOptions,
1022 ) -> Result<Vec<DocExtractResult>> {
1023 let mut results = Vec::with_capacity(pages.len());
1024 for page in pages {
1027 let messages = [Message {
1028 role: MessageRole::User,
1029 content: vec![
1030 ContentBlock::Text(OLMOCR_PROMPT.to_string()),
1031 ContentBlock::Image(page),
1032 ],
1033 }];
1034 let options = GenerationOptions {
1035 max_tokens: Some(DOC_EXTRACT_MAX_TOKENS),
1036 temperature: Some(DOC_EXTRACT_TEMPERATURE),
1037 top_p: None,
1038 width: None,
1039 height: None,
1040 };
1041 let generated = self.generator.generate(&messages, options).await?;
1042 results.push(crate::doc_parse::parse_by_style(
1043 self.style,
1044 &generated.text,
1045 ));
1046 }
1047 Ok(results)
1048 }
1049}
1050
1051struct MistralRsDiffusionService {
1056 model: Model,
1057 model_id: String,
1058}
1059
1060impl ModelInfo for MistralRsDiffusionService {
1061 fn model_id(&self) -> &str {
1062 &self.model_id
1063 }
1064}
1065
1066#[async_trait]
1067impl GeneratorModel for MistralRsDiffusionService {
1068 async fn generate(
1069 &self,
1070 messages: &[Message],
1071 options: GenerationOptions,
1072 ) -> Result<GenerationResult> {
1073 use mistralrs::DiffusionGenerationParams;
1074
1075 let prompt = extract_last_user_prompt(messages);
1077
1078 let height = options.height.unwrap_or(720) as usize;
1079 let width = options.width.unwrap_or(1280) as usize;
1080
1081 let response = self
1082 .model
1083 .generate_image(
1084 prompt,
1085 mistralrs::ImageGenerationResponseFormat::B64Json,
1086 DiffusionGenerationParams { height, width },
1087 None,
1088 )
1089 .await
1090 .map_err(|e| {
1091 RuntimeError::InferenceError(format!("Diffusion inference failed: {}", e))
1092 })?;
1093
1094 let first = response.data.first().ok_or_else(|| {
1096 RuntimeError::InferenceError("Diffusion response returned no image data".to_string())
1097 })?;
1098 let b64 = first.b64_json.as_deref().ok_or_else(|| {
1099 RuntimeError::InferenceError("Diffusion response missing b64_json data".to_string())
1100 })?;
1101 let image_data = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, b64)
1102 .map_err(|e| {
1103 RuntimeError::InferenceError(format!("Failed to decode diffusion output: {}", e))
1104 })?;
1105
1106 Ok(GenerationResult {
1107 text: String::new(),
1108 usage: None,
1109 images: vec![crate::traits::GeneratedImage {
1110 data: image_data,
1111 media_type: "image/png".to_string(),
1112 }],
1113 audio: None,
1114 })
1115 }
1116}
1117
1118struct MistralRsSpeechService {
1123 model: Model,
1124 model_id: String,
1125}
1126
1127impl ModelInfo for MistralRsSpeechService {
1128 fn model_id(&self) -> &str {
1129 &self.model_id
1130 }
1131}
1132
1133#[async_trait]
1134impl GeneratorModel for MistralRsSpeechService {
1135 async fn generate(
1136 &self,
1137 messages: &[Message],
1138 _options: GenerationOptions,
1139 ) -> Result<GenerationResult> {
1140 let prompt = extract_last_user_prompt(messages);
1142
1143 let (pcm_data, sample_rate, channels) =
1144 self.model.generate_speech(prompt).await.map_err(|e| {
1145 RuntimeError::InferenceError(format!("Speech inference failed: {}", e))
1146 })?;
1147
1148 Ok(GenerationResult {
1149 text: String::new(),
1150 usage: None,
1151 images: vec![],
1152 audio: Some(crate::traits::AudioOutput {
1153 pcm_data: (*pcm_data).clone(),
1154 sample_rate,
1155 channels,
1156 }),
1157 })
1158 }
1159}
1160
1161#[cfg(test)]
1162mod tests {
1163 use super::*;
1164
1165 #[test]
1170 fn validate_embeddings_valid() {
1171 let vecs = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
1172 assert!(validate_embeddings(&vecs).is_ok());
1173 }
1174
1175 #[test]
1176 fn validate_embeddings_empty() {
1177 assert!(validate_embeddings(&[]).is_ok());
1178 }
1179
1180 #[test]
1181 fn validate_embeddings_nan() {
1182 let vecs = vec![vec![1.0, f32::NAN, 3.0]];
1183 let err = validate_embeddings(&vecs).unwrap_err();
1184 assert!(err.to_string().contains("NaN"));
1185 }
1186
1187 #[test]
1188 fn validate_embeddings_inf() {
1189 let vecs = vec![vec![1.0, f32::INFINITY, 3.0]];
1190 let err = validate_embeddings(&vecs).unwrap_err();
1191 assert!(err.to_string().contains("Inf"));
1192 }
1193
1194 #[test]
1195 fn validate_embeddings_all_nan() {
1196 let vecs = vec![vec![f32::NAN, f32::NAN, f32::NAN]];
1197 let err = validate_embeddings(&vecs).unwrap_err();
1198 assert!(err.to_string().contains("3 NaN"));
1199 }
1200
1201 #[test]
1206 fn parse_model_dtype_valid() {
1207 assert!(matches!(parse_model_dtype("auto"), Ok(ModelDType::Auto)));
1208 assert!(matches!(parse_model_dtype("f16"), Ok(ModelDType::F16)));
1209 assert!(matches!(parse_model_dtype("bf16"), Ok(ModelDType::BF16)));
1210 assert!(matches!(parse_model_dtype("f32"), Ok(ModelDType::F32)));
1211 }
1212
1213 #[test]
1214 fn parse_model_dtype_case_insensitive() {
1215 assert!(matches!(parse_model_dtype("F16"), Ok(ModelDType::F16)));
1216 assert!(matches!(parse_model_dtype("BF16"), Ok(ModelDType::BF16)));
1217 assert!(matches!(parse_model_dtype("Auto"), Ok(ModelDType::Auto)));
1218 }
1219
1220 #[test]
1221 fn parse_model_dtype_invalid() {
1222 let err = parse_model_dtype("int8").unwrap_err();
1223 assert!(err.to_string().contains("Unknown dtype"));
1224 }
1225
1226 #[test]
1231 fn resolve_model_dtype_explicit_overrides_force_cpu() {
1232 let opts = MistralRsOptions {
1233 dtype: Some("f16".to_string()),
1234 force_cpu: true,
1235 ..Default::default()
1236 };
1237 assert!(matches!(resolve_model_dtype(&opts), Ok(ModelDType::F16)));
1238 }
1239
1240 #[test]
1241 fn resolve_model_dtype_force_cpu_defaults_f32() {
1242 let opts = MistralRsOptions {
1243 force_cpu: true,
1244 ..Default::default()
1245 };
1246 assert!(matches!(resolve_model_dtype(&opts), Ok(ModelDType::F32)));
1247 }
1248
1249 #[test]
1250 fn resolve_model_dtype_no_gpu_defaults_f32() {
1251 let opts = MistralRsOptions::default();
1253 if !has_gpu_support() {
1254 assert!(matches!(resolve_model_dtype(&opts), Ok(ModelDType::F32)));
1255 }
1256 }
1257
1258 mod extract_last_user_prompt_tests {
1259 use super::*;
1260
1261 #[test]
1262 fn returns_last_user_text() {
1263 let messages = vec![
1264 Message::user("first"),
1265 Message::assistant("reply"),
1266 Message::user("second"),
1267 ];
1268 assert_eq!(extract_last_user_prompt(&messages), "second");
1269 }
1270
1271 #[test]
1272 fn skips_system_and_assistant() {
1273 let messages = vec![
1274 Message::system("system prompt"),
1275 Message::assistant("assistant reply"),
1276 ];
1277 assert_eq!(extract_last_user_prompt(&messages), "");
1278 }
1279
1280 #[test]
1281 fn empty_messages_returns_empty() {
1282 assert_eq!(extract_last_user_prompt(&[]), "");
1283 }
1284 }
1285}
1286
1287#[cfg(test)]
1288mod doc_extract_tests {
1289 use super::*;
1290 use crate::traits::{DocBlockKind, DocOutputFormat};
1291
1292 struct MockVisionGen {
1296 reply: String,
1297 }
1298
1299 impl ModelInfo for MockVisionGen {
1300 fn model_id(&self) -> &str {
1301 "mock/vision"
1302 }
1303 }
1304
1305 #[async_trait]
1306 impl GeneratorModel for MockVisionGen {
1307 async fn generate(
1308 &self,
1309 messages: &[Message],
1310 _options: GenerationOptions,
1311 ) -> Result<GenerationResult> {
1312 assert_eq!(messages.len(), 1, "one message per request");
1313 let image_count = messages[0]
1314 .content
1315 .iter()
1316 .filter(|b| matches!(b, ContentBlock::Image(_)))
1317 .count();
1318 assert_eq!(image_count, 1, "exactly one page image per request");
1319 let has_prompt = messages[0]
1320 .content
1321 .iter()
1322 .any(|b| matches!(b, ContentBlock::Text(t) if t.contains("front matter")));
1323 assert!(has_prompt, "the olmOCR prompt must be included");
1324 Ok(GenerationResult {
1325 text: self.reply.clone(),
1326 usage: None,
1327 images: vec![],
1328 audio: None,
1329 })
1330 }
1331 }
1332
1333 fn page() -> ImageInput {
1334 ImageInput::Bytes {
1335 data: vec![0u8; 4],
1336 media_type: "image/png".to_string(),
1337 }
1338 }
1339
1340 fn options() -> DocExtractOptions {
1341 DocExtractOptions {
1342 output: DocOutputFormat::Markdown,
1343 include_tables: true,
1344 include_formulas: true,
1345 include_bboxes: true,
1346 }
1347 }
1348
1349 #[tokio::test]
1350 async fn olmocr_extractor_builds_prompt_and_parses_output() {
1351 let generator: Arc<dyn GeneratorModel> = Arc::new(MockVisionGen {
1352 reply: "# Title\n\nA paragraph.".to_string(),
1353 });
1354 let extractor = MistralRsDocumentExtractor {
1355 generator,
1356 model_id: "allenai/olmOCR-2-7B-1025".to_string(),
1357 style: crate::doc_parse::DocStyle::OlmOcr,
1358 };
1359
1360 let results = extractor.extract(vec![page()], options()).await.unwrap();
1361 assert_eq!(results.len(), 1);
1362 assert_eq!(results[0].blocks[0].kind, DocBlockKind::Heading);
1363 assert_eq!(results[0].blocks[0].content, "Title");
1364 }
1365
1366 #[tokio::test]
1367 async fn olmocr_extractor_processes_each_page_singly() {
1368 let generator: Arc<dyn GeneratorModel> = Arc::new(MockVisionGen {
1369 reply: "body".to_string(),
1370 });
1371 let extractor = MistralRsDocumentExtractor {
1372 generator,
1373 model_id: "olmocr".to_string(),
1374 style: crate::doc_parse::DocStyle::OlmOcr,
1375 };
1376
1377 let results = extractor
1380 .extract(vec![page(), page()], options())
1381 .await
1382 .unwrap();
1383 assert_eq!(results.len(), 2);
1384 }
1385}