mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-10 08:28:25 +00:00
fix: close opt-in diagnostic logging gaps
This commit is contained in:
parent
8d9bd9045a
commit
47a9b4998a
5 changed files with 119 additions and 5 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue