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:
Yassin Kortam 2026-06-26 19:37:19 +03:00 committed by GitHub
parent 4476923ac4
commit fc644cff3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 27 additions and 2 deletions

View file

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

View file

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