mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
fix(logging): keep secret-free log extras as their original objects
A non-string extra was scrubbed by a safe_dumps round trip, which handed every user-attached handler a JSON-shaped copy even when nothing in it was redacted. The record now keeps the original object whenever the plain and the scrubbed renderings compare equal, so only an extra that carried a secret comes back as its scrubbed shape
This commit is contained in:
parent
7c0cca4d36
commit
04eae31769
2 changed files with 52 additions and 1 deletions
|
|
@ -90,7 +90,9 @@ def _is_redacted(record: logging.LogRecord) -> bool:
|
|||
|
||||
def _redact_extra_value(key: str, value: object) -> object:
|
||||
try:
|
||||
return json.loads(safe_dumps({key: value}, value_transform=_redact_structured_value))[key]
|
||||
rendered: Final = safe_dumps({key: value})
|
||||
scrubbed: Final = safe_dumps({key: value}, value_transform=_redact_structured_value)
|
||||
return value if scrubbed == rendered else json.loads(scrubbed)[key]
|
||||
except (TypeError, ValueError, KeyError):
|
||||
return _redact_string(str(value))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import ast
|
||||
import asyncio
|
||||
import base64
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
|
@ -1024,6 +1025,54 @@ def test_unserializable_extra_never_breaks_the_filter(monkeypatch, extra):
|
|||
assert "payload" in rendered
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, slots=True)
|
||||
class _RequestExtra:
|
||||
model: str
|
||||
attempt: int
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"extra",
|
||||
(
|
||||
("gpt-4o", 2),
|
||||
{"gpt-4o", "gpt-4o-mini"},
|
||||
{"models": ("gpt-4o", "gpt-4o-mini")},
|
||||
_RequestExtra(model="gpt-4o", attempt=2),
|
||||
),
|
||||
ids=("tuple", "set", "nested_tuple", "dataclass"),
|
||||
)
|
||||
def test_secret_free_extra_keeps_its_original_object(monkeypatch, extra):
|
||||
"""A host application's own handler on a litellm logger reads extras by type, so a
|
||||
container that carried no secret must reach it untouched, not as its JSON shape."""
|
||||
monkeypatch.setattr("litellm._logging._ENABLE_SECRET_REDACTION", True)
|
||||
record = _make_record(logging.WARNING, "request sent")
|
||||
record.payload = extra
|
||||
|
||||
assert SecretRedactionFilter().filter(record) is True
|
||||
|
||||
assert record.payload is extra
|
||||
assert "payload" in json.loads(JsonFormatter().format(record))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"extra",
|
||||
(("gpt-4o", "sk-1234567890abcdefghij"), {"gpt-4o", "sk-1234567890abcdefghij"}),
|
||||
ids=("tuple", "set"),
|
||||
)
|
||||
def test_extra_that_carried_a_secret_comes_back_scrubbed(monkeypatch, extra):
|
||||
monkeypatch.setattr("litellm._logging._ENABLE_SECRET_REDACTION", True)
|
||||
record = _make_record(logging.WARNING, "request sent")
|
||||
record.payload = extra
|
||||
|
||||
assert SecretRedactionFilter().filter(record) is True
|
||||
rendered = JsonFormatter().format(record)
|
||||
|
||||
assert isinstance(record.payload, list)
|
||||
assert sorted(record.payload) == ["REDACTED", "gpt-4o"]
|
||||
assert "sk-1234567890abcdefghij" not in rendered
|
||||
assert "REDACTED" in rendered
|
||||
|
||||
|
||||
def test_unscrubbed_record_is_still_redacted_by_the_formatter(monkeypatch):
|
||||
"""Records that never met SecretRedactionFilter (uvicorn's, in JSON mode) keep
|
||||
their formatter-side redaction."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue