From fc644cff3d5e593d14b4d3618801d66eedbf5a7f Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Fri, 26 Jun 2026 19:37:19 +0300 Subject: [PATCH] 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. --- litellm/litellm_core_utils/safe_json_dumps.py | 5 ++-- .../test_safe_json_dumps.py | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/litellm/litellm_core_utils/safe_json_dumps.py b/litellm/litellm_core_utils/safe_json_dumps.py index 154306d01b8..81cd8e57798 100644 --- a/litellm/litellm_core_utils/safe_json_dumps.py +++ b/litellm/litellm_core_utils/safe_json_dumps.py @@ -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): diff --git a/tests/test_litellm/litellm_core_utils/test_safe_json_dumps.py b/tests/test_litellm/litellm_core_utils/test_safe_json_dumps.py index 74574370e46..ad24105588d 100644 --- a/tests/test_litellm/litellm_core_utils/test_safe_json_dumps.py +++ b/tests/test_litellm/litellm_core_utils/test_safe_json_dumps.py @@ -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