fix: close opt-in diagnostic logging gaps

This commit is contained in:
moe-berri 2026-10-28 13:09:42 -07:00
parent 8d9bd9045a
commit 47a9b4998a
5 changed files with 119 additions and 5 deletions

View file

@ -1016,7 +1016,9 @@ def print_startup_message(message: str) -> None:
level, whole and unredacted as the print showed it; a blank spacer produces no record.
"""
if not litellm.json_logs:
print(message) # noqa: T201 # plain mode: this is the operator-terminal print it replaces
print( # noqa: T201 # plain mode: this is the operator-terminal print it replaces
REDACTED_BY_LITELLM if diagnostic_message_logging_disabled() else message
)
return
text: Final = _ANSI_ESCAPE.sub("", message).strip("\n")
if not text:

View file

@ -4566,10 +4566,12 @@ class ProxyBaseLLMRequestProcessing:
e,
)
stream_error_status: Final = error_status_code(e, status.HTTP_500_INTERNAL_SERVER_ERROR)
log_safe_diagnostic(
verbose_proxy_logger, "request_failed", status_code=stream_error_status, level=logging.ERROR
)
if isinstance(e, HTTPException):
raise e
stream_error_status: Final = error_status_code(e, status.HTTP_500_INTERNAL_SERVER_ERROR)
log_safe_diagnostic(verbose_proxy_logger, "request_failed", status_code=stream_error_status)
proxy_exception: Final = ProxyException(
message=redact_internal_details_from_client_message(getattr(e, "message", str(e))),
type=openai_error_type(e, stream_error_status),

View file

@ -1,3 +1,4 @@
import json
import zoneinfo
from datetime import datetime, timezone
from typing import Final
@ -9,8 +10,6 @@ import pytest
import respx
import litellm
from litellm.integrations.cloudzero.cz_stream_api import CloudZeroStreamer
@ -198,3 +197,47 @@ def test_cloudzero_http_error_diagnostics_respect_privacy_without_changing_excep
streamer._send_daily_batch("2026-01-01", pl.DataFrame({"cost/cost": [1.0]}), "replace_hourly")
assert captured.value.response.text == marker
assert (marker in capsys.readouterr().out) is not redact
@pytest.mark.parametrize("redact", (False, True))
def test_cloudzero_skips_invalid_timestamp_without_logging_content_when_private(
redact: bool, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", redact)
marker: Final = "synthetic-private-invalid-timestamp"
streamer: Final = CloudZeroStreamer("test-key", "test-connection")
data: Final = pl.DataFrame(
{"time/usage_start": [marker, "2026-01-01T00:00:00Z"], "cost/cost": [9.0, 1.0]}
)
with respx.mock:
route: Final = respx.post(
f"{streamer.base_url}/v2/connections/billing/anycost/test-connection/billing_drops"
).respond(status_code=200)
streamer.send_batched(data)
assert len(route.calls) == 1
assert json.loads(route.calls[0].request.content) == {
"month": "2026-01",
"operation": "replace_hourly",
"data": [{"time/usage_start": "2026-01-01T00:00:00Z", "cost/cost": "1"}],
}
assert (marker in capsys.readouterr().out) is not redact
@pytest.mark.parametrize("redact", (False, True))
def test_cloudzero_network_error_preserved_without_logging_content_when_private(
redact: bool, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", redact)
marker: Final = "synthetic-private-network-error"
streamer: Final = CloudZeroStreamer("test-key", "test-connection")
failure: Final = httpx.ConnectError(marker)
with respx.mock:
respx.post(
f"{streamer.base_url}/v2/connections/billing/anycost/test-connection/billing_drops"
).mock(side_effect=failure)
with pytest.raises(httpx.ConnectError) as captured:
streamer.send_batched(
pl.DataFrame({"time/usage_start": ["2026-01-01T00:00:00Z"], "cost/cost": [1.0]})
)
assert captured.value is failure
assert (marker in capsys.readouterr().out) is not redact

View file

@ -12249,3 +12249,56 @@ async def test_debug_message_privacy_preserves_stream_content(
assert all("PRIVATE_STREAM_INPUT_42896" not in message for message in messages)
else:
assert any("PRIVATE_STREAM_INPUT_42896" in message for message in messages)
@pytest.mark.asyncio
@pytest.mark.parametrize("privacy", [False, True])
@pytest.mark.parametrize("http_error", [False, True])
async def test_stream_failure_retains_error_status_without_debug_logging(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, privacy: bool, http_error: bool
) -> None:
import logging
from litellm._logging import verbose_proxy_logger
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", privacy)
monkeypatch.setattr(litellm, "callbacks", [])
monkeypatch.setattr(litellm, "success_callback", [])
monkeypatch.setattr(litellm, "_async_success_callback", [])
monkeypatch.setattr(litellm, "include_cost_in_streaming_usage", False)
monkeypatch.setattr(verbose_proxy_logger, "level", logging.ERROR)
ProxyLogging._callback_capabilities_cache.clear()
failure: Final = (
HTTPException(status_code=429, detail="PRIVATE_STREAM_FAILURE_42896")
if http_error
else RuntimeError("PRIVATE_STREAM_FAILURE_42896")
)
async def upstream() -> AsyncGenerator[str, None]:
yield "data: started\n\n"
raise failure
stream: Final = ProxyBaseLLMRequestProcessing.async_streaming_data_generator(
response=upstream(),
user_api_key_dict=ProxyUserAPIKeyAuth(),
request_data={"model": "example-model"},
proxy_logging_obj=ProxyLogging(user_api_key_cache=DualCache()),
serialize_chunk=str,
serialize_error=str,
)
assert await anext(stream) == "data: started\n\n"
if http_error:
with pytest.raises(HTTPException) as raised:
await anext(stream)
assert raised.value is failure
else:
remaining: Final = tuple([chunk async for chunk in stream])
assert len(remaining) == 1
assert "PRIVATE_STREAM_FAILURE_42896" in remaining[0]
messages: Final = tuple(record.getMessage() for record in caplog.records if record.name == verbose_proxy_logger.name)
if privacy:
assert any(f"event=request_failed status={429 if http_error else 500}" in message for message in messages)
assert all("PRIVATE_STREAM_FAILURE_42896" not in message for message in messages)
else:
assert any("PRIVATE_STREAM_FAILURE_42896" in message for message in messages)
assert all("event=request_failed" not in message for message in messages)

View file

@ -1827,6 +1827,20 @@ def test_print_verbose_formats_args_only_when_set_verbose_is_on(monkeypatch, cap
assert capsys.readouterr().out == "key k1 value cached-vector\n"
@pytest.mark.parametrize("privacy", [False, True])
@pytest.mark.parametrize("json_logs", [False, True])
def test_startup_message_respects_diagnostic_privacy(
monkeypatch: pytest.MonkeyPatch, capfd: pytest.CaptureFixture[str], privacy: bool, json_logs: bool
) -> None:
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", privacy)
monkeypatch.setattr(litellm, "json_logs", json_logs)
print_startup_message("PRIVATE_CONFIGURED_MODEL_42896")
out, err = capfd.readouterr()
message: Final = json.loads(out)["message"] if json_logs else out.strip()
assert message == ("redacted-by-litellm" if privacy else "PRIVATE_CONFIGURED_MODEL_42896")
assert err == ""
def test_print_startup_message_prints_plain_mode_text_unchanged(
monkeypatch: pytest.MonkeyPatch, capfd: pytest.CaptureFixture[str]
) -> None: