From 5b5363cd5447d898b1c981beb997436e6b167cf1 Mon Sep 17 00:00:00 2001 From: Josh Minzner Date: Tue, 28 Apr 2026 14:54:54 -0400 Subject: [PATCH] test: mirror PDF tool-result tests under tests/test_litellm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Duplicate the three Bedrock and three Anthropic tool-result tests into tests/test_litellm/ so they're picked up by `make test-unit` (and its coverage report). The originals in tests/llm_translation/ stay — they run under integration and remain the canonical translation-suite regression cases. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...llm_core_utils_prompt_templates_factory.py | 109 +++++++++++++++ .../chat/test_converse_transformation.py | 127 ++++++++++++++++++ 2 files changed, 236 insertions(+) diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py index 72cfd89408d..d9b458ad8bd 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py @@ -2476,3 +2476,112 @@ def test_bedrock_tools_pt_passes_ttl_for_claude_4_5(): cache_blocks_old = [b for b in result_old if "cachePoint" in b] assert len(cache_blocks_old) == 1 assert "ttl" not in cache_blocks_old[0]["cachePoint"] + + +def test_convert_to_anthropic_tool_result_openai_file_pdf_becomes_document(): + """ + OpenAI `{type: "file", file: {file_data: "data:application/pdf;..."}}` inside + a tool-message content list should translate to an Anthropic document block + inside the tool_result content. Reuses anthropic_process_openai_file_message, + which already handles this for user messages. + """ + from litellm.litellm_core_utils.prompt_templates.factory import ( + convert_to_anthropic_tool_result, + ) + + pdf_b64 = "JVBERi0xLjQKJeLjz9MK" + message = { + "tool_call_id": "toolu_pdf_1", + "role": "tool", + "name": "fetch_document", + "content": [ + { + "type": "file", + "file": { + "file_data": f"data:application/pdf;base64,{pdf_b64}", + "filename": "summary.pdf", + }, + }, + ], + } + + result = convert_to_anthropic_tool_result(message) + + assert result["type"] == "tool_result" + assert result["tool_use_id"] == "toolu_pdf_1" + content = result["content"] + assert isinstance(content, list) and len(content) == 1 + block = content[0] + assert block["type"] == "document" + assert block["source"]["type"] == "base64" + assert block["source"]["media_type"] == "application/pdf" + assert block["source"]["data"] == pdf_b64 + + +def test_convert_to_anthropic_tool_result_image_url_pdf_data_uri_becomes_document(): + """ + Regression: a PDF sent as an `image_url` data URI on the tool-result path + must translate to an Anthropic document block (not an image block — Anthropic + rejects image blocks whose media_type is a non-image like application/pdf). + """ + from litellm.litellm_core_utils.prompt_templates.factory import ( + convert_to_anthropic_tool_result, + ) + + pdf_b64 = "JVBERi0xLjQKJeLjz9MK" + message = { + "tool_call_id": "toolu_pdf_img_1", + "role": "tool", + "name": "fetch_document", + "content": [ + { + "type": "image_url", + "image_url": { + "url": f"data:application/pdf;base64,{pdf_b64}", + }, + }, + ], + } + + result = convert_to_anthropic_tool_result(message) + + content = result["content"] + assert isinstance(content, list) and len(content) == 1 + block = content[0] + assert block["type"] == "document" + assert block["source"]["media_type"] == "application/pdf" + assert block["source"]["data"] == pdf_b64 + + +def test_convert_to_anthropic_tool_result_image_url_png_still_becomes_image(): + """ + Regression: image_url with a real image mime type must continue to translate + to an Anthropic image block. Locks in existing behavior after the + data-URI-mime-type branching for PDFs. + """ + from litellm.litellm_core_utils.prompt_templates.factory import ( + convert_to_anthropic_tool_result, + ) + + png_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGBgAAAABQABXvMqOgAAAABJRU5ErkJggg==" + message = { + "tool_call_id": "toolu_png_1", + "role": "tool", + "name": "fetch_image", + "content": [ + { + "type": "image_url", + "image_url": { + "url": f"data:image/png;base64,{png_b64}", + }, + }, + ], + } + + result = convert_to_anthropic_tool_result(message) + + content = result["content"] + assert isinstance(content, list) and len(content) == 1 + block = content[0] + assert block["type"] == "image" + assert block["source"]["media_type"] == "image/png" diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py index 38a59c694e7..02477c24c42 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -4146,3 +4146,130 @@ def test_transform_response_finish_reason_stop_when_json_mode_filters_all_tools( # finish_reason must be "stop", not "tool_calls" assert result.choices[0].finish_reason == "stop" + + +def test_bedrock_tool_message_openai_file_pdf_becomes_document(): + """ + OpenAI Chat Completions `{type: "file", file: {file_data: "data:application/pdf;...", filename}}` + inside a tool message content list should translate to a Bedrock + toolResult.content[].document block. + """ + from litellm.litellm_core_utils.prompt_templates.factory import ( + _bedrock_converse_messages_pt, + ) + + pdf_b64 = "JVBERi0xLjQKJeLjz9MK" # tiny "%PDF-1.4\n" header + messages = [ + {"role": "user", "content": "Summarize the attached PDF."}, + { + "tool_call_id": "tooluse_pdf_1", + "role": "tool", + "name": "fetch_document", + "content": [ + { + "type": "file", + "file": { + "file_data": f"data:application/pdf;base64,{pdf_b64}", + "filename": "summary.pdf", + }, + }, + ], + }, + ] + + translated_msg = _bedrock_converse_messages_pt( + messages=messages, model="", llm_provider="" + ) + + tool_result = translated_msg[-1]["content"][-1]["toolResult"] + assert tool_result["toolUseId"] == "tooluse_pdf_1" + assert len(tool_result["content"]) == 1 + block = tool_result["content"][0] + assert "document" in block, f"expected document block, got {block}" + assert block["document"]["format"] == "pdf" + assert block["document"]["source"]["bytes"] == pdf_b64 + assert block["document"]["name"].startswith("DocumentPDFmessages_") + assert block["document"]["name"].endswith("_pdf") + + +def test_bedrock_tool_message_image_url_pdf_data_uri_becomes_document(): + """ + Regression for the processor-returns-document-but-wrapper-drops-it bug: + when a caller sends a PDF as an `image_url` data URI on the tool-result path, + BedrockImageProcessor correctly returns a {"document": ...} block, but the + tool-result wrapper only appended the "image" case, silently dropping documents. + """ + from litellm.litellm_core_utils.prompt_templates.factory import ( + _bedrock_converse_messages_pt, + ) + + pdf_b64 = "JVBERi0xLjQKJeLjz9MK" + messages = [ + {"role": "user", "content": "Summarize the attached PDF."}, + { + "tool_call_id": "tooluse_pdf_img_1", + "role": "tool", + "name": "fetch_document", + "content": [ + { + "type": "image_url", + "image_url": { + "url": f"data:application/pdf;base64,{pdf_b64}", + }, + }, + ], + }, + ] + + translated_msg = _bedrock_converse_messages_pt( + messages=messages, model="", llm_provider="" + ) + + tool_result = translated_msg[-1]["content"][-1]["toolResult"] + assert tool_result["toolUseId"] == "tooluse_pdf_img_1" + assert len(tool_result["content"]) == 1 + block = tool_result["content"][0] + assert "document" in block, f"expected document block, got {block}" + assert block["document"]["format"] == "pdf" + assert block["document"]["source"]["bytes"] == pdf_b64 + + +def test_bedrock_tool_message_image_url_png_still_becomes_image(): + """ + Regression: image_url with an image mime type must continue to translate + to a Bedrock image block (not document). Locks in existing behavior after + the document-passthrough fix. + """ + from litellm.litellm_core_utils.prompt_templates.factory import ( + _bedrock_converse_messages_pt, + ) + + png_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGBgAAAABQABXvMqOgAAAABJRU5ErkJggg==" + messages = [ + {"role": "user", "content": "Describe the attached image."}, + { + "tool_call_id": "tooluse_png_1", + "role": "tool", + "name": "fetch_image", + "content": [ + { + "type": "image_url", + "image_url": { + "url": f"data:image/png;base64,{png_b64}", + }, + }, + ], + }, + ] + + translated_msg = _bedrock_converse_messages_pt( + messages=messages, model="", llm_provider="" + ) + + tool_result = translated_msg[-1]["content"][-1]["toolResult"] + assert len(tool_result["content"]) == 1 + block = tool_result["content"][0] + assert "image" in block, f"expected image block, got {block}" + assert "document" not in block + assert block["image"]["format"] == "png" + assert block["image"]["source"]["bytes"] == png_b64