Skip to content

Commit 82e17f0

Browse files
committed
Fail fast when LiteLLM file_uri MIME type cannot be determined
Signed-off-by: Yizuki_Ame <yinzimike@gmail.com>
1 parent 0fedb3b commit 82e17f0

2 files changed

Lines changed: 100 additions & 57 deletions

File tree

src/google/adk/models/lite_llm.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,6 @@ def _get_provider_from_model(model: str) -> str:
233233
return ""
234234

235235

236-
# Default MIME type when none can be inferred
237-
_DEFAULT_MIME_TYPE = "application/octet-stream"
238-
239-
240236
def _infer_mime_type_from_uri(uri: str) -> Optional[str]:
241237
"""Attempts to infer MIME type from a URI's path extension.
242238
@@ -811,6 +807,7 @@ async def _content_to_message_param(
811807
follow_up = await _content_to_message_param(
812808
types.Content(role=content.role, parts=non_tool_parts),
813809
provider=provider,
810+
model=model,
814811
)
815812
follow_up_messages = (
816813
follow_up if isinstance(follow_up, list) else [follow_up]
@@ -1081,27 +1078,27 @@ async def _get_content(
10811078
})
10821079
continue
10831080

1084-
# Determine MIME type: use explicit value, infer from URI, or use default.
1081+
# Determine MIME type: use explicit value, infer from URI, or fail fast
1082+
# only when a file block still needs to be constructed.
10851083
mime_type = part.file_data.mime_type
10861084
if not mime_type:
10871085
mime_type = _infer_mime_type_from_uri(part.file_data.file_uri)
10881086
if not mime_type and part.file_data.display_name:
10891087
guessed_mime_type, _ = mimetypes.guess_type(part.file_data.display_name)
10901088
mime_type = guessed_mime_type
1091-
if not mime_type:
1092-
# LiteLLM's Vertex AI backend requires format for GCS URIs.
1093-
mime_type = _DEFAULT_MIME_TYPE
1094-
logger.debug(
1095-
"Could not determine MIME type for file_uri %s, using default: %s",
1096-
part.file_data.file_uri,
1097-
mime_type,
1098-
)
1099-
mime_type = _normalize_mime_type(mime_type)
1089+
1090+
normalized_mime_type = (
1091+
_normalize_mime_type(mime_type) if mime_type else None
1092+
)
11001093

11011094
if provider in _FILE_ID_REQUIRED_PROVIDERS and _is_http_url(
11021095
part.file_data.file_uri
11031096
):
1104-
url_content_type = _media_url_content_type(mime_type)
1097+
url_content_type = (
1098+
_media_url_content_type(normalized_mime_type)
1099+
if normalized_mime_type
1100+
else None
1101+
)
11051102
if url_content_type:
11061103
content_objects.append({
11071104
"type": url_content_type,
@@ -1125,10 +1122,21 @@ async def _get_content(
11251122
})
11261123
continue
11271124

1125+
if not normalized_mime_type:
1126+
logger.warning(
1127+
"Could not determine MIME type for file_uri %s",
1128+
part.file_data.file_uri,
1129+
)
1130+
raise ValueError(
1131+
"Cannot determine MIME type for file_uri "
1132+
f"'{part.file_data.file_uri}'. Please set `file_data.mime_type` "
1133+
"explicitly or use a file URI with a recognizable extension."
1134+
)
1135+
11281136
file_object: ChatCompletionFileUrlObject = {
11291137
"file_id": part.file_data.file_uri,
11301138
}
1131-
file_object["format"] = mime_type
1139+
file_object["format"] = normalized_mime_type
11321140
content_objects.append({
11331141
"type": "file",
11341142
"file": file_object,

tests/unittests/models/test_litellm.py

Lines changed: 76 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1882,15 +1882,7 @@ async def test_content_to_message_param_user_message_file_uri_only(
18821882

18831883
@pytest.mark.asyncio
18841884
async def test_content_to_message_param_user_message_file_uri_without_mime_type():
1885-
"""Test handling of file_data without mime_type (GcsArtifactService scenario).
1886-
1887-
When using GcsArtifactService, artifacts may have file_uri (gs://...) but
1888-
without mime_type set. LiteLLM's Vertex AI backend requires the format
1889-
field to be present, so we infer MIME type from the URI extension or use
1890-
a default fallback to ensure compatibility.
1891-
1892-
See: https://github.com/google/adk-python/issues/3787
1893-
"""
1885+
"""Test file_data without mime_type fails fast when inference is impossible."""
18941886
file_part = types.Part(
18951887
file_data=types.FileData(
18961888
file_uri="gs://agent-artifact-bucket/app/user/session/artifact/0"
@@ -1904,22 +1896,11 @@ async def test_content_to_message_param_user_message_file_uri_without_mime_type(
19041896
],
19051897
)
19061898

1907-
message = await _content_to_message_param(content)
1908-
assert message == {
1909-
"role": "user",
1910-
"content": [
1911-
{"type": "text", "text": "Analyze this file."},
1912-
{
1913-
"type": "file",
1914-
"file": {
1915-
"file_id": (
1916-
"gs://agent-artifact-bucket/app/user/session/artifact/0"
1917-
),
1918-
"format": "application/octet-stream",
1919-
},
1920-
},
1921-
],
1922-
}
1899+
with pytest.raises(
1900+
ValueError,
1901+
match="Cannot determine MIME type for file_uri",
1902+
):
1903+
await _content_to_message_param(content)
19231904

19241905

19251906
@pytest.mark.asyncio
@@ -2027,6 +2008,50 @@ async def test_content_to_message_param_function_response_with_extra_parts():
20272008
]
20282009

20292010

2011+
@pytest.mark.asyncio
2012+
async def test_content_to_message_param_function_response_preserves_model_for_file_uri():
2013+
tool_part = types.Part(
2014+
function_response=types.FunctionResponse(
2015+
name="load_document",
2016+
response={"status": "success"},
2017+
)
2018+
)
2019+
tool_part.function_response.id = "tool_call_1"
2020+
2021+
file_part = types.Part(
2022+
file_data=types.FileData(
2023+
file_uri="gs://bucket/path/to/document.pdf",
2024+
mime_type="application/pdf",
2025+
)
2026+
)
2027+
content = types.Content(role="user", parts=[tool_part, file_part])
2028+
2029+
messages = await _content_to_message_param(
2030+
content,
2031+
provider="vertex_ai",
2032+
model="vertex_ai/gemini-2.5-flash",
2033+
)
2034+
2035+
assert isinstance(messages, list)
2036+
assert messages == [
2037+
{
2038+
"role": "tool",
2039+
"tool_call_id": "tool_call_1",
2040+
"content": '{"status": "success"}',
2041+
},
2042+
{
2043+
"role": "user",
2044+
"content": [{
2045+
"type": "file",
2046+
"file": {
2047+
"file_id": "gs://bucket/path/to/document.pdf",
2048+
"format": "application/pdf",
2049+
},
2050+
}],
2051+
},
2052+
]
2053+
2054+
20302055
@pytest.mark.asyncio
20312056
async def test_content_to_message_param_function_response_preserves_string():
20322057
"""Tests that string responses are used directly without double-serialization.
@@ -2999,6 +3024,26 @@ async def test_get_content_file_uri_anthropic_openai_file_id_falls_back_to_text(
29993024
]
30003025

30013026

3027+
@pytest.mark.asyncio
3028+
@pytest.mark.parametrize(
3029+
"provider,model",
3030+
[
3031+
("openai", "openai/gpt-4o"),
3032+
("azure", "azure/gpt-4"),
3033+
],
3034+
)
3035+
async def test_get_content_file_uri_file_id_required_missing_mime_falls_back_to_text(
3036+
provider, model
3037+
):
3038+
parts = [
3039+
types.Part(file_data=types.FileData(file_uri="gs://bucket/artifact/0"))
3040+
]
3041+
content = await _get_content(parts, provider=provider, model=model)
3042+
assert content == [
3043+
{"type": "text", "text": '[File reference: "gs://bucket/artifact/0"]'}
3044+
]
3045+
3046+
30023047
@pytest.mark.asyncio
30033048
async def test_get_content_file_uri_vertex_ai_non_gemini_falls_back_to_text():
30043049
parts = [
@@ -3097,27 +3142,17 @@ async def test_get_content_file_uri_infers_from_display_name():
30973142

30983143
@pytest.mark.asyncio
30993144
async def test_get_content_file_uri_default_mime_type():
3100-
"""Test that file_uri without extension uses default MIME type.
3101-
3102-
When file_data has a file_uri without a recognizable extension and no explicit
3103-
mime_type, a default MIME type should be used to ensure compatibility with
3104-
LiteLLM backends.
3105-
3106-
See: https://github.com/google/adk-python/issues/3787
3107-
"""
3145+
"""Test that file_uri without extension raises a helpful ValueError."""
31083146
# Use Part constructor directly to create file_data without mime_type
31093147
# (types.Part.from_uri requires a valid mime_type when it can't infer)
31103148
parts = [
31113149
types.Part(file_data=types.FileData(file_uri="gs://bucket/artifact/0"))
31123150
]
3113-
content = await _get_content(parts)
3114-
assert content[0] == {
3115-
"type": "file",
3116-
"file": {
3117-
"file_id": "gs://bucket/artifact/0",
3118-
"format": "application/octet-stream",
3119-
},
3120-
}
3151+
with pytest.raises(
3152+
ValueError,
3153+
match="Cannot determine MIME type for file_uri",
3154+
):
3155+
await _get_content(parts)
31213156

31223157

31233158
@pytest.mark.asyncio

0 commit comments

Comments
 (0)