mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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:
commit
04818a3554
2 changed files with 88 additions and 0 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue