mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
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:
parent
d447be15b9
commit
e12ab58d13
2 changed files with 103 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue