Merge pull request #18957 from BerriAI/litellm_respones_api_gaurdrails1

Fix: gaurdrail moderation support with responses API
This commit is contained in:
Sameer Kankute 2026-01-12 18:16:22 +05:30 committed by GitHub
commit 8b5e2bcf25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 134 additions and 8 deletions

View file

@ -178,6 +178,33 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail):
},
)
def _extract_user_content_from_data(self, data: Dict[str, Any]) -> Optional[str]:
"""
Extract user content from request data, supporting both Chat Completions and Responses API.
For Chat Completions: extracts from 'messages' field
For Responses API: extracts from 'input' field
Returns:
The extracted user content string, or None if no content found
"""
# Try to get messages first (Chat Completions API)
messages: Optional[List["AllMessageValues"]] = data.get("messages")
if messages is not None:
return self.get_user_prompt(messages)
# Try to get input (Responses API)
input_data = data.get("input")
if input_data is not None:
# input can be a string or a list of message-like objects
if isinstance(input_data, str):
return input_data
elif isinstance(input_data, list):
# Treat input as messages and extract user content
return self.get_user_prompt(input_data)
return None
@log_guardrail_information
async def async_pre_call_hook(
self,
@ -210,14 +237,14 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail):
if call_type == "moderation":
return data
new_messages: Optional[List["AllMessageValues"]] = data.get("messages")
if new_messages is None:
user_prompt = self._extract_user_content_from_data(data)
if user_prompt is None:
verbose_proxy_logger.warning(
"OpenAI Moderation: not running guardrail. No messages in data"
"OpenAI Moderation: not running guardrail. No messages or input in data"
)
return data
user_prompt = self.get_user_prompt(new_messages)
if user_prompt:
verbose_proxy_logger.debug(
f"OpenAI Moderation: User prompt: {user_prompt[:100]}..." # Log first 100 chars for debugging
@ -265,14 +292,15 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail):
if call_type == "moderation":
return data
new_messages: Optional[List["AllMessageValues"]] = data.get("messages")
if new_messages is None:
# Extract user content from either messages or input field
user_prompt = self._extract_user_content_from_data(data)
if user_prompt is None:
verbose_proxy_logger.warning(
"OpenAI Moderation: not running guardrail. No messages in data"
"OpenAI Moderation: not running guardrail. No messages or input in data"
)
return data
user_prompt = self.get_user_prompt(new_messages)
if user_prompt:
moderation_response = await self.async_make_request(
input_text=user_prompt,

View file

@ -85,3 +85,101 @@ async def test_openai_moderation_error_raising(monkeypatch):
print("Got exception: ", e)
assert "Violated content safety policy" in str(e)
pass
@pytest.mark.asyncio
async def test_openai_moderation_responses_api_input_field():
"""
Tests that OpenAI Moderation works with Responses API input field.
This test verifies the fix for the issue where moderation was skipped
for Responses API because it only checked for 'messages' field but
Responses API uses 'input' field instead.
"""
from unittest.mock import AsyncMock, MagicMock, patch
from litellm.types.llms.openai import (
OpenAIModerationResponse,
OpenAIModerationResult,
)
from litellm.proxy.guardrails.guardrail_hooks.openai.moderations import (
OpenAIModerationGuardrail,
)
# Initialize the open-source OpenAI Moderation guardrail
openai_mod = OpenAIModerationGuardrail(
guardrail_name="openai-moderation-test",
api_key="fake-key-for-testing",
model="omni-moderation-latest",
)
_api_key = "sk-12345"
_api_key = hash_token("sk-12345")
user_api_key_dict = UserAPIKeyAuth(api_key=_api_key)
# Mock the async_make_request to return a flagged response
mock_moderation_response = OpenAIModerationResponse(
id="modr-123",
model="omni-moderation-latest",
results=[
OpenAIModerationResult(
flagged=True,
categories={"violence": True, "hate": False},
category_scores={"violence": 0.95, "hate": 0.1},
category_applied_input_types=None,
)
],
)
with patch.object(
openai_mod, "async_make_request", return_value=mock_moderation_response
):
# Test 1: Responses API with input as string
try:
await openai_mod.async_moderation_hook(
data={
"model": "gpt-4o",
"input": "I want to hurt people",
},
user_api_key_dict=user_api_key_dict,
call_type="responses",
)
pytest.fail("Should have raised HTTPException for flagged content")
except Exception as e:
print("Got exception for string input: ", e)
assert "Violated OpenAI moderation policy" in str(e)
# Test 2: Responses API with input as list of messages
try:
await openai_mod.async_moderation_hook(
data={
"model": "gpt-4o",
"input": [
{"role": "user", "content": "I want to hurt people"}
],
},
user_api_key_dict=user_api_key_dict,
call_type="responses",
)
pytest.fail("Should have raised HTTPException for flagged content")
except Exception as e:
print("Got exception for list input: ", e)
assert "Violated OpenAI moderation policy" in str(e)
# Test 3: Verify it still works with messages field (Chat Completions)
try:
await openai_mod.async_moderation_hook(
data={
"model": "gpt-4o",
"messages": [
{"role": "user", "content": "I want to hurt people"}
],
},
user_api_key_dict=user_api_key_dict,
call_type="completion",
)
pytest.fail("Should have raised HTTPException for flagged content")
except Exception as e:
print("Got exception for messages field: ", e)
assert "Violated OpenAI moderation policy" in str(e)
print("✓ All Responses API moderation tests passed!")