From d07225040c6bd6491ae1922a80f420eeb0adf890 Mon Sep 17 00:00:00 2001 From: Alejandro Sotoca Date: Tue, 24 Feb 2026 12:54:29 +0100 Subject: [PATCH 1/2] fix(models): pass (filename, bytes, mime_type) tuple to acreate_file for OpenAI/Azure providers When uploading non-image file attachments (e.g. PDFs) via the LiteLLM integration, the `acreate_file` call for OpenAI and Azure providers was passing only raw bytes as the `file` argument. LiteLLM and the OpenAI Files API expect a multipart upload with a `(filename, bytes, content_type)` tuple so the Content-Type header is set correctly. Passing raw bytes caused the stored file to have MIME type `None`, which the chat completions API then rejected with: Invalid file data: 'file_id'. Expected a file with an application/pdf MIME type, but got unsupported MIME type 'None'. Fix: pass `(display_name, data, mime_type)` to `acreate_file`, using the part's `display_name` when available, or a sensible default filename derived from the MIME type via the new `_filename_for_mime` helper. Fixes #4174 --- src/google/adk/models/lite_llm.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index dad5543fed..f9c6b3412f 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -138,6 +138,24 @@ # Providers that require file_id instead of inline file_data _FILE_ID_REQUIRED_PROVIDERS = frozenset({"openai", "azure"}) +# Default filenames for file uploads when display_name is missing. +# OpenAI derives the MIME type from the filename extension during upload, +# so passing a proper name ensures the stored file gets the right content-type. +_MIME_TO_FILENAME = { + "application/pdf": "document.pdf", + "application/json": "document.json", + "application/msword": "document.doc", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "document.docx", + "application/vnd.openxmlformats-officedocument.presentationml.presentation": "document.pptx", + "application/x-sh": "script.sh", +} + + +def _filename_for_mime(mime_type: str) -> str: + """Return a default filename for a MIME type so uploads get the right content-type.""" + return _MIME_TO_FILENAME.get(mime_type, "document.bin") + + _MISSING_TOOL_RESULT_MESSAGE = ( "Error: Missing tool result (tool execution may have been interrupted " "before a response was recorded)." @@ -840,10 +858,18 @@ async def _get_content( url_content_type: {"url": data_uri}, }) elif mime_type in _SUPPORTED_FILE_CONTENT_MIME_TYPES: - # OpenAI/Azure require file_id from uploaded file, not inline data + # OpenAI/Azure require file_id from uploaded file, not inline data. + # Pass (filename, content, content_type) so the upload gets the right MIME type. if provider in _FILE_ID_REQUIRED_PROVIDERS: + display_name = getattr( + part.inline_data, "display_name", None + ) or _filename_for_mime(part.inline_data.mime_type) file_response = await litellm.acreate_file( - file=part.inline_data.data, + file=( + display_name, + part.inline_data.data, + part.inline_data.mime_type, + ), purpose="assistants", custom_llm_provider=provider, ) From 3e2608478cb6b0a31664cd571661613aaacad5cb Mon Sep 17 00:00:00 2001 From: Alejandro Sotoca Date: Tue, 24 Feb 2026 13:01:09 +0100 Subject: [PATCH 2/2] test(models): update and extend acreate_file tests for OpenAI/Azure providers Update existing tests that assert acreate_file is called with raw bytes to instead expect the (filename, bytes, mime_type) tuple required by the LiteLLM/OpenAI multipart upload API. Add test_get_content_pdf_openai_uses_display_name_as_filename to verify that part.inline_data.display_name is used as the filename when available, falling back to _filename_for_mime otherwise. --- tests/unittests/models/test_litellm.py | 30 +++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/unittests/models/test_litellm.py b/tests/unittests/models/test_litellm.py index 8e353efb21..fed123dcdc 100644 --- a/tests/unittests/models/test_litellm.py +++ b/tests/unittests/models/test_litellm.py @@ -3958,7 +3958,27 @@ async def test_get_content_pdf_openai_uses_file_id(mocker): assert "file_data" not in content[0]["file"] mock_acreate_file.assert_called_once_with( - file=b"test_pdf_data", + file=("document.pdf", b"test_pdf_data", "application/pdf"), + purpose="assistants", + custom_llm_provider="openai", + ) + + +@pytest.mark.asyncio +async def test_get_content_pdf_openai_uses_display_name_as_filename(mocker): + """Test that display_name is used as filename when available.""" + mock_file_response = mocker.create_autospec(litellm.FileObject) + mock_file_response.id = "file-abc123" + mock_acreate_file = AsyncMock(return_value=mock_file_response) + mocker.patch.object(litellm, "acreate_file", new=mock_acreate_file) + + part = types.Part.from_bytes(data=b"test_pdf_data", mime_type="application/pdf") + part.inline_data.display_name = "my_report.pdf" + content = await _get_content([part], provider="openai") + + assert content[0]["file"]["file_id"] == "file-abc123" + mock_acreate_file.assert_called_once_with( + file=("my_report.pdf", b"test_pdf_data", "application/pdf"), purpose="assistants", custom_llm_provider="openai", ) @@ -3997,7 +4017,7 @@ async def test_get_content_pdf_azure_uses_file_id(mocker): assert content[0]["file"]["file_id"] == "file-xyz789" mock_acreate_file.assert_called_once_with( - file=b"test_pdf_data", + file=("document.pdf", b"test_pdf_data", "application/pdf"), purpose="assistants", custom_llm_provider="azure", ) @@ -4041,7 +4061,11 @@ async def test_get_completion_inputs_openai_file_upload(mocker): assert content[1]["type"] == "file" assert content[1]["file"]["file_id"] == "file-uploaded123" - mock_acreate_file.assert_called_once() + mock_acreate_file.assert_called_once_with( + file=("document.pdf", b"test_pdf_content", "application/pdf"), + purpose="assistants", + custom_llm_provider="openai", + ) @pytest.mark.asyncio