From dfcfa0b043cbc0dcef492adae53e048afd214098 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:40:39 -0700 Subject: [PATCH] fix(anthropic): match text and pdf media types case-insensitively and detect downloaded pdfs by signature --- .../prompt_templates/factory.py | 13 ++++++----- ...llm_core_utils_prompt_templates_factory.py | 22 ++++++++++++++++--- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 746745cc791..bd22ce6ed91 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -1852,7 +1852,7 @@ def _anthropic_text_document_source( media_type_with_parameters: str, data: str ) -> AnthropicContentParamSourceText | None: media_type, _, _ = media_type_with_parameters.partition(";") - if media_type.strip() != "text/plain": + if media_type.strip().lower() != "text/plain": return None content_type_header: Final = Message() content_type_header["content-type"] = media_type_with_parameters @@ -2338,7 +2338,9 @@ def _anthropic_native_tool_use_block( def anthropic_content_element_factory( image_chunk: GenericImageParsingChunk, ) -> AnthropicMessagesImageParam | AnthropicMessagesDocumentParam: - if image_chunk["media_type"].partition(";")[0].strip().lower() == "application/pdf": + if image_chunk["media_type"].partition(";")[0].strip().lower() == "application/pdf" or _base64_holds_a_pdf( + image_chunk["data"] + ): _anthropic_content_element: AnthropicMessagesDocumentParam | AnthropicMessagesImageParam = ( AnthropicMessagesDocumentParam( type="document", @@ -2365,9 +2367,10 @@ def anthropic_content_element_factory( def select_anthropic_content_block_type_for_file( format: str, ) -> Literal["document", "image", "container_upload"]: - if format == "application/pdf" or format == "text/plain": + media_type: Final = format.strip().lower() + if media_type == "application/pdf" or media_type == "text/plain": return "document" - elif format in ["image/jpeg", "image/png", "image/gif", "image/webp"]: + elif media_type in ["image/jpeg", "image/png", "image/gif", "image/webp"]: return "image" else: return "container_upload" @@ -2427,7 +2430,7 @@ def anthropic_process_openai_file_message( type="document", source=AnthropicContentParamSource( type="base64", - media_type=media_type.strip(), + media_type=media_type.strip().lower(), data=image_chunk["data"], ), ) diff --git a/tests/unit/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py b/tests/unit/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py index 9cb574bada5..71946245034 100644 --- a/tests/unit/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py +++ b/tests/unit/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py @@ -5221,6 +5221,7 @@ _TEXT_DATA_URI_WITH_CHARSET = "data:text/plain;charset=utf-8;base64," + _TEXT_B6 _LATIN1_TEXT_DATA_URI = "data:text/plain;charset=iso-8859-1;base64," + base64.b64encode( "the secret code word is MANGĂ“\n".encode("latin-1") ).decode() +_OCTET_TEXT_DATA_URI = "data:application/octet-stream;base64," + _TEXT_B64 @pytest.mark.parametrize( @@ -5239,6 +5240,15 @@ _LATIN1_TEXT_DATA_URI = "data:text/plain;charset=iso-8859-1;base64," + base64.b6 {"type": "image_url", "image_url": {"url": _LATIN1_TEXT_DATA_URI.replace("text/plain", "TEXT/PLAIN", 1)}}, "the secret code word is MANGĂ“\n", ), + ({"type": "file", "file": {"file_data": "data:TEXT/PLAIN;base64," + _TEXT_B64}}, "the secret code word is MANGO\n"), + ( + {"type": "file", "file": {"file_data": _OCTET_TEXT_DATA_URI, "format": "Text/Plain"}}, + "the secret code word is MANGO\n", + ), + ( + {"type": "image_url", "image_url": {"url": _OCTET_TEXT_DATA_URI, "format": "Text/Plain"}}, + "the secret code word is MANGO\n", + ), ], ids=[ "image_url", @@ -5248,6 +5258,9 @@ _LATIN1_TEXT_DATA_URI = "data:text/plain;charset=iso-8859-1;base64," + base64.b6 "file_latin1", "image_url_uppercase", "uppercase_latin1", + "file_uppercase", + "file_mixed_case_format", + "image_url_mixed_case_format", ], ) def test_anthropic_messages_pt_text_plain_data_uri_becomes_text_source_document(block, expected_text): @@ -9436,13 +9449,16 @@ def test_anthropic_messages_pt_keeps_a_native_tool_use_answered_by_a_tool_messag ] -@pytest.mark.parametrize("pdf_format", (None, "Application/PDF")) -def test_create_anthropic_image_param_sends_a_downloaded_pdf_as_a_document(monkeypatch, pdf_format): +@pytest.mark.parametrize( + "pdf_format, content_type", + ((None, "application/pdf"), ("Application/PDF", "application/pdf"), (None, "image/png"), ("image/png", "application/pdf")), +) +def test_create_anthropic_image_param_sends_a_downloaded_pdf_as_a_document(monkeypatch, pdf_format, content_type): from litellm.litellm_core_utils.prompt_templates.image_handling import in_memory_cache pdf_bytes = b"%PDF-1.4\n1 0 obj\n" mock_client = MagicMock() - mock_client.get.return_value = httpx.Response(200, content=pdf_bytes, headers={"Content-Type": "application/pdf"}) + mock_client.get.return_value = httpx.Response(200, content=pdf_bytes, headers={"Content-Type": content_type}) monkeypatch.setattr(litellm, "user_url_validation", False, raising=False) monkeypatch.setattr(litellm, "module_level_client", mock_client, raising=False) in_memory_cache.flush_cache()