diff --git a/litellm/llms/openai/openai.py b/litellm/llms/openai/openai.py index ee0efb88a38..2120a21dbd9 100644 --- a/litellm/llms/openai/openai.py +++ b/litellm/llms/openai/openai.py @@ -649,7 +649,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): if custom_llm_provider is not None and custom_llm_provider != "openai": model_response.model = f"{custom_llm_provider}/{model}" - for _ in range(2): # if call fails due to alternating messages, retry with reformatted message + for attempt in range(2): # if call fails due to alternating messages, retry with reformatted message try: max_retries = inference_params.pop("max_retries", 2) if acompletion is True: @@ -778,11 +778,10 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): return final_response_obj except openai.UnprocessableEntityError as e: ## check if body contains unprocessable params - related issue https://github.com/BerriAI/litellm/issues/4800 - if litellm.drop_params is True or drop_params is True: + if attempt == 0 and (litellm.drop_params is True or drop_params is True): inference_params = drop_params_from_unprocessable_entity_error(e, inference_params) else: raise e - # e.message except Exception as e: if print_verbose is not None: print_verbose(f"openai.py: Received openai error - {e}") @@ -858,7 +857,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): litellm_params=litellm_params, headers=headers or {}, ) - for _ in range(2): # if call fails due to alternating messages, retry with reformatted message + for attempt in range(2): # if call fails due to alternating messages, retry with reformatted message try: openai_aclient: AsyncOpenAI = self._get_openai_client( is_async=True, @@ -930,11 +929,10 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): return final_response_obj except openai.UnprocessableEntityError as e: ## check if body contains unprocessable params - related issue https://github.com/BerriAI/litellm/issues/4800 - if litellm.drop_params is True or drop_params is True: + if attempt == 0 and (litellm.drop_params is True or drop_params is True): data = drop_params_from_unprocessable_entity_error(e, data) else: raise e - # e.message except Exception as e: exception_response = getattr(e, "response", None) status_code = getattr(e, "status_code", 500) @@ -1038,7 +1036,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): ) data["stream"] = True data.update(self.get_stream_options(stream_options=stream_options, api_base=api_base)) - for _ in range(2): + for attempt in range(2): try: openai_aclient: AsyncOpenAI = self._get_openai_client( is_async=True, @@ -1081,7 +1079,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): return streamwrapper except openai.UnprocessableEntityError as e: ## check if body contains unprocessable params - related issue https://github.com/BerriAI/litellm/issues/4800 - if litellm.drop_params is True or drop_params is True: + if attempt == 0 and (litellm.drop_params is True or drop_params is True): data = drop_params_from_unprocessable_entity_error(e, data) else: raise e diff --git a/tests/test_litellm/llms/openai/test_openai_422_retry_exhaustion.py b/tests/test_litellm/llms/openai/test_openai_422_retry_exhaustion.py new file mode 100644 index 00000000000..d46a8148f00 --- /dev/null +++ b/tests/test_litellm/llms/openai/test_openai_422_retry_exhaustion.py @@ -0,0 +1,97 @@ +""" +Regression tests for issue #32221: with drop_params=True, two consecutive 422s +with an unstructured body must raise instead of silently returning None. +""" + +from unittest.mock import AsyncMock, MagicMock + +import httpx +import openai +import pytest + +from litellm.llms.openai.common_utils import OpenAIError +from litellm.llms.openai.openai import OpenAIChatCompletion, OpenAIConfig +from litellm.types.utils import ModelResponse + + +def _unstructured_422_error() -> openai.UnprocessableEntityError: + return openai.UnprocessableEntityError( + message="Error code: 422 - content moderation rejected the request", + response=httpx.Response( + 422, + request=httpx.Request("POST", "https://api.openai.com/v1/chat/completions"), + json={"error": {"message": "content moderation rejected the request"}}, + ), + body={"message": "content moderation rejected the request"}, + ) + + +@pytest.mark.asyncio +async def test_acompletion_raises_after_unstructured_422_retry_exhaustion(): + mock_client = MagicMock() + mock_client.chat.completions.with_raw_response.create = AsyncMock(side_effect=_unstructured_422_error()) + + with pytest.raises((openai.UnprocessableEntityError, OpenAIError)) as exc_info: + await OpenAIChatCompletion().acompletion( + messages=[{"role": "user", "content": "hi"}], + optional_params={}, + litellm_params={}, + provider_config=OpenAIConfig(), + model="gpt-4o", + model_response=ModelResponse(), + logging_obj=MagicMock(), + timeout=30.0, + api_key="sk-test", + client=mock_client, + drop_params=True, + ) + + assert exc_info.value.status_code == 422 + assert mock_client.chat.completions.with_raw_response.create.await_count == 2 + + +def test_completion_raises_after_unstructured_422_retry_exhaustion(): + mock_client = MagicMock(spec=openai.OpenAI) + mock_client.api_key = "sk-test" + mock_client._base_url = MagicMock() + mock_client.chat.completions.with_raw_response.create.side_effect = _unstructured_422_error() + + with pytest.raises((openai.UnprocessableEntityError, OpenAIError)) as exc_info: + OpenAIChatCompletion().completion( + model_response=ModelResponse(), + timeout=30.0, + optional_params={}, + litellm_params={}, + logging_obj=MagicMock(), + model="gpt-4o", + messages=[{"role": "user", "content": "hi"}], + api_key="sk-test", + client=mock_client, + drop_params=True, + ) + + assert exc_info.value.status_code == 422 + assert mock_client.chat.completions.with_raw_response.create.call_count == 2 + + +@pytest.mark.asyncio +async def test_async_streaming_raises_after_unstructured_422_retry_exhaustion(): + mock_client = MagicMock() + mock_client.chat.completions.with_raw_response.create = AsyncMock(side_effect=_unstructured_422_error()) + + with pytest.raises((openai.UnprocessableEntityError, OpenAIError)) as exc_info: + await OpenAIChatCompletion().async_streaming( + timeout=30.0, + messages=[{"role": "user", "content": "hi"}], + optional_params={}, + litellm_params={}, + provider_config=OpenAIConfig(), + model="gpt-4o", + logging_obj=MagicMock(), + api_key="sk-test", + client=mock_client, + drop_params=True, + ) + + assert exc_info.value.status_code == 422 + assert mock_client.chat.completions.with_raw_response.create.await_count == 2