From 68f1ba1e5aa0ef5d670e2ddd0e1c16e66b7a7272 Mon Sep 17 00:00:00 2001 From: bunnysayzz Date: Tue, 13 Oct 2026 01:27:04 +0000 Subject: [PATCH] fix(anthropic): reject non-image document blocks before wrapping as image_url When an Anthropic pass-through request contains a document content block with a non-image media_type (e.g. application/pdf), the adapter was calling _translate_anthropic_image_to_openai() and wrapping the result as ChatCompletionImageObject(type="image_url", ...). This produced a data URI with media_type application/pdf as the image_url URL, which downstream backends (e.g. ChatGPT) reject because image_url must carry an image/* MIME type. Fix: in the document block handler (transformation.py line 517), check the source media_type before forwarding. If it does not start with "image/", raise BadRequestError with a clear message explaining that PDF documents must be converted to images before sending. Image documents (image/jpeg, image/png, etc.) continue to pass through unchanged. Adds two regression tests: - test_document_pdf_raises_bad_request_error: verifies BadRequestError is raised for application/pdf, with the media_type in the error message. - test_document_image_jpeg_passes_through: verifies image/jpeg documents are still translated to image_url correctly. Fixes #46117 --- .../pass_through/adapters/transformation.py | 19 ++++- ...al_pass_through_adapters_transformation.py | 74 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index 2ee23852894..ca7e7c6b7cf 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -515,8 +515,25 @@ class LiteLLMAnthropicMessagesAdapter: self._add_prompt_cache_breakpoint_if_present(content, image_obj) ) elif content.get("type") == "document": - # Convert Anthropic document format (PDF, etc.) to OpenAI format + # Convert Anthropic document format to OpenAI format. + # Only image/* media types can be forwarded as image_url; + # non-image documents (e.g. application/pdf) are not + # accepted by image-only backends and must be rejected + # with a clear error rather than silently producing a + # malformed request. source = content.get("source", {}) + media_type = source.get("media_type", "") if isinstance(source, dict) else "" + if media_type and not str(media_type).startswith("image/"): + raise litellm.BadRequestError( + message=( + f"Document content block with media_type='{media_type}' cannot be " + "forwarded to this model as an image_url. Only image/* documents " + "are supported. For PDF documents, convert the pages to images " + "before sending." + ), + model=model, + llm_provider="anthropic", + ) openai_image_url = self._translate_anthropic_image_to_openai(cast(dict, source)) if openai_image_url: diff --git a/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index 2eee8549c0b..be1393e78c7 100644 --- a/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -5143,3 +5143,77 @@ def test_eager_input_streaming_tool_reaches_bedrock_converse_as_beta(): assert data["additionalModelRequestFields"]["anthropic_beta"] == ["fine-grained-tool-streaming-2025-05-14"] assert data["toolConfig"]["tools"][0]["toolSpec"]["inputSchema"]["json"] == EAGER_INPUT_SCHEMA + + +def test_document_pdf_raises_bad_request_error() -> None: + """ + Regression test for https://github.com/BerriAI/litellm/issues/46117 + + An Anthropic document block with media_type='application/pdf' must raise + BadRequestError when translated to OpenAI format, not silently produce a + malformed image_url with a PDF MIME type that the downstream backend rejects. + """ + adapter = LiteLLMAnthropicMessagesAdapter() + + messages = [ + AnthropicMessagesUserMessageParam( + role="user", + content=[ + { + "type": "document", + "source": { + "type": "base64", + "media_type": "application/pdf", + "data": "JVBERi0xLjQK", # minimal synthetic PDF header + }, + }, + {"type": "text", "text": "Summarize this document."}, + ], + ) + ] + + with pytest.raises(litellm.BadRequestError) as exc_info: + adapter.translate_anthropic_messages_to_openai( + messages=messages, + model="chatgpt/gpt-6.1-sol", + ) + + assert "application/pdf" in str(exc_info.value) + + +def test_document_image_jpeg_passes_through() -> None: + """ + An Anthropic document block with media_type='image/jpeg' must still be + translated to image_url without raising an error. + """ + adapter = LiteLLMAnthropicMessagesAdapter() + + messages = [ + AnthropicMessagesUserMessageParam( + role="user", + content=[ + { + "type": "document", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": "/9j/4AAQSkZJRgAB", # minimal JPEG header + }, + }, + {"type": "text", "text": "Describe this image."}, + ], + ) + ] + + # Should not raise + result = adapter.translate_anthropic_messages_to_openai( + messages=messages, + model="chatgpt/gpt-6.1-sol", + ) + # The image document should be translated to an image_url content block + user_msg = next(m for m in result if m.get("role") == "user") + content = user_msg.get("content", []) + image_blocks = [c for c in content if isinstance(c, dict) and c.get("type") == "image_url"] + assert len(image_blocks) == 1 + url = image_blocks[0]["image_url"]["url"] + assert url.startswith("data:image/jpeg;base64,")