From b88fcef57aea70e8f3e16dd7a0151ecf889dcf42 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 16 Jun 2026 20:47:26 -0700 Subject: [PATCH] Add MistralOcrConfig transform in Rust --- .../src/llms/mistral/ocr/transformation.rs | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 litellm-rust/src/llms/mistral/ocr/transformation.rs diff --git a/litellm-rust/src/llms/mistral/ocr/transformation.rs b/litellm-rust/src/llms/mistral/ocr/transformation.rs new file mode 100644 index 00000000000..f84b4287fa6 --- /dev/null +++ b/litellm-rust/src/llms/mistral/ocr/transformation.rs @@ -0,0 +1,159 @@ +//! Mistral OCR transformation. Mirrors `mistral_ocr_transformation.py`. Pure: no I/O. + +use serde_json::{Map, Value}; + +use crate::llms::base_llm::ocr::transformation::{ + BaseOcrConfig, DocumentType, Headers, OcrResponse, +}; + +/// Default Mistral API base. +pub const MISTRAL_DEFAULT_API_BASE: &str = "https://api.mistral.ai/v1"; + +/// Env var holding the Mistral API key. +pub const MISTRAL_API_KEY_ENV: &str = "MISTRAL_API_KEY"; + +/// Error message raised when no Mistral API key is available. +pub const MISSING_KEY_MESSAGE: &str = "Missing Mistral API Key - A call is being made to Mistral but no key is set either in the environment variables or via params"; + +/// Mistral OCR transformation configuration. +/// +/// Reference: +#[derive(Debug, Default, Clone)] +pub struct MistralOcrConfig; + +impl MistralOcrConfig { + pub fn new() -> Self { + Self + } +} + +impl BaseOcrConfig for MistralOcrConfig { + fn get_supported_ocr_params(&self, _model: &str) -> Vec { + [ + "pages", + "include_image_base64", + "image_limit", + "image_min_size", + "bbox_annotation_format", + "document_annotation_format", + "document_annotation_prompt", + "extract_header", + "extract_footer", + "table_format", + "confidence_scores_granularity", + "id", + ] + .iter() + .map(|s| s.to_string()) + .collect() + } + + fn validate_environment( + &self, + _model: &str, + api_key: Option<&str>, + env_lookup: &dyn Fn(&str) -> Option, + ) -> Result { + // Get API key from environment if not provided. + let resolved = match api_key { + Some(k) => Some(k.to_string()), + None => env_lookup(MISTRAL_API_KEY_ENV), + }; + + let resolved = resolved.ok_or_else(|| MISSING_KEY_MESSAGE.to_string())?; + + Ok(vec![( + "Authorization".to_string(), + format!("Bearer {resolved}"), + )]) + } + + fn get_complete_url(&self, api_base: Option<&str>, _model: &str) -> String { + let base = api_base.unwrap_or(MISTRAL_DEFAULT_API_BASE); + + // Ensure no trailing slash. + let base = base.trim_end_matches('/'); + + // Remove /v1 if it's already in the base to avoid duplication. + if base.ends_with("/v1") { + format!("{base}/ocr") + } else { + format!("{base}/v1/ocr") + } + } + + fn transform_ocr_request( + &self, + model: &str, + document: &DocumentType, + optional_params: &Map, + ) -> Result { + // Document must be a dict (Mistral-format), passed through as-is. + if !document.is_object() { + return Err(format!( + "Expected document dict, got {}", + json_type_name(document) + )); + } + + let mut data = Map::new(); + data.insert("model".to_string(), Value::String(model.to_string())); + data.insert("document".to_string(), document.clone()); + + // Add all (already-filtered) optional parameters. + for (k, v) in optional_params { + data.insert(k.clone(), v.clone()); + } + + Ok(Value::Object(data)) + } + + fn transform_ocr_response( + &self, + model: &str, + response_json: &Value, + ) -> Result { + let pages_value = response_json + .get("pages") + .cloned() + .unwrap_or_else(|| Value::Array(vec![])); + let pages = serde_json::from_value(pages_value) + .map_err(|e| format!("Error parsing Mistral OCR pages: {e}"))?; + + let resolved_model = response_json + .get("model") + .and_then(|m| m.as_str()) + .unwrap_or(model) + .to_string(); + + let document_annotation = response_json.get("document_annotation").cloned(); + + let usage_info = match response_json.get("usage_info") { + Some(Value::Null) | None => None, + Some(v) => Some( + serde_json::from_value(v.clone()) + .map_err(|e| format!("Error parsing Mistral OCR usage_info: {e}"))?, + ), + }; + + Ok(OcrResponse { + pages, + model: resolved_model, + document_annotation, + usage_info, + object: "ocr".to_string(), + }) + } +} + +/// Human-readable JSON type name, matching Python's `type(...)`-style error text. +fn json_type_name(v: &Value) -> &'static str { + match v { + Value::Null => "null", + Value::Bool(_) => "bool", + Value::Number(_) => "number", + Value::String(_) => "str", + Value::Array(_) => "list", + Value::Object(_) => "dict", + } +}