From 690b2e7d58d0688f5299706960267b8fc5bda2e4 Mon Sep 17 00:00:00 2001 From: chjnett Date: Thu, 27 Aug 2026 16:26:11 +0900 Subject: [PATCH] fix(proxy): include litellm_call_id in LLM API exception logs _log_llm_api_exception() logged the error with only the exception text and a timestamp, with no request-correlating id even though the caller (_handle_llm_api_exception) has self.data in scope with the litellm_call_id. Under concurrent traffic there was no way to tell which client request a timeout/error log line belonged to, so it could not be cross-referenced against the x-litellm-call-id response header or the spend logs table. Thread litellm_call_id into _log_llm_api_exception and emit it via logging extra so the JsonFormatter surfaces it as a first-class field on the structured record. Closes #37532. Signed-off-by: chjnett --- litellm/proxy/common_request_processing.py | 14 ++++++-- .../proxy/test_common_request_processing.py | 35 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 315fbcba310..e5b273817cb 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -1418,7 +1418,7 @@ def _has_attribute_error_in_chain(exc: Exception) -> bool: _CLIENT_DISCONNECT_DETAIL: Final = "Client disconnected the request" -def _log_llm_api_exception(e: Exception) -> None: +def _log_llm_api_exception(e: Exception, litellm_call_id: str | None = None) -> None: if getattr(e, "status_code", None) == 499 and getattr(e, "detail", None) == _CLIENT_DISCONNECT_DETAIL: verbose_proxy_logger.info( "litellm.proxy.proxy_server._handle_llm_api_exception(): client disconnected, upstream LLM request cancelled" @@ -1429,7 +1429,15 @@ def _log_llm_api_exception(e: Exception) -> None: if is_expected_client_error(e) and not litellm.log_client_error_tracebacks else verbose_proxy_logger.exception ) - log_fn("litellm.proxy.proxy_server._handle_llm_api_exception(): Exception occured - %s", e) + log_fn( + "litellm.proxy.proxy_server._handle_llm_api_exception(): Exception occured - %s", + e, + extra=( + {"litellm_call_id": litellm_call_id} + if litellm_call_id is not None + else None + ), + ) async def _cancel_llm_call_on_client_disconnect( @@ -3164,7 +3172,7 @@ class ProxyBaseLLMRequestProcessing: version: str | None = None, ): """Raises ProxyException (OpenAI API compatible) if an exception is raised""" - _log_llm_api_exception(e) + _log_llm_api_exception(e, self.data.get("litellm_call_id")) # Allow callbacks to transform the error response transformed_exception: Final = await proxy_logging_obj.post_call_failure_hook( user_api_key_dict=user_api_key_dict, diff --git a/tests/test_litellm/proxy/test_common_request_processing.py b/tests/test_litellm/proxy/test_common_request_processing.py index 64318778bc2..a77e01ac1ba 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -7409,3 +7409,38 @@ def test_log_llm_api_exception_traceback_only_for_unexpected_errors(exc, expect_ records = [r for r in caplog.records if "_handle_llm_api_exception(): Exception occured" in r.getMessage()] assert len(records) == 1 assert (records[0].exc_info is not None) is expect_traceback + + +def test_log_llm_api_exception_includes_litellm_call_id(caplog): + """Regression for #37532: proxy error log lines carry litellm_call_id so a + logged exception can be correlated back to the request that failed.""" + from litellm._logging import verbose_proxy_logger + from litellm.proxy.common_request_processing import _log_llm_api_exception + + verbose_proxy_logger.propagate = True + try: + with caplog.at_level("ERROR", logger="LiteLLM Proxy"): + _log_llm_api_exception(ValueError("boom"), "abc-123") + finally: + verbose_proxy_logger.propagate = False + + records = [r for r in caplog.records if "_handle_llm_api_exception(): Exception occured" in r.getMessage()] + assert len(records) == 1 + assert records[0].litellm_call_id == "abc-123" + + +def test_log_llm_api_exception_without_call_id_omits_field(caplog): + """When no call id is available, the record still logs without the extra field.""" + from litellm._logging import verbose_proxy_logger + from litellm.proxy.common_request_processing import _log_llm_api_exception + + verbose_proxy_logger.propagate = True + try: + with caplog.at_level("ERROR", logger="LiteLLM Proxy"): + _log_llm_api_exception(ValueError("boom")) + finally: + verbose_proxy_logger.propagate = False + + records = [r for r in caplog.records if "_handle_llm_api_exception(): Exception occured" in r.getMessage()] + assert len(records) == 1 + assert not hasattr(records[0], "litellm_call_id")