From ed0350fa3cefa160d58a1f49d0083e8cd14149bc Mon Sep 17 00:00:00 2001 From: OmriShukrun_ Date: Sat, 18 Apr 2026 17:43:46 +0300 Subject: [PATCH] Use sanitize deep copy style to replace deepcopy usage --- .../guardrail_hooks/noma/noma_v2.py | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/noma/noma_v2.py b/litellm/proxy/guardrails/guardrail_hooks/noma/noma_v2.py index 071613ad5f9..cddf73bd3b6 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/noma/noma_v2.py +++ b/litellm/proxy/guardrails/guardrail_hooks/noma/noma_v2.py @@ -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"