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 8b94c34942c..6dee99375ac 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 @@ -422,6 +422,7 @@ class _PromptCacheTokenCounts(TypedDict, total=False): _CACHED_MESSAGE_PROVIDER_FIELDS: Final = TypeAdapter(_CachedMessageProviderFields) _CACHED_USAGE_DETAILS: Final = TypeAdapter(_CachedUsageDetails) _FIELD_MAP: Final = TypeAdapter(dict[str, object]) +_CHOICE_FIELD_MAPS: Final = TypeAdapter(list[dict[str, object]]) _COMPLETION_TOKENS_DETAILS: Final = TypeAdapter(CompletionTokensDetailsWrapper) _PROMPT_TOKENS_DETAILS: Final = TypeAdapter(PromptTokensDetailsWrapper) _PROMPT_CACHE_TOKEN_COUNTS: Final = TypeAdapter(_PromptCacheTokenCounts) @@ -757,6 +758,20 @@ def convert_to_model_response_object( model="", ) + response_fields: Final = _FIELD_MAP.validate_python(response_object) + if any( + choice.get("finish_reason") == "abort" + for choice in _CHOICE_FIELD_MAPS.validate_python(response_fields["choices"]) + ): + from litellm.exceptions import APIError + + raise APIError( + status_code=500, + message="Upstream generation aborted (finish_reason=abort)", + llm_provider="", + model=str(response_fields.get("model") or ""), + ) + for idx, choice in enumerate(response_object["choices"]): ## HANDLE JSON MODE - anthropic returns single function call] tool_calls = choice["message"].get("tool_calls", None) diff --git a/tests/unit/litellm_core_utils/llm_response_utils/test_convert_dict_to_response.py b/tests/unit/litellm_core_utils/llm_response_utils/test_convert_dict_to_response.py index 0c601a6b2c8..e21a2c9e296 100644 --- a/tests/unit/litellm_core_utils/llm_response_utils/test_convert_dict_to_response.py +++ b/tests/unit/litellm_core_utils/llm_response_utils/test_convert_dict_to_response.py @@ -228,6 +228,30 @@ def test_convert_missing_choices_raises_api_error() -> None: assert "no 'choices'" in str(exc_info.value) +@pytest.mark.parametrize("finish_reasons", [("abort",), ("stop", "abort"), ("abort", "stop")]) +def test_convert_aborted_choice_raises_api_error(finish_reasons: tuple[str, ...]) -> None: + from litellm.exceptions import APIError + + resp: Final = { + "id": "x", + "created": 1, + "model": "Qwen/Qwen3-8B", + "object": "chat.completion", + "choices": [ + {"index": index, "message": {"role": "assistant", "content": "Oslo is 4C and"}, "finish_reason": reason} + for index, reason in enumerate(finish_reasons) + ], + } + with pytest.raises(APIError, match="finish_reason=abort") as exc_info: + convert_to_model_response_object( + response_object=resp, + model_response_object=ModelResponse(), + response_type="completion", + ) + assert exc_info.value.status_code == 500 + assert exc_info.value.model == "Qwen/Qwen3-8B" + + @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, type_name: str) -> None: