fix(convert_dict_to_response): handle empty choices list without raising 500 APIError (Fixes #40276)

This commit is contained in:
shotsan 2026-09-08 13:44:50 -07:00
parent 9fffda4117
commit c1132d0454
2 changed files with 72 additions and 4 deletions

View file

@ -179,7 +179,7 @@ async def convert_to_streaming_response_async(
choice_list: Final[list[StreamingChoices]] = []
if not response_object.get("choices"):
if "choices" not in response_object or not isinstance(response_object["choices"], Iterable):
from litellm.exceptions import APIError
raise APIError(
@ -287,7 +287,7 @@ def convert_to_streaming_response(
model_response_object: Final = ModelResponseStream()
choice_list: Final[list[StreamingChoices]] = []
if not response_object.get("choices"):
if "choices" not in response_object or not isinstance(response_object["choices"], Iterable):
from litellm.exceptions import APIError
raise APIError(
@ -623,7 +623,7 @@ def convert_to_model_response_object(
return convert_to_streaming_response(response_object=response_object)
choice_list: Final[list[Choices]] = []
if not response_object.get("choices") or not isinstance(response_object["choices"], Iterable):
if "choices" not in response_object or not isinstance(response_object["choices"], Iterable):
from litellm.exceptions import APIError
raise APIError(

View file

@ -1,4 +1,4 @@
import pytest
from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
@ -99,3 +99,71 @@ def test_handle_invalid_parallel_tool_calls_skips_custom_tool_calls():
)
result = _handle_invalid_parallel_tool_calls([custom_tool_call, function_tool_call])
assert result == [custom_tool_call, function_tool_call]
def test_convert_empty_choices_response() -> None:
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
convert_to_streaming_response,
)
resp = {
"id": "x",
"created": 1,
"model": "gemini-3.5-flash",
"object": "chat.completion",
"choices": [],
"usage": {"prompt_tokens": 10, "completion_tokens": 0, "total_tokens": 10},
"vertex_ai_safety_results": ["blocked"],
}
result = convert_to_model_response_object(
response_object=resp,
model_response_object=ModelResponse(),
response_type="completion",
)
assert result.choices == []
assert getattr(result, "vertex_ai_safety_results") == ["blocked"]
# Test sync streaming generator handles empty choices
sync_stream = list(convert_to_streaming_response(response_object=resp))
assert len(sync_stream) == 1
assert sync_stream[0].choices == []
@pytest.mark.asyncio
async def test_convert_empty_choices_response_async() -> None:
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
convert_to_streaming_response_async,
)
resp = {
"id": "x",
"created": 1,
"model": "gemini-3.5-flash",
"object": "chat.completion",
"choices": [],
"usage": {"prompt_tokens": 10, "completion_tokens": 0, "total_tokens": 10},
}
async_chunks = []
async for chunk in convert_to_streaming_response_async(response_object=resp):
async_chunks.append(chunk)
assert len(async_chunks) == 1
assert async_chunks[0].choices == []
def test_convert_missing_choices_raises_api_error() -> None:
from litellm.exceptions import APIError
resp = {
"id": "x",
"created": 1,
"model": "gemini-3.5-flash",
"object": "chat.completion",
}
with pytest.raises(APIError) as exc_info:
convert_to_model_response_object(
response_object=resp,
model_response_object=ModelResponse(),
response_type="completion",
)
assert "no 'choices'" in str(exc_info.value)