mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-31 06:56:59 +00:00
fix(openai): keep the upstream error body on e.response for OpenAI and Azure errors (#49079)
* fix(openai): keep the upstream error response on mapped exceptions so e.response.text has the wire body * fix: keep the upstream error body on 429 and 5xx errors RateLimitError, InternalServerError, BadGatewayError, and ServiceUnavailableError rebuilt their response and dropped the upstream body. They now keep the body and exactly the upstream headers, and an unread streaming response no longer raises. * fix: keep a gzip-encoded 429 or 5xx error readable The kept body is already decoded, so the rebuilt response drops content-encoding instead of decoding the body a second time and raising DecodingError. * fix: keep the upstream headers and drop credentials on every mapped provider error The copied error response now keeps every upstream header and carries a request with only the method and the URL, so no provider key, query secret, or prompt reaches e.request. Timeout and APIError get the response too, and azure/ chat, streaming, embedding, and image errors attach it * fix: keep the upstream error body on gateway timeouts and re-raised gateway errors * test(azure): cover the 404, 500, 502, and 504 error bodies and the missing-key placeholder * fix: keep no e.response on timeouts and errors litellm raises itself * fix: keep the upstream elapsed time on error responses and the upstream body on azure_text errors * fix: keep the upstream body without credentials on Azure content filter errors and message-matched OpenAI errors --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
parent
ccc56d27ad
commit
f32ac94ad1
17 changed files with 895 additions and 80 deletions
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
## LiteLLM versions of the OpenAI Exception Types
|
||||
|
||||
import datetime
|
||||
import enum
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, Final
|
||||
|
|
@ -115,6 +116,54 @@ def validate_rate_limit_type(value: object) -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def _upstream_body(response: object) -> bytes:
|
||||
if not isinstance(response, httpx.Response):
|
||||
return b""
|
||||
try:
|
||||
return response.content
|
||||
except httpx.ResponseNotRead:
|
||||
return b""
|
||||
|
||||
|
||||
def _request_without_credentials(upstream: object, fallback_url: str) -> httpx.Request:
|
||||
if not isinstance(upstream, httpx.Response):
|
||||
return httpx.Request(method="POST", url=fallback_url)
|
||||
try:
|
||||
upstream_request: Final = upstream.request
|
||||
except RuntimeError:
|
||||
return httpx.Request(method="POST", url=fallback_url)
|
||||
return httpx.Request(
|
||||
method=upstream_request.method,
|
||||
url=upstream_request.url.copy_with(userinfo=b"", query=None, fragment=None),
|
||||
)
|
||||
|
||||
|
||||
def _upstream_elapsed(upstream: object) -> datetime.timedelta | None:
|
||||
if not isinstance(upstream, httpx.Response):
|
||||
return None
|
||||
try:
|
||||
return upstream.elapsed
|
||||
except RuntimeError:
|
||||
return None
|
||||
|
||||
|
||||
def error_response_without_credentials(status_code: int, upstream: object, fallback_url: str) -> httpx.Response:
|
||||
response: Final = httpx.Response(
|
||||
status_code=status_code,
|
||||
stream=httpx.ByteStream(_upstream_body(upstream)),
|
||||
request=_request_without_credentials(upstream, fallback_url),
|
||||
)
|
||||
_ = response.read()
|
||||
response.headers = httpx.Headers(upstream.headers if isinstance(upstream, httpx.Response) else None)
|
||||
elapsed: Final = _upstream_elapsed(upstream)
|
||||
if elapsed is not None:
|
||||
response.elapsed = elapsed
|
||||
return response
|
||||
|
||||
|
||||
_NO_UPSTREAM_REQUEST_URL: Final = "https://cloud.google.com/vertex-ai/"
|
||||
|
||||
|
||||
_MINIMAL_ERROR_RESPONSE: httpx.Response | None = None
|
||||
|
||||
|
||||
|
|
@ -519,20 +568,11 @@ class RateLimitError(openai.RateLimitError):
|
|||
# headers stay reachable on `e.response.headers` for callers that
|
||||
# explicitly want them; only the proxy-supplied `headers=` kwarg
|
||||
# makes it onto `self.headers`.
|
||||
_response_headers: Final = getattr(response, "headers", None) if response is not None else None
|
||||
self.headers: dict[str, str] | None = {k: str(v) for k, v in headers.items()} if headers else None
|
||||
# Mirrors FastAPI HTTPException.detail so the same instance can be
|
||||
# serialized through both the ProxyException and HTTPException paths.
|
||||
self.detail = detail if detail is not None else self.message
|
||||
self.response = httpx.Response(
|
||||
status_code=429,
|
||||
headers=_response_headers,
|
||||
content=response.content if response is not None else None,
|
||||
request=httpx.Request(
|
||||
method="POST",
|
||||
url=" https://cloud.google.com/vertex-ai/",
|
||||
),
|
||||
)
|
||||
self.response = error_response_without_credentials(429, response, _NO_UPSTREAM_REQUEST_URL)
|
||||
super().__init__(
|
||||
self.message,
|
||||
response=self.response, # pyright: ignore[reportArgumentType] # SDK accepts HTTPX at runtime
|
||||
|
|
@ -757,15 +797,7 @@ class ServiceUnavailableError(openai.APIStatusError):
|
|||
self.litellm_debug_info = litellm_debug_info
|
||||
self.max_retries = max_retries
|
||||
self.num_retries = num_retries
|
||||
_response_headers: Final = getattr(response, "headers", None) if response is not None else None
|
||||
self.response = httpx.Response(
|
||||
status_code=self.status_code,
|
||||
headers=_response_headers,
|
||||
request=httpx.Request(
|
||||
method="POST",
|
||||
url=" https://cloud.google.com/vertex-ai/",
|
||||
),
|
||||
)
|
||||
self.response = error_response_without_credentials(self.status_code, response, _NO_UPSTREAM_REQUEST_URL)
|
||||
super().__init__(
|
||||
self.message, response=self.response, body=None
|
||||
) # Call the base class constructor with the parameters it needs
|
||||
|
|
@ -808,15 +840,7 @@ class BadGatewayError(openai.APIStatusError):
|
|||
self.litellm_debug_info = litellm_debug_info
|
||||
self.max_retries = max_retries
|
||||
self.num_retries = num_retries
|
||||
_response_headers: Final = getattr(response, "headers", None) if response is not None else None
|
||||
self.response = httpx.Response(
|
||||
status_code=self.status_code,
|
||||
headers=_response_headers,
|
||||
request=httpx.Request(
|
||||
method="POST",
|
||||
url=" https://cloud.google.com/vertex-ai/",
|
||||
),
|
||||
)
|
||||
self.response = error_response_without_credentials(self.status_code, response, _NO_UPSTREAM_REQUEST_URL)
|
||||
super().__init__(
|
||||
self.message,
|
||||
response=self.response, # pyright: ignore[reportArgumentType] # SDK accepts HTTPX at runtime
|
||||
|
|
@ -862,15 +886,7 @@ class InternalServerError(openai.InternalServerError):
|
|||
self.litellm_debug_info = litellm_debug_info
|
||||
self.max_retries = max_retries
|
||||
self.num_retries = num_retries
|
||||
_response_headers: Final = getattr(response, "headers", None) if response is not None else None
|
||||
self.response = httpx.Response(
|
||||
status_code=self.status_code,
|
||||
headers=_response_headers,
|
||||
request=httpx.Request(
|
||||
method="POST",
|
||||
url=" https://cloud.google.com/vertex-ai/",
|
||||
),
|
||||
)
|
||||
self.response = error_response_without_credentials(self.status_code, response, _NO_UPSTREAM_REQUEST_URL)
|
||||
super().__init__(
|
||||
self.message,
|
||||
response=self.response, # pyright: ignore[reportArgumentType] # SDK accepts HTTPX at runtime
|
||||
|
|
@ -908,6 +924,7 @@ class APIError(openai.APIError):
|
|||
max_retries: int | None = None,
|
||||
num_retries: int | None = None,
|
||||
body: object | None = None,
|
||||
response: httpx.Response | None = None,
|
||||
):
|
||||
self.status_code = status_code
|
||||
self.message = f"litellm.APIError: {message}"
|
||||
|
|
@ -916,6 +933,8 @@ class APIError(openai.APIError):
|
|||
self.litellm_debug_info = litellm_debug_info
|
||||
self.max_retries = max_retries
|
||||
self.num_retries = num_retries
|
||||
if response is not None:
|
||||
self.response = response
|
||||
if request is None:
|
||||
request = httpx.Request(method="POST", url="https://api.openai.com/v1")
|
||||
super().__init__(self.message, request=request, body=body)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ from ..exceptions import (
|
|||
ServiceUnavailableError,
|
||||
Timeout,
|
||||
UnprocessableEntityError,
|
||||
error_response_without_credentials,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -305,6 +306,16 @@ def _litellm_proxy_response(
|
|||
)
|
||||
|
||||
|
||||
def _without_credentials(response: httpx.Response | None) -> httpx.Response | None:
|
||||
if not isinstance(response, httpx.Response):
|
||||
return response
|
||||
return error_response_without_credentials(response.status_code, response, "https://docs.litellm.ai/docs")
|
||||
|
||||
|
||||
def _upstream_reply(response: httpx.Response | None) -> httpx.Response | None:
|
||||
return response if response is not None and (response.headers or response.content) else None
|
||||
|
||||
|
||||
def _map_openai_exception(
|
||||
*,
|
||||
model: str,
|
||||
|
|
@ -315,7 +326,7 @@ def _map_openai_exception(
|
|||
exception_provider: str,
|
||||
extra_information: str,
|
||||
) -> None:
|
||||
response: Final = _litellm_proxy_response(original_exception, custom_llm_provider)
|
||||
response: Final = _without_credentials(_litellm_proxy_response(original_exception, custom_llm_provider))
|
||||
if (cyber_policy_body := _openai_cyber_policy_body(original_exception)) is not None:
|
||||
raise ContentPolicyViolationError(
|
||||
message=f"ContentPolicyViolationError: {exception_provider} - {original_exception}",
|
||||
|
|
@ -378,6 +389,7 @@ def _map_openai_exception(
|
|||
model=model,
|
||||
llm_provider=custom_llm_provider,
|
||||
litellm_debug_info=extra_information,
|
||||
response=_upstream_reply(response),
|
||||
)
|
||||
elif (
|
||||
("invalid_request_error" in error_str and "content_policy_violation" in error_str)
|
||||
|
|
@ -449,6 +461,7 @@ def _map_openai_exception(
|
|||
model=model,
|
||||
llm_provider=custom_llm_provider,
|
||||
body=getattr(original_exception, "body", None),
|
||||
response=_upstream_reply(response),
|
||||
)
|
||||
elif "Request too large" in error_str:
|
||||
raise RateLimitError(
|
||||
|
|
@ -479,6 +492,7 @@ def _map_openai_exception(
|
|||
model=model,
|
||||
request=_request,
|
||||
litellm_debug_info=extra_information,
|
||||
response=_upstream_reply(response),
|
||||
)
|
||||
elif hasattr(original_exception, "status_code"):
|
||||
if original_exception.status_code == 400:
|
||||
|
|
@ -512,6 +526,7 @@ def _map_openai_exception(
|
|||
model=model,
|
||||
llm_provider=custom_llm_provider,
|
||||
litellm_debug_info=extra_information,
|
||||
response=_upstream_reply(response),
|
||||
)
|
||||
elif original_exception.status_code == 422:
|
||||
raise BadRequestError(
|
||||
|
|
@ -563,6 +578,7 @@ def _map_openai_exception(
|
|||
llm_provider=custom_llm_provider,
|
||||
litellm_debug_info=extra_information,
|
||||
exception_status_code=original_exception.status_code,
|
||||
response=_upstream_reply(response),
|
||||
)
|
||||
else:
|
||||
raise APIError(
|
||||
|
|
@ -572,6 +588,7 @@ def _map_openai_exception(
|
|||
model=model,
|
||||
request=getattr(original_exception, "request", None),
|
||||
litellm_debug_info=extra_information,
|
||||
response=_upstream_reply(response),
|
||||
)
|
||||
else:
|
||||
# if no status code then it is an APIConnectionError: https://github.com/openai/openai-python#handling-errors
|
||||
|
|
@ -2019,6 +2036,7 @@ def _map_azure_exception(
|
|||
exception_provider: str,
|
||||
extra_information: str,
|
||||
) -> None:
|
||||
response: Final = _without_credentials(getattr(original_exception, "response", None))
|
||||
message = get_error_message(error_obj=original_exception)
|
||||
if message is None:
|
||||
if hasattr(original_exception, "message"):
|
||||
|
|
@ -2054,7 +2072,7 @@ def _map_azure_exception(
|
|||
llm_provider="azure",
|
||||
model=model,
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
)
|
||||
elif "DeploymentNotFound" in error_str:
|
||||
raise NotFoundError(
|
||||
|
|
@ -2062,7 +2080,7 @@ def _map_azure_exception(
|
|||
llm_provider="azure",
|
||||
model=model,
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
)
|
||||
elif azure_error_code == "content_policy_violation" or ExceptionCheckers.is_azure_content_policy_violation_error(
|
||||
error_str
|
||||
|
|
@ -2076,6 +2094,7 @@ def _map_azure_exception(
|
|||
model=model,
|
||||
extra_information=extra_information,
|
||||
original_exception=original_exception,
|
||||
response=response,
|
||||
)
|
||||
elif azure_error_code == "invalid_encrypted_content" or "could not be verified" in error_str:
|
||||
helpful_message: Final = (
|
||||
|
|
@ -2094,7 +2113,7 @@ def _map_azure_exception(
|
|||
llm_provider="azure",
|
||||
model=model,
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
body=getattr(original_exception, "body", None),
|
||||
)
|
||||
elif getattr(original_exception, "status_code", None) in (None, 400) and (
|
||||
|
|
@ -2106,7 +2125,7 @@ def _map_azure_exception(
|
|||
llm_provider="azure",
|
||||
model=model,
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
body=getattr(original_exception, "body", None),
|
||||
)
|
||||
elif "invalid_request_error" in error_str and getattr(original_exception, "status_code", None) in (None, 400):
|
||||
|
|
@ -2115,7 +2134,7 @@ def _map_azure_exception(
|
|||
llm_provider="azure",
|
||||
model=model,
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
body=getattr(original_exception, "body", None),
|
||||
)
|
||||
elif "The api_key client option must be set either by passing api_key to the client or by setting" in error_str:
|
||||
|
|
@ -2124,7 +2143,7 @@ def _map_azure_exception(
|
|||
llm_provider=custom_llm_provider,
|
||||
model=model,
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
)
|
||||
elif "Connection error" in error_str:
|
||||
raise APIConnectionError(
|
||||
|
|
@ -2140,7 +2159,7 @@ def _map_azure_exception(
|
|||
llm_provider="azure",
|
||||
model=model,
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
body=getattr(original_exception, "body", None),
|
||||
)
|
||||
elif original_exception.status_code == 401:
|
||||
|
|
@ -2149,7 +2168,7 @@ def _map_azure_exception(
|
|||
llm_provider="azure",
|
||||
model=model,
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
)
|
||||
elif original_exception.status_code == 404:
|
||||
raise NotFoundError(
|
||||
|
|
@ -2157,7 +2176,7 @@ def _map_azure_exception(
|
|||
llm_provider="azure",
|
||||
model=model,
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
)
|
||||
elif original_exception.status_code == 408:
|
||||
raise Timeout(
|
||||
|
|
@ -2165,6 +2184,7 @@ def _map_azure_exception(
|
|||
model=model,
|
||||
litellm_debug_info=extra_information,
|
||||
llm_provider="azure",
|
||||
response=_upstream_reply(response),
|
||||
)
|
||||
elif original_exception.status_code == 422:
|
||||
raise BadRequestError(
|
||||
|
|
@ -2172,7 +2192,7 @@ def _map_azure_exception(
|
|||
model=model,
|
||||
llm_provider="azure",
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
)
|
||||
elif original_exception.status_code == 429:
|
||||
raise RateLimitError(
|
||||
|
|
@ -2180,7 +2200,7 @@ def _map_azure_exception(
|
|||
model=model,
|
||||
llm_provider="azure",
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
)
|
||||
elif original_exception.status_code == 502:
|
||||
raise BadGatewayError(
|
||||
|
|
@ -2188,7 +2208,7 @@ def _map_azure_exception(
|
|||
model=model,
|
||||
llm_provider="azure",
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
)
|
||||
elif original_exception.status_code == 503:
|
||||
raise ServiceUnavailableError(
|
||||
|
|
@ -2196,7 +2216,7 @@ def _map_azure_exception(
|
|||
model=model,
|
||||
llm_provider="azure",
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
)
|
||||
elif original_exception.status_code == 504: # gateway timeout error
|
||||
raise Timeout(
|
||||
|
|
@ -2205,6 +2225,7 @@ def _map_azure_exception(
|
|||
litellm_debug_info=extra_information,
|
||||
llm_provider="azure",
|
||||
exception_status_code=original_exception.status_code,
|
||||
response=_upstream_reply(response),
|
||||
)
|
||||
else:
|
||||
raise APIError(
|
||||
|
|
@ -2214,6 +2235,7 @@ def _map_azure_exception(
|
|||
litellm_debug_info=extra_information,
|
||||
model=model,
|
||||
request=httpx.Request(method="POST", url="https://openai.com/"),
|
||||
response=_upstream_reply(response),
|
||||
)
|
||||
else:
|
||||
# if no status code then it is an APIConnectionError: https://github.com/openai/openai-python#handling-errors
|
||||
|
|
@ -2589,17 +2611,20 @@ def exception_type(
|
|||
model=model,
|
||||
llm_provider=custom_llm_provider,
|
||||
litellm_debug_info=extra_information,
|
||||
response=_upstream_reply(
|
||||
_without_credentials(_litellm_proxy_response(mappable_exception, custom_llm_provider))
|
||||
),
|
||||
)
|
||||
|
||||
if (
|
||||
custom_llm_provider == "litellm_proxy"
|
||||
): # handle special case where calling litellm proxy + exception str contains error message
|
||||
extract_and_raise_litellm_exception(
|
||||
response=_litellm_proxy_response(mappable_exception, custom_llm_provider),
|
||||
response=_without_credentials(_litellm_proxy_response(mappable_exception, custom_llm_provider)),
|
||||
error_str=error_str,
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
body=getattr(original_exception, "body", None),
|
||||
body=getattr(mappable_exception, "body", None),
|
||||
)
|
||||
if (
|
||||
custom_llm_provider == "openai"
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ from ..openai.common_utils import (
|
|||
build_output_token_limit_response,
|
||||
is_output_token_limit_error,
|
||||
model_dump_provider_response,
|
||||
upstream_error_response,
|
||||
)
|
||||
from .common_utils import (
|
||||
AzureOpenAIError,
|
||||
|
|
@ -399,6 +400,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
|
|||
raise AzureOpenAIError(
|
||||
status_code=status_code,
|
||||
message=str(e),
|
||||
response=upstream_error_response(e),
|
||||
headers=error_headers,
|
||||
body=error_body,
|
||||
)
|
||||
|
|
@ -662,6 +664,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
|
|||
raise AzureOpenAIError(
|
||||
status_code=status_code,
|
||||
message=message,
|
||||
response=upstream_error_response(e),
|
||||
headers=error_headers,
|
||||
body=error_body,
|
||||
)
|
||||
|
|
@ -855,7 +858,12 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
|
|||
if error_headers is None and error_response:
|
||||
error_headers = getattr(error_response, "headers", None)
|
||||
error_text = error_response.text
|
||||
raise AzureOpenAIError(status_code=status_code, message=error_text, headers=error_headers)
|
||||
raise AzureOpenAIError(
|
||||
status_code=status_code,
|
||||
message=error_text,
|
||||
response=upstream_error_response(e),
|
||||
headers=error_headers,
|
||||
)
|
||||
|
||||
async def make_async_azure_httpx_request(
|
||||
self,
|
||||
|
|
@ -1190,7 +1198,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
|
|||
except Exception as e:
|
||||
error_code: Final[object] = getattr(e, "status_code", None)
|
||||
if error_code is not None:
|
||||
raise AzureOpenAIError(status_code=error_code, message=str(e))
|
||||
raise AzureOpenAIError(status_code=error_code, message=str(e), response=upstream_error_response(e))
|
||||
else:
|
||||
raise AzureOpenAIError(status_code=500, message=str(e))
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging
|
|||
from litellm.litellm_core_utils.prompt_templates.factory import prompt_factory
|
||||
from litellm.utils import CustomStreamWrapper, ModelResponse, TextCompletionResponse
|
||||
|
||||
from ...openai.common_utils import upstream_error_response
|
||||
from ...openai.completion.transformation import OpenAITextCompletionConfig
|
||||
from ..common_utils import AzureOpenAIError, BaseAzureLLM
|
||||
|
||||
|
|
@ -183,7 +184,12 @@ class AzureTextCompletion(BaseAzureLLM):
|
|||
error_response: Final[object] = getattr(e, "response", None)
|
||||
if error_headers is None and error_response:
|
||||
error_headers = getattr(error_response, "headers", None)
|
||||
raise AzureOpenAIError(status_code=status_code, message=str(e), headers=error_headers)
|
||||
raise AzureOpenAIError(
|
||||
status_code=status_code,
|
||||
message=str(e),
|
||||
headers=error_headers,
|
||||
response=upstream_error_response(e),
|
||||
)
|
||||
|
||||
async def acompletion(
|
||||
self,
|
||||
|
|
@ -244,7 +250,12 @@ class AzureTextCompletion(BaseAzureLLM):
|
|||
error_response: Final[object] = getattr(e, "response", None)
|
||||
if error_headers is None and error_response:
|
||||
error_headers = getattr(error_response, "headers", None)
|
||||
raise AzureOpenAIError(status_code=status_code, message=str(e), headers=error_headers)
|
||||
raise AzureOpenAIError(
|
||||
status_code=status_code,
|
||||
message=str(e),
|
||||
headers=error_headers,
|
||||
response=upstream_error_response(e),
|
||||
)
|
||||
|
||||
def streaming(
|
||||
self,
|
||||
|
|
@ -355,4 +366,9 @@ class AzureTextCompletion(BaseAzureLLM):
|
|||
error_response: Final[object] = getattr(e, "response", None)
|
||||
if error_headers is None and error_response:
|
||||
error_headers = getattr(error_response, "headers", None)
|
||||
raise AzureOpenAIError(status_code=status_code, message=str(e), headers=error_headers)
|
||||
raise AzureOpenAIError(
|
||||
status_code=status_code,
|
||||
message=str(e),
|
||||
headers=error_headers,
|
||||
response=upstream_error_response(e),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from typing import Final
|
||||
|
||||
import httpx
|
||||
|
||||
from litellm.exceptions import ContentPolicyViolationError
|
||||
|
||||
|
||||
|
|
@ -14,6 +16,7 @@ class AzureOpenAIExceptionMapping:
|
|||
model: str,
|
||||
extra_information: str,
|
||||
original_exception: Exception,
|
||||
response: httpx.Response | None,
|
||||
) -> ContentPolicyViolationError:
|
||||
"""
|
||||
Create a content policy violation error
|
||||
|
|
@ -39,7 +42,7 @@ class AzureOpenAIExceptionMapping:
|
|||
llm_provider="azure",
|
||||
model=model,
|
||||
litellm_debug_info=extra_information,
|
||||
response=getattr(original_exception, "response", None),
|
||||
response=response,
|
||||
provider_specific_fields={
|
||||
# Preserve legacy key for backward compatibility.
|
||||
"innererror": inner_error,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ if TYPE_CHECKING:
|
|||
from aiohttp import ClientSession
|
||||
|
||||
import litellm
|
||||
from litellm.exceptions import error_response_without_credentials
|
||||
from litellm.litellm_core_utils.completion_timeout import CompletionTimeout
|
||||
from litellm.litellm_core_utils.core_helpers import as_str_mapping, decoded_json_object
|
||||
from litellm.litellm_core_utils.token_counter import token_counter, usage_tokenizer_model
|
||||
|
|
@ -148,6 +149,14 @@ class OpenAIError(BaseLLMException):
|
|||
)
|
||||
|
||||
|
||||
def upstream_error_response(error: Exception) -> httpx.Response | None:
|
||||
if not isinstance(error, (openai.APIStatusError, httpx.HTTPStatusError)):
|
||||
return None
|
||||
return error_response_without_credentials(
|
||||
error.response.status_code, error.response, "https://docs.litellm.ai/docs"
|
||||
)
|
||||
|
||||
|
||||
_DECODED_JSON: Final = TypeAdapter(object)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from litellm.types.llms.openai import AllMessageValues, OpenAITextCompletionUser
|
|||
from litellm.types.utils import LlmProviders, ModelResponse, TextCompletionResponse
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
from ..common_utils import BaseOpenAILLM, OpenAIError
|
||||
from ..common_utils import BaseOpenAILLM, OpenAIError, upstream_error_response
|
||||
from ..workload_identity import build_async_openai_client, build_openai_client
|
||||
from .transformation import OpenAITextCompletionConfig
|
||||
|
||||
|
|
@ -164,7 +164,12 @@ class OpenAITextCompletion(BaseLLM):
|
|||
error_response: Final[object] = getattr(e, "response", None)
|
||||
if error_headers is None and error_response:
|
||||
error_headers = getattr(error_response, "headers", None)
|
||||
raise OpenAIError(status_code=status_code, message=error_text, headers=error_headers)
|
||||
raise OpenAIError(
|
||||
status_code=status_code,
|
||||
message=error_text,
|
||||
response=upstream_error_response(e),
|
||||
headers=error_headers,
|
||||
)
|
||||
|
||||
async def acompletion(
|
||||
self,
|
||||
|
|
@ -220,7 +225,12 @@ class OpenAITextCompletion(BaseLLM):
|
|||
error_response: Final[object] = getattr(e, "response", None)
|
||||
if error_headers is None and error_response:
|
||||
error_headers = getattr(error_response, "headers", None)
|
||||
raise OpenAIError(status_code=status_code, message=error_text, headers=error_headers)
|
||||
raise OpenAIError(
|
||||
status_code=status_code,
|
||||
message=error_text,
|
||||
response=upstream_error_response(e),
|
||||
headers=error_headers,
|
||||
)
|
||||
|
||||
def streaming(
|
||||
self,
|
||||
|
|
@ -261,7 +271,12 @@ class OpenAITextCompletion(BaseLLM):
|
|||
error_response: object = getattr(e, "response", None)
|
||||
if error_headers is None and error_response:
|
||||
error_headers = getattr(error_response, "headers", None)
|
||||
raise OpenAIError(status_code=status_code, message=error_text, headers=error_headers)
|
||||
raise OpenAIError(
|
||||
status_code=status_code,
|
||||
message=error_text,
|
||||
response=upstream_error_response(e),
|
||||
headers=error_headers,
|
||||
)
|
||||
streamwrapper: Final = CustomStreamWrapper(
|
||||
completion_stream=response,
|
||||
model=model,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from litellm.utils import ProviderConfigManager
|
|||
|
||||
from ...base_llm.image_variations.transformation import BaseImageVariationConfig
|
||||
from ...custom_httpx.llm_http_handler import LiteLLMLoggingObj
|
||||
from ..common_utils import OpenAIError
|
||||
from ..common_utils import OpenAIError, upstream_error_response
|
||||
from ..workload_identity import build_async_openai_client, build_openai_client
|
||||
|
||||
|
||||
|
|
@ -129,7 +129,12 @@ class OpenAIImageVariationsHandler:
|
|||
error_response: Final[object] = getattr(e, "response", None)
|
||||
if error_headers is None and error_response:
|
||||
error_headers = getattr(error_response, "headers", None)
|
||||
raise OpenAIError(status_code=status_code, message=error_text, headers=error_headers)
|
||||
raise OpenAIError(
|
||||
status_code=status_code,
|
||||
message=error_text,
|
||||
response=upstream_error_response(e),
|
||||
headers=error_headers,
|
||||
)
|
||||
|
||||
def image_variations(
|
||||
self,
|
||||
|
|
@ -245,4 +250,9 @@ class OpenAIImageVariationsHandler:
|
|||
error_response: Final[object] = getattr(e, "response", None)
|
||||
if error_headers is None and error_response:
|
||||
error_headers = getattr(error_response, "headers", None)
|
||||
raise OpenAIError(status_code=status_code, message=error_text, headers=error_headers)
|
||||
raise OpenAIError(
|
||||
status_code=status_code,
|
||||
message=error_text,
|
||||
response=upstream_error_response(e),
|
||||
headers=error_headers,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ from .common_utils import (
|
|||
is_openai_backed_api_base,
|
||||
is_output_token_limit_error,
|
||||
model_dump_provider_response,
|
||||
upstream_error_response,
|
||||
)
|
||||
from .workload_identity import (
|
||||
build_async_openai_client,
|
||||
|
|
@ -983,6 +984,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
raise OpenAIError(
|
||||
status_code=status_code,
|
||||
message=error_text,
|
||||
response=upstream_error_response(e),
|
||||
headers=error_headers,
|
||||
body=error_body,
|
||||
)
|
||||
|
|
@ -1110,6 +1112,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
raise OpenAIError(
|
||||
status_code=status_code,
|
||||
message=message,
|
||||
response=upstream_error_response(e),
|
||||
headers=error_headers,
|
||||
body=exception_body,
|
||||
)
|
||||
|
|
@ -1282,6 +1285,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
raise OpenAIError(
|
||||
status_code=getattr(e, "status_code", 500),
|
||||
message=str(e),
|
||||
response=upstream_error_response(e),
|
||||
headers=error_headers,
|
||||
body=exception_body,
|
||||
)
|
||||
|
|
@ -1409,7 +1413,12 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
error_response: Final[object] = getattr(e, "response", None)
|
||||
if error_headers is None and error_response:
|
||||
error_headers = getattr(error_response, "headers", None)
|
||||
raise OpenAIError(status_code=status_code, message=error_text, headers=error_headers)
|
||||
raise OpenAIError(
|
||||
status_code=status_code,
|
||||
message=error_text,
|
||||
response=upstream_error_response(e),
|
||||
headers=error_headers,
|
||||
)
|
||||
|
||||
def embedding(
|
||||
self,
|
||||
|
|
@ -1499,7 +1508,12 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
error_response: Final[object] = getattr(e, "response", None)
|
||||
if error_headers is None and error_response:
|
||||
error_headers = getattr(error_response, "headers", None)
|
||||
raise OpenAIError(status_code=status_code, message=error_text, headers=error_headers)
|
||||
raise OpenAIError(
|
||||
status_code=status_code,
|
||||
message=error_text,
|
||||
response=upstream_error_response(e),
|
||||
headers=error_headers,
|
||||
)
|
||||
|
||||
async def aimage_generation(
|
||||
self,
|
||||
|
|
@ -1666,7 +1680,11 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
original_response=str(e),
|
||||
)
|
||||
if hasattr(e, "status_code"):
|
||||
raise OpenAIError(status_code=getattr(e, "status_code", 500), message=str(e))
|
||||
raise OpenAIError(
|
||||
status_code=getattr(e, "status_code", 500),
|
||||
message=str(e),
|
||||
response=upstream_error_response(e),
|
||||
)
|
||||
else:
|
||||
raise OpenAIError(status_code=500, message=str(e))
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
|||
from litellm.llms.bedrock.common_utils import BedrockError
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
|
||||
from litellm.llms.openai.common_utils import OpenAIError
|
||||
from litellm.llms.vertex_ai.common_utils import VertexAIError
|
||||
from litellm.proxy.common_utils.openai_error_payload import openai_error_type
|
||||
from litellm.types.utils import LlmProviders
|
||||
|
||||
|
|
@ -54,8 +55,8 @@ def test_openai_cyber_policy_uses_the_structured_error_code(shape: str, code: st
|
|||
assert caught.value.code == code
|
||||
assert caught.value.body == body
|
||||
assert caught.value.status_code == 400
|
||||
assert caught.value.response is response
|
||||
assert caught.value.request is request
|
||||
assert caught.value.response.status_code == 400
|
||||
assert caught.value.request.url == request.url
|
||||
assert caught.value.response.headers["x-request-id"] == "cyber-policy-request"
|
||||
|
||||
|
||||
|
|
@ -2940,3 +2941,80 @@ def test_transport_timeout_maps_to_timeout(
|
|||
|
||||
assert exc_info.value.status_code == 408
|
||||
assert f"Request timed out. Error_str: {reason}" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_bedrock_server_error_keeps_the_aws_body_without_the_signed_request() -> None:
|
||||
body: Final = '{"message":"Bedrock is unable to process your request."}'
|
||||
signed_request: Final = httpx.Request(
|
||||
"POST",
|
||||
"https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-opus-5-5/converse",
|
||||
headers={"authorization": "AWS4-HMAC-SHA256 Credential=AKIDSIGNED/20261029/us-east-1/bedrock/aws4_request"},
|
||||
content=b'{"messages":[{"role":"user","content":[{"text":"hi"}]}]}',
|
||||
)
|
||||
upstream: Final = httpx.Response(
|
||||
500, content=body, headers={"x-amzn-requestid": "req-aws"}, request=signed_request
|
||||
)
|
||||
|
||||
with pytest.raises(litellm.ServiceUnavailableError) as raised:
|
||||
exception_type(
|
||||
model="bedrock/anthropic.claude-opus-5-5",
|
||||
original_exception=BedrockError(status_code=500, message=body, response=upstream),
|
||||
custom_llm_provider="bedrock",
|
||||
)
|
||||
|
||||
assert raised.value.response.text == body
|
||||
assert raised.value.response.headers["x-amzn-requestid"] == "req-aws"
|
||||
assert "AKIDSIGNED" not in str(raised.value.request.headers)
|
||||
assert raised.value.request.content == b""
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ("gemini", "vertex_ai"))
|
||||
def test_gemini_server_error_response_carries_the_upstream_error_body(provider: str) -> None:
|
||||
body: Final = '{"error":{"code":500,"message":"Internal error encountered.","status":"INTERNAL"}}'
|
||||
|
||||
with pytest.raises(litellm.InternalServerError) as raised:
|
||||
exception_type(
|
||||
model="gemini-3.1-pro",
|
||||
original_exception=VertexAIError(status_code=500, message=body, headers=None),
|
||||
custom_llm_provider=provider,
|
||||
)
|
||||
|
||||
assert raised.value.response.status_code == 500
|
||||
assert raised.value.response.json() == json.loads(body)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status_code", "message", "mapped_error"),
|
||||
(
|
||||
(500, "The server had an error processing your request. Sorry about that!", litellm.InternalServerError),
|
||||
(520, "Web server is returning an unknown error", litellm.InternalServerError),
|
||||
(504, "A timeout occurred", litellm.Timeout),
|
||||
(500, "Mistral API raised a streaming error", litellm.APIError),
|
||||
),
|
||||
)
|
||||
def test_openai_error_matched_by_its_message_keeps_the_upstream_body(
|
||||
status_code: int, message: str, mapped_error: type[Exception]
|
||||
) -> None:
|
||||
body: Final = json.dumps({"error": {"message": message, "type": "server_error", "param": None, "code": None}})
|
||||
sent_request: Final = httpx.Request(
|
||||
"POST",
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
headers={"authorization": "Bearer sk-openai-secret"},
|
||||
content=b'{"messages":[{"role":"user","content":"hi"}]}',
|
||||
)
|
||||
upstream: Final = httpx.Response(
|
||||
status_code, content=body, headers={"x-request-id": "req-openai"}, request=sent_request
|
||||
)
|
||||
|
||||
with pytest.raises(mapped_error) as raised:
|
||||
exception_type(
|
||||
model="gpt-5.6",
|
||||
original_exception=openai.APIStatusError(message, response=upstream, body=json.loads(body)),
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
|
||||
error_response: Final = getattr(raised.value, "response")
|
||||
assert error_response.text == body
|
||||
assert error_response.headers["x-request-id"] == "req-openai"
|
||||
assert "sk-openai-secret" not in str(error_response.request.headers)
|
||||
assert error_response.request.content == b""
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import datetime
|
||||
import json
|
||||
from collections.abc import Awaitable, Callable, Mapping
|
||||
from typing import Final
|
||||
|
|
@ -12,6 +13,7 @@ import respx
|
|||
import litellm
|
||||
from litellm.exceptions import BadRequestError, ContentPolicyViolationError, ContextWindowExceededError
|
||||
from litellm.litellm_core_utils.exception_mapping_utils import exception_type
|
||||
from litellm.llms.azure.common_utils import AzureOpenAIError
|
||||
|
||||
|
||||
@respx.mock
|
||||
|
|
@ -632,3 +634,202 @@ def test_azure_content_policy_wins_over_context_length_code(monkeypatch: pytest.
|
|||
error: Final = _azure_chat_error(monkeypatch, _azure_error_response(400, policy_error))
|
||||
|
||||
assert type(error) is ContentPolicyViolationError
|
||||
|
||||
|
||||
_AZURE_REFUSAL_BODY: Final = '{"error":{"message":"azure refused","type":"upstream_refusal","code":"refused"}}'
|
||||
_AZURE_DEPLOYMENT_NOT_FOUND_BODY: Final = '{"error":{"code":"DeploymentNotFound","message":"no such deployment"}}'
|
||||
_AZURE_INTERNAL_ERROR_BODY: Final = '{"error":{"code":"InternalServerError","message":"Internal server error"}}'
|
||||
_AZURE_CONTENT_FILTER_BODY: Final = json.dumps(
|
||||
{
|
||||
"error": {
|
||||
"message": "The response was filtered due to the prompt triggering Azure OpenAI's content management policy.",
|
||||
"type": None,
|
||||
"param": "prompt",
|
||||
"code": "content_filter",
|
||||
"status": 400,
|
||||
"innererror": {
|
||||
"code": "ResponsibleAIPolicyViolation",
|
||||
"content_filter_result": {"violence": {"filtered": True, "severity": "high"}},
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
_AZURE_KEY: Final = "azure-provider-secret"
|
||||
_AZURE_BASE: Final = "https://azure-upstream.openai.azure.com"
|
||||
_AZURE_ARGS: Final = {"api_base": _AZURE_BASE, "api_version": "2024-10-21", "api_key": _AZURE_KEY, "max_retries": 0}
|
||||
|
||||
|
||||
async def _azure_completion() -> None:
|
||||
litellm.completion(model="azure/gpt-4.1-mini", messages=[{"role": "user", "content": "hi"}], **_AZURE_ARGS)
|
||||
|
||||
|
||||
async def _azure_completion_stream() -> None:
|
||||
list(
|
||||
litellm.completion(
|
||||
model="azure/gpt-4.1-mini", messages=[{"role": "user", "content": "hi"}], stream=True, **_AZURE_ARGS
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def _azure_acompletion() -> None:
|
||||
await litellm.acompletion(model="azure/gpt-4.1-mini", messages=[{"role": "user", "content": "hi"}], **_AZURE_ARGS)
|
||||
|
||||
|
||||
async def _azure_acompletion_stream() -> None:
|
||||
stream: Final = await litellm.acompletion(
|
||||
model="azure/gpt-4.1-mini", messages=[{"role": "user", "content": "hi"}], stream=True, **_AZURE_ARGS
|
||||
)
|
||||
async for _ in stream:
|
||||
pass
|
||||
|
||||
|
||||
async def _azure_embedding() -> None:
|
||||
litellm.embedding(model="azure/text-embedding-3-large", input=["hi"], **_AZURE_ARGS)
|
||||
|
||||
|
||||
async def _azure_aembedding() -> None:
|
||||
await litellm.aembedding(model="azure/text-embedding-3-large", input=["hi"], **_AZURE_ARGS)
|
||||
|
||||
|
||||
async def _azure_image_generation() -> None:
|
||||
litellm.image_generation(model="azure/gpt-image-2", prompt="a cat", **_AZURE_ARGS)
|
||||
|
||||
|
||||
async def _azure_text_completion() -> None:
|
||||
litellm.text_completion(model="azure_text/gpt-35-turbo-instruct", prompt="hi", **_AZURE_ARGS)
|
||||
|
||||
|
||||
async def _azure_text_completion_stream() -> None:
|
||||
list(litellm.text_completion(model="azure_text/gpt-35-turbo-instruct", prompt="hi", stream=True, **_AZURE_ARGS))
|
||||
|
||||
|
||||
async def _azure_atext_completion() -> None:
|
||||
await litellm.atext_completion(model="azure_text/gpt-35-turbo-instruct", prompt="hi", **_AZURE_ARGS)
|
||||
|
||||
|
||||
async def _azure_atext_completion_stream() -> None:
|
||||
stream: Final = await litellm.atext_completion(
|
||||
model="azure_text/gpt-35-turbo-instruct", prompt="hi", stream=True, **_AZURE_ARGS
|
||||
)
|
||||
async for _ in stream:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"route",
|
||||
(
|
||||
_azure_completion,
|
||||
_azure_completion_stream,
|
||||
_azure_acompletion,
|
||||
_azure_acompletion_stream,
|
||||
_azure_embedding,
|
||||
_azure_aembedding,
|
||||
_azure_image_generation,
|
||||
_azure_text_completion,
|
||||
_azure_text_completion_stream,
|
||||
_azure_atext_completion,
|
||||
_azure_atext_completion_stream,
|
||||
),
|
||||
ids=lambda route: route.__name__.removeprefix("_azure_"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("status_code", "body", "mapped_error"),
|
||||
(
|
||||
(400, _AZURE_REFUSAL_BODY, BadRequestError),
|
||||
(400, _AZURE_CONTENT_FILTER_BODY, BadRequestError),
|
||||
(403, _AZURE_REFUSAL_BODY, litellm.APIError),
|
||||
(404, _AZURE_DEPLOYMENT_NOT_FOUND_BODY, litellm.NotFoundError),
|
||||
(500, _AZURE_INTERNAL_ERROR_BODY, openai.APIError),
|
||||
(502, _AZURE_REFUSAL_BODY, litellm.BadGatewayError),
|
||||
(504, _AZURE_REFUSAL_BODY, litellm.Timeout),
|
||||
),
|
||||
)
|
||||
@respx.mock
|
||||
async def test_azure_error_keeps_the_upstream_body_without_the_api_key(
|
||||
route: Callable[[], Awaitable[None]],
|
||||
status_code: int,
|
||||
body: str,
|
||||
mapped_error: type[Exception],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
respx.post(url__regex=r"https://azure-upstream\.openai\.azure\.com/.*").mock(
|
||||
return_value=httpx.Response(status_code, content=body, headers={"content-type": "application/json"})
|
||||
)
|
||||
|
||||
with pytest.raises(mapped_error) as raised:
|
||||
await route()
|
||||
|
||||
error_response: Final = getattr(raised.value, "response")
|
||||
assert error_response.status_code == status_code
|
||||
assert error_response.text == body
|
||||
assert error_response.json() == json.loads(body)
|
||||
for exposed in (error_response.request, getattr(raised.value, "request")):
|
||||
assert _AZURE_KEY not in str(exposed.headers)
|
||||
assert exposed.content == b""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"route",
|
||||
(
|
||||
_azure_completion,
|
||||
_azure_completion_stream,
|
||||
_azure_acompletion,
|
||||
_azure_acompletion_stream,
|
||||
_azure_embedding,
|
||||
_azure_aembedding,
|
||||
_azure_text_completion,
|
||||
_azure_atext_completion,
|
||||
_azure_atext_completion_stream,
|
||||
),
|
||||
ids=lambda route: route.__name__.removeprefix("_azure_"),
|
||||
)
|
||||
@respx.mock
|
||||
async def test_azure_error_response_keeps_the_upstream_elapsed_time(
|
||||
route: Callable[[], Awaitable[None]], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
respx.post(url__regex=r"https://azure-upstream\.openai\.azure\.com/.*").mock(
|
||||
return_value=httpx.Response(400, content=_AZURE_REFUSAL_BODY, headers={"content-type": "application/json"})
|
||||
)
|
||||
|
||||
with pytest.raises(BadRequestError) as raised:
|
||||
await route()
|
||||
|
||||
assert raised.value.response.elapsed > datetime.timedelta(0)
|
||||
|
||||
|
||||
def test_azure_client_side_missing_key_error_keeps_the_empty_placeholder_response() -> None:
|
||||
missing_key: Final = openai.OpenAIError(
|
||||
"The api_key client option must be set either by passing api_key to the client or by setting the "
|
||||
"OPENAI_API_KEY environment variable"
|
||||
)
|
||||
|
||||
with pytest.raises(litellm.AuthenticationError) as raised:
|
||||
exception_type(model="azure/gpt-4.1-mini", original_exception=missing_key, custom_llm_provider="azure")
|
||||
|
||||
assert raised.value.response.status_code == 401
|
||||
assert raised.value.response.text == ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status_code", "message", "mapped_error"),
|
||||
(
|
||||
(500, "azure_client is not an instance of AzureOpenAI or OpenAI", litellm.APIError),
|
||||
(408, "the deployment did not answer in time", litellm.Timeout),
|
||||
(504, "the deployment did not answer in time", litellm.Timeout),
|
||||
),
|
||||
)
|
||||
def test_azure_error_litellm_raises_itself_has_no_upstream_response(
|
||||
status_code: int, message: str, mapped_error: type[Exception]
|
||||
) -> None:
|
||||
with pytest.raises(mapped_error) as raised:
|
||||
exception_type(
|
||||
model="azure/gpt-4.1-mini",
|
||||
original_exception=AzureOpenAIError(status_code=status_code, message=message),
|
||||
custom_llm_provider="azure",
|
||||
)
|
||||
|
||||
assert getattr(raised.value, "response", None) is None
|
||||
|
|
|
|||
|
|
@ -93,24 +93,34 @@ async def test_acompletion_forwards_client_headers_to_provider(
|
|||
assert request_headers["x-mycorp-llmcall-id"] == "abc-123"
|
||||
|
||||
|
||||
_REJECTION_BODY: Final = '{"error":{"message":"bad prompt","type":"invalid_request_error"}}'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rejected_completions_endpoint():
|
||||
return respx.post("https://api.openai.com/v1/completions").mock(
|
||||
return_value=Response(
|
||||
400,
|
||||
json={"error": {"message": "bad prompt", "type": "invalid_request_error"}},
|
||||
headers={"x-request-id": "req-1"},
|
||||
content=_REJECTION_BODY,
|
||||
headers={"x-request-id": "req-1", "content-type": "application/json"},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _assert_carries_the_upstream_rejection(rejected: litellm.BadRequestError) -> None:
|
||||
assert rejected.status_code == 400
|
||||
assert rejected.litellm_response_headers["x-request-id"] == "req-1"
|
||||
assert rejected.response.text == _REJECTION_BODY
|
||||
assert rejected.response.json() == json.loads(_REJECTION_BODY)
|
||||
assert rejected.response.headers["x-request-id"] == "req-1"
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_completion_surfaces_the_status_and_headers_of_a_rejected_request(rejected_completions_endpoint):
|
||||
with pytest.raises(litellm.BadRequestError) as rejected:
|
||||
text_completion(model="gpt-3.5-turbo-instruct", prompt="hello")
|
||||
|
||||
assert rejected.value.status_code == 400
|
||||
assert rejected.value.litellm_response_headers["x-request-id"] == "req-1"
|
||||
_assert_carries_the_upstream_rejection(rejected.value)
|
||||
|
||||
|
||||
@respx.mock
|
||||
|
|
@ -118,8 +128,7 @@ def test_streaming_completion_surfaces_the_status_and_headers_of_a_rejected_requ
|
|||
with pytest.raises(litellm.BadRequestError) as rejected:
|
||||
list(text_completion(model="gpt-3.5-turbo-instruct", prompt="hello", stream=True))
|
||||
|
||||
assert rejected.value.status_code == 400
|
||||
assert rejected.value.litellm_response_headers["x-request-id"] == "req-1"
|
||||
_assert_carries_the_upstream_rejection(rejected.value)
|
||||
|
||||
|
||||
@respx.mock
|
||||
|
|
@ -130,8 +139,7 @@ async def test_acompletion_surfaces_the_status_and_headers_of_a_rejected_request
|
|||
with pytest.raises(litellm.BadRequestError) as rejected:
|
||||
await atext_completion(model="gpt-3.5-turbo-instruct", prompt="hello")
|
||||
|
||||
assert rejected.value.status_code == 400
|
||||
assert rejected.value.litellm_response_headers["x-request-id"] == "req-1"
|
||||
_assert_carries_the_upstream_rejection(rejected.value)
|
||||
|
||||
|
||||
@respx.mock
|
||||
|
|
|
|||
0
tests/unit/llms/openai/image_variations/__init__.py
Normal file
0
tests/unit/llms/openai/image_variations/__init__.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import json
|
||||
from io import BytesIO
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.llms.openai.common_utils import OpenAIError
|
||||
|
||||
_UPSTREAM_ERROR_BODY: Final = '{"error":{"message":"image is not a valid PNG","type":"invalid_request_error"}}'
|
||||
|
||||
|
||||
def _upstream_rejection(_request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
400,
|
||||
content=_UPSTREAM_ERROR_BODY,
|
||||
headers={"x-request-id": "req_variation_400", "content-type": "application/json"},
|
||||
)
|
||||
|
||||
|
||||
_VARIATION_KWARGS: Final = {
|
||||
"custom_llm_provider": "openai",
|
||||
"api_key": "transport-only",
|
||||
"api_base": "https://upstream.example/v1",
|
||||
"max_retries": 0,
|
||||
}
|
||||
|
||||
|
||||
async def _request_variation(is_async: bool) -> None:
|
||||
if is_async:
|
||||
await litellm.aimage_variation(image=BytesIO(b"image-bytes"), **_VARIATION_KWARGS)
|
||||
return
|
||||
litellm.image_variation(image=BytesIO(b"image-bytes"), **_VARIATION_KWARGS)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("is_async", (False, True), ids=("sync", "async"))
|
||||
async def test_rejected_image_variation_carries_the_upstream_error_response(
|
||||
is_async: bool, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
transport: Final = httpx.MockTransport(_upstream_rejection)
|
||||
monkeypatch.setattr(litellm, "client_session", httpx.Client(transport=transport))
|
||||
monkeypatch.setattr(litellm, "aclient_session", httpx.AsyncClient(transport=transport))
|
||||
|
||||
with pytest.raises((litellm.BadRequestError, OpenAIError)) as raised:
|
||||
await _request_variation(is_async)
|
||||
|
||||
assert raised.value.response.text == _UPSTREAM_ERROR_BODY
|
||||
assert raised.value.response.json() == json.loads(_UPSTREAM_ERROR_BODY)
|
||||
assert raised.value.response.headers["x-request-id"] == "req_variation_400"
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import asyncio
|
||||
import gzip
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
from collections.abc import Callable, Mapping
|
||||
from collections.abc import Awaitable, Callable, Mapping
|
||||
from types import ModuleType
|
||||
from typing import Final, Literal, Protocol
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
|
@ -375,6 +376,277 @@ async def test_acompletion_preserves_public_errors_for_both_http_clients(
|
|||
assert not http_client.is_closed
|
||||
|
||||
|
||||
_UPSTREAM_ERROR_BODY: Final = (
|
||||
'{"error":{"message":"upstream rejected the request","type":"invalid_request_error",'
|
||||
'"param":"response_format.type","code":"invalid_value"}}'
|
||||
)
|
||||
_UPSTREAM_BASE: Final = "https://upstream.example/v1"
|
||||
_UPSTREAM_TRANSPORT: Final = httpx.MockTransport(
|
||||
lambda _request: httpx.Response(
|
||||
400,
|
||||
content=_UPSTREAM_ERROR_BODY,
|
||||
headers={"x-request-id": "req_upstream_400", "x-upstream-trace": "trace-7", "content-type": "application/json"},
|
||||
)
|
||||
)
|
||||
_MESSAGES: Final = [{"role": "user", "content": "hi"}]
|
||||
|
||||
|
||||
def _rejecting_client() -> OpenAI:
|
||||
return OpenAI(
|
||||
api_key="transport-only",
|
||||
base_url=_UPSTREAM_BASE,
|
||||
max_retries=0,
|
||||
http_client=httpx.Client(transport=_UPSTREAM_TRANSPORT),
|
||||
)
|
||||
|
||||
|
||||
def _rejecting_async_client() -> AsyncOpenAI:
|
||||
return AsyncOpenAI(
|
||||
api_key="transport-only",
|
||||
base_url=_UPSTREAM_BASE,
|
||||
max_retries=0,
|
||||
http_client=httpx.AsyncClient(transport=_UPSTREAM_TRANSPORT),
|
||||
)
|
||||
|
||||
|
||||
def _upstream(provider: str, model: str) -> dict[str, object]:
|
||||
return {"model": f"{provider}/{model}", "api_base": _UPSTREAM_BASE, "api_key": "transport-only", "num_retries": 0}
|
||||
|
||||
|
||||
async def _completion(provider: str) -> None:
|
||||
litellm.completion(messages=_MESSAGES, client=_rejecting_client(), **_upstream(provider, "gpt-5.6"))
|
||||
|
||||
|
||||
async def _completion_stream(provider: str) -> None:
|
||||
stream: Final = litellm.completion(
|
||||
messages=_MESSAGES, client=_rejecting_client(), stream=True, **_upstream(provider, "gpt-5.6")
|
||||
)
|
||||
list(stream)
|
||||
|
||||
|
||||
async def _acompletion(provider: str) -> None:
|
||||
await litellm.acompletion(messages=_MESSAGES, client=_rejecting_async_client(), **_upstream(provider, "gpt-5.6"))
|
||||
|
||||
|
||||
async def _acompletion_stream(provider: str) -> None:
|
||||
stream: Final = await litellm.acompletion(
|
||||
messages=_MESSAGES, client=_rejecting_async_client(), stream=True, **_upstream(provider, "gpt-5.6")
|
||||
)
|
||||
async for _ in stream:
|
||||
pass
|
||||
|
||||
|
||||
async def _embedding(provider: str) -> None:
|
||||
litellm.embedding(input=["hi"], client=_rejecting_client(), **_upstream(provider, "text-embedding-3-large"))
|
||||
|
||||
|
||||
async def _aembedding(provider: str) -> None:
|
||||
await litellm.aembedding(
|
||||
input=["hi"], client=_rejecting_async_client(), **_upstream(provider, "text-embedding-3-large")
|
||||
)
|
||||
|
||||
|
||||
async def _image_generation(provider: str) -> None:
|
||||
litellm.image_generation(prompt="a cat", client=_rejecting_client(), **_upstream(provider, "gpt-image-2"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("provider", ("openai", "litellm_proxy"))
|
||||
@pytest.mark.parametrize(
|
||||
"route",
|
||||
(_completion, _completion_stream, _acompletion, _acompletion_stream, _embedding, _aembedding, _image_generation),
|
||||
ids=lambda route: route.__name__.lstrip("_"),
|
||||
)
|
||||
async def test_mapped_error_carries_the_upstream_error_response(
|
||||
provider: str, route: Callable[[str], Awaitable[None]]
|
||||
) -> None:
|
||||
with pytest.raises(litellm.BadRequestError) as raised:
|
||||
await route(provider)
|
||||
|
||||
assert raised.value.response.text == _UPSTREAM_ERROR_BODY
|
||||
assert raised.value.response.json() == json.loads(_UPSTREAM_ERROR_BODY)
|
||||
assert raised.value.response.headers["x-request-id"] == "req_upstream_400"
|
||||
assert raised.value.response.headers["x-upstream-trace"] == "trace-7"
|
||||
|
||||
|
||||
_UPSTREAM_SERVER_ERROR_BODY: Final = (
|
||||
'{"error":{"message":"The server had an error while processing your request.","type":"server_error",'
|
||||
'"param":null,"code":null}}'
|
||||
)
|
||||
_CHUNKED_UPSTREAM_HEADERS: Final = {
|
||||
"content-type": "application/json",
|
||||
"transfer-encoding": "chunked",
|
||||
"x-request-id": "req_upstream_5xx",
|
||||
}
|
||||
|
||||
|
||||
def _chunked_upstream_client(status_code: int, content_encoding: Literal["gzip"] | None) -> httpx.AsyncClient:
|
||||
body: Final = _UPSTREAM_SERVER_ERROR_BODY.encode()
|
||||
encoding_headers: Final = {"content-encoding": content_encoding} if content_encoding else {}
|
||||
return httpx.AsyncClient(
|
||||
transport=httpx.MockTransport(
|
||||
lambda _request: httpx.Response(
|
||||
status_code,
|
||||
headers={**_CHUNKED_UPSTREAM_HEADERS, **encoding_headers},
|
||||
stream=httpx.ByteStream(gzip.compress(body) if content_encoding else body),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("provider", ("openai", "litellm_proxy"))
|
||||
@pytest.mark.parametrize("stream", (False, True))
|
||||
@pytest.mark.parametrize(
|
||||
("status_code", "mapped_error"),
|
||||
(
|
||||
(429, litellm.RateLimitError),
|
||||
(500, litellm.InternalServerError),
|
||||
(502, litellm.BadGatewayError),
|
||||
(503, litellm.ServiceUnavailableError),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("content_encoding", (None, "gzip"))
|
||||
async def test_retryable_error_keeps_the_decoded_upstream_body_and_its_headers(
|
||||
provider: str,
|
||||
stream: bool,
|
||||
status_code: int,
|
||||
mapped_error: type[Exception],
|
||||
content_encoding: Literal["gzip"] | None,
|
||||
) -> None:
|
||||
client: Final = AsyncOpenAI(
|
||||
api_key="transport-only",
|
||||
base_url=_UPSTREAM_BASE,
|
||||
max_retries=0,
|
||||
http_client=_chunked_upstream_client(status_code, content_encoding),
|
||||
)
|
||||
|
||||
with pytest.raises(mapped_error) as raised:
|
||||
await litellm.acompletion(messages=_MESSAGES, client=client, stream=stream, **_upstream(provider, "gpt-5.6"))
|
||||
|
||||
error_response: Final = getattr(raised.value, "response")
|
||||
assert error_response.status_code == status_code
|
||||
assert error_response.text == _UPSTREAM_SERVER_ERROR_BODY
|
||||
assert error_response.json() == json.loads(_UPSTREAM_SERVER_ERROR_BODY)
|
||||
assert dict(error_response.headers) == {
|
||||
**_CHUNKED_UPSTREAM_HEADERS,
|
||||
**({"content-encoding": content_encoding} if content_encoding else {}),
|
||||
}
|
||||
|
||||
|
||||
_REFUSAL_BODY: Final = '{"error":{"message":"upstream refused","type":"upstream_refusal","param":null,"code":null}}'
|
||||
_GATEWAY_TIMEOUT_BODY: Final = (
|
||||
'{"error":{"message":"litellm.Timeout: APITimeoutError - Request timed out.","type":"invalid_request_error",'
|
||||
'"param":null,"code":"408"}}'
|
||||
)
|
||||
_PROVIDER_KEY: Final = "sk-provider-secret"
|
||||
_QUERY_SECRET: Final = "query-secret"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ("openai", "litellm_proxy"))
|
||||
@pytest.mark.parametrize(
|
||||
("status_code", "mapped_error", "body"),
|
||||
(
|
||||
(400, litellm.BadRequestError, _REFUSAL_BODY),
|
||||
(403, litellm.APIError, _REFUSAL_BODY),
|
||||
(408, litellm.Timeout, _REFUSAL_BODY),
|
||||
(408, litellm.Timeout, _GATEWAY_TIMEOUT_BODY),
|
||||
(409, litellm.APIError, _REFUSAL_BODY),
|
||||
(413, litellm.APIError, _REFUSAL_BODY),
|
||||
(429, litellm.RateLimitError, _REFUSAL_BODY),
|
||||
(500, litellm.InternalServerError, _REFUSAL_BODY),
|
||||
(504, litellm.Timeout, _REFUSAL_BODY),
|
||||
),
|
||||
)
|
||||
def test_mapped_error_keeps_the_upstream_body_without_the_provider_credentials(
|
||||
provider: str, status_code: int, mapped_error: type[Exception], body: str
|
||||
) -> None:
|
||||
client: Final = OpenAI(
|
||||
api_key=_PROVIDER_KEY,
|
||||
base_url=_UPSTREAM_BASE,
|
||||
default_query={"key": _QUERY_SECRET},
|
||||
max_retries=0,
|
||||
http_client=httpx.Client(
|
||||
transport=httpx.MockTransport(
|
||||
lambda _request: httpx.Response(
|
||||
status_code, content=body, headers={"content-type": "application/json"}
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(mapped_error) as raised:
|
||||
litellm.completion(
|
||||
model=f"{provider}/gpt-5.6",
|
||||
messages=_MESSAGES,
|
||||
client=client,
|
||||
api_base=_UPSTREAM_BASE,
|
||||
api_key=_PROVIDER_KEY,
|
||||
num_retries=0,
|
||||
)
|
||||
|
||||
error_response: Final = getattr(raised.value, "response")
|
||||
assert error_response.status_code == status_code
|
||||
assert error_response.text == body
|
||||
assert error_response.json() == json.loads(body)
|
||||
assert str(error_response.request.url) == f"{_UPSTREAM_BASE}/chat/completions"
|
||||
for exposed in (error_response.request, getattr(raised.value, "request")):
|
||||
assert _QUERY_SECRET not in str(exposed.url)
|
||||
assert _PROVIDER_KEY not in str(exposed.headers)
|
||||
assert exposed.content == b""
|
||||
|
||||
|
||||
def _time_out(request: httpx.Request) -> httpx.Response:
|
||||
raise httpx.ReadTimeout("no reply", request=request)
|
||||
|
||||
|
||||
async def _sync_completion_timing_out(provider: str) -> None:
|
||||
litellm.completion(
|
||||
model=f"{provider}/gpt-5.6",
|
||||
messages=_MESSAGES,
|
||||
client=OpenAI(
|
||||
api_key=_PROVIDER_KEY,
|
||||
base_url=_UPSTREAM_BASE,
|
||||
max_retries=0,
|
||||
http_client=httpx.Client(transport=httpx.MockTransport(_time_out)),
|
||||
),
|
||||
api_base=_UPSTREAM_BASE,
|
||||
api_key=_PROVIDER_KEY,
|
||||
num_retries=0,
|
||||
)
|
||||
|
||||
|
||||
async def _async_stream_timing_out(provider: str) -> None:
|
||||
stream: Final = await litellm.acompletion(
|
||||
model=f"{provider}/gpt-5.6",
|
||||
messages=_MESSAGES,
|
||||
stream=True,
|
||||
client=AsyncOpenAI(
|
||||
api_key=_PROVIDER_KEY,
|
||||
base_url=_UPSTREAM_BASE,
|
||||
max_retries=0,
|
||||
http_client=httpx.AsyncClient(transport=httpx.MockTransport(_time_out)),
|
||||
),
|
||||
api_base=_UPSTREAM_BASE,
|
||||
api_key=_PROVIDER_KEY,
|
||||
num_retries=0,
|
||||
)
|
||||
async for _ in stream:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("provider", ("openai", "litellm_proxy"))
|
||||
@pytest.mark.parametrize("call", (_sync_completion_timing_out, _async_stream_timing_out))
|
||||
async def test_timeout_before_any_reply_has_no_upstream_response(
|
||||
provider: str, call: Callable[[str], Awaitable[None]]
|
||||
) -> None:
|
||||
with pytest.raises(litellm.Timeout) as raised:
|
||||
await call(provider)
|
||||
|
||||
assert getattr(raised.value, "response", None) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acompletion_streams_text_deltas_over_injected_transport(
|
||||
http_backend: ModuleType, sdk_provider: str, monkeypatch: pytest.MonkeyPatch
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import asyncio
|
|||
import contextvars
|
||||
import copy
|
||||
import datetime
|
||||
import gzip
|
||||
import json
|
||||
from collections.abc import AsyncIterable, Mapping
|
||||
from types import MappingProxyType, SimpleNamespace
|
||||
|
|
@ -13,6 +14,7 @@ from tests.unit.proxy.router_entry_point_stubs import route_entry_points_to_stub
|
|||
|
||||
import anthropic
|
||||
import httpx
|
||||
import openai
|
||||
import pytest
|
||||
import respx
|
||||
from fastapi import HTTPException, Request, Response, status
|
||||
|
|
@ -11675,6 +11677,67 @@ async def test_handle_llm_api_exception_forwards_litellm_response_headers_when_r
|
|||
assert exc_info.value.headers["llm_provider-x-request-id"] == "req_openai_400"
|
||||
|
||||
|
||||
_UPSTREAM_REJECTION_BODY: Final = b'{"error":{"message":"upstream said no","type":"upstream_rejection"}}'
|
||||
|
||||
|
||||
def _upstream_rejection_wire(content_encoding: Literal["gzip"] | None) -> tuple[bytes, dict[str, str]]:
|
||||
wire_body: Final = gzip.compress(_UPSTREAM_REJECTION_BODY) if content_encoding else _UPSTREAM_REJECTION_BODY
|
||||
return wire_body, {
|
||||
"x-request-id": "req_upstream",
|
||||
"x-upstream-trace": "trace-7",
|
||||
"content-type": "application/json",
|
||||
"content-length": str(len(wire_body)),
|
||||
**({"content-encoding": content_encoding} if content_encoding else {}),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("status_code", "mapped_error"),
|
||||
(
|
||||
(400, litellm.BadRequestError),
|
||||
(429, litellm.RateLimitError),
|
||||
(503, litellm.ServiceUnavailableError),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("content_encoding", (None, "gzip"))
|
||||
async def test_handle_llm_api_exception_forwards_exactly_the_upstream_headers_of_a_mapped_openai_error(
|
||||
status_code: int, mapped_error: type[Exception], content_encoding: Literal["gzip"] | None
|
||||
) -> None:
|
||||
wire_body, upstream_headers = _upstream_rejection_wire(content_encoding)
|
||||
transport: Final = httpx.MockTransport(
|
||||
lambda _request: httpx.Response(status_code, content=wire_body, headers=upstream_headers)
|
||||
)
|
||||
with pytest.raises(mapped_error) as mapped:
|
||||
await litellm.acompletion(
|
||||
model="openai/gpt-5.6",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
num_retries=0,
|
||||
client=openai.AsyncOpenAI(
|
||||
api_key="transport-only",
|
||||
base_url="https://upstream.example/v1",
|
||||
max_retries=0,
|
||||
http_client=httpx.AsyncClient(transport=transport),
|
||||
),
|
||||
)
|
||||
proxy_logging_obj: Final = MagicMock()
|
||||
proxy_logging_obj.post_call_failure_hook = AsyncMock(return_value=None)
|
||||
proxy_logging_obj.post_call_response_headers_hook = AsyncMock(return_value={})
|
||||
|
||||
with pytest.raises(ProxyException) as forwarded:
|
||||
await ProxyBaseLLMRequestProcessing(data={}).handle_llm_api_exception(
|
||||
e=mapped.value,
|
||||
user_api_key_dict=ProxyUserAPIKeyAuth(api_key="sk-test"),
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
)
|
||||
|
||||
assert forwarded.value.code == str(status_code)
|
||||
assert "upstream said no" in forwarded.value.message
|
||||
assert {name: value for name, value in forwarded.value.headers.items() if name.startswith("llm_provider-")} == {
|
||||
f"llm_provider-{name}": value for name, value in upstream_headers.items()
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_llm_api_exception_logs_bug_report_for_unmapped_error(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from typing import Final
|
|||
import httpx
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.exceptions import (
|
||||
BadRequestError,
|
||||
ContentPolicyViolationError,
|
||||
|
|
@ -418,3 +419,21 @@ class TestBedrockErrorHeaders:
|
|||
headers = get_response_headers(dict(_response_headers))
|
||||
|
||||
assert headers.get("llm_provider-x-amzn-requestid") == "req-proxy-500"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error_class",
|
||||
(RateLimitError, ServiceUnavailableError, litellm.BadGatewayError, litellm.InternalServerError),
|
||||
)
|
||||
def test_error_built_from_an_unread_streaming_response_keeps_its_headers(error_class: type[Exception]) -> None:
|
||||
unread: Final = httpx.Response(
|
||||
500,
|
||||
headers={"x-request-id": "req-unread"},
|
||||
stream=httpx.ByteStream(b'{"error":"never read"}'),
|
||||
)
|
||||
|
||||
error: Final = error_class(message="upstream failed", llm_provider="openai", model="gpt-5.6", response=unread)
|
||||
|
||||
error_response: Final = getattr(error, "response")
|
||||
assert error_response.text == ""
|
||||
assert dict(error_response.headers) == {"x-request-id": "req-unread"}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue