mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-06 07:48:06 +00:00
fix(decisions): preserve upstream error body and response on decisions exception mapping (fixes #48539)
Signed-off-by: gauravatwork117 <gauravatwork117@gmail.com>
This commit is contained in:
parent
7abff3dd63
commit
7625eaf3cd
3 changed files with 47 additions and 3 deletions
|
|
@ -2328,6 +2328,7 @@ def _map_exception_by_status(
|
|||
return
|
||||
message: Final = f"{exception_provider} - {error_str}"
|
||||
response: Final = original_exception.response if hasattr(original_exception, "response") else None
|
||||
body: Final = getattr(original_exception, "body", None)
|
||||
match status_code:
|
||||
case 401:
|
||||
raise AuthenticationError(
|
||||
|
|
@ -2366,6 +2367,7 @@ def _map_exception_by_status(
|
|||
model=model,
|
||||
llm_provider=custom_llm_provider,
|
||||
response=response,
|
||||
body=body,
|
||||
litellm_debug_info=extra_information,
|
||||
)
|
||||
case 500:
|
||||
|
|
@ -2414,6 +2416,7 @@ def _map_exception_by_status(
|
|||
model=model,
|
||||
llm_provider=custom_llm_provider,
|
||||
response=response,
|
||||
body=body,
|
||||
litellm_debug_info=extra_information,
|
||||
)
|
||||
case _:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import inspect
|
|||
import json
|
||||
import ssl
|
||||
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Coroutine, Iterator, Mapping, Sequence
|
||||
from contextlib import asynccontextmanager
|
||||
from contextlib import asynccontextmanager, suppress
|
||||
from functools import lru_cache, partial, reduce
|
||||
from itertools import chain
|
||||
from types import MappingProxyType
|
||||
|
|
@ -573,6 +573,14 @@ def _unwrap_provider_error(error: Exception) -> Exception:
|
|||
return error
|
||||
|
||||
|
||||
def _parse_error_body(text: object) -> object | None:
|
||||
if not isinstance(text, str) or not text.strip().startswith(("{", "[")):
|
||||
return None
|
||||
with suppress(json.JSONDecodeError, ValueError):
|
||||
return json.loads(text)
|
||||
return None
|
||||
|
||||
|
||||
def _merge_messages_header_sources(*sources: Mapping[str, object] | None) -> Mapping[str, object]:
|
||||
"""Later sources win per header, except ``anthropic-beta``, which unions every source so client betas
|
||||
never drop a deployment's configured ones."""
|
||||
|
|
@ -6689,11 +6697,15 @@ class BaseLLMHTTPHandler:
|
|||
else:
|
||||
error_headers = {}
|
||||
|
||||
parsed_body: Final = _parse_error_body(error_text)
|
||||
|
||||
if provider_config is None:
|
||||
raise BaseLLMException(
|
||||
status_code=status_code,
|
||||
message=error_text,
|
||||
headers=error_headers,
|
||||
response=error_response if isinstance(error_response, httpx.Response) else None,
|
||||
body=parsed_body,
|
||||
status_code_is_synthesized=not isinstance(received_status_code, int),
|
||||
)
|
||||
|
||||
|
|
@ -6702,6 +6714,11 @@ class BaseLLMHTTPHandler:
|
|||
status_code=status_code,
|
||||
headers=error_headers,
|
||||
)
|
||||
if isinstance(provider_error, BaseLLMException):
|
||||
if isinstance(error_response, httpx.Response):
|
||||
provider_error.response = error_response
|
||||
if isinstance(parsed_body, dict) and provider_error.body is None:
|
||||
provider_error.body = parsed_body
|
||||
if not isinstance(received_status_code, int):
|
||||
provider_error.status_code_is_synthesized = True
|
||||
raise provider_error
|
||||
|
|
|
|||
|
|
@ -761,18 +761,42 @@ async def test_invalid_question_is_rejected_before_http(respx_mock: respx.MockRo
|
|||
|
||||
|
||||
def test_upstream_bad_request_maps_to_litellm_error(respx_mock: respx.MockRouter) -> None:
|
||||
error_payload: Final = {"error": {"message": "invalid decision"}}
|
||||
respx_mock.post("https://api.perplexity.ai/v1/decisions").respond(
|
||||
status_code=400,
|
||||
json={"error": {"message": "invalid decision"}},
|
||||
json=error_payload,
|
||||
)
|
||||
|
||||
with pytest.raises(litellm.BadRequestError):
|
||||
with pytest.raises(litellm.BadRequestError) as exc_info:
|
||||
litellm.systemone(
|
||||
model="perplexity/pplx-decider-v1-27b",
|
||||
state="review",
|
||||
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
|
||||
api_key="caller-key",
|
||||
)
|
||||
assert exc_info.value.body == error_payload
|
||||
assert exc_info.value.response is not None
|
||||
assert json.loads(exc_info.value.response.text) == error_payload
|
||||
|
||||
|
||||
async def test_decisions_upstream_error_preserves_body_and_response(respx_mock: respx.MockRouter) -> None:
|
||||
error_payload: Final = {"detail": {"error_type": "api_usage_error", "message": "Unknown model: jev-9"}}
|
||||
respx_mock.post("https://api.typesafe.ai/v1/systemone").respond(
|
||||
status_code=400,
|
||||
json=error_payload,
|
||||
)
|
||||
|
||||
with pytest.raises(litellm.BadRequestError) as exc_info:
|
||||
await litellm.asystemone(
|
||||
model="typesafe/jev-1.13.0",
|
||||
state={"finding": "x"},
|
||||
questions={"exp": {"type": "noul", "instructions": "Is it exploitable?"}},
|
||||
api_key="sk-test",
|
||||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert exc_info.value.body == error_payload
|
||||
assert exc_info.value.response is not None
|
||||
assert json.loads(exc_info.value.response.text) == error_payload
|
||||
|
||||
|
||||
def test_unreachable_upstream_maps_to_a_connection_error(respx_mock: respx.MockRouter) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue