feat(rust-ocr): port file-input document normalization and proxy body guards with 1:1 parity tests

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
This commit is contained in:
Devin AI 2026-09-03 13:50:35 +00:00
parent 11a02b9581
commit 77dad9695f
5 changed files with 869 additions and 79 deletions

View file

@ -40,3 +40,19 @@ pub(crate) const MESSAGES_ROUTE_PATH: &str = "/v1/messages";
#[cfg(feature = "server")]
pub(crate) const MESSAGES_HEADERS_NOT_FORWARDED: &[&str] =
&["authorization", "connection", "content-length", "host"];
/// Fallback MIME type for OCR file input whose type cannot be detected.
pub(crate) const OCR_DEFAULT_MIME_TYPE: &str = "application/octet-stream";
/// File extension (lowercase, no dot) to MIME type for OCR file input.
pub(crate) const OCR_MIME_TYPES_BY_EXTENSION: &[(&str, &str)] = &[
("pdf", "application/pdf"),
("png", "image/png"),
("jpg", "image/jpeg"),
("jpeg", "image/jpeg"),
("gif", "image/gif"),
("webp", "image/webp"),
("tiff", "image/tiff"),
("tif", "image/tiff"),
("bmp", "image/bmp"),
];

View file

@ -0,0 +1,550 @@
use std::io::Read;
use std::path::{Path, PathBuf};
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use litellm_core::error::Error;
use serde_json::{Value, json};
use crate::constants::{OCR_DEFAULT_MIME_TYPE, OCR_MIME_TYPES_BY_EXTENSION};
pub enum FileInput {
Path(PathBuf),
Bytes(Vec<u8>),
Reader {
name: Option<String>,
reader: Box<dyn Read>,
},
Str(String),
Unsupported(String),
}
pub fn get_mime_type(file_path: &str) -> String {
let extension = Path::new(file_path)
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase());
extension
.and_then(|ext| {
OCR_MIME_TYPES_BY_EXTENSION
.iter()
.find(|(known, _)| *known == ext)
.map(|(_, mime)| (*mime).to_string())
})
.unwrap_or_else(|| OCR_DEFAULT_MIME_TYPE.to_string())
}
fn is_valid_mime_type(mime_type: &str) -> bool {
let Some((kind, subtype)) = mime_type.split_once('/') else {
return false;
};
let is_token = |part: &str| {
!part.is_empty()
&& part
.chars()
.all(|c| c.is_alphanumeric() || matches!(c, '_' | '.' | '+' | '-'))
};
is_token(kind) && is_token(subtype)
}
fn read_file_input(file: FileInput) -> Result<(Vec<u8>, String), Error> {
match file {
FileInput::Str(_) => Err(Error::InvalidRequest(
"OCR file input does not accept bare str values. Pass bytes, a pathlib.Path, or a \
file-like object. To OCR a local file from a path, call open(path, 'rb') yourself."
.to_string(),
)),
FileInput::Path(path) => {
if !path.is_file() {
return Err(Error::InvalidRequest(format!(
"File not found: {}",
path.display()
)));
}
let bytes = std::fs::read(&path).map_err(|err| {
Error::InvalidRequest(format!("File not found: {} ({err})", path.display()))
})?;
Ok((bytes, get_mime_type(&path.to_string_lossy())))
}
FileInput::Bytes(bytes) => Ok((bytes, OCR_DEFAULT_MIME_TYPE.to_string())),
FileInput::Reader { name, mut reader } => {
let mut bytes = Vec::new();
reader
.read_to_end(&mut bytes)
.map_err(|err| Error::InvalidRequest(format!("File could not be read: {err}")))?;
let mime_type = name
.filter(|name| !name.is_empty())
.map(|name| get_mime_type(&name))
.unwrap_or_else(|| OCR_DEFAULT_MIME_TYPE.to_string());
Ok((bytes, mime_type))
}
FileInput::Unsupported(type_name) => Err(Error::InvalidRequest(format!(
"Unsupported file input type: {type_name}. Expected pathlib.Path, bytes, or a \
file-like object."
))),
}
}
fn data_uri_document(mime_type: &str, bytes: &[u8]) -> Value {
let data_uri = format!("data:{mime_type};base64,{}", BASE64_STANDARD.encode(bytes));
if mime_type.starts_with("image/") {
return json!({"type": "image_url", "image_url": data_uri});
}
json!({"type": "document_url", "document_url": data_uri})
}
pub fn convert_file_document_to_url_document(
file: Option<FileInput>,
mime_type_override: Option<&str>,
) -> Result<Value, Error> {
let file = file.ok_or_else(|| {
Error::InvalidRequest(
"document with type='file' must include a 'file' field containing a pathlib.Path, \
file-like object, or bytes"
.to_string(),
)
})?;
let (bytes, detected_mime_type) = read_file_input(file)?;
if bytes.is_empty() {
return Err(Error::InvalidRequest(
"File is empty or could not be read".to_string(),
));
}
let mime_type = mime_type_override.unwrap_or(&detected_mime_type);
if !is_valid_mime_type(mime_type) {
return Err(Error::InvalidRequest(format!(
"Invalid MIME type: {mime_type}"
)));
}
Ok(data_uri_document(mime_type, &bytes))
}
pub fn build_document_from_upload(
file_content: Vec<u8>,
filename: Option<&str>,
content_type: Option<&str>,
) -> Result<Value, Error> {
let header_mime_type = content_type
.and_then(|value| value.split(';').next())
.map(str::trim)
.filter(|value| !value.is_empty() && *value != OCR_DEFAULT_MIME_TYPE);
let mime_type = header_mime_type
.map(str::to_string)
.or_else(|| filename.map(get_mime_type))
.unwrap_or_else(|| OCR_DEFAULT_MIME_TYPE.to_string());
convert_file_document_to_url_document(Some(FileInput::Bytes(file_content)), Some(&mime_type))
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use std::sync::atomic::{AtomicU64, Ordering};
use super::*;
struct TempFile(PathBuf);
impl TempFile {
fn with_suffix(suffix: &str, content: &[u8]) -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"litellm-ocr-file-input-{}-{unique}{suffix}",
std::process::id()
));
std::fs::write(&path, content).unwrap();
Self(path)
}
}
impl Drop for TempFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
fn data_uri_payload<'a>(document: &'a Value, field: &str) -> &'a str {
document[field].as_str().unwrap()
}
fn decode_base64(data_uri: &str) -> Vec<u8> {
let (_, encoded) = data_uri.split_once(";base64,").unwrap();
BASE64_STANDARD.decode(encoded).unwrap()
}
fn invalid_request_message(error: Error) -> String {
match error {
Error::InvalidRequest(message) => message,
other => panic!("expected InvalidRequest, got {other:?}"),
}
}
#[test]
fn mime_type_detects_pdf() {
assert_eq!(get_mime_type("document.pdf"), "application/pdf");
}
#[test]
fn mime_type_detects_png() {
assert_eq!(get_mime_type("image.png"), "image/png");
}
#[test]
fn mime_type_detects_jpg() {
assert_eq!(get_mime_type("photo.jpg"), "image/jpeg");
}
#[test]
fn mime_type_detects_jpeg() {
assert_eq!(get_mime_type("photo.jpeg"), "image/jpeg");
}
#[test]
fn mime_type_detects_gif() {
assert_eq!(get_mime_type("animation.gif"), "image/gif");
}
#[test]
fn mime_type_detects_webp() {
assert_eq!(get_mime_type("image.webp"), "image/webp");
}
#[test]
fn mime_type_detects_tiff() {
assert_eq!(get_mime_type("scan.tiff"), "image/tiff");
}
#[test]
fn mime_type_detects_tif() {
assert_eq!(get_mime_type("scan.tif"), "image/tiff");
}
#[test]
fn mime_type_detects_bmp() {
assert_eq!(get_mime_type("bitmap.bmp"), "image/bmp");
}
#[test]
fn mime_type_is_case_insensitive() {
assert_eq!(get_mime_type("DOCUMENT.PDF"), "application/pdf");
assert_eq!(get_mime_type("IMAGE.PNG"), "image/png");
}
#[test]
fn mime_type_falls_back_for_unknown_extension() {
assert_eq!(get_mime_type("file.xyz123"), "application/octet-stream");
}
#[test]
fn convert_pdf_path_to_document_url() {
let content = b"%PDF-1.4 test content";
let file = TempFile::with_suffix(".pdf", content);
let result =
convert_file_document_to_url_document(Some(FileInput::Path(file.0.clone())), None)
.unwrap();
assert_eq!(result["type"], "document_url");
let uri = data_uri_payload(&result, "document_url");
assert!(uri.starts_with("data:application/pdf;base64,"));
assert_eq!(decode_base64(uri), content);
}
#[test]
fn convert_image_path_to_image_url() {
let content = b"\x89PNG\r\n\x1a\n fake png content";
let file = TempFile::with_suffix(".png", content);
let result =
convert_file_document_to_url_document(Some(FileInput::Path(file.0.clone())), None)
.unwrap();
assert_eq!(result["type"], "image_url");
let uri = data_uri_payload(&result, "image_url");
assert!(uri.starts_with("data:image/png;base64,"));
assert_eq!(decode_base64(uri), content);
}
#[test]
fn convert_rejects_bare_str_path() {
let error = convert_file_document_to_url_document(
Some(FileInput::Str("/etc/passwd".to_string())),
None,
)
.unwrap_err();
assert!(invalid_request_message(error).contains("does not accept bare str values"));
}
#[test]
fn convert_pathbuf_matches_path_behavior() {
let file = TempFile::with_suffix(".pdf", b"test pdf content");
let result =
convert_file_document_to_url_document(Some(FileInput::Path(file.0.clone())), None)
.unwrap();
assert_eq!(result["type"], "document_url");
assert!(
data_uri_payload(&result, "document_url").starts_with("data:application/pdf;base64,")
);
}
#[test]
fn convert_raw_bytes_uses_fallback_mime() {
let content = b"raw bytes content";
let result =
convert_file_document_to_url_document(Some(FileInput::Bytes(content.to_vec())), None)
.unwrap();
assert_eq!(result["type"], "document_url");
let uri = data_uri_payload(&result, "document_url");
assert!(uri.starts_with("data:application/octet-stream;base64,"));
assert_eq!(decode_base64(uri), content);
}
#[test]
fn convert_raw_bytes_with_explicit_mime_type() {
let result = convert_file_document_to_url_document(
Some(FileInput::Bytes(b"raw pdf content".to_vec())),
Some("application/pdf"),
)
.unwrap();
assert_eq!(result["type"], "document_url");
assert!(
data_uri_payload(&result, "document_url").starts_with("data:application/pdf;base64,")
);
}
#[test]
fn convert_raw_bytes_with_image_mime_type() {
let result = convert_file_document_to_url_document(
Some(FileInput::Bytes(b"raw image content".to_vec())),
Some("image/jpeg"),
)
.unwrap();
assert_eq!(result["type"], "image_url");
assert!(data_uri_payload(&result, "image_url").starts_with("data:image/jpeg;base64,"));
}
#[test]
fn convert_reader_without_name() {
let content = b"file-like content";
let result = convert_file_document_to_url_document(
Some(FileInput::Reader {
name: None,
reader: Box::new(Cursor::new(content.to_vec())),
}),
None,
)
.unwrap();
assert_eq!(result["type"], "document_url");
let uri = data_uri_payload(&result, "document_url");
assert!(uri.contains("base64,"));
assert_eq!(decode_base64(uri), content);
}
#[test]
fn convert_reader_with_name_detects_mime() {
let result = convert_file_document_to_url_document(
Some(FileInput::Reader {
name: Some("test_image.png".to_string()),
reader: Box::new(Cursor::new(b"file-like png content".to_vec())),
}),
None,
)
.unwrap();
assert_eq!(result["type"], "image_url");
assert!(data_uri_payload(&result, "image_url").starts_with("data:image/png;base64,"));
}
#[test]
fn convert_errors_for_missing_file_field() {
let error = convert_file_document_to_url_document(None, None).unwrap_err();
assert!(invalid_request_message(error).contains("must include a 'file' field"));
}
#[test]
fn convert_errors_for_nonexistent_path() {
let error = convert_file_document_to_url_document(
Some(FileInput::Path(PathBuf::from(
"/nonexistent/path/to/file.pdf",
))),
None,
)
.unwrap_err();
assert!(invalid_request_message(error).contains("File not found"));
}
#[test]
fn convert_errors_for_empty_file() {
let file = TempFile::with_suffix(".pdf", b"");
let error =
convert_file_document_to_url_document(Some(FileInput::Path(file.0.clone())), None)
.unwrap_err();
assert!(invalid_request_message(error).contains("File is empty"));
}
#[test]
fn convert_errors_for_unsupported_type() {
let error = convert_file_document_to_url_document(
Some(FileInput::Unsupported("int".to_string())),
None,
)
.unwrap_err();
assert!(invalid_request_message(error).contains("Unsupported file input type"));
}
#[test]
fn convert_errors_for_invalid_mime_type() {
let error = convert_file_document_to_url_document(
Some(FileInput::Bytes(b"some content".to_vec())),
Some("text/html; charset=utf-8\nX-Injected: true"),
)
.unwrap_err();
assert!(invalid_request_message(error).contains("Invalid MIME type"));
}
#[test]
fn convert_explicit_mime_overrides_path_detection() {
let file = TempFile::with_suffix(".pdf", b"some content");
let result = convert_file_document_to_url_document(
Some(FileInput::Path(file.0.clone())),
Some("image/png"),
)
.unwrap();
assert_eq!(result["type"], "image_url");
assert!(data_uri_payload(&result, "image_url").starts_with("data:image/png;base64,"));
}
#[test]
fn upload_builds_document_url_for_pdf() {
let content = b"%PDF-1.4 test content";
let result = build_document_from_upload(
content.to_vec(),
Some("document.pdf"),
Some("application/pdf"),
)
.unwrap();
assert_eq!(result["type"], "document_url");
let uri = data_uri_payload(&result, "document_url");
assert!(uri.starts_with("data:application/pdf;base64,"));
assert_eq!(decode_base64(uri), content);
}
#[test]
fn upload_builds_image_url_for_png() {
let result = build_document_from_upload(
b"\x89PNG fake png".to_vec(),
Some("screenshot.png"),
Some("image/png"),
)
.unwrap();
assert_eq!(result["type"], "image_url");
assert!(data_uri_payload(&result, "image_url").starts_with("data:image/png;base64,"));
}
#[test]
fn upload_builds_image_url_for_jpeg() {
let result = build_document_from_upload(
b"\xff\xd8\xff fake jpeg".to_vec(),
Some("photo.jpg"),
Some("image/jpeg"),
)
.unwrap();
assert_eq!(result["type"], "image_url");
assert!(data_uri_payload(&result, "image_url").starts_with("data:image/jpeg;base64,"));
}
#[test]
fn upload_detects_mime_from_filename_when_content_type_is_octet_stream() {
let result = build_document_from_upload(
b"pdf content".to_vec(),
Some("report.pdf"),
Some("application/octet-stream"),
)
.unwrap();
assert_eq!(result["type"], "document_url");
assert!(
data_uri_payload(&result, "document_url").starts_with("data:application/pdf;base64,")
);
}
#[test]
fn upload_detects_mime_from_filename_when_content_type_is_none() {
let result =
build_document_from_upload(b"png content".to_vec(), Some("image.png"), None).unwrap();
assert_eq!(result["type"], "image_url");
assert!(data_uri_payload(&result, "image_url").starts_with("data:image/png;base64,"));
}
#[test]
fn upload_falls_back_to_octet_stream_for_unknown() {
let result = build_document_from_upload(b"unknown content".to_vec(), None, None).unwrap();
assert_eq!(result["type"], "document_url");
assert!(data_uri_payload(&result, "document_url").contains("application/octet-stream"));
}
#[test]
fn upload_preserves_binary_content_through_base64() {
let content = b"Hello, World! \x00\x01\x02\xff";
let result =
build_document_from_upload(content.to_vec(), Some("test.pdf"), Some("application/pdf"))
.unwrap();
assert_eq!(
decode_base64(data_uri_payload(&result, "document_url")),
content
);
}
#[test]
fn upload_strips_mime_parameters_from_content_type() {
let result = build_document_from_upload(
b"%PDF-1.4 test".to_vec(),
Some("doc.pdf"),
Some("application/pdf; charset=utf-8"),
)
.unwrap();
assert_eq!(result["type"], "document_url");
assert!(
data_uri_payload(&result, "document_url").starts_with("data:application/pdf;base64,")
);
}
#[test]
fn upload_strips_multiple_mime_parameters() {
let result = build_document_from_upload(
b"image data".to_vec(),
Some("img.png"),
Some("image/png; charset=utf-8; boundary=something"),
)
.unwrap();
assert_eq!(result["type"], "image_url");
assert!(data_uri_payload(&result, "image_url").starts_with("data:image/png;base64,"));
}
}

