From 4c8e9d78dc2b60c985a7ff9ed83470a76d278a63 Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Sat, 29 Aug 2026 14:19:47 -0700 Subject: [PATCH] fix(logging): keep transformed server failures and config JSON mode on the error threshold When a failure callback rewrites a malformed key rejection into a non-401 response, the proxy now also logs that response at ERROR on stderr, after the WARNING that describes the malformed key. The JSON handler installed by litellm_settings.json_logs carries the LITELLM_LOG level like the JSON_LOGS handler, so LITELLM_LOG=ERROR hides warnings in both JSON modes. --- litellm/_logging.py | 1 + litellm/proxy/auth/auth_exception_handler.py | 13 +++++++++++-- .../proxy/auth/test_auth_exception_handler.py | 11 ++++++----- tests/test_litellm/test_logging.py | 16 ++++++++++++++++ 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/litellm/_logging.py b/litellm/_logging.py index 0ecab9b754d..297736715cd 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -690,6 +690,7 @@ def _turn_on_json(): - Adds a JSON formatter to all loggers """ handler: Final = LevelRoutingStreamHandler() + handler.setLevel(numeric_level) handler.setFormatter(JsonFormatter()) _initialize_loggers_with_handler(handler) # Set up exception handlers diff --git a/litellm/proxy/auth/auth_exception_handler.py b/litellm/proxy/auth/auth_exception_handler.py index 464cb0a4caf..89255a48d62 100644 --- a/litellm/proxy/auth/auth_exception_handler.py +++ b/litellm/proxy/auth/auth_exception_handler.py @@ -150,6 +150,7 @@ class UserAPIKeyAuthExceptionHandler: request=request, use_x_forwarded_for=general_settings.get("use_x_forwarded_for") is True, ) + log_extra: Final = {"requester_ip": requester_ip} is_invalid_virtual_key: Final = is_invalid_virtual_key_error(e) is_quiet_log: Final = is_invalid_virtual_key and not litellm.log_client_error_tracebacks logger: Final = verbose_proxy_stdout_logger if is_quiet_log else verbose_proxy_logger @@ -159,7 +160,7 @@ class UserAPIKeyAuthExceptionHandler: e, requester_ip, exc_info=True if litellm.log_client_error_tracebacks or not is_expected_client_error(e) else None, - extra={"requester_ip": requester_ip}, + extra=log_extra, ) # Log this exception to OTEL, Datadog etc. Reuse the identity resolved @@ -207,4 +208,12 @@ class UserAPIKeyAuthExceptionHandler: if transformed_exception is not None: e = transformed_exception - raise mark_invalid_virtual_key_error(_as_proxy_exception(e), is_invalid_virtual_key) + final_exception: Final = mark_invalid_virtual_key_error(_as_proxy_exception(e), is_invalid_virtual_key) + if is_quiet_log and str(final_exception.code) != str(status.HTTP_401_UNAUTHORIZED): + verbose_proxy_logger.error( + "litellm.proxy.proxy_server.user_api_key_auth(): Exception occured - %s\nRequester IP Address:%s", + final_exception, + requester_ip, + extra=log_extra, + ) + raise final_exception diff --git a/tests/test_litellm/proxy/auth/test_auth_exception_handler.py b/tests/test_litellm/proxy/auth/test_auth_exception_handler.py index b20c7f3cdc7..3cc3e854807 100644 --- a/tests/test_litellm/proxy/auth/test_auth_exception_handler.py +++ b/tests/test_litellm/proxy/auth/test_auth_exception_handler.py @@ -882,7 +882,7 @@ async def test_handle_authentication_error_keeps_unexpected_source_traceback_aft @pytest.mark.asyncio -async def test_handle_authentication_error_does_not_preserve_invalid_virtual_key_marker_for_callback_503(caplog): +async def test_handle_authentication_error_logs_callback_503_at_error_level_without_the_marker(caplog): handler = UserAPIKeyAuthExceptionHandler() transformed_exception = HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, @@ -895,11 +895,12 @@ async def test_handle_authentication_error_does_not_preserve_invalid_virtual_key ) records = _auth_failure_records(caplog) - assert len(records) == 1 - assert records[0].name == verbose_proxy_stdout_logger.name - assert records[0].levelno == logging.WARNING - assert records[0].exc_info is None + assert [(r.name, r.levelno, r.exc_info) for r in records] == [ + (verbose_proxy_stdout_logger.name, logging.WARNING, None), + (verbose_proxy_logger.name, logging.ERROR, None), + ] assert "LiteLLM Virtual Key expected" in records[0].getMessage() + assert "Authentication service temporarily unavailable" in records[1].getMessage() assert exc_info.value.message == "Authentication service temporarily unavailable" assert exc_info.value.code == str(status.HTTP_503_SERVICE_UNAVAILABLE) assert not hasattr(exc_info.value, "_litellm_invalid_virtual_key_error") diff --git a/tests/test_litellm/test_logging.py b/tests/test_litellm/test_logging.py index fe9fb5c8ef6..a66e74420fa 100644 --- a/tests/test_litellm/test_logging.py +++ b/tests/test_litellm/test_logging.py @@ -11,6 +11,7 @@ from typing import List import pytest import litellm +import litellm._logging as litellm_logging from litellm._logging import ( _COLOR_LOG_FORMAT, _PLAIN_LOG_FORMAT, @@ -986,6 +987,21 @@ def test_error_threshold_suppresses_invalid_key_warning_like_ordinary_warnings(c assert "ordinary proxy error" in err +def test_config_json_mode_handler_honors_the_litellm_log_threshold(monkeypatch, capsys): + monkeypatch.setattr(litellm_logging, "numeric_level", logging.ERROR) + _turn_on_json() + + verbose_proxy_stdout_logger.warning("invalid virtual key") + verbose_proxy_logger.warning("ordinary proxy warning") + verbose_proxy_logger.error("ordinary proxy error") + + out, err = capsys.readouterr() + assert out == "" + assert "invalid virtual key" not in err + assert "ordinary proxy warning" not in err + assert "ordinary proxy error" in err + + def test_ordinary_proxy_records_still_propagate_to_root_handler(): root_logger = logging.getLogger() root_stream = StringIO()