diff --git a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py index 29b75812bfb..87524d86c61 100644 --- a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py +++ b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py @@ -3,7 +3,7 @@ import json import re import time import traceback -from collections.abc import Sequence +from collections.abc import Mapping, Sequence from typing import Final, Literal, cast import litellm @@ -151,6 +151,16 @@ def _clear_later_replay_slice_metadata(choice: StreamingChoices) -> None: del choice.enhancements +def _invalid_choices_message(response_object: Mapping[str, object]) -> str: + raw_keys: Final = list(response_object.keys()) + if "choices" not in response_object: + return f"LiteLLM: provider returned a response with no 'choices'. Raw keys: {raw_keys}" + return ( + f"LiteLLM: provider returned 'choices' that is not a list ({type(response_object['choices']).__name__}). " + f"Raw keys: {raw_keys}" + ) + + async def convert_to_streaming_response_async( response_object: dict | None = None, ): @@ -184,9 +194,7 @@ async def convert_to_streaming_response_async( raise APIError( status_code=500, - message=( - f"LiteLLM: provider returned a response with no 'choices'. Raw keys: {list(response_object.keys())}" - ), + message=_invalid_choices_message(response_object), llm_provider="", model="", ) @@ -292,9 +300,7 @@ def convert_to_streaming_response( raise APIError( status_code=500, - message=( - f"LiteLLM: provider returned a response with no 'choices'. Raw keys: {list(response_object.keys())}" - ), + message=_invalid_choices_message(response_object), llm_provider="", model="", ) @@ -628,10 +634,7 @@ def convert_to_model_response_object( raise APIError( status_code=500, - message=( - "LiteLLM: provider returned a response with no 'choices'. " - f"Raw keys: {list(response_object.keys())}" - ), + message=_invalid_choices_message(response_object), llm_provider="", model="", ) diff --git a/tests/test_litellm/litellm_core_utils/llm_response_utils/test_convert_dict_to_response.py b/tests/test_litellm/litellm_core_utils/llm_response_utils/test_convert_dict_to_response.py index c3e99f01cec..8e46ae21de6 100644 --- a/tests/test_litellm/litellm_core_utils/llm_response_utils/test_convert_dict_to_response.py +++ b/tests/test_litellm/litellm_core_utils/llm_response_utils/test_convert_dict_to_response.py @@ -167,9 +167,9 @@ def test_convert_missing_choices_raises_api_error() -> None: assert "no 'choices'" in str(exc_info.value) -@pytest.mark.parametrize("choices", [{}, "", None, 0]) +@pytest.mark.parametrize(("choices", "type_name"), [({}, "dict"), ("", "str"), (None, "NoneType"), (0, "int")]) @pytest.mark.asyncio -async def test_convert_non_list_choices_raises_api_error(choices: object) -> None: +async def test_convert_non_list_choices_raises_api_error(choices: object, type_name: str) -> None: from litellm.exceptions import APIError from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import ( convert_to_streaming_response, @@ -183,14 +183,15 @@ async def test_convert_non_list_choices_raises_api_error(choices: object) -> Non "object": "chat.completion", "choices": choices, } - with pytest.raises(APIError, match="no 'choices'"): + expected: Final = f"'choices' that is not a list \\({type_name}\\)" + with pytest.raises(APIError, match=expected): convert_to_model_response_object( response_object=resp, model_response_object=ModelResponse(), response_type="completion", ) - with pytest.raises(APIError, match="no 'choices'"): + with pytest.raises(APIError, match=expected): list(convert_to_streaming_response(response_object=resp)) - with pytest.raises(APIError, match="no 'choices'"): + with pytest.raises(APIError, match=expected): async for _ in convert_to_streaming_response_async(response_object=resp): pass diff --git a/tests/test_litellm/litellm_core_utils/test_streaming_handler.py b/tests/test_litellm/litellm_core_utils/test_streaming_handler.py index ced7d6d677d..37e2031fdf4 100644 --- a/tests/test_litellm/litellm_core_utils/test_streaming_handler.py +++ b/tests/test_litellm/litellm_core_utils/test_streaming_handler.py @@ -2670,6 +2670,7 @@ async def test_cached_response_without_choices_streams_a_single_stop_chunk( chunks: Final = tuple([chunk async for chunk in wrapper]) + assert len(chunks) == 1 assert tuple(choice.finish_reason for chunk in chunks for choice in chunk.choices) == ("stop",) assert all(choice.delta.content in (None, "") for chunk in chunks for choice in chunk.choices)