fix(proxy): preserve PII-redacted prompt in image generation requests

When a pre-call guardrail (e.g. Presidio) redacts data['prompt'] for
/v1/images/generations, the endpoint was unconditionally rebuilding
the prompt from messages, discarding the redaction. The upstream model
received the original unredacted text.

This preserves the redacted prompt by only rebuilding from messages
if the guardrail didn't modify it.

Fixes #46473
This commit is contained in:
Aishwary Dongre 2026-10-15 10:27:05 +05:30
parent c0663d5202
commit 11b31dc37a
2 changed files with 66 additions and 3 deletions

View file

@ -145,9 +145,12 @@ async def image_generation(
user_api_key_dict=user_api_key_dict, data=data, call_type="image_generation"
)
messages: Final = data.get("messages")
if isinstance(messages, list) and messages:
data["prompt"] = get_str_from_messages(messages)
# Guardrails may redact data["prompt"] directly. Only rebuild from messages if prompt wasn't modified.
prompt_after_hook: Final = data.get("prompt")
if prompt_after_hook == prompt_value:
messages: Final = data.get("messages")
if isinstance(messages, list) and messages:
data["prompt"] = get_str_from_messages(messages)
data.pop("messages", None)
## ROUTE TO CORRECT ENDPOINT ##

View file

@ -292,3 +292,63 @@ async def test_non_default_guardrail_skipped_for_image_generation():
assert (
guardrail.called is False
), "Opt-in guardrail should not fire without explicit request"
# ---------------------------------------------------------------------------
# 6. Pre-call PII guardrail redaction is preserved for image generation
# ---------------------------------------------------------------------------
class MaskingGuardrail(CustomGuardrail):
"""Guardrail that masks PII in the prompt (pre-call hook)."""
def __init__(self):
super().__init__(
guardrail_name="masking_guardrail",
default_on=True,
event_hook=GuardrailEventHooks.pre_call,
)
async def async_pre_call_hook(
self,
data: dict,
user_api_key_dict: UserAPIKeyAuth,
call_type: str,
) -> dict:
# Simulate PII redaction: replace SSN pattern with [REDACTED]
prompt = data.get("prompt", "")
if "123-45-6789" in prompt:
data["prompt"] = prompt.replace("123-45-6789", "[REDACTED_SSN]")
return data
@pytest.mark.asyncio
async def test_pre_call_pii_redaction_preserved_for_image_generation():
"""
Verify that when a pre-call guardrail redacts data["prompt"], the redacted
value is preserved and not overwritten by rebuilding from messages.
This tests the fix for issue #46473 where the image generation endpoint
was rebuilding prompt from messages after the guardrail ran, which
discarded the redacted value.
"""
guardrail = MaskingGuardrail()
original_prompt = "Generate an image for user SSN: 123-45-6789"
with patch("litellm.callbacks", [guardrail]):
proxy_logging = ProxyLogging(user_api_key_cache=DualCache())
data = {"model": "dall-e-3", "prompt": original_prompt}
user_api_key_dict = UserAPIKeyAuth(api_key="test-key")
# Run pre_call_hook (this is what the endpoint does)
result = await proxy_logging.pre_call_hook(
user_api_key_dict=user_api_key_dict,
data=data,
call_type="image_generation",
)
# The prompt should be redacted
assert result["prompt"] == "Generate an image for user SSN: [REDACTED_SSN]"
# Original prompt should NOT appear in the result
assert "123-45-6789" not in result["prompt"]