mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
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:
parent
9275cf42ed
commit
2700ffe8c9
4 changed files with 148 additions and 1 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue