mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
fix(core): add strip_null_bytes() to safe_dumps to prevent PostgreSQL 22P05 errors
Null bytes (\x00) in LLM request/response payloads cause PostgreSQL to raise '22P05: invalid byte sequence for encoding UTF8: 0x00' when spend logs are written to the database. Changes: - Add strip_null_bytes() helper to safe_json_dumps.py that recursively removes \x00 chars from strings, dicts, lists, tuples and sets - Inline null byte removal into safe_dumps() _serialize() for str paths so all JSON serialization through safe_dumps() is automatically safe - In spend_tracking_utils.py: replace json.dumps() with safe_dumps() for messages and request_body serialization; add strip_null_bytes() call in _sanitize_request_body_for_spend_logs_payload string handling Centralizing the fix in safe_dumps() is more robust than ad-hoc stripping at each call site. Fixes #24310 Related: #21290, #15519
This commit is contained in:
parent
d7c419bfee
commit
220f39be5a
2 changed files with 24 additions and 5 deletions
|
|
@ -6,10 +6,26 @@ from pydantic import BaseModel
|
|||
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
|
||||
|
||||
|
||||
def strip_null_bytes(data: Any) -> Any:
|
||||
"""Recursively remove \\x00 null bytes from strings to prevent PostgreSQL 22P05 errors."""
|
||||
if isinstance(data, str):
|
||||
return data.replace("\x00", "")
|
||||
if isinstance(data, dict):
|
||||
return {k: strip_null_bytes(v) for k, v in data.items()}
|
||||
if isinstance(data, list):
|
||||
return [strip_null_bytes(item) for item in data]
|
||||
if isinstance(data, tuple):
|
||||
return tuple(strip_null_bytes(item) for item in data)
|
||||
if isinstance(data, set):
|
||||
return {strip_null_bytes(item) for item in data}
|
||||
return data
|
||||
|
||||
|
||||
def safe_dumps(data: Any, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH) -> str:
|
||||
"""
|
||||
Recursively serialize data while detecting circular references.
|
||||
If a circular reference is detected then a marker string is returned.
|
||||
Null bytes are stripped from strings to prevent PostgreSQL 22P05 errors.
|
||||
"""
|
||||
|
||||
def _serialize(obj: Any, seen: set, depth: int) -> Any:
|
||||
|
|
@ -17,7 +33,9 @@ def safe_dumps(data: Any, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH) -> str:
|
|||
if depth > max_depth:
|
||||
return "MaxDepthExceeded"
|
||||
# Base-case: if it is a primitive, simply return it.
|
||||
if isinstance(obj, (str, int, float, bool, type(None))):
|
||||
if isinstance(obj, str):
|
||||
return obj.replace("\x00", "")
|
||||
if isinstance(obj, (int, float, bool, type(None))):
|
||||
return obj
|
||||
# Check for circular reference.
|
||||
if id(obj) in seen:
|
||||
|
|
@ -51,7 +69,7 @@ def safe_dumps(data: Any, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH) -> str:
|
|||
else:
|
||||
# Fall back to string conversion for non-serializable objects.
|
||||
try:
|
||||
return str(obj)
|
||||
return str(obj).replace("\x00", "")
|
||||
except Exception:
|
||||
return "Unserializable Object"
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from litellm.litellm_core_utils.core_helpers import (
|
|||
get_litellm_metadata_from_kwargs,
|
||||
reconstruct_model_name,
|
||||
)
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps, strip_null_bytes
|
||||
from litellm.proxy._types import SpendLogsMetadata, SpendLogsPayload
|
||||
from litellm.proxy.utils import PrismaClient, hash_token
|
||||
from litellm.types.utils import (
|
||||
|
|
@ -621,7 +621,7 @@ def _get_messages_for_spend_logs_payload(
|
|||
messages = standard_logging_payload.get("messages")
|
||||
if messages is not None:
|
||||
try:
|
||||
return json.dumps(messages, default=str)
|
||||
return safe_dumps(messages)
|
||||
except Exception:
|
||||
return "{}"
|
||||
return "{}"
|
||||
|
|
@ -660,6 +660,7 @@ def _sanitize_request_body_for_spend_logs_payload(
|
|||
elif isinstance(value, list):
|
||||
return [_sanitize_value(item) for item in value]
|
||||
elif isinstance(value, str):
|
||||
value = strip_null_bytes(value)
|
||||
if len(value) > max_string_length_prompt_in_db:
|
||||
# Keep 35% from beginning and 65% from end (end is usually more important)
|
||||
# This split ensures we keep more context from the end of conversations
|
||||
|
|
@ -804,7 +805,7 @@ def _get_proxy_server_request_for_spend_logs_payload(
|
|||
perform_redaction(model_call_details=_request_body, result=None)
|
||||
|
||||
_request_body = _sanitize_request_body_for_spend_logs_payload(_request_body)
|
||||
_request_body_json_str = json.dumps(_request_body, default=str)
|
||||
_request_body_json_str = safe_dumps(_request_body)
|
||||
if LITELLM_TRUNCATED_PAYLOAD_FIELD in _request_body_json_str:
|
||||
verbose_proxy_logger.info(
|
||||
"Spend Log: request body was truncated before storing in DB. %s",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue