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.
This commit is contained in:
crp4222 2026-07-03 20:04:13 +01:00
parent 66eb3d56ad
commit 6c6dfcaee1
2 changed files with 24 additions and 1 deletions

View file

@ -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

View file

@ -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 "<PERSON_1>" 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