mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
fix(convert_dict_to_response): name the non-list choices type in the converter error
When a provider returns choices as null, an object, a string or a number, the converter said the response had no 'choices' even though the key was present in the raw keys it listed. A shared message now keeps the old wording for a missing key and names the offending type otherwise. The cached-stream regression test also pins the chunk count so a leaked extra chunk fails it.
This commit is contained in:
parent
29af8b7349
commit
cbeac27656
3 changed files with 21 additions and 16 deletions
|
|
@ -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="",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue