fix: capture provider request id in failure logging payloads (#40045)

* fix: capture provider request id in failure logging payloads

* fix: include common provider request id headers

* test: remove accidental formatter churn

* test: type provider request id cases

* test: cover azure and google request id headers

* refactor: share provider request id headers from constants

* chore: remove redundant request header comment

* test: cover provider header lookup failures
This commit is contained in:
yucheng-berri 2026-09-07 11:12:34 -07:00 • committed by GitHub
parent 9275cf42ed
commit 2700ffe8c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 148 additions and 1 deletions

View file

@ -1901,6 +1901,16 @@ HTTP_FRAMING_HEADERS: Final[frozenset[str]] = frozenset(
}
)
PROVIDER_REQUEST_ID_HEADERS: Final[tuple[str, ...]] = (
"x-amzn-requestid",
"x-request-id",
"request-id",
"x-ms-request-id",
"apim-request-id",
"x-goog-request-id",
"cf-ray",
)
# Browser-facing security headers that a malicious or misconfigured upstream
# provider must not be able to set on the proxy's own response.
BROWSER_SECURITY_HEADERS: Final[frozenset[str]] = frozenset(

View file

@ -42,6 +42,7 @@ from litellm.caching.caching_handler import LLMCachingHandler
from litellm.constants import (
DEFAULT_MOCK_RESPONSE_COMPLETION_TOKEN_COUNT,
DEFAULT_MOCK_RESPONSE_PROMPT_TOKEN_COUNT,
PROVIDER_REQUEST_ID_HEADERS,
SENTRY_DENYLIST,
SENTRY_PII_DENYLIST,
)
@ -255,6 +256,30 @@ _in_memory_loggers: Final[list[CustomLogger]] = []
_STANDARD_LOGGING_METADATA_KEYS: Final[frozenset[str]] = frozenset(StandardLoggingMetadata.__annotations__.keys())
def _get_provider_request_id(original_exception: Exception) -> str | None:
try:
error_response: Final = getattr(original_exception, "response", None)
header_sources: Final = (
_get_response_headers(original_exception),
getattr(error_response, "headers", None),
getattr(original_exception, "litellm_response_headers", None),
)
return next(
(
str(value)
for expected_header_name in PROVIDER_REQUEST_ID_HEADERS
for headers in header_sources
if isinstance(headers, Mapping)
for header_name, value in headers.items()
if isinstance(header_name, str) and header_name.lower() == expected_header_name and value
),
None,
)
except Exception:
return None
### GLOBAL VARIABLES ###
# Cache custom pricing keys as frozenset for O(1) lookups instead of looping through 49 keys
@ -5664,6 +5689,7 @@ class StandardLoggingPayloadSetup:
rate_limit_category: Final = validate_rate_limit_category(getattr(original_exception, "category", None))
rate_limit_type: Final = validate_rate_limit_type(getattr(original_exception, "rate_limit_type", None))
budget_error: Final = original_exception if isinstance(original_exception, BudgetExceededError) else None
provider_request_id: Final = _get_provider_request_id(original_exception) if original_exception else None
return StandardLoggingPayloadErrorInformation(
error_code=error_status,
@ -5671,6 +5697,7 @@ class StandardLoggingPayloadSetup:
llm_provider=_llm_provider_in_exception,
traceback=_redact_string(traceback_info),
error_message=_redact_string(error_message),
error_provider_request_id=provider_request_id,
error_rate_limit_category=rate_limit_category,
error_rate_limit_type=rate_limit_type,
error_budget_entity_type=budget_error.entity_type if budget_error else None,

View file

@ -3064,6 +3064,7 @@ class StandardLoggingPayloadErrorInformation(TypedDict, total=False):
llm_provider: str | None
traceback: str | None
error_message: str | None
error_provider_request_id: ReadOnly[str | None]
# error_rate_limit_category:
# For 429 / rate-limit errors, the source of the rate limit. One of the
# string values defined by `litellm.exceptions.RateLimitErrorCategory`

View file

@ -1,7 +1,9 @@
import asyncio
import contextlib
import datetime
import os
import sys
import asyncio
from typing import Literal
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@ -6439,6 +6441,113 @@ def test_get_standard_logging_object_payload_survives_logging_obj_without_timing
assert payload["hidden_params"]["litellm_overhead_time_ms"] is None
@pytest.mark.parametrize(
("header_name", "header_source"),
(
("x-amzn-RequestId", "response"),
("x-request-id", "response"),
("request-id", "response"),
("x-ms-request-id", "response"),
("apim-request-id", "response"),
("x-goog-request-id", "response"),
("cf-ray", "response"),
("X-Request-Id", "litellm_response_headers"),
("X-MS-Request-ID", "headers"),
),
)
def test_failure_standard_logging_payload_captures_provider_request_id(
logging_obj: LitellmLogging,
header_name: str,
header_source: Literal["response", "litellm_response_headers", "headers"],
):
from litellm.litellm_core_utils.litellm_logging import (
get_standard_logging_object_payload,
)
request_id = "provider-request-123"
request = httpx.Request("POST", "https://provider.example/v1/chat/completions")
response = httpx.Response(429, headers={header_name: request_id}, request=request)
provider_error = httpx.HTTPStatusError("provider error", request=request, response=response)
if header_source == "litellm_response_headers":
response.headers.clear()
provider_error.litellm_response_headers = {header_name: request_id}
elif header_source == "headers":
response.headers.clear()
provider_error.headers = {header_name: request_id}
now = datetime.datetime.now()
payload = get_standard_logging_object_payload(
kwargs={"litellm_call_id": "call-1", "model": "test-model", "messages": []},
init_response_obj={},
start_time=now,
end_time=now,
logging_obj=logging_obj,
status="failure",
original_exception=provider_error,
)
assert payload is not None
assert payload["error_information"] is not None
assert payload["error_information"]["error_provider_request_id"] == request_id
def test_get_error_information_ignores_unsupported_headers() -> None:
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
request = httpx.Request("POST", "https://provider.example/v1/chat/completions")
response = httpx.Response(429, headers={"retry-after": "3"}, request=request)
provider_error = httpx.HTTPStatusError("provider error", request=request, response=response)
error_information = StandardLoggingPayloadSetup.get_error_information(provider_error)
assert error_information["error_provider_request_id"] is None
def test_get_error_information_uses_header_precedence_and_fallback() -> None:
from litellm.exceptions import RateLimitError
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
request = httpx.Request("POST", "https://provider.example/v1/chat/completions")
response = httpx.Response(
429,
headers={"x-request-id": "response-id", "x-amzn-requestid": "amazon-id"},
request=request,
)
provider_error = RateLimitError(
message="provider error",
llm_provider="test-provider",
model="test-model",
response=response,
headers={"retry-after": "3"},
)
error_information = StandardLoggingPayloadSetup.get_error_information(provider_error)
assert error_information["error_provider_request_id"] == "amazon-id"
def test_get_error_information_ignores_malformed_headers() -> None:
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
provider_error = Exception("provider error")
provider_error.headers = [("x-request-id", "provider-request-123")]
error_information = StandardLoggingPayloadSetup.get_error_information(provider_error)
assert error_information["error_provider_request_id"] is None
def test_get_provider_request_id_ignores_header_lookup_errors() -> None:
from litellm.litellm_core_utils.litellm_logging import _get_provider_request_id
class HeaderLookupError(Exception):
@property
def response(self) -> object:
raise RuntimeError("headers unavailable")
assert _get_provider_request_id(HeaderLookupError("provider error")) is None
def test_get_standard_logging_object_payload_failure_status_keeps_overhead_none(logging_obj):
"""A post_call guardrail can fail the request after the upstream call succeeded; the failure
payload keeps litellm_overhead_time_ms None, matching responses that carry their own _hidden_params."""