mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(openai): preserve 403 error metadata and fallback responses
This commit is contained in:
parent
8d18bb40c0
commit
1087085322
4 changed files with 48 additions and 81 deletions
|
|
@ -481,6 +481,7 @@ class PermissionDeniedError(openai.PermissionDeniedError):
|
|||
litellm_debug_info: str | None = None,
|
||||
max_retries: int | None = None,
|
||||
num_retries: int | None = None,
|
||||
body: object | None = None,
|
||||
):
|
||||
self.status_code = 403
|
||||
self.message = f"litellm.PermissionDeniedError: {message}"
|
||||
|
|
@ -489,15 +490,12 @@ class PermissionDeniedError(openai.PermissionDeniedError):
|
|||
self.litellm_debug_info = litellm_debug_info
|
||||
self.max_retries = max_retries
|
||||
self.num_retries = num_retries
|
||||
if response is not None and getattr(response, "request", None) is not None:
|
||||
if response is not None and getattr(response, "_request", None) is not None:
|
||||
self.response = response
|
||||
else:
|
||||
self.response = httpx.Response(
|
||||
status_code=self.status_code,
|
||||
request=httpx.Request(method="POST", url="https://litellm.ai"),
|
||||
)
|
||||
self.response = error_response_without_credentials(self.status_code, response, "https://litellm.ai")
|
||||
super().__init__(
|
||||
self.message, response=self.response, body=None
|
||||
self.message, response=self.response, body=body
|
||||
) # Call the base class constructor with the parameters it needs
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
|||
|
|
@ -446,6 +446,7 @@ def _map_openai_exception(
|
|||
model=model,
|
||||
response=response,
|
||||
litellm_debug_info=extra_information,
|
||||
body=getattr(original_exception, "body", None),
|
||||
)
|
||||
if invalid_request_status == 404:
|
||||
raise NotFoundError(
|
||||
|
|
@ -531,6 +532,7 @@ def _map_openai_exception(
|
|||
model=model,
|
||||
response=response,
|
||||
litellm_debug_info=extra_information,
|
||||
body=getattr(original_exception, "body", None),
|
||||
)
|
||||
elif original_exception.status_code == 404:
|
||||
raise NotFoundError(
|
||||
|
|
|
|||
|
|
@ -29,6 +29,41 @@ from litellm.proxy.common_utils.openai_error_payload import openai_error_type
|
|||
from litellm.types.utils import LlmProviders
|
||||
|
||||
|
||||
def test_permission_denied_preserves_response_without_request() -> None:
|
||||
upstream: Final = httpx.Response(
|
||||
403, content=b'{"error":"denied"}', headers={"x-request-id": "denied-request"}
|
||||
)
|
||||
error: Final = litellm.PermissionDeniedError(
|
||||
message="Forbidden", model="gpt-4o", llm_provider="openai", response=upstream
|
||||
)
|
||||
assert (error.status_code, error.response.status_code, error.request.method) == (403, 403, "POST")
|
||||
assert error.response.content == b'{"error":"denied"}'
|
||||
assert error.response.headers["x-request-id"] == "denied-request"
|
||||
|
||||
|
||||
def test_permission_denied_can_be_created_without_upstream_response() -> None:
|
||||
error: Final = litellm.PermissionDeniedError(message="Forbidden", model="gpt-4o", llm_provider="openai")
|
||||
assert (error.status_code, error.response.status_code, error.request.method) == (403, 403, "POST")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("error_type", ("request_forbidden", "invalid_request_error"))
|
||||
def test_openai_403_keeps_upstream_response(error_type: str) -> None:
|
||||
payload: Final = {"error": {"message": "Forbidden", "type": error_type, "param": "organization", "code": "denied"}}
|
||||
request: Final = httpx.Request("POST", "https://provider.example/v1/chat/completions")
|
||||
upstream: Final = httpx.Response(403, request=request, json=payload, headers={"x-request-id": "denied-request"})
|
||||
original: Final = openai.PermissionDeniedError(
|
||||
f"Error code: 403 - {json.dumps(payload)}", response=upstream, body=payload["error"]
|
||||
)
|
||||
with pytest.raises(litellm.PermissionDeniedError) as caught:
|
||||
exception_type(model="gpt-4o", original_exception=original, custom_llm_provider="openai")
|
||||
assert caught.value.request.url == request.url
|
||||
assert (caught.value.status_code, caught.value.model, caught.value.llm_provider) == (403, "gpt-4o", "openai")
|
||||
assert caught.value.response.json() == payload
|
||||
assert caught.value.response.headers["x-request-id"] == "denied-request"
|
||||
assert caught.value.body == payload["error"]
|
||||
assert (caught.value.type, caught.value.param, caught.value.code) == (error_type, "organization", "denied")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", ["sdk", "http"])
|
||||
@pytest.mark.parametrize("code", ["cyber_policy", "invalid_prompt"])
|
||||
def test_openai_cyber_policy_uses_the_structured_error_code(shape: str, code: str) -> None:
|
||||
|
|
@ -1103,6 +1138,7 @@ def test_azure_404_with_invalid_request_error_type_maps_to_not_found():
|
|||
[
|
||||
(400, "Invalid 'temperature'", litellm.BadRequestError),
|
||||
(401, "Invalid API key", litellm.AuthenticationError),
|
||||
(403, "Forbidden", litellm.PermissionDeniedError),
|
||||
(404, "The model does not exist", litellm.NotFoundError),
|
||||
],
|
||||
)
|
||||
|
|
@ -1141,8 +1177,8 @@ def test_openai_invalid_request_error_without_a_status_code_maps_to_bad_request(
|
|||
|
||||
|
||||
@pytest.mark.parametrize("provider", ["openai", "groq", "deepseek"])
|
||||
@pytest.mark.parametrize("status_code", [403, 409, 422, 429, 500, 503])
|
||||
def test_openai_invalid_request_error_keeps_bad_request_for_statuses_other_than_401_and_404(
|
||||
@pytest.mark.parametrize("status_code", [409, 422, 429, 500, 503])
|
||||
def test_openai_invalid_request_error_keeps_bad_request_for_other_statuses(
|
||||
provider: str, status_code: int
|
||||
) -> None:
|
||||
message: Final = "Cannot cancel a batch that is already completed."
|
||||
|
|
@ -1244,7 +1280,7 @@ OPENAI_SHAPED = {
|
|||
400: (litellm.BadRequestError, 400),
|
||||
401: (litellm.AuthenticationError, 401),
|
||||
402: (litellm.PaymentRequiredError, 402),
|
||||
403: (litellm.APIError, 403),
|
||||
403: (litellm.PermissionDeniedError, 403),
|
||||
404: (litellm.NotFoundError, 404),
|
||||
408: (litellm.Timeout, 408),
|
||||
422: (litellm.BadRequestError, 422),
|
||||
|
|
@ -1260,7 +1296,7 @@ STATUS_KEYED = {**OPENAI_SHAPED, 403: PERMISSION_DENIED}
|
|||
DEVIATIONS_FROM_THE_OPENAI_SHAPE = {
|
||||
"ai21": {402: (litellm.APIError, 402)},
|
||||
"anthropic": {403: PERMISSION_DENIED},
|
||||
"azure": {402: (litellm.APIError, 402), 500: (litellm.APIError, 500)},
|
||||
"azure": {402: (litellm.APIError, 402), 403: (litellm.APIError, 403), 500: (litellm.APIError, 500)},
|
||||
"azure_ai": {402: (litellm.APIError, 402)},
|
||||
"bedrock": {
|
||||
403: PERMISSION_DENIED,
|
||||
|
|
@ -1278,6 +1314,7 @@ DEVIATIONS_FROM_THE_OPENAI_SHAPE = {
|
|||
"groq": {402: (litellm.APIError, 402)},
|
||||
"huggingface": {
|
||||
402: (litellm.APIError, 402),
|
||||
403: (litellm.APIError, 403),
|
||||
404: (litellm.APIError, 404),
|
||||
422: (litellm.APIError, 422),
|
||||
500: (litellm.APIError, 500),
|
||||
|
|
@ -1297,7 +1334,7 @@ DEVIATIONS_FROM_THE_OPENAI_SHAPE = {
|
|||
},
|
||||
"ollama": {403: PERMISSION_DENIED},
|
||||
"openai": {402: (litellm.APIError, 402)},
|
||||
"openrouter": {402: (litellm.APIError, 402), 500: (litellm.APIError, 500)},
|
||||
"openrouter": {402: (litellm.APIError, 402), 403: (litellm.APIError, 403), 500: (litellm.APIError, 500)},
|
||||
"perplexity": {402: (litellm.APIError, 402)},
|
||||
"replicate": {
|
||||
403: (litellm.APIError, 500),
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
"""
|
||||
Unit test verifying that OpenAI HTTP 403 responses map to litellm.PermissionDeniedError.
|
||||
|
||||
Closes #49936: [Bug]: OpenAI HTTP 403 raises APIError or BadRequestError instead of PermissionDeniedError
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import litellm
|
||||
from litellm.exceptions import PermissionDeniedError
|
||||
from litellm.litellm_core_utils.exception_mapping_utils import exception_type
|
||||
|
||||
|
||||
class MockOpenAIHTTP403Exception(Exception):
|
||||
def __init__(self, status_code: int = 403, body: dict = None, message: str = "Forbidden"):
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
self.body = body or {}
|
||||
self.message = message
|
||||
|
||||
|
||||
def test_openai_403_request_forbidden_raises_permission_denied_error():
|
||||
"""Verify HTTP 403 with request_forbidden body raises PermissionDeniedError."""
|
||||
exc = MockOpenAIHTTP403Exception(
|
||||
status_code=403,
|
||||
body={
|
||||
"error": {
|
||||
"message": "Country, region, or territory not supported",
|
||||
"type": "request_forbidden",
|
||||
"param": None,
|
||||
"code": "unsupported_country_region_territory",
|
||||
}
|
||||
},
|
||||
message="Country, region, or territory not supported",
|
||||
)
|
||||
|
||||
with pytest.raises(PermissionDeniedError) as exc_info:
|
||||
exception_type(
|
||||
model="gpt-4o",
|
||||
original_exception=exc,
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
assert isinstance(exc_info.value, PermissionDeniedError)
|
||||
|
||||
|
||||
def test_openai_403_invalid_request_error_raises_permission_denied_error():
|
||||
"""Verify HTTP 403 with invalid_request_error body raises PermissionDeniedError."""
|
||||
exc = MockOpenAIHTTP403Exception(
|
||||
status_code=403,
|
||||
body={
|
||||
"error": {
|
||||
"message": "Forbidden",
|
||||
"type": "invalid_request_error",
|
||||
"param": None,
|
||||
"code": None,
|
||||
}
|
||||
},
|
||||
message="Forbidden: invalid_request_error",
|
||||
)
|
||||
|
||||
with pytest.raises(PermissionDeniedError) as exc_info:
|
||||
exception_type(
|
||||
model="gpt-4o",
|
||||
original_exception=exc,
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
assert isinstance(exc_info.value, PermissionDeniedError)
|
||||
Loading…
Add table
Reference in a new issue