mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
fix(convert_dict_to_response): accept only a real list as choices and keep /v1/messages alive on an empty one
Narrows the no-choices guard so a dict, string, or None still raises the APIError while an empty list passes through, guards the non-stream Anthropic bridge against indexing an empty choices list, and repairs test_completion_missing_role, whose raw-response mock was patched in as the create() callable itself so the handler only ever saw a MagicMock
This commit is contained in:
parent
c0d1fd45f3
commit
35def27e7b
5 changed files with 52 additions and 6 deletions
|
|
@ -3,7 +3,7 @@ import json
|
|||
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
|
||||
|
|
@ -179,7 +179,7 @@ async def convert_to_streaming_response_async(
|
|||
|
||||
choice_list: Final[list[StreamingChoices]] = []
|
||||
|
||||
if "choices" not in response_object or not isinstance(response_object["choices"], Iterable):
|
||||
if not isinstance(response_object.get("choices"), list):
|
||||
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 "choices" not in response_object or not isinstance(response_object["choices"], Iterable):
|
||||
if not isinstance(response_object.get("choices"), list):
|
||||
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 "choices" not in response_object or not isinstance(response_object["choices"], Iterable):
|
||||
if not isinstance(response_object.get("choices"), list):
|
||||
from litellm.exceptions import APIError
|
||||
|
||||
raise APIError(
|
||||
|
|
|
|||
|
|
@ -1487,8 +1487,9 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
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"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
|
||||
|
|
@ -167,3 +169,31 @@ 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.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
|
||||
|
|
|
|||
|
|
@ -41,6 +41,21 @@ from litellm.types.utils import (
|
|||
)
|
||||
|
||||
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue