Merge pull request #38267 from BerriAI/litellm_fix_responses_user_document_drop

fix(anthropic): carry user-content document blocks through the /v1/messages responses bridge
This commit is contained in:
Mateo Wang 2026-08-25 15:20:30 -07:00 committed by GitHub
commit 04818a3554
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 88 additions and 0 deletions

View file

@ -214,6 +214,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
system text -> message(role=system, input_text)
user text -> message(role=user, input_text)
user image -> message(role=user, input_image)
user document -> message(role=user, input_file)
user tool_result -> function_call_output
assistant text -> message(role=assistant, output_text)
assistant thinking -> reasoning
@ -268,6 +269,12 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
{"type": "input_image", "image_url": url}, block.get("prompt_cache_breakpoint")
)
)
elif btype == "document":
file_part = self._translate_anthropic_document_block_to_file_part(block)
if file_part:
user_parts.append(
with_prompt_cache_breakpoint(file_part, block.get("prompt_cache_breakpoint"))
)
elif btype == "tool_result":
tool_use_id = block.get("tool_use_id", "")
inner = block.get("content")

View file

@ -1688,6 +1688,87 @@ class TestToolResultDocuments:
]
class TestUserContentDocuments:
"""Documents in plain user content must survive translation (LIT-6144): each
document block becomes an input_file part of the user message, in block order,
exactly like image blocks become input_image parts. Untranslatable documents
are dropped without disturbing the surrounding parts."""
PDF_B64 = "JVBERi0xLjQKJSBQT05H"
PDF_DATA_URI = "data:application/pdf;base64,JVBERi0xLjQKJSBQT05H"
PDF_URL = "https://example.com/report.pdf"
EXPLICIT = {"mode": "explicit"}
def _translate(self, user_content):
return _ADAPTER.translate_messages_to_responses_input([{"role": "user", "content": user_content}])
@staticmethod
def _user_content(items):
return next(item for item in items if item.get("type") == "message" and item.get("role") == "user")["content"]
def _base64_document(self, **extra):
return {
"type": "document",
"source": {"type": "base64", "media_type": "application/pdf", "data": self.PDF_B64},
**extra,
}
def test_document_then_text_keeps_block_order(self):
content = self._user_content(
self._translate([self._base64_document(), {"type": "text", "text": "what does the pdf say?"}])
)
assert content == [
{"type": "input_file", "filename": "document.pdf", "file_data": self.PDF_DATA_URI},
{"type": "input_text", "text": "what does the pdf say?"},
]
def test_document_title_becomes_filename(self):
content = self._user_content(self._translate([self._base64_document(title="quarterly-report.pdf")]))
assert content == [
{"type": "input_file", "filename": "quarterly-report.pdf", "file_data": self.PDF_DATA_URI}
]
def test_url_document_becomes_file_url_part(self):
content = self._user_content(
self._translate([{"type": "document", "source": {"type": "url", "url": self.PDF_URL}}])
)
assert content == [{"type": "input_file", "file_url": self.PDF_URL}]
def test_document_only_content_still_produces_user_message(self):
content = self._user_content(self._translate([self._base64_document()]))
assert content == [{"type": "input_file", "filename": "document.pdf", "file_data": self.PDF_DATA_URI}]
def test_empty_base64_data_drops_only_the_document_part(self):
content = self._user_content(
self._translate(
[
{"type": "text", "text": "still here"},
{"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": ""}},
]
)
)
assert content == [{"type": "input_text", "text": "still here"}]
def test_non_dict_source_drops_only_the_document_part(self):
content = self._user_content(
self._translate([{"type": "text", "text": "still here"}, {"type": "document", "source": self.PDF_URL}])
)
assert content == [{"type": "input_text", "text": "still here"}]
def test_document_breakpoint_rides_on_the_file_part(self):
content = self._user_content(
self._translate([self._base64_document(prompt_cache_breakpoint=self.EXPLICIT)])
)
assert content == [
{
"type": "input_file",
"filename": "document.pdf",
"file_data": self.PDF_DATA_URI,
"prompt_cache_breakpoint": self.EXPLICIT,
}
]
def _contains_key(value, key) -> bool:
if isinstance(value, dict):
return key in value or any(_contains_key(v, key) for v in value.values())