View file

@ -3,11 +3,17 @@ use litellm_core::call_lifecycle::CallLifecycle;
use serde_json::Value;
mod common_utils;
mod file_input;
mod handler;
mod hooks;
mod prepare;
mod request_body;
mod types;
pub use file_input::{
FileInput, build_document_from_upload, convert_file_document_to_url_document, get_mime_type,
};
pub use request_body::{parse_ocr_json_body, parse_ocr_multipart_form};
pub use types::OcrRequest;
use handler::execute_ocr_provider_call;

View file

@ -0,0 +1,133 @@
use litellm_core::error::Error;
use serde_json::{Map, Value};
use super::file_input::build_document_from_upload;
pub fn parse_ocr_json_body(body: &[u8]) -> Result<Map<String, Value>, Error> {
let data: Value = serde_json::from_slice(body).map_err(|err| {
Error::InvalidRequest(format!(
"Invalid JSON in request body: {err}. Ensure the request body is valid JSON with \
Content-Type: application/json, or use multipart/form-data for file uploads."
))
})?;
let Value::Object(data) = data else {
return Err(Error::InvalidRequest(
"OCR request body must be a JSON object".to_string(),
));
};
let document_type = data
.get("document")
.and_then(Value::as_object)
.and_then(|document| document.get("type"))
.and_then(Value::as_str);
if document_type == Some("file") {
return Err(Error::InvalidRequest(
"document type 'file' is not supported through the JSON API. To upload a local file, \
use multipart/form-data with a 'file' field. For JSON requests, use 'document_url' \
or 'image_url' document types."
.to_string(),
));
}
Ok(data)
}
pub fn parse_ocr_multipart_form(
file_content: Vec<u8>,
filename: Option<&str>,
content_type: Option<&str>,
fields: &[(String, String)],
) -> Result<Map<String, Value>, Error> {
if file_content.is_empty() {
return Err(Error::InvalidRequest("Uploaded file is empty".to_string()));
}
let document = build_document_from_upload(file_content, filename, content_type)?;
let mut data = Map::new();
data.insert("document".to_string(), document);
for (name, value) in fields {
if name == "file" || name == "document" {
continue;
}
let parsed =
serde_json::from_str::<Value>(value).unwrap_or_else(|_| Value::String(value.clone()));
data.insert(name.clone(), parsed);
}
Ok(data)
}
#[cfg(test)]
mod tests {
use super::*;
fn invalid_request_message(error: Error) -> String {
match error {
Error::InvalidRequest(message) => message,
other => panic!("expected InvalidRequest, got {other:?}"),
}
}
#[test]
fn json_body_rejects_file_type_document() {
let body = br#"{"model": "mistral/mistral-ocr-latest", "document": {"type": "file", "file": "/etc/passwd"}}"#;
let error = parse_ocr_json_body(body).unwrap_err();
assert!(invalid_request_message(error).contains("not supported through the JSON API"));
}
#[test]
fn json_body_accepts_document_url_type() {
let body = br#"{"model": "mistral/mistral-ocr-latest", "document": {"type": "document_url", "document_url": "https://example.com/doc.pdf"}}"#;
let data = parse_ocr_json_body(body).unwrap();
assert_eq!(data["document"]["type"], "document_url");
assert_eq!(data["model"], "mistral/mistral-ocr-latest");
}
#[test]
fn json_body_rejects_invalid_json() {
let error = parse_ocr_json_body(b"not valid json{{{").unwrap_err();
assert!(invalid_request_message(error).contains("Invalid JSON in request body"));
}
#[test]
fn multipart_ignores_document_form_field_injection() {
let fields = vec![
(
"model".to_string(),
"mistral/mistral-ocr-latest".to_string(),
),
(
"document".to_string(),
r#"{"type": "file", "file": "/etc/passwd"}"#.to_string(),
),
("pages".to_string(), "[0,1,2]".to_string()),
];
let data = parse_ocr_multipart_form(
b"%PDF-1.4 legit content".to_vec(),
Some("legit.pdf"),
Some("application/pdf"),
&fields,
)
.unwrap();
assert_eq!(data["document"]["type"], "document_url");
assert!(
data["document"]["document_url"]
.as_str()
.unwrap()
.starts_with("data:application/pdf;base64,")
);
assert_eq!(data["model"], "mistral/mistral-ocr-latest");
assert_eq!(data["pages"], serde_json::json!([0, 1, 2]));
}
#[test]
fn multipart_rejects_empty_upload() {
let error = parse_ocr_multipart_form(Vec::new(), Some("empty.pdf"), None, &[]).unwrap_err();
assert!(invalid_request_message(error).contains("Uploaded file is empty"));
}
}

