@@ -1574,13 +1574,13 @@ async def test_content_to_message_param_user_message_with_file_uri(
15741574 )
15751575
15761576 message = await _content_to_message_param (content )
1577- assert message [ "role" ] == "user"
1578- assert isinstance ( message [ "content" ], list )
1579- assert message [ "content" ][ 0 ][ "type" ] == "text"
1580- assert message [ "content" ][ 0 ][ "text" ] == "Summarize this file."
1581- assert message [ "content" ][ 1 ][ "type" ] == "file"
1582- assert message [ "content" ][ 1 ][ "file" ][ "file_id" ] == file_uri
1583- assert "format" not in message [ "content" ][ 1 ][ "file" ]
1577+ assert message == {
1578+ "role" : "user" ,
1579+ "content" : [
1580+ { "type" : "text" , "text" : "Summarize this file." },
1581+ { "type" : "file" , "file" : { "file_id" : file_uri , "format" : mime_type }},
1582+ ],
1583+ }
15841584
15851585
15861586@pytest .mark .asyncio
@@ -1597,11 +1597,88 @@ async def test_content_to_message_param_user_message_file_uri_only(
15971597 )
15981598
15991599 message = await _content_to_message_param (content )
1600- assert message ["role" ] == "user"
1601- assert isinstance (message ["content" ], list )
1602- assert message ["content" ][0 ]["type" ] == "file"
1603- assert message ["content" ][0 ]["file" ]["file_id" ] == file_uri
1604- assert "format" not in message ["content" ][0 ]["file" ]
1600+ assert message == {
1601+ "role" : "user" ,
1602+ "content" : [
1603+ {"type" : "file" , "file" : {"file_id" : file_uri , "format" : mime_type }},
1604+ ],
1605+ }
1606+
1607+
1608+ @pytest .mark .asyncio
1609+ async def test_content_to_message_param_user_message_file_uri_without_mime_type ():
1610+ """Test handling of file_data without mime_type (GcsArtifactService scenario).
1611+
1612+ When using GcsArtifactService, artifacts may have file_uri (gs://...) but
1613+ without mime_type set. LiteLLM's Vertex AI backend requires the format
1614+ field to be present, so we infer MIME type from the URI extension or use
1615+ a default fallback to ensure compatibility.
1616+
1617+ See: https://github.com/google/adk-python/issues/3787
1618+ """
1619+ file_part = types .Part (
1620+ file_data = types .FileData (
1621+ file_uri = "gs://agent-artifact-bucket/app/user/session/artifact/0"
1622+ )
1623+ )
1624+ content = types .Content (
1625+ role = "user" ,
1626+ parts = [
1627+ types .Part .from_text (text = "Analyze this file." ),
1628+ file_part ,
1629+ ],
1630+ )
1631+
1632+ message = await _content_to_message_param (content )
1633+ assert message == {
1634+ "role" : "user" ,
1635+ "content" : [
1636+ {"type" : "text" , "text" : "Analyze this file." },
1637+ {
1638+ "type" : "file" ,
1639+ "file" : {
1640+ "file_id" : (
1641+ "gs://agent-artifact-bucket/app/user/session/artifact/0"
1642+ ),
1643+ "format" : "application/octet-stream" ,
1644+ },
1645+ },
1646+ ],
1647+ }
1648+
1649+
1650+ @pytest .mark .asyncio
1651+ async def test_content_to_message_param_user_message_file_uri_infer_mime_type ():
1652+ """Test MIME type inference from file_uri extension.
1653+
1654+ When file_data has a file_uri with a recognizable extension but no explicit
1655+ mime_type, the MIME type should be inferred from the extension.
1656+
1657+ See: https://github.com/google/adk-python/issues/3787
1658+ """
1659+ file_part = types .Part (
1660+ file_data = types .FileData (
1661+ file_uri = "gs://bucket/path/to/document.pdf" ,
1662+ )
1663+ )
1664+ content = types .Content (
1665+ role = "user" ,
1666+ parts = [file_part ],
1667+ )
1668+
1669+ message = await _content_to_message_param (content )
1670+ assert message == {
1671+ "role" : "user" ,
1672+ "content" : [
1673+ {
1674+ "type" : "file" ,
1675+ "file" : {
1676+ "file_id" : "gs://bucket/path/to/document.pdf" ,
1677+ "format" : "application/pdf" ,
1678+ },
1679+ },
1680+ ],
1681+ }
16051682
16061683
16071684@pytest .mark .asyncio
@@ -1995,9 +2072,112 @@ async def test_get_content_file_bytes(file_data, mime_type, expected_base64):
19952072async def test_get_content_file_uri (file_uri , mime_type ):
19962073 parts = [types .Part .from_uri (file_uri = file_uri , mime_type = mime_type )]
19972074 content = await _get_content (parts )
1998- assert content [0 ]["type" ] == "file"
1999- assert content [0 ]["file" ]["file_id" ] == file_uri
2000- assert "format" not in content [0 ]["file" ]
2075+ assert content [0 ] == {
2076+ "type" : "file" ,
2077+ "file" : {"file_id" : file_uri , "format" : mime_type },
2078+ }
2079+
2080+
2081+ @pytest .mark .asyncio
2082+ async def test_get_content_file_uri_infer_mime_type ():
2083+ """Test MIME type inference from file_uri extension.
2084+
2085+ When file_data has a file_uri with a recognizable extension but no explicit
2086+ mime_type, the MIME type should be inferred from the extension.
2087+
2088+ See: https://github.com/google/adk-python/issues/3787
2089+ """
2090+ # Use Part constructor directly to test MIME type inference in _get_content
2091+ # (types.Part.from_uri does its own inference, so we bypass it)
2092+ parts = [
2093+ types .Part (
2094+ file_data = types .FileData (file_uri = "gs://bucket/path/to/document.pdf" )
2095+ )
2096+ ]
2097+ content = await _get_content (parts )
2098+ assert content [0 ] == {
2099+ "type" : "file" ,
2100+ "file" : {
2101+ "file_id" : "gs://bucket/path/to/document.pdf" ,
2102+ "format" : "application/pdf" ,
2103+ },
2104+ }
2105+
2106+
2107+ @pytest .mark .asyncio
2108+ async def test_get_content_file_uri_versioned_infer_mime_type ():
2109+ """Test MIME type inference from versioned artifact URIs."""
2110+ parts = [
2111+ types .Part (
2112+ file_data = types .FileData (
2113+ file_uri = "gs://bucket/path/to/document.pdf/0"
2114+ )
2115+ )
2116+ ]
2117+ content = await _get_content (parts )
2118+ assert content [0 ]["file" ]["format" ] == "application/pdf"
2119+
2120+
2121+ @pytest .mark .asyncio
2122+ async def test_get_content_file_uri_infers_from_display_name ():
2123+ """Test MIME type inference from display_name when URI lacks extension."""
2124+ parts = [
2125+ types .Part (
2126+ file_data = types .FileData (
2127+ file_uri = "gs://bucket/artifact/0" ,
2128+ display_name = "document.pdf" ,
2129+ )
2130+ )
2131+ ]
2132+ content = await _get_content (parts )
2133+ assert content [0 ]["file" ]["format" ] == "application/pdf"
2134+
2135+
2136+ @pytest .mark .asyncio
2137+ async def test_get_content_file_uri_default_mime_type ():
2138+ """Test that file_uri without extension uses default MIME type.
2139+
2140+ When file_data has a file_uri without a recognizable extension and no explicit
2141+ mime_type, a default MIME type should be used to ensure compatibility with
2142+ LiteLLM backends.
2143+
2144+ See: https://github.com/google/adk-python/issues/3787
2145+ """
2146+ # Use Part constructor directly to create file_data without mime_type
2147+ # (types.Part.from_uri requires a valid mime_type when it can't infer)
2148+ parts = [
2149+ types .Part (file_data = types .FileData (file_uri = "gs://bucket/artifact/0" ))
2150+ ]
2151+ content = await _get_content (parts )
2152+ assert content [0 ] == {
2153+ "type" : "file" ,
2154+ "file" : {
2155+ "file_id" : "gs://bucket/artifact/0" ,
2156+ "format" : "application/octet-stream" ,
2157+ },
2158+ }
2159+
2160+
2161+ @pytest .mark .asyncio
2162+ @pytest .mark .parametrize (
2163+ "uri,expected_mime_type" ,
2164+ [
2165+ ("gs://bucket/file.pdf" , "application/pdf" ),
2166+ ("gs://bucket/path/to/document.json" , "application/json" ),
2167+ ("gs://bucket/image.png" , "image/png" ),
2168+ ("gs://bucket/image.jpg" , "image/jpeg" ),
2169+ ("gs://bucket/audio.mp3" , "audio/mpeg" ),
2170+ ("gs://bucket/video.mp4" , "video/mp4" ),
2171+ ],
2172+ )
2173+ async def test_get_content_file_uri_mime_type_inference (
2174+ uri , expected_mime_type
2175+ ):
2176+ """Test MIME type inference from various file extensions."""
2177+ # Use Part constructor directly to test MIME type inference in _get_content
2178+ parts = [types .Part (file_data = types .FileData (file_uri = uri ))]
2179+ content = await _get_content (parts )
2180+ assert content [0 ]["file" ]["format" ] == expected_mime_type
20012181
20022182
20032183@pytest .mark .asyncio
0 commit comments