Use sanitize deep copy style to replace deepcopy usage

This commit is contained in:
OmriShukrun_ 2026-04-18 17:43:46 +03:00
parent af35286cb2
commit ed0350fa3c

View file

@ -7,7 +7,6 @@
import enum
import json
import os
from copy import deepcopy
from datetime import datetime
from typing import TYPE_CHECKING, Any, Literal, Optional, Type, cast
from urllib.parse import urlparse
@ -131,6 +130,35 @@ class NomaV2Guardrail(CustomGuardrail):
raise ValueError("Noma v2 response missing valid action")
@staticmethod
def _json_safe_copy(data: Any) -> Any:
"""JSON round-trip producing a deep, JSON-serializable copy of `data`.
Fast path: `json.dumps` (C-level) with a `default=` callback that
handles Pydantic models and falls back to `str(obj)` for anything
non-serializable (e.g. uvloop.Loop, locks, other C-extension objects
whose __reduce__ raises which is why this replaces deepcopy).
Slow path: if `json.dumps` raises (circular refs, etc.), retry with
`safe_dumps` which walks the tree in Python with explicit cycle
detection. Finally `safe_json_loads` parses back to a Python tree.
"""
def _default(obj: Any) -> Any:
if hasattr(obj, "model_dump"):
try:
return obj.model_dump()
except Exception:
pass
return str(obj)
try:
json_str = json.dumps(data, default=_default)
except (ValueError, TypeError):
json_str = safe_dumps(data)
return safe_json_loads(json_str, default={})
def _build_scan_payload(
self,
inputs: GenericGuardrailAPIInputs,
@ -139,7 +167,12 @@ class NomaV2Guardrail(CustomGuardrail):
logging_obj: Optional["LiteLLMLoggingObj"],
application_id: Optional[str],
) -> dict:
payload_request_data = deepcopy(request_data)
# JSON round-trip rather than deepcopy: on post_call / during_call /
# during_mcp_call, request_data contains uvloop.Loop and other C-extension
# objects whose __reduce__ raises, crashing pickle-based copy.
payload_request_data = self._json_safe_copy(request_data)
if not isinstance(payload_request_data, dict):
payload_request_data = {}
if logging_obj is not None:
payload_request_data["litellm_logging_obj"] = getattr(
logging_obj, "model_call_details", None
@ -157,20 +190,8 @@ class NomaV2Guardrail(CustomGuardrail):
@staticmethod
def _sanitize_payload_for_transport(payload: dict) -> dict:
def _default(obj: Any) -> Any:
if hasattr(obj, "model_dump"):
try:
return obj.model_dump()
except Exception:
pass
return str(obj)
safe_payload = NomaV2Guardrail._json_safe_copy(payload)
try:
json_str = json.dumps(payload, default=_default)
except (ValueError, TypeError):
json_str = safe_dumps(payload)
safe_payload = safe_json_loads(json_str, default={})
if safe_payload == {} and payload:
verbose_proxy_logger.warning(
"Noma v2 guardrail: payload serialization failed, falling back to empty payload"