From c44e3598dd276b41da2935e493d33004db6f10b6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 01:14:43 +0000 Subject: [PATCH] fix(proxy): include litellm_call_id in LLM API exception logs Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/common_request_processing.py | 22 ++++++-- .../proxy/test_common_request_processing.py | 56 +++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index a0b69ecb0bf..7bd9d7c0a54 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -1322,13 +1322,19 @@ 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: 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" + "litellm.proxy.proxy_server._handle_llm_api_exception(): client disconnected, upstream LLM request cancelled - litellm_call_id=%s", + litellm_call_id, ) return - verbose_proxy_logger.exception("litellm.proxy.proxy_server._handle_llm_api_exception(): Exception occured - %s", e) + verbose_proxy_logger.exception( + "litellm.proxy.proxy_server._handle_llm_api_exception(): Exception occured - litellm_call_id=%s - %s", + litellm_call_id, + e, + extra={"litellm_call_id": litellm_call_id}, + ) async def _cancel_llm_call_on_client_disconnect( @@ -3046,7 +3052,15 @@ class ProxyBaseLLMRequestProcessing: version: str | None = None, ): """Raises ProxyException (OpenAI API compatible) if an exception is raised""" - _log_llm_api_exception(e) + _logging_obj_for_call_id: Final[LiteLLMLoggingObj | None] = self.data.get("litellm_logging_obj", None) + _log_llm_api_exception( + e, + ( + _logging_obj_for_call_id.litellm_call_id + if _logging_obj_for_call_id + else 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 355c6d27eb2..41034df3223 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -6637,3 +6637,59 @@ async def test_a_broken_hook_does_not_replace_the_real_error_with_its_own_bug(): error_frame = json.loads(collected[-2].decode().removeprefix("data: ").strip()) assert error_frame["error"]["message"] == "rate limited" assert "audit backend" not in collected[-2].decode() + + +class TestErrorLogCarriesCallId: + """Regression for LIT-5856 / #37532: the ERROR line emitted for a failed LLM + request must carry the same litellm_call_id the client got back in the + x-litellm-call-id response header, so a logged exception can be correlated + with a specific request.""" + + async def _invoke(self, data: dict) -> None: + from litellm.proxy._types import ProxyException, UserAPIKeyAuth + + processor = ProxyBaseLLMRequestProcessing(data=data) + proxy_logging_obj = MagicMock() + proxy_logging_obj.post_call_failure_hook = AsyncMock(return_value=None) + proxy_logging_obj.post_call_response_headers_hook = AsyncMock(return_value={}) + with pytest.raises(ProxyException): + await processor._handle_llm_api_exception( + e=ValueError("upstream blew up"), + user_api_key_dict=UserAPIKeyAuth(api_key="sk-test"), + proxy_logging_obj=proxy_logging_obj, + ) + + async def test_call_id_from_logging_obj_is_logged(self, caplog): + call_id = str(uuid.uuid4()) + logging_obj = MagicMock() + logging_obj.litellm_call_id = call_id + with caplog.at_level("ERROR", logger="LiteLLM Proxy"): + await self._invoke({"litellm_logging_obj": logging_obj, "litellm_call_id": "stale-id"}) + + record = next(r for r in caplog.records if r.levelname == "ERROR") + assert record.litellm_call_id == call_id + assert call_id in record.getMessage() + + async def test_call_id_falls_back_to_request_data(self, caplog): + call_id = str(uuid.uuid4()) + with caplog.at_level("ERROR", logger="LiteLLM Proxy"): + await self._invoke({"litellm_call_id": call_id}) + + record = next(r for r in caplog.records if r.levelname == "ERROR") + assert record.litellm_call_id == call_id + assert call_id in record.getMessage() + + def test_client_disconnect_log_carries_call_id(self, caplog): + from litellm.proxy.common_request_processing import ( + _CLIENT_DISCONNECT_DETAIL, + _log_llm_api_exception, + ) + + call_id = str(uuid.uuid4()) + with caplog.at_level("INFO", logger="LiteLLM Proxy"): + _log_llm_api_exception( + HTTPException(status_code=499, detail=_CLIENT_DISCONNECT_DETAIL), + call_id, + ) + + assert call_id in caplog.records[-1].getMessage()