fix(openai): raise after unstructured 422 retry exhaustion

With drop_params enabled, the retry loops in completion, acompletion, and
async_streaming swallowed openai.UnprocessableEntityError on every attempt
and fell off the end after two identical 422s, implicitly returning None.
The proxy then answered 200 with body null and spend logs, failure
callbacks, and router fallbacks never fired.

Only drop params on the first attempt; re-raise the error on the last so
callers see the 422.

Fixes #32221
This commit is contained in:
Justin Wilson 2026-08-22 22:32:50 -05:00
parent d447be15b9
commit e12ab58d13
2 changed files with 103 additions and 8 deletions

View file

@ -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

View file

@ -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