Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import re
import time
import traceback
from collections.abc import Iterable, Sequence
from collections.abc import Sequence
from typing import Final, Literal, cast

import litellm
Expand Down Expand Up @@ -179,7 +179,7 @@ async def convert_to_streaming_response_async(

choice_list: Final[list[StreamingChoices]] = []

if not response_object.get("choices"):
if not isinstance(response_object.get("choices"), list):
from litellm.exceptions import APIError

raise APIError(
Expand Down Expand Up @@ -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 not isinstance(response_object.get("choices"), list):
from litellm.exceptions import APIError

raise APIError(
Expand Down Expand Up @@ -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 not isinstance(response_object.get("choices"), list):
from litellm.exceptions import APIError

raise APIError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1487,8 +1487,9 @@ def translate_openai_response_to_anthropic(
anthropic_content.insert(0, polyfill_result.compaction_block)

## extract finish reason
openai_finish_reason: Final = response.choices[0].finish_reason if response.choices else "stop"
translated_finish_reason: Final = self._translate_openai_finish_reason_to_anthropic(
openai_finish_reason=response.choices[0].finish_reason
openai_finish_reason=openai_finish_reason
)
anthropic_finish_reason: Final = (
"refusal"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Final

import pytest

from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
Expand Down Expand Up @@ -99,3 +101,99 @@ 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
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
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
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
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)


@pytest.mark.parametrize("choices", [{}, "", None, 0])
@pytest.mark.asyncio
async def test_convert_non_list_choices_raises_api_error(choices: object) -> None:
from litellm.exceptions import APIError
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
convert_to_streaming_response,
convert_to_streaming_response_async,
)

resp: Final = {
"id": "x",
"created": 1,
"model": "gemini-3.5-flash",
"object": "chat.completion",
"choices": choices,
}
with pytest.raises(APIError, match="no 'choices'"):
convert_to_model_response_object(
response_object=resp,
model_response_object=ModelResponse(),
response_type="completion",
)
with pytest.raises(APIError, match="no 'choices'"):
list(convert_to_streaming_response(response_object=resp))
with pytest.raises(APIError, match="no 'choices'"):
async for _ in convert_to_streaming_response_async(response_object=resp):
pass
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@
)


def test_translate_openai_response_to_anthropic_empty_choices() -> None:
response: Final = ModelResponse(
id="chatcmpl-empty",
model="gemini-3.5-flash",
choices=[],
usage=Usage(prompt_tokens=10, completion_tokens=0, total_tokens=10),
)

result: Final = LiteLLMAnthropicMessagesAdapter().translate_openai_response_to_anthropic(response)

assert result["content"] == []
assert result["stop_reason"] == "end_turn"
assert result["usage"]["input_tokens"] == 10


def test_translate_chat_refusal_to_anthropic_response():
response = ModelResponse(
id="chatcmpl-refusal",
Expand Down
2 changes: 1 addition & 1 deletion tests/test_litellm/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def test_completion_missing_role(openai_api_response):
print(f"openai_api_response: {openai_api_response}")

with patch.object(
client.chat.completions.with_raw_response, "create", mock_raw_response
client.chat.completions.with_raw_response, "create", MagicMock(return_value=mock_raw_response)
) as mock_create:
litellm.completion(
model="gpt-4o-mini",
Expand Down
Loading