mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
perf(spend-logs): only strip NUL bytes in safe_dumps when present (#31424)
safe_dumps ran strip_null_bytes (a str.replace) on every string value and every dict key during recursive serialization. NUL bytes are vanishingly rare, so for the common case this was pure overhead that scaled with payload size; with store_prompts_in_spend_logs the full prompt and response are serialized on every request, so it landed directly in the per-request hot path. Guard the strip behind a cheap "\x00" in obj membership check so NUL-free strings are returned untouched. Behavior is unchanged: NUL bytes are still stripped from values, keys, nested structures, and the str() fallback.
This commit is contained in:
parent
4476923ac4
commit
fc644cff3d
2 changed files with 27 additions and 2 deletions
|
|
@ -24,7 +24,7 @@ def safe_dumps(data: Any, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH) -> str:
|
|||
return "MaxDepthExceeded"
|
||||
# Base-case: if it is a primitive, simply return it.
|
||||
if isinstance(obj, str):
|
||||
return strip_null_bytes(obj)
|
||||
return obj.replace("\x00", "") if "\x00" in obj else obj
|
||||
if isinstance(obj, (int, float, bool, type(None))):
|
||||
return obj
|
||||
# Check for circular reference.
|
||||
|
|
@ -36,7 +36,8 @@ def safe_dumps(data: Any, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH) -> str:
|
|||
result = {}
|
||||
for k, v in obj.items():
|
||||
if isinstance(k, (str)):
|
||||
result[strip_null_bytes(k)] = _serialize(v, seen, depth + 1)
|
||||
clean_k = k.replace("\x00", "") if "\x00" in k else k
|
||||
result[clean_k] = _serialize(v, seen, depth + 1)
|
||||
seen.remove(id(obj))
|
||||
return result
|
||||
elif isinstance(obj, list):
|
||||
|
|
|
|||
|
|
@ -174,6 +174,30 @@ def test_null_byte_stripped_in_fallback_str():
|
|||
assert json.loads(out)["obj"] == "objrepr"
|
||||
|
||||
|
||||
def test_clean_strings_are_not_run_through_replace():
|
||||
"""Regression for LIT-3910.
|
||||
|
||||
safe_dumps must not call ``str.replace`` on NUL-free strings. Running the
|
||||
NUL strip unconditionally on every value and dict key (the v1.89.x behavior)
|
||||
added per-request serialization overhead that scaled with payload size and
|
||||
showed up under ``store_prompts_in_spend_logs``. Clean strings, which are the
|
||||
overwhelming majority, must be returned untouched.
|
||||
"""
|
||||
|
||||
class ReplaceForbidden(str):
|
||||
def replace(self, *args, **kwargs):
|
||||
raise AssertionError("safe_dumps ran str.replace on a NUL-free string")
|
||||
|
||||
data = {
|
||||
ReplaceForbidden("clean_key"): ReplaceForbidden("clean_value"),
|
||||
"nested": [ReplaceForbidden("a"), {"deep": ReplaceForbidden("b")}],
|
||||
}
|
||||
result = json.loads(safe_dumps(data))
|
||||
assert result["clean_key"] == "clean_value"
|
||||
assert result["nested"][0] == "a"
|
||||
assert result["nested"][1]["deep"] == "b"
|
||||
|
||||
|
||||
def test_pydantic_base_model():
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue