mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-12 03:48:56 +00:00
fix(exception_mapping): classify data inspection rejections as content policy violations
A 400 whose body carries data_inspection_failed in any spelling (snake_case code or the CamelCase native message) is now a ContentPolicyViolationError for OpenAI compatible providers, so ContentPolicyViolationErrorRetries can target it without retrying every 400. Other statuses carrying the marker keep their class
This commit is contained in:
parent
000b3685ed
commit
2f92547835
2 changed files with 119 additions and 0 deletions
|
|
@ -37,6 +37,8 @@ from ..exceptions import (
|
|||
UnprocessableEntityError,
|
||||
)
|
||||
|
||||
_DATA_INSPECTION_FAILED: Final = re.compile(r"\bdata_?inspection_?failed\b", re.IGNORECASE)
|
||||
|
||||
|
||||
class ExceptionCheckers:
|
||||
"""
|
||||
|
|
@ -135,6 +137,10 @@ class ExceptionCheckers:
|
|||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_data_inspection_failed_error(error_str: str, status_code: int | None) -> bool:
|
||||
return status_code == 400 and _DATA_INSPECTION_FAILED.search(error_str) is not None
|
||||
|
||||
|
||||
def get_error_message(error_obj) -> str | None:
|
||||
"""
|
||||
|
|
@ -369,6 +375,9 @@ def _map_openai_exception(
|
|||
("invalid_request_error" in error_str and "content_policy_violation" in error_str)
|
||||
or ("Invalid prompt" in error_str and "violating our usage policy" in error_str)
|
||||
or ("request was rejected as a result of the safety system" in error_str.lower())
|
||||
or ExceptionCheckers.is_data_inspection_failed_error(
|
||||
error_str, status_code=getattr(original_exception, "status_code", None)
|
||||
)
|
||||
):
|
||||
raise ContentPolicyViolationError(
|
||||
message=f"ContentPolicyViolationError: {exception_provider} - {message}",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
import traceback
|
||||
from collections.abc import Awaitable, Callable
|
||||
from types import MappingProxyType
|
||||
from typing import Any, Final, Literal, Optional, Union
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -401,6 +402,115 @@ def test_openai_compatible_429_still_maps_to_rate_limit():
|
|||
assert excinfo.value.status_code == 429
|
||||
|
||||
|
||||
# Alibaba's OpenAI compatible endpoint rejects flagged input with these bodies. The snake_case code was observed on
|
||||
# the compatible-mode endpoint in 2026-10, the CamelCase message is the one quoted in BerriAI/litellm#41682 (2026-09)
|
||||
_INSPECTION_REJECTION_SNAKE_CASE: Final = (
|
||||
'{"error":{"code":"data_inspection_failed","param":null,'
|
||||
'"message":"Input text data may contain inappropriate content.","type":"data_inspection_failed"}}'
|
||||
)
|
||||
_INSPECTION_REJECTION_CAMEL_CASE: Final = (
|
||||
'{"error":{"message":"InternalError.Algo.DataInspectionFailed: '
|
||||
'Input text data may contain inappropriate content.",'
|
||||
'"type":"invalid_request_error","code":"invalid_request_error"}}'
|
||||
)
|
||||
_INSPECTION_REJECTION_BODIES: Final = MappingProxyType(
|
||||
{
|
||||
"snake_case": _INSPECTION_REJECTION_SNAKE_CASE,
|
||||
"snake_case_in_data_frame": f"data: {_INSPECTION_REJECTION_SNAKE_CASE}\n\n",
|
||||
"camel_case": _INSPECTION_REJECTION_CAMEL_CASE,
|
||||
"camel_case_in_data_frame": f"data: {_INSPECTION_REJECTION_CAMEL_CASE}\n\n",
|
||||
}
|
||||
)
|
||||
_INSPECTION_PROVIDERS: Final = ("dashscope", "qwencloud", "openai")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("custom_llm_provider", _INSPECTION_PROVIDERS)
|
||||
@pytest.mark.parametrize("body_shape", list(_INSPECTION_REJECTION_BODIES))
|
||||
def test_data_inspection_rejection_maps_to_content_policy_violation(body_shape: str, custom_llm_provider: str):
|
||||
body: Final = _INSPECTION_REJECTION_BODIES[body_shape]
|
||||
|
||||
with pytest.raises(litellm.ContentPolicyViolationError) as excinfo:
|
||||
exception_type(
|
||||
model=f"{custom_llm_provider}/qwen-plus",
|
||||
original_exception=OpenAIError(status_code=400, message=body, headers={}),
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
|
||||
assert excinfo.value.status_code == 400
|
||||
assert excinfo.value.llm_provider == custom_llm_provider
|
||||
assert "inspectionfailed" in str(excinfo.value).lower().replace("_", "")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("custom_llm_provider", _INSPECTION_PROVIDERS)
|
||||
def test_other_content_policy_violation_still_maps_to_content_policy_violation(custom_llm_provider: str):
|
||||
body: Final = '{"error":{"message":"rejected","type":"invalid_request_error","code":"content_policy_violation"}}'
|
||||
|
||||
with pytest.raises(litellm.ContentPolicyViolationError) as excinfo:
|
||||
exception_type(
|
||||
model=f"{custom_llm_provider}/qwen-plus",
|
||||
original_exception=OpenAIError(status_code=400, message=body, headers={}),
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
|
||||
assert excinfo.value.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.parametrize("custom_llm_provider", _INSPECTION_PROVIDERS)
|
||||
def test_ordinary_400_next_to_the_inspection_mapping_stays_bad_request(custom_llm_provider: str):
|
||||
body: Final = (
|
||||
'{"error":{"message":"Temperature should be in [0.0, 2.0)","type":"invalid_request_error",'
|
||||
'"code":"invalid_parameter_error"}}'
|
||||
)
|
||||
|
||||
with pytest.raises(litellm.BadRequestError) as excinfo:
|
||||
exception_type(
|
||||
model=f"{custom_llm_provider}/qwen-plus",
|
||||
original_exception=OpenAIError(status_code=400, message=body, headers={}),
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
|
||||
assert not isinstance(excinfo.value, litellm.ContentPolicyViolationError)
|
||||
assert excinfo.value.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status_code", [401, 429, 500, 502])
|
||||
@pytest.mark.parametrize("custom_llm_provider", _INSPECTION_PROVIDERS)
|
||||
def test_data_inspection_marker_under_a_non_400_status_is_not_a_content_policy_violation(
|
||||
custom_llm_provider: str, status_code: int
|
||||
):
|
||||
with pytest.raises(openai.APIStatusError) as excinfo:
|
||||
exception_type(
|
||||
model=f"{custom_llm_provider}/qwen-plus",
|
||||
original_exception=OpenAIError(
|
||||
status_code=status_code, message=_INSPECTION_REJECTION_SNAKE_CASE, headers={}
|
||||
),
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
|
||||
assert not isinstance(excinfo.value, litellm.ContentPolicyViolationError)
|
||||
assert getattr(excinfo.value, "status_code", None) == status_code
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("error_str", "status_code", "expected"),
|
||||
[
|
||||
("data_inspection_failed", 400, True),
|
||||
("DataInspectionFailed", 400, True),
|
||||
("InternalError.Algo.DataInspectionFailed: blocked", 400, True),
|
||||
("DATA_INSPECTION_FAILED", 400, True),
|
||||
("datainspectionfailed", 400, True),
|
||||
("data_inspection_failed", 500, False),
|
||||
("data_inspection_failed", None, False),
|
||||
("metadata_inspection_failed", 400, False),
|
||||
("data_inspection_failed_again", 400, False),
|
||||
("data inspection failed", 400, False),
|
||||
("Temperature should be in [0.0, 2.0)", 400, False),
|
||||
],
|
||||
)
|
||||
def test_is_data_inspection_failed_error(error_str: str, status_code: int | None, expected: bool):
|
||||
assert ExceptionCheckers.is_data_inspection_failed_error(error_str, status_code=status_code) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error_message",
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue