fix(guardrails): snapshot model_call_details before serializing noma v2 scan payload

This commit is contained in:
Devin AI 2026-07-28 18:11:37 +00:00
parent b930e2fc2b
commit 4505b53c6f
2 changed files with 53 additions and 4 deletions

View file

@ -131,9 +131,18 @@ class NomaV2Guardrail(CustomGuardrail):
logging_obj: Optional["LiteLLMLoggingObj"],
application_id: Optional[str],
) -> dict:
payload_request_data = self._sanitize_payload_for_transport(request_data)
if logging_obj is not None:
payload_request_data["litellm_logging_obj"] = getattr(logging_obj, "model_call_details", None)
model_call_details = getattr(logging_obj, "model_call_details", None) if logging_obj is not None else None
request_data_to_send = (
{
**request_data,
"litellm_logging_obj": dict(model_call_details)
if isinstance(model_call_details, dict)
else model_call_details,
}
if logging_obj is not None
else request_data
)
payload_request_data = self._sanitize_payload_for_transport(request_data_to_send)
payload: dict[str, Any] = {
"inputs": inputs,
@ -157,7 +166,7 @@ class NomaV2Guardrail(CustomGuardrail):
try:
json_str = json.dumps(payload, default=_default)
except (ValueError, TypeError):
except (ValueError, TypeError, RuntimeError):
json_str = safe_dumps(payload)
safe_payload = safe_json_loads(json_str, default={})

View file

@ -236,6 +236,46 @@ class TestNomaV2Configuration:
assert "logging_obj" not in payload
assert request_data["litellm_logging_obj"] == "<Logging object>"
def test_build_scan_payload_snapshots_live_model_call_details(
self, noma_v2_guardrail
):
"""Regression test: model_call_details is mutated by logging handlers on other
threads, so serializing it by reference raises RuntimeError('dictionary changed
size during iteration')."""
class _MutatingOnSerialize:
def __init__(self, target: dict) -> None:
self._target = target
def model_dump(self) -> dict:
self._target[f"late_key_{len(self._target)}"] = "written_by_logging"
return {"serialized": True}
model_call_details: dict = {
"model": "gpt-4.1-mini",
"call_type": "acompletion",
}
model_call_details["standard_callback_dynamic_params"] = _MutatingOnSerialize(
model_call_details
)
class _LoggingObj:
def __init__(self) -> None:
self.model_call_details = model_call_details
payload = noma_v2_guardrail._build_scan_payload(
inputs={"texts": ["hello"]},
request_data={"messages": [{"role": "user", "content": "hello"}]},
input_type="request",
logging_obj=_LoggingObj(),
application_id="test-app",
)
sent = payload["request_data"]["litellm_logging_obj"]
assert sent["model"] == "gpt-4.1-mini"
assert sent["standard_callback_dynamic_params"] == {"serialized": True}
assert not any(key.startswith("late_key_") for key in sent)
@pytest.mark.asyncio
async def test_call_noma_scan_sanitizes_response_model_dump_object(
self, noma_v2_guardrail