Merge pull request #38855 from BerriAI/litellm_/backport-38251-rc-1-99-0

[Backport rc/1.99.0] fix(anthropic): translate tool_result document blocks in the /v1/messages bridge
This commit is contained in:
yuneng-jiang 2026-08-29 17:51:18 -07:00 committed by GitHub
commit 1411852684
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 74 additions and 3 deletions

View file

@ -433,7 +433,7 @@ class LiteLLMAnthropicMessagesAdapter:
content_items = list(content.get("content", []))
# Single-item text keeps the backward-compatible string format; a single
# image becomes a structured image_url part
# image or document becomes a structured image_url part
if len(content_items) == 1:
c = content_items[0]
if isinstance(c, str):
@ -453,7 +453,7 @@ class LiteLLMAnthropicMessagesAdapter:
)
self._add_cache_control_if_applicable(content, tool_result, model)
tool_message_list.append(tool_result)
elif c.get("type") == "image":
elif c.get("type") in ("image", "document"):
image_part = self._tool_result_image_part(c.get("source"))
tool_result = ChatCompletionToolMessage(
role="tool",
@ -481,7 +481,7 @@ class LiteLLMAnthropicMessagesAdapter:
text=c.get("text", ""),
)
)
elif c.get("type") == "image":
elif c.get("type") in ("image", "document"):
image_part = self._tool_result_image_part(c.get("source"))
if image_part:
combined_content_parts.append(image_part)

View file

@ -1,3 +1,4 @@
import base64
from typing import Any, cast
import pytest
@ -11,6 +12,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
)
from litellm.litellm_core_utils.prompt_templates.factory import (
THOUGHT_SIGNATURE_SEPARATOR,
_bedrock_converse_messages_pt,
)
from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import (
OPENAI_MAX_TOOL_NAME_LENGTH,
@ -3830,6 +3832,75 @@ def test_tool_result_plain_text_unchanged_by_openai_transform():
assert _image_urls_in_user_messages(result) == []
TOOL_RESULT_PDF_B64 = base64.b64encode(b"%PDF-1.4 minimal regression fixture").decode()
def _base64_pdf_block():
return {
"type": "document",
"source": {"type": "base64", "media_type": "application/pdf", "data": TOOL_RESULT_PDF_B64},
}
def test_tool_result_single_document_kept_as_pdf_data_url():
adapter = LiteLLMAnthropicMessagesAdapter()
translated = adapter.translate_anthropic_messages_to_openai(
messages=[
_anthropic_tool_use_turn("toolu_01"),
_anthropic_tool_result_turn({"toolu_01": [_base64_pdf_block()]}),
]
)
tool_messages = [m for m in translated if m.get("role") == "tool"]
assert len(tool_messages) == 1
assert tool_messages[0]["content"] == [
{
"type": "image_url",
"image_url": {"url": f"data:application/pdf;base64,{TOOL_RESULT_PDF_B64}"},
}
]
def test_tool_result_text_and_document_reach_bedrock_converse_tool_result():
"""Claude Code >= 2.1.245 sends Read-tool PDF output as a document block inside
tool_result; dropping it left bedrock converse models blind to the PDF content."""
adapter = LiteLLMAnthropicMessagesAdapter()
translated = adapter.translate_anthropic_messages_to_openai(
messages=[
AnthropicMessagesUserMessageParam(role="user", content="Read pong.pdf"),
_anthropic_tool_use_turn("toolu_01"),
_anthropic_tool_result_turn(
{
"toolu_01": [
{"type": "text", "text": "PDF file read: pong.pdf (579 bytes)"},
_base64_pdf_block(),
]
}
),
]
)
converse_messages = _bedrock_converse_messages_pt(
messages=translated,
model="anthropic.claude-haiku-4-5-20251001-v1:0",
llm_provider="bedrock_converse",
)
tool_results = [
block["toolResult"]
for message in converse_messages
for block in message["content"]
if "toolResult" in block
]
assert len(tool_results) == 1
documents = [part["document"] for part in tool_results[0]["content"] if "document" in part]
assert len(documents) == 1
assert documents[0]["format"] == "pdf"
assert documents[0]["source"]["bytes"] == TOOL_RESULT_PDF_B64
texts = [part["text"] for part in tool_results[0]["content"] if "text" in part]
assert texts == ["PDF file read: pong.pdf (579 bytes)"]
def test_translate_anthropic_to_openai_carries_prompt_cache_breakpoint_on_system_and_user_blocks():
explicit = {"mode": "explicit"}
openai_request, _ = LiteLLMAnthropicMessagesAdapter().translate_anthropic_to_openai(