diff --git a/litellm/ocr/main.py b/litellm/ocr/main.py index e5c7c8db037..593b0e4093a 100644 --- a/litellm/ocr/main.py +++ b/litellm/ocr/main.py @@ -494,11 +494,21 @@ def convert_file_document_to_url_document(document: dict[str, Any]) -> dict[str, if not file_bytes: raise ValueError("File is empty or could not be read") - if mime_type == "application/octet-stream": - mime_type = sniff_mime_type_from_bytes(file_bytes) or mime_type - - if "mime_type" in document: - mime_type = document["mime_type"] + explicit_mime = document.get("mime_type") + explicit_mime = explicit_mime if isinstance(explicit_mime, str) else None + resolved_mime = next( + ( + candidate + for candidate in (explicit_mime, mime_type) + if candidate and candidate != "application/octet-stream" + ), + None, + ) + mime_type = ( + resolved_mime + or sniff_mime_type_from_bytes(file_bytes) + or "application/octet-stream" + ) if not _MIME_PATTERN.match(mime_type): raise ValueError(f"Invalid MIME type: {mime_type}") diff --git a/tests/test_litellm/ocr/test_ocr_file_input.py b/tests/test_litellm/ocr/test_ocr_file_input.py index 35628d0aedc..30bcc51beec 100644 --- a/tests/test_litellm/ocr/test_ocr_file_input.py +++ b/tests/test_litellm/ocr/test_ocr_file_input.py @@ -77,7 +77,9 @@ class TestSniffMimeTypeFromBytes: assert sniff_mime_type_from_bytes(b"GIF89a rest") == "image/gif" def test_should_detect_webp_from_magic_bytes(self): - assert sniff_mime_type_from_bytes(b"RIFF\x00\x00\x00\x00WEBPrest") == "image/webp" + assert ( + sniff_mime_type_from_bytes(b"RIFF\x00\x00\x00\x00WEBPrest") == "image/webp" + ) def test_should_detect_tiff_from_magic_bytes(self): assert sniff_mime_type_from_bytes(b"II*\x00 rest") == "image/tiff" @@ -226,6 +228,31 @@ class TestConvertFileDocumentToUrlDocument: assert result["type"] == "image_url" assert result["image_url"].startswith("data:image/png;base64,") + def test_should_sniff_when_explicit_mime_is_octet_stream(self): + """The proxy multipart path always passes an explicit mime_type, using + application/octet-stream as its unknown-type fallback. That placeholder must + not defeat content sniffing, otherwise uploaded PDFs regress to octet-stream.""" + content = b"%PDF-1.4\n1 0 obj\n<< >>\nendobj\n" + + result = convert_file_document_to_url_document( + {"type": "file", "file": content, "mime_type": "application/octet-stream"} + ) + + assert result["type"] == "document_url" + assert result["document_url"].startswith("data:application/pdf;base64,") + + def test_should_prefer_named_extension_over_sniffed_magic_number(self): + """A file-like object's name extension stays authoritative over sniffing.""" + file_obj = BytesIO(b"\x89PNG\r\n\x1a\n not really a png") + file_obj.name = "report.pdf" + + result = convert_file_document_to_url_document( + {"type": "file", "file": file_obj} + ) + + assert result["type"] == "document_url" + assert result["document_url"].startswith("data:application/pdf;base64,") + def test_should_fallback_to_octet_stream_for_unrecognized_raw_bytes(self): """Bytes matching no known signature stay octet-stream so behavior is unchanged.""" content = b"totally unrecognized payload" @@ -235,7 +262,9 @@ class TestConvertFileDocumentToUrlDocument: ) assert result["type"] == "document_url" - assert result["document_url"].startswith("data:application/octet-stream;base64,") + assert result["document_url"].startswith( + "data:application/octet-stream;base64," + ) def test_should_convert_raw_bytes_with_explicit_mime_type(self): """Raw bytes with explicit mime_type should use the specified MIME type."""