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:
mateo-berri 2026-09-16 14:17:48 -07:00
parent 7c0cca4d36
commit 04eae31769
2 changed files with 52 additions and 1 deletions

View file

@ -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))

View file

@ -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."""