From c35b0e3714b1eb5c4b0263364c4bba4b968f4c65 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 25 Apr 2026 23:43:08 -0500 Subject: [PATCH] fix(guardrails/peyeeye): wrap json decode errors, simplify session cleanup - Move response.json() inside the try/except in _post so JSON decode failures surface as PEyeEyeGuardrailAPIError rather than a raw ValueError. - Drop the redundant ses_-prefix check in async_post_call_success_hook; peyeeye_session_mode == 'stateful' is already the gate, and the prefix check skipped legitimate cleanup if the API ever returned a different id shape. - Add test_redact_json_decode_error_raises_typed covering the new wrapping branch. --- .../guardrail_hooks/peyeeye/peyeeye.py | 7 +++--- .../guardrail_hooks/test_peyeeye.py | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/peyeeye/peyeeye.py b/litellm/proxy/guardrails/guardrail_hooks/peyeeye/peyeeye.py index f86d0b3f22b..9a9776a1610 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/peyeeye/peyeeye.py +++ b/litellm/proxy/guardrails/guardrail_hooks/peyeeye/peyeeye.py @@ -211,8 +211,9 @@ class PEyeEyeGuardrail(CustomGuardrail): new_parts.append(part) message.content = new_parts - # Clean up: drop the session server-side and from cache. - if self.peyeeye_session_mode == "stateful" and session_id.startswith("ses_"): + # Clean up: drop the stateful session server-side. Stateless + # ``skey_…`` blobs hold no server-side state, so skip the DELETE. + if self.peyeeye_session_mode == "stateful": try: await self._delete_session(session_id) except Exception as e: @@ -288,9 +289,9 @@ class PEyeEyeGuardrail(CustomGuardrail): url=url, headers=self._headers(), json=body, timeout=15.0 ) response.raise_for_status() + return response.json() except Exception as e: self._reraise_api_error(e, path) - return response.json() def _headers(self) -> Dict[str, str]: return { diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_peyeeye.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_peyeeye.py index bc33c2218b6..9359681c158 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_peyeeye.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_peyeeye.py @@ -236,6 +236,30 @@ async def test_redact_api_error_raises_typed(): ) +@pytest.mark.asyncio +async def test_redact_json_decode_error_raises_typed(): + """If response.json() blows up, surface a typed PEyeEyeGuardrailAPIError.""" + g = PEyeEyeGuardrail(peyeeye_api_key="pk", guardrail_name="t") + g.async_handler = MagicMock() + + bad = MagicMock() + bad.raise_for_status = MagicMock() + bad.json.side_effect = ValueError("not json") + g.async_handler.post = AsyncMock(return_value=bad) + + cache = DualCache() + with pytest.raises(PEyeEyeGuardrailAPIError): + await g.async_pre_call_hook( + UserAPIKeyAuth(api_key="x"), + cache, + { + "messages": [{"role": "user", "content": "hi"}], + "litellm_call_id": "json-err", + }, + "completion", + ) + + @pytest.mark.asyncio async def test_pre_call_raises_on_length_mismatch(): """If /v1/redact returns fewer texts than sent, refuse to forward."""