fix(openai): preserve 403 error metadata and fallback responses

This commit is contained in:
sidsri14 2026-11-05 22:15:12 +05:30
parent 8d18bb40c0
commit 1087085322
4 changed files with 48 additions and 81 deletions

View file

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

View file

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

View file

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

View file

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