From 85ed500871ff55c74d16e809ddae0d4db66cbc3a Mon Sep 17 00:00:00 2001 From: George Weale Date: Fri, 10 Oct 2025 08:38:47 -0700 Subject: [PATCH] fix: Add support for file URIs in LiteLLM content conversion to fix issue #3131 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit changed the LiteLLM content conversion so Part.file_data.file_uri (like the gs://…) becomes a file object with file_id, making sure GCS-backed files reach LiteLLM proxies instead of being dropped add unit tests covering both _get_content and _content_to_message_param paths for file URIs PiperOrigin-RevId: 817658432 --- src/google/adk/models/lite_llm.py | 13 ++++++- tests/unittests/models/test_litellm.py | 54 ++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index d8b4d7ce..94d9831c 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -65,8 +65,9 @@ _NEW_LINE = "\n" _EXCLUDED_PART_FIELD = {"inline_data": {"data"}} -class ChatCompletionFileUrlObject(TypedDict): +class ChatCompletionFileUrlObject(TypedDict, total=False): file_data: str + file_id: str format: str @@ -281,6 +282,16 @@ def _get_content( }) else: raise ValueError("LiteLlm(BaseLlm) does not support this content part.") + elif part.file_data and part.file_data.file_uri: + file_object: ChatCompletionFileUrlObject = { + "file_id": part.file_data.file_uri, + } + if part.file_data.mime_type: + file_object["format"] = part.file_data.mime_type + content_objects.append({ + "type": "file", + "file": file_object, + }) return content_objects diff --git a/tests/unittests/models/test_litellm.py b/tests/unittests/models/test_litellm.py index 84fd7f26..2fbacc0f 100644 --- a/tests/unittests/models/test_litellm.py +++ b/tests/unittests/models/test_litellm.py @@ -1005,6 +1005,47 @@ def test_content_to_message_param_user_message(): assert message["content"] == "Test prompt" +def test_content_to_message_param_user_message_with_file_uri(): + file_part = types.Part.from_uri( + file_uri="gs://bucket/document.pdf", mime_type="application/pdf" + ) + content = types.Content( + role="user", + parts=[ + types.Part.from_text(text="Summarize this file."), + file_part, + ], + ) + + message = _content_to_message_param(content) + assert message["role"] == "user" + assert isinstance(message["content"], list) + assert message["content"][0]["type"] == "text" + assert message["content"][0]["text"] == "Summarize this file." + assert message["content"][1]["type"] == "file" + assert message["content"][1]["file"]["file_id"] == "gs://bucket/document.pdf" + assert message["content"][1]["file"]["format"] == "application/pdf" + + +def test_content_to_message_param_user_message_file_uri_only(): + file_part = types.Part.from_uri( + file_uri="gs://bucket/only.pdf", mime_type="application/pdf" + ) + content = types.Content( + role="user", + parts=[ + file_part, + ], + ) + + message = _content_to_message_param(content) + assert message["role"] == "user" + assert isinstance(message["content"], list) + assert message["content"][0]["type"] == "file" + assert message["content"][0]["file"]["file_id"] == "gs://bucket/only.pdf" + assert message["content"][0]["file"]["format"] == "application/pdf" + + def test_content_to_message_param_multi_part_function_response(): part1 = types.Part.from_function_response( name="function_one", @@ -1183,6 +1224,19 @@ def test_get_content_pdf(): assert content[0]["file"]["format"] == "application/pdf" +def test_get_content_file_uri(): + parts = [ + types.Part.from_uri( + file_uri="gs://bucket/document.pdf", + mime_type="application/pdf", + ) + ] + content = _get_content(parts) + assert content[0]["type"] == "file" + assert content[0]["file"]["file_id"] == "gs://bucket/document.pdf" + assert content[0]["file"]["format"] == "application/pdf" + + def test_get_content_audio(): parts = [ types.Part.from_bytes(data=b"test_audio_data", mime_type="audio/mpeg")