fix(anthropic): match text and pdf media types case-insensitively and detect downloaded pdfs by signature
Some checks failed
Cost map pins recheck / rerun-cost-map-guard (push) Waiting to run
LiteLLM Rust DB / db-queries (push) Waiting to run
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled

This commit is contained in:
mateo-berri 2026-10-29 15:40:39 -07:00
parent 108387a9a8
commit dfcfa0b043
2 changed files with 27 additions and 8 deletions

View file

@ -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"],
),
)

View file

@ -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()