This commit is contained in:
Zhong Zhiyi 2026-11-05 23:12:42 +00:00 • committed by GitHub
commit 7c54b2b4fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 200 additions and 1 deletions

View file

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

View file

@ -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",
[