diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index 0d3e48c49fd..019fd6db1c5 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -520,8 +520,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 9e0bd5c60b8..17e219da10e 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 @@ -5235,3 +5235,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,")