Files
litellm/tests/test_litellm/proxy/hooks/test_image_generation_guardrails.py
T
2026-02-11 15:45:47 +05:30

294 lines
9.6 KiB
Python

"""
Tests that guardrails (post_call_success_hook) fire for image generation requests.
The /images/generations endpoint in proxy/image_endpoints/endpoints.py calls
proxy_logging_obj.post_call_success_hook after a successful image generation.
These tests verify:
1. CustomGuardrail.async_post_call_success_hook is invoked for image generation.
2. A guardrail can inspect and transform the image response.
3. A guardrail that raises blocks the response (exception propagates).
"""
import os
import sys
from typing import Any, Optional
from unittest.mock import patch
import pytest
sys.path.insert(0, os.path.abspath("../../../.."))
import litellm
from litellm.caching.caching import DualCache
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.integrations.custom_logger import CustomLogger
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.utils import ProxyLogging
from litellm.types.guardrails import GuardrailEventHooks
from litellm.types.utils import ImageObject, ImageResponse
def _make_image_response(**kwargs) -> ImageResponse:
"""Helper to build a minimal ImageResponse for tests."""
return ImageResponse(
data=[ImageObject(url="https://example.com/img.png")],
**kwargs,
)
# ---------------------------------------------------------------------------
# 1. Hook is invoked for image generation responses
# ---------------------------------------------------------------------------
class TrackingGuardrail(CustomGuardrail):
"""Guardrail that records whether it was called and with what args."""
def __init__(self):
super().__init__(
guardrail_name="tracking_guardrail",
default_on=True,
event_hook=GuardrailEventHooks.post_call,
)
self.called = False
self.received_data: Optional[dict] = None
self.received_response: Optional[Any] = None
async def async_post_call_success_hook(
self,
data: dict,
user_api_key_dict: UserAPIKeyAuth,
response: Any,
) -> Any:
self.called = True
self.received_data = data
self.received_response = response
return response
@pytest.mark.asyncio
async def test_post_call_success_hook_invoked_for_image_generation():
"""
Verify that a default-on guardrail's async_post_call_success_hook is
called when ProxyLogging.post_call_success_hook is invoked with an
ImageResponse (the same path used by the /images/generations endpoint).
"""
guardrail = TrackingGuardrail()
image_response = _make_image_response()
with patch("litellm.callbacks", [guardrail]):
proxy_logging = ProxyLogging(user_api_key_cache=DualCache())
data = {"model": "dall-e-3", "prompt": "A sunset over mountains"}
user_api_key_dict = UserAPIKeyAuth(api_key="test-key")
result = await proxy_logging.post_call_success_hook(
data=data,
response=image_response,
user_api_key_dict=user_api_key_dict,
)
assert guardrail.called is True, "Guardrail hook was not invoked for image generation"
assert guardrail.received_data is not None
assert guardrail.received_data["model"] == "dall-e-3"
assert isinstance(guardrail.received_response, ImageResponse)
# The response should be passed through unchanged
assert result is image_response
# ---------------------------------------------------------------------------
# 2. Guardrail can transform image generation response
# ---------------------------------------------------------------------------
class TransformingGuardrail(CustomGuardrail):
"""Guardrail that replaces the image URL in the response."""
def __init__(self):
super().__init__(
guardrail_name="transforming_guardrail",
default_on=True,
event_hook=GuardrailEventHooks.post_call,
)
async def async_post_call_success_hook(
self,
data: dict,
user_api_key_dict: UserAPIKeyAuth,
response: Any,
) -> Any:
# Return a modified image response (e.g., watermarked URL)
return ImageResponse(
data=[ImageObject(url="https://example.com/watermarked.png")],
)
@pytest.mark.asyncio
async def test_guardrail_can_transform_image_response():
"""
Verify that a guardrail can replace the ImageResponse returned to the client.
"""
guardrail = TransformingGuardrail()
original_response = _make_image_response()
with patch("litellm.callbacks", [guardrail]):
proxy_logging = ProxyLogging(user_api_key_cache=DualCache())
data = {"model": "dall-e-3", "prompt": "A sunset"}
user_api_key_dict = UserAPIKeyAuth(api_key="test-key")
result = await proxy_logging.post_call_success_hook(
data=data,
response=original_response,
user_api_key_dict=user_api_key_dict,
)
assert result is not original_response
assert isinstance(result, ImageResponse)
assert result.data[0].url == "https://example.com/watermarked.png"
# ---------------------------------------------------------------------------
# 3. Guardrail that raises blocks the image response
# ---------------------------------------------------------------------------
class BlockingGuardrail(CustomGuardrail):
"""Guardrail that raises on unsafe image prompts."""
def __init__(self):
super().__init__(
guardrail_name="blocking_guardrail",
default_on=True,
event_hook=GuardrailEventHooks.post_call,
)
async def async_post_call_success_hook(
self,
data: dict,
user_api_key_dict: UserAPIKeyAuth,
response: Any,
) -> Any:
raise ValueError("Image content blocked by guardrail")
@pytest.mark.asyncio
async def test_guardrail_exception_propagates_for_image_generation():
"""
Verify that an exception raised in a guardrail's post_call_success_hook
propagates up (the proxy endpoint wraps this in an error response).
"""
guardrail = BlockingGuardrail()
with patch("litellm.callbacks", [guardrail]):
proxy_logging = ProxyLogging(user_api_key_cache=DualCache())
data = {"model": "dall-e-3", "prompt": "Something unsafe"}
user_api_key_dict = UserAPIKeyAuth(api_key="test-key")
with pytest.raises(ValueError, match="Image content blocked by guardrail"):
await proxy_logging.post_call_success_hook(
data=data,
response=_make_image_response(),
user_api_key_dict=user_api_key_dict,
)
# ---------------------------------------------------------------------------
# 4. Non-guardrail CustomLogger also fires for image generation
# ---------------------------------------------------------------------------
class TrackingLogger(CustomLogger):
"""Plain CustomLogger (not a guardrail) that tracks invocations."""
def __init__(self):
self.called = False
self.received_response = None
async def async_post_call_success_hook(
self,
data: dict,
user_api_key_dict: UserAPIKeyAuth,
response: Any,
) -> Any:
self.called = True
self.received_response = response
return response
@pytest.mark.asyncio
async def test_custom_logger_post_call_success_hook_fires_for_image_generation():
"""
Verify that a plain CustomLogger (non-guardrail) callback also has its
async_post_call_success_hook invoked for image generation responses.
"""
logger = TrackingLogger()
image_response = _make_image_response()
with patch("litellm.callbacks", [logger]):
proxy_logging = ProxyLogging(user_api_key_cache=DualCache())
data = {"model": "dall-e-3", "prompt": "A cat"}
user_api_key_dict = UserAPIKeyAuth(api_key="test-key")
result = await proxy_logging.post_call_success_hook(
data=data,
response=image_response,
user_api_key_dict=user_api_key_dict,
)
assert logger.called is True
assert isinstance(logger.received_response, ImageResponse)
assert result is image_response
# ---------------------------------------------------------------------------
# 5. Guardrail with should_run_guardrail=False is skipped
# ---------------------------------------------------------------------------
class OptInGuardrail(CustomGuardrail):
"""Guardrail that is NOT default_on, so it only runs if explicitly requested."""
def __init__(self):
super().__init__(
guardrail_name="opt_in_guardrail",
default_on=False,
event_hook=GuardrailEventHooks.post_call,
)
self.called = False
async def async_post_call_success_hook(
self,
data: dict,
user_api_key_dict: UserAPIKeyAuth,
response: Any,
) -> Any:
self.called = True
return response
@pytest.mark.asyncio
async def test_non_default_guardrail_skipped_for_image_generation():
"""
Verify that a guardrail with default_on=False is NOT invoked for image
generation unless the request explicitly enables it.
"""
guardrail = OptInGuardrail()
with patch("litellm.callbacks", [guardrail]):
proxy_logging = ProxyLogging(user_api_key_cache=DualCache())
# No guardrails key in data -> should_run_guardrail returns False
data = {"model": "dall-e-3", "prompt": "A sunset"}
user_api_key_dict = UserAPIKeyAuth(api_key="test-key")
await proxy_logging.post_call_success_hook(
data=data,
response=_make_image_response(),
user_api_key_dict=user_api_key_dict,
)
assert guardrail.called is False, "Opt-in guardrail should not fire without explicit request"