View file

@ -17,7 +17,9 @@
"litellm-rust/crates/core/src/providers/azure_ai/ocr/transformation.rs",
"litellm-rust/crates/core/src/providers/vertex_ai/ocr/transformation.rs",
"litellm-rust/crates/core/src/providers/mistral/ocr/transformation.rs",
"litellm-rust/crates/ai-gateway/src/integrations/custom_logger/mod.rs"
"litellm-rust/crates/ai-gateway/src/integrations/custom_logger/mod.rs",
"litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"litellm-rust/crates/ai-gateway/src/ocr/request_body.rs"
],
"entries": [
{
@ -389,236 +391,314 @@
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestGetMimeType::test_should_detect_pdf_mime_type",
"status": "unmapped",
"reason": "file-normalization: MIME detection is Python-only preprocessing before the bridge call"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "mime_type_detects_pdf",
"justification": "both assert .pdf resolves to application/pdf"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestGetMimeType::test_should_detect_png_mime_type",
"status": "unmapped",
"reason": "file-normalization: MIME detection"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "mime_type_detects_png",
"justification": "both assert .png resolves to image/png"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestGetMimeType::test_should_detect_jpg_mime_type",
"status": "unmapped",
"reason": "file-normalization: MIME detection"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "mime_type_detects_jpg",
"justification": "both assert .jpg resolves to image/jpeg"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestGetMimeType::test_should_detect_jpeg_mime_type",
"status": "unmapped",
"reason": "file-normalization: MIME detection"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "mime_type_detects_jpeg",
"justification": "both assert .jpeg resolves to image/jpeg"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestGetMimeType::test_should_detect_gif_mime_type",
"status": "unmapped",
"reason": "file-normalization: MIME detection"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "mime_type_detects_gif",
"justification": "both assert .gif resolves to image/gif"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestGetMimeType::test_should_detect_webp_mime_type",
"status": "unmapped",
"reason": "file-normalization: MIME detection"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "mime_type_detects_webp",
"justification": "both assert .webp resolves to image/webp"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestGetMimeType::test_should_detect_tiff_mime_type",
"status": "unmapped",
"reason": "file-normalization: MIME detection"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "mime_type_detects_tiff",
"justification": "both assert .tiff resolves to image/tiff"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestGetMimeType::test_should_detect_tif_mime_type",
"status": "unmapped",
"reason": "file-normalization: MIME detection"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "mime_type_detects_tif",
"justification": "both assert .tif resolves to image/tiff"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestGetMimeType::test_should_detect_bmp_mime_type",
"status": "unmapped",
"reason": "file-normalization: MIME detection"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "mime_type_detects_bmp",
"justification": "both assert .bmp resolves to image/bmp"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestGetMimeType::test_should_be_case_insensitive",
"status": "unmapped",
"reason": "file-normalization: MIME detection case handling"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "mime_type_is_case_insensitive",
"justification": "both assert .PDF/.PNG resolve to the same MIME as lowercase"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestGetMimeType::test_should_fallback_for_unknown_extension",
"status": "unmapped",
"reason": "file-normalization: MIME detection fallback"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "mime_type_falls_back_for_unknown_extension",
"justification": "both assert an unknown extension resolves to a non-empty fallback (Rust always application/octet-stream, Python may consult mimetypes first)"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_convert_pdf_pathlib_path_to_document_url",
"status": "unmapped",
"reason": "file-normalization: local-path-to-data-URI conversion happens in Python"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_pdf_path_to_document_url",
"justification": "both write a .pdf temp file, assert type document_url, data:application/pdf;base64 prefix and round-tripped bytes"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_convert_image_pathlib_path_to_image_url",
"status": "unmapped",
"reason": "file-normalization: local-path-to-data-URI conversion"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_image_path_to_image_url",
"justification": "both write a .png temp file, assert type image_url, data:image/png;base64 prefix and round-tripped bytes"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_reject_bare_str_path",
"status": "unmapped",
"reason": "file-normalization: arbitrary-file-read guard on bare str paths"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_rejects_bare_str_path",
"justification": "both assert a bare string path is rejected with the \"does not accept bare str values\" error"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_convert_pathlib_path",
"status": "unmapped",
"reason": "file-normalization: local-path-to-data-URI conversion"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_pathbuf_matches_path_behavior",
"justification": "both assert a filesystem path value produces document_url with application/pdf data URI"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_convert_raw_bytes",
"status": "unmapped",
"reason": "file-normalization: raw-bytes-to-data-URI conversion"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_raw_bytes_uses_fallback_mime",
"justification": "both assert raw bytes fall back to application/octet-stream document_url and round-trip"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_convert_raw_bytes_with_explicit_mime_type",
"status": "unmapped",
"reason": "file-normalization: explicit MIME override on raw bytes"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_raw_bytes_with_explicit_mime_type",
"justification": "both assert an explicit application/pdf mime_type on raw bytes yields document_url"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_convert_raw_bytes_with_image_mime_type",
"status": "unmapped",
"reason": "file-normalization: explicit MIME override on raw bytes"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_raw_bytes_with_image_mime_type",
"justification": "both assert an explicit image/jpeg mime_type on raw bytes yields image_url"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_convert_file_like_object",
"status": "unmapped",
"reason": "file-normalization: file-like-object conversion"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_reader_without_name",
"justification": "both assert a nameless file-like reader is base64 encoded into document_url with the original bytes"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_convert_file_like_object_with_name",
"status": "unmapped",
"reason": "file-normalization: file-like-object name-based MIME detection"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_reader_with_name_detects_mime",
"justification": "both assert a file-like reader named *.png yields image_url with image/png"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_raise_error_for_missing_file_field",
"status": "unmapped",
"reason": "file-normalization: missing-field validation"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_errors_for_missing_file_field",
"justification": "both assert an absent file value fails with \"must include a 'file' field\""
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_raise_error_for_nonexistent_pathlib_path",
"status": "unmapped",
"reason": "file-normalization: missing-file validation"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_errors_for_nonexistent_path",
"justification": "both assert a nonexistent path fails with \"File not found\""
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_raise_error_for_empty_file",
"status": "unmapped",
"reason": "file-normalization: empty-file validation"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_errors_for_empty_file",
"justification": "both assert an empty file fails with \"File is empty\""
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_raise_error_for_unsupported_type",
"status": "unmapped",
"reason": "file-normalization: unsupported input type validation"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_errors_for_unsupported_type",
"justification": "both assert an unsupported input type fails with \"Unsupported file input type\""
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_raise_error_for_invalid_mime_type",
"status": "unmapped",
"reason": "file-normalization: MIME-type injection validation"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_errors_for_invalid_mime_type",
"justification": "both assert a mime_type containing header-injection characters fails with \"Invalid MIME type\""
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestConvertFileDocumentToUrlDocument::test_should_override_mime_type_for_pathlib_path",
"status": "unmapped",
"reason": "file-normalization: explicit MIME override precedence"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "convert_explicit_mime_overrides_path_detection",
"justification": "both assert explicit image/png overrides the .pdf extension and yields image_url"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestBuildDocumentFromUpload::test_should_build_document_url_for_pdf",
"status": "unmapped",
"reason": "file-normalization: multipart upload conversion"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "upload_builds_document_url_for_pdf",
"justification": "both assert a PDF upload yields document_url with application/pdf data URI and round-tripped bytes"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestBuildDocumentFromUpload::test_should_build_image_url_for_png",
"status": "unmapped",
"reason": "file-normalization: multipart upload conversion"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "upload_builds_image_url_for_png",
"justification": "both assert a PNG upload yields image_url with image/png data URI"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestBuildDocumentFromUpload::test_should_build_image_url_for_jpeg",
"status": "unmapped",
"reason": "file-normalization: multipart upload conversion"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "upload_builds_image_url_for_jpeg",
"justification": "both assert a JPEG upload yields image_url with image/jpeg data URI"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestBuildDocumentFromUpload::test_should_detect_mime_from_filename_when_content_type_is_octet_stream",
"status": "unmapped",
"reason": "file-normalization: filename-based MIME fallback"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "upload_detects_mime_from_filename_when_content_type_is_octet_stream",
"justification": "both assert application/octet-stream content type defers to the .pdf filename"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestBuildDocumentFromUpload::test_should_detect_mime_from_filename_when_content_type_is_none",
"status": "unmapped",
"reason": "file-normalization: filename-based MIME fallback"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "upload_detects_mime_from_filename_when_content_type_is_none",
"justification": "both assert a missing content type defers to the .png filename"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestBuildDocumentFromUpload::test_should_fallback_to_octet_stream_for_unknown",
"status": "unmapped",
"reason": "file-normalization: default MIME fallback"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "upload_falls_back_to_octet_stream_for_unknown",
"justification": "both assert no filename and no content type yields application/octet-stream document_url"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestBuildDocumentFromUpload::test_should_preserve_base64_content_correctly",
"status": "unmapped",
"reason": "file-normalization: binary round-trip through base64"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "upload_preserves_binary_content_through_base64",
"justification": "both decode the data URI and assert the arbitrary binary bytes round-trip"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestBuildDocumentFromUpload::test_should_strip_mime_parameters_from_content_type",
"status": "unmapped",
"reason": "file-normalization: content-type parameter stripping"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "upload_strips_mime_parameters_from_content_type",
"justification": "both assert \"application/pdf; charset=utf-8\" yields a data:application/pdf;base64 URI"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestBuildDocumentFromUpload::test_should_strip_mime_parameters_with_multiple_params",
"status": "unmapped",
"reason": "file-normalization: content-type parameter stripping"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/file_input.rs",
"rust_test": "upload_strips_multiple_mime_parameters",
"justification": "both assert multiple content-type parameters are stripped to image/png"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestProxySecurityGuard::test_should_reject_file_type_document_in_json_body",
"status": "unmapped",
"reason": "proxy-layer JSON-body file-type guard, a different mechanism than Rust's URL-fetch SSRF guard"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/request_body.rs",
"rust_test": "json_body_rejects_file_type_document",
"justification": "both assert a JSON body with document.type=file fails with \"not supported through the JSON API\""
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestProxySecurityGuard::test_should_accept_document_url_type_in_json_body",
"status": "unmapped",
"reason": "proxy-layer JSON-body parsing"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/request_body.rs",
"rust_test": "json_body_accepts_document_url_type",
"justification": "both assert a document_url JSON body parses and keeps document.type"
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestProxySecurityGuard::test_should_raise_on_invalid_json_body",
"status": "unmapped",
"reason": "proxy-layer JSON-body parsing error path"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/request_body.rs",
"rust_test": "json_body_rejects_invalid_json",
"justification": "both assert malformed JSON fails with \"Invalid JSON in request body\""
},
{
"python_file": "tests/test_litellm/ocr/test_ocr_file_input.py",
"python_test": "TestProxySecurityGuard::test_should_ignore_document_form_field_injection",
"status": "unmapped",
"reason": "proxy-layer multipart form-field injection guard, a different mechanism than Rust's URL-fetch SSRF guard"
"status": "mapped",
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/request_body.rs",
"rust_test": "multipart_ignores_document_form_field_injection",
"justification": "both assert an injected document form field is ignored and the uploaded PDF becomes the document_url data URI"
},
{
"python_file": "tests/test_litellm/llms/mistral/ocr/test_mistral_ocr_transformation.py",
@ -1079,6 +1159,11 @@
"rust_file": "litellm-rust/crates/ai-gateway/src/integrations/custom_logger/mod.rs",
"rust_test": "rust_custom_logger_reads_success_payload_for_ocr",
"reason": "Rust-internal custom-logger dispatch for OCR payloads has no Python unit test at this layer"
},
{
"rust_file": "litellm-rust/crates/ai-gateway/src/ocr/request_body.rs",
"rust_test": "multipart_rejects_empty_upload",
"reason": "empty-upload rejection is asserted inline in the Python multipart parser but has no dedicated Python test"
}
]
}