mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-15 09:12:18 +00:00
Merge c780fef1c2 into 7f1b0a4633
This commit is contained in:
commit
7c54b2b4fb
2 changed files with 200 additions and 1 deletions
|
|
@ -279,6 +279,54 @@ class _ProviderHTTPException(Protocol):
|
|||
_ERROR_BODY_ADAPTER: Final = TypeAdapter(dict[str, object])
|
||||
|
||||
|
||||
def _body_request_id(raw_body: object) -> str | None:
|
||||
try:
|
||||
body: Final = _ERROR_BODY_ADAPTER.validate_python(raw_body)
|
||||
except ValidationError:
|
||||
return None
|
||||
|
||||
body_request_id: Final = body.get("request_id")
|
||||
if isinstance(body_request_id, str) and body_request_id:
|
||||
return body_request_id
|
||||
|
||||
nested_error: Final = as_str_mapping(body.get("error"))
|
||||
if nested_error is None:
|
||||
return None
|
||||
nested_request_id: Final = nested_error.get("request_id")
|
||||
return nested_request_id if isinstance(nested_request_id, str) and nested_request_id else None
|
||||
|
||||
|
||||
def _get_error_request_id(error_obj: _ProviderHTTPException) -> str | None:
|
||||
body_request_id: Final = _body_request_id(attribute_or_none(error_obj, "body"))
|
||||
if body_request_id is not None:
|
||||
return body_request_id
|
||||
|
||||
exception_request_id: Final = attribute_or_none(error_obj, "request_id")
|
||||
if isinstance(exception_request_id, str) and exception_request_id:
|
||||
return exception_request_id
|
||||
|
||||
response: Final = attribute_or_none(error_obj, "response")
|
||||
if not isinstance(response, httpx.Response):
|
||||
return None
|
||||
try:
|
||||
response_body: Final = _ERROR_BODY_ADAPTER.validate_json(response.content)
|
||||
except (ValidationError, httpx.ResponseNotRead):
|
||||
return None
|
||||
return _body_request_id(response_body)
|
||||
|
||||
|
||||
def _get_error_message_with_request_id(error_obj: _ProviderHTTPException) -> str | None:
|
||||
extracted_message: Final = get_error_message(error_obj=error_obj)
|
||||
if extracted_message is None:
|
||||
return None
|
||||
|
||||
request_id: Final = _get_error_request_id(error_obj)
|
||||
if request_id is not None and request_id not in extracted_message:
|
||||
return f"{extracted_message} (request_id: {request_id})"
|
||||
|
||||
return extracted_message
|
||||
|
||||
|
||||
def _openai_cyber_policy_body(original_exception: _ProviderHTTPException) -> Mapping[str, object] | None:
|
||||
raw_body: Final[object] = getattr(original_exception, "body", None)
|
||||
try:
|
||||
|
|
@ -341,7 +389,7 @@ def _map_openai_exception(
|
|||
body=dict(cyber_policy_body),
|
||||
)
|
||||
# custom_llm_provider is openai, make it OpenAI
|
||||
message = get_error_message(error_obj=original_exception)
|
||||
message = _get_error_message_with_request_id(error_obj=original_exception)
|
||||
if message is None:
|
||||
if hasattr(original_exception, "message"):
|
||||
message = original_exception.message
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import openai
|
|||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from openai import AsyncOpenAI, OpenAI
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
import litellm
|
||||
from litellm import completion
|
||||
|
|
@ -16,6 +17,7 @@ from litellm.exceptions import GuardrailRaisedException
|
|||
from litellm.litellm_core_utils.exception_mapping_utils import (
|
||||
ExceptionCheckers,
|
||||
_get_body_error_code,
|
||||
_get_error_message_with_request_id,
|
||||
_get_response_headers,
|
||||
exception_type,
|
||||
extract_and_raise_litellm_exception,
|
||||
|
|
@ -576,6 +578,155 @@ def test_openai_compatible_429_still_maps_to_rate_limit():
|
|||
assert excinfo.value.status_code == 429
|
||||
|
||||
|
||||
class _FakeErrorObj:
|
||||
"""Minimal stand-in for an SDK exception carrying a parsed error body."""
|
||||
|
||||
def __init__(self, body=None, request_id=None):
|
||||
self.body = body
|
||||
if request_id is not None:
|
||||
self.request_id = request_id
|
||||
|
||||
|
||||
def test_error_message_with_request_id_appends_top_level_request_id():
|
||||
"""DashScope/Bailian returns `request_id` as a top-level sibling of
|
||||
`error` in the response body. Extracting only `error.message` dropped it
|
||||
from the final error text (issue #47839)."""
|
||||
error_obj = _FakeErrorObj(
|
||||
body={
|
||||
"error": {
|
||||
"message": "Incorrect API key provided.",
|
||||
"type": "invalid_request_error",
|
||||
"param": None,
|
||||
"code": "invalid_api_key",
|
||||
},
|
||||
"request_id": "343604ce-3795-9363-8be6-f4aa3f4613ee",
|
||||
}
|
||||
)
|
||||
|
||||
assert _get_error_message_with_request_id(error_obj=error_obj) == (
|
||||
"Incorrect API key provided. (request_id: 343604ce-3795-9363-8be6-f4aa3f4613ee)"
|
||||
)
|
||||
|
||||
|
||||
def test_error_message_with_request_id_appends_request_id_for_flat_body():
|
||||
error_obj = _FakeErrorObj(body={"message": "Something went wrong", "request_id": "req_flat_123"})
|
||||
|
||||
assert _get_error_message_with_request_id(error_obj=error_obj) == "Something went wrong (request_id: req_flat_123)"
|
||||
|
||||
|
||||
def test_error_message_with_request_id_appends_nested_error_request_id():
|
||||
error_obj = _FakeErrorObj(body={"error": {"message": "Bad request", "request_id": "req_nested_456"}})
|
||||
|
||||
assert _get_error_message_with_request_id(error_obj=error_obj) == "Bad request (request_id: req_nested_456)"
|
||||
|
||||
|
||||
def test_error_message_with_request_id_with_non_dict_body_returns_none():
|
||||
error_obj = _FakeErrorObj(body="not-a-dict")
|
||||
|
||||
assert _get_error_message_with_request_id(error_obj=error_obj) is None
|
||||
|
||||
|
||||
def test_error_message_with_request_id_without_request_id_is_unchanged():
|
||||
error_obj = _FakeErrorObj(body={"error": {"message": "Plain error", "code": "some_code"}})
|
||||
|
||||
assert _get_error_message_with_request_id(error_obj=error_obj) == "Plain error"
|
||||
|
||||
|
||||
def test_error_message_with_request_id_does_not_duplicate_request_id():
|
||||
error_obj = _FakeErrorObj(body={"message": "Failed (request_id: req_dup_1)", "request_id": "req_dup_1"})
|
||||
|
||||
assert _get_error_message_with_request_id(error_obj=error_obj) == "Failed (request_id: req_dup_1)"
|
||||
|
||||
|
||||
def test_error_message_includes_the_exception_request_id() -> None:
|
||||
request_id: Final = "provider-request-from-exception"
|
||||
original_exception: Final = _FakeErrorObj(body={"message": "Rejected request"}, request_id=request_id)
|
||||
|
||||
assert _get_error_message_with_request_id(original_exception) == f"Rejected request (request_id: {request_id})"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("response_content", (b"not-json", b"[]", b'{"request_id": 42}', None))
|
||||
def test_error_message_survives_a_response_without_a_readable_request_id(response_content: bytes | None) -> None:
|
||||
request: Final = httpx.Request("POST", "https://provider.example/v1/chat/completions")
|
||||
response: Final = (
|
||||
httpx.Response(401, request=request, content=response_content)
|
||||
if response_content is not None
|
||||
else httpx.Response(401, request=request, stream=httpx.ByteStream(b"unread"))
|
||||
)
|
||||
original_exception: Final = OpenAIError(
|
||||
status_code=401, message="Rejected request", body={"message": "Rejected request"}, response=response
|
||||
)
|
||||
|
||||
assert _get_error_message_with_request_id(original_exception) == "Rejected request"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("include_request_id_header", (False, True))
|
||||
def test_dashscope_auth_error_keeps_request_id_from_the_sdk_response(include_request_id_header: bool) -> None:
|
||||
request_id: Final = "provider-request-from-response"
|
||||
message: Final = "Incorrect API key provided."
|
||||
provider_body: Final = {"error": {"message": message, "type": "invalid_request_error"}, "request_id": request_id}
|
||||
|
||||
def reject_request(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
401,
|
||||
request=request,
|
||||
json=provider_body,
|
||||
headers={"x-request-id": request_id} if include_request_id_header else {},
|
||||
)
|
||||
|
||||
with (
|
||||
httpx.Client(transport=httpx.MockTransport(reject_request)) as http_client,
|
||||
OpenAI(api_key="unit-test-credential", http_client=http_client, max_retries=0) as client,
|
||||
pytest.raises(openai.AuthenticationError) as sdk_error,
|
||||
):
|
||||
client.chat.completions.create(model="test-model", messages=[{"role": "user", "content": "Hello"}])
|
||||
|
||||
original_exception: Final = OpenAIError(
|
||||
status_code=sdk_error.value.status_code,
|
||||
message=sdk_error.value.message,
|
||||
response=sdk_error.value.response,
|
||||
body=TypeAdapter(dict[str, object]).validate_python(sdk_error.value.body),
|
||||
)
|
||||
|
||||
with pytest.raises(litellm.AuthenticationError) as mapped:
|
||||
exception_type(model="test-model", original_exception=original_exception, custom_llm_provider="dashscope")
|
||||
|
||||
assert mapped.value.message.count(request_id) == 1
|
||||
assert message in mapped.value.message
|
||||
|
||||
|
||||
def test_dashscope_auth_error_message_includes_provider_request_id():
|
||||
"""End-to-end (issue #47839): a DashScope 401 through the
|
||||
OpenAI-compatible exception mapping must keep the provider request id
|
||||
in the final AuthenticationError message."""
|
||||
dashscope_body = {
|
||||
"error": {
|
||||
"message": "Incorrect API key provided. For details, see: "
|
||||
"https://help.aliyun.com/zh/model-studio/error-code#apikey-error",
|
||||
"type": "invalid_request_error",
|
||||
"param": None,
|
||||
"code": "invalid_api_key",
|
||||
},
|
||||
"request_id": "343604ce-3795-9363-8be6-f4aa3f4613ee",
|
||||
}
|
||||
original_exception = OpenAIError(
|
||||
status_code=401,
|
||||
message=f"Error code: 401 - {dashscope_body}",
|
||||
headers={},
|
||||
body=dashscope_body,
|
||||
)
|
||||
|
||||
with pytest.raises(litellm.AuthenticationError) as excinfo:
|
||||
exception_type(
|
||||
model="dashscope/deepseek-v4-flash",
|
||||
original_exception=original_exception,
|
||||
custom_llm_provider="dashscope",
|
||||
)
|
||||
|
||||
assert "343604ce-3795-9363-8be6-f4aa3f4613ee" in str(excinfo.value)
|
||||
assert "Incorrect API key provided" in str(excinfo.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error_message",
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue