From 6c6dfcaee14b283fbff06f9f6408a0894b5f0694 Mon Sep 17 00:00:00 2001 From: crp4222 <121295348+crp4222@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:04:13 +0100 Subject: [PATCH] fix(guardrails): keep the privaite restore map out of background request state Background Responses results are fetched by a later poll that the post-call success and streaming hooks never see, so the fake-to-original map stashed in request metadata would persist in stored request state with no consumer left to pop it. The pre-call hook now skips the stash for background requests; those responses keep their placeholders, which is the fail-closed behavior. Regression test included. --- .../guardrail_hooks/privaite/privaite.py | 10 +++++++++- .../guardrails/guardrail_hooks/test_privaite.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/privaite/privaite.py b/litellm/proxy/guardrails/guardrail_hooks/privaite/privaite.py index 51aec492e51..460d478a9c6 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/privaite/privaite.py +++ b/litellm/proxy/guardrails/guardrail_hooks/privaite/privaite.py @@ -282,10 +282,18 @@ class PrivaiteGuardrail(CustomGuardrail): raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc - if mapping is not None and self.deanonymize and not mapping.is_empty: + if ( + mapping is not None + and self.deanonymize + and not mapping.is_empty + and not data.get("background") + ): # Carry a plain fake->original dict to the post-call hook on the same # request, the same channel LiteLLM's Presidio guardrail uses. The # post-call hook pops it again so it does not linger in metadata. + # Background requests are excluded: their result is fetched by a later + # poll the post-call hooks never see, so the map would persist in + # request state with no consumer; those responses keep placeholders. data.setdefault("metadata", {})["privaite_map"] = dict(mapping.get_all_fakes()) return data diff --git a/tests/unit/proxy/guardrails/guardrail_hooks/test_privaite.py b/tests/unit/proxy/guardrails/guardrail_hooks/test_privaite.py index bab8c633dc4..b7234a8e5fc 100644 --- a/tests/unit/proxy/guardrails/guardrail_hooks/test_privaite.py +++ b/tests/unit/proxy/guardrails/guardrail_hooks/test_privaite.py @@ -347,6 +347,21 @@ async def test_pre_call_without_pii_does_not_stash(): assert "privaite_map" not in out.get("metadata", {}) +@pytest.mark.asyncio +async def test_pre_call_background_request_anonymizes_but_does_not_stash(): + # Background results are fetched by a later poll the post-call hooks never + # see, so the fake->original map must not persist in request state. + gr = _make_guardrail() + data = { + "input": "Email Marie Dupont at marie@acme.com", + "background": True, + } + out = await gr.async_pre_call_hook(None, None, data, "aresponses") + assert "Marie Dupont" not in out["input"] + assert "" in out["input"] + assert "privaite_map" not in out.get("metadata", {}) + + @pytest.mark.asyncio async def test_pre_call_clears_client_supplied_map(): # metadata is caller-controlled: a client-supplied privaite_map must be