Guardrails - add logging to all unified_guardrails + link to custom code guardrail templates (#20900)

* feat(guardrail_hooks/): add guardrail logging to all unified guardrails

ensures unified guardrails use the 'log_guardrail_information' decorator for logging

* fix(custom_guardrail.py): don't log inputs on guardrail response - just emit state

* refactor: don't double log bedrock guardrail information

* feat: add in-product nudges for contributing + trying community custom code guardrails

allows users to contribute / share custom code guardrails
This commit is contained in:
Krish Dholakia 2026-02-10 15:13:54 -08:00 • committed by GitHub
parent a6f90586ac
commit 10d891a365
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 418 additions and 116 deletions

View file

@ -2277,6 +2277,7 @@ jobs:
- run: python ./tests/code_coverage_tests/router_code_coverage.py - run: python ./tests/code_coverage_tests/router_code_coverage.py
- run: python ./tests/code_coverage_tests/test_chat_completion_imports.py - run: python ./tests/code_coverage_tests/test_chat_completion_imports.py
- run: python ./tests/code_coverage_tests/info_log_check.py - run: python ./tests/code_coverage_tests/info_log_check.py
- run: python ./tests/code_coverage_tests/check_guardrail_apply_decorator.py
- run: python ./tests/code_coverage_tests/test_ban_set_verbose.py - run: python ./tests/code_coverage_tests/test_ban_set_verbose.py
- run: python ./tests/code_coverage_tests/code_qa_check_tests.py - run: python ./tests/code_coverage_tests/code_qa_check_tests.py
- run: python ./tests/code_coverage_tests/check_get_model_cost_key_performance.py - run: python ./tests/code_coverage_tests/check_get_model_cost_key_performance.py

View file

@ -616,6 +616,7 @@ class CustomGuardrail(CustomLogger):
end_time: Optional[float] = None, end_time: Optional[float] = None,
duration: Optional[float] = None, duration: Optional[float] = None,
event_type: Optional[GuardrailEventHooks] = None, event_type: Optional[GuardrailEventHooks] = None,
original_inputs: Optional[Dict] = None,
): ):
""" """
Add StandardLoggingGuardrailInformation to the request data Add StandardLoggingGuardrailInformation to the request data
@ -625,6 +626,17 @@ class CustomGuardrail(CustomLogger):
# Convert None to empty dict to satisfy type requirements # Convert None to empty dict to satisfy type requirements
guardrail_response = {} if response is None else response guardrail_response = {} if response is None else response
# For apply_guardrail functions in custom_code_guardrail scenario,
# simplify the logged response to "allow", "deny", or "mask"
if original_inputs is not None and isinstance(response, dict):
# Check if inputs were modified by comparing them
if self._inputs_were_modified(original_inputs, response):
guardrail_response = "mask"
else:
guardrail_response = "allow"
verbose_logger.debug(f"Guardrail response: {response}")
self.add_standard_logging_guardrail_information_to_request_data( self.add_standard_logging_guardrail_information_to_request_data(
guardrail_json_response=guardrail_response, guardrail_json_response=guardrail_response,
request_data=request_data, request_data=request_data,
@ -650,8 +662,14 @@ class CustomGuardrail(CustomLogger):
This gets logged on downsteam Langfuse, DataDog, etc. This gets logged on downsteam Langfuse, DataDog, etc.
""" """
# For custom_code_guardrail scenario, log as "deny" instead of full exception
# Check if this is from custom_code_guardrail by checking the class name
guardrail_response: Union[Exception, str] = e
if "CustomCodeGuardrail" in self.__class__.__name__:
guardrail_response = "deny"
self.add_standard_logging_guardrail_information_to_request_data( self.add_standard_logging_guardrail_information_to_request_data(
guardrail_json_response=e, guardrail_json_response=guardrail_response,
request_data=request_data, request_data=request_data,
guardrail_status="guardrail_failed_to_respond", guardrail_status="guardrail_failed_to_respond",
duration=duration, duration=duration,
@ -661,6 +679,25 @@ class CustomGuardrail(CustomLogger):
) )
raise e raise e
def _inputs_were_modified(self, original_inputs: Dict, response: Dict) -> bool:
"""
Compare original inputs with response to determine if content was modified.
Returns True if the inputs were modified (mask scenario), False otherwise (allow scenario).
"""
# Get all keys from both dictionaries
all_keys = set(original_inputs.keys()) | set(response.keys())
# Compare each key's value
for key in all_keys:
original_value = original_inputs.get(key)
response_value = response.get(key)
if original_value != response_value:
return True
# No modifications detected
return False
def mask_content_in_string( def mask_content_in_string(
self, self,
content_string: str, content_string: str,
@ -768,6 +805,12 @@ def log_guardrail_information(func):
self: CustomGuardrail = args[0] self: CustomGuardrail = args[0]
request_data: dict = kwargs.get("data") or kwargs.get("request_data") or {} request_data: dict = kwargs.get("data") or kwargs.get("request_data") or {}
event_type = _infer_event_type_from_function_name(func.__name__) event_type = _infer_event_type_from_function_name(func.__name__)
# Store original inputs for comparison (for apply_guardrail functions)
original_inputs = None
if func.__name__ == "apply_guardrail" and "inputs" in kwargs:
original_inputs = kwargs.get("inputs")
try: try:
response = await func(*args, **kwargs) response = await func(*args, **kwargs)
return self._process_response( return self._process_response(
@ -777,6 +820,7 @@ def log_guardrail_information(func):
end_time=datetime.now().timestamp(), end_time=datetime.now().timestamp(),
duration=(datetime.now() - start_time).total_seconds(), duration=(datetime.now() - start_time).total_seconds(),
event_type=event_type, event_type=event_type,
original_inputs=original_inputs,
) )
except Exception as e: except Exception as e:
return self._process_error( return self._process_error(
@ -794,6 +838,12 @@ def log_guardrail_information(func):
self: CustomGuardrail = args[0] self: CustomGuardrail = args[0]
request_data: dict = kwargs.get("data") or kwargs.get("request_data") or {} request_data: dict = kwargs.get("data") or kwargs.get("request_data") or {}
event_type = _infer_event_type_from_function_name(func.__name__) event_type = _infer_event_type_from_function_name(func.__name__)
# Store original inputs for comparison (for apply_guardrail functions)
original_inputs = None
if func.__name__ == "apply_guardrail" and "inputs" in kwargs:
original_inputs = kwargs.get("inputs")
try: try:
response = func(*args, **kwargs) response = func(*args, **kwargs)
return self._process_response( return self._process_response(
@ -801,6 +851,7 @@ def log_guardrail_information(func):
request_data=request_data, request_data=request_data,
duration=(datetime.now() - start_time).total_seconds(), duration=(datetime.now() - start_time).total_seconds(),
event_type=event_type, event_type=event_type,
original_inputs=original_inputs,
) )
except Exception as e: except Exception as e:
return self._process_error( return self._process_error(

View file

@ -795,9 +795,9 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
######################################################### #########################################################
########## 1. Make the Bedrock API request ########## ########## 1. Make the Bedrock API request ##########
######################################################### #########################################################
bedrock_guardrail_response: Optional[ bedrock_guardrail_response: Optional[Union[BedrockGuardrailResponse, str]] = (
Union[BedrockGuardrailResponse, str] None
] = None )
try: try:
bedrock_guardrail_response = await self.make_bedrock_api_request( bedrock_guardrail_response = await self.make_bedrock_api_request(
source="INPUT", messages=filtered_messages, request_data=data source="INPUT", messages=filtered_messages, request_data=data
@ -867,9 +867,9 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
######################################################### #########################################################
########## 1. Make the Bedrock API request ########## ########## 1. Make the Bedrock API request ##########
######################################################### #########################################################
bedrock_guardrail_response: Optional[ bedrock_guardrail_response: Optional[Union[BedrockGuardrailResponse, str]] = (
Union[BedrockGuardrailResponse, str] None
] = None )
try: try:
bedrock_guardrail_response = await self.make_bedrock_api_request( bedrock_guardrail_response = await self.make_bedrock_api_request(
source="INPUT", messages=filtered_messages, request_data=data source="INPUT", messages=filtered_messages, request_data=data

View file

@ -35,7 +35,10 @@ from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Type, cast
from fastapi import HTTPException from fastapi import HTTPException
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.integrations.custom_guardrail import (
CustomGuardrail,
log_guardrail_information,
)
from litellm.types.guardrails import GuardrailEventHooks from litellm.types.guardrails import GuardrailEventHooks
from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel
from litellm.types.utils import GenericGuardrailAPIInputs from litellm.types.utils import GenericGuardrailAPIInputs
@ -179,6 +182,7 @@ class CustomCodeGuardrail(CustomGuardrail):
self._compile_error = f"Failed to compile custom code: {e}" self._compile_error = f"Failed to compile custom code: {e}"
raise CustomCodeCompilationError(self._compile_error) from e raise CustomCodeCompilationError(self._compile_error) from e
@log_guardrail_information
async def apply_guardrail( async def apply_guardrail(
self, self,
inputs: GenericGuardrailAPIInputs, inputs: GenericGuardrailAPIInputs,

View file

@ -23,7 +23,10 @@ import httpx
import litellm import litellm
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.caching.caching import DualCache from litellm.caching.caching import DualCache
from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.integrations.custom_guardrail import (
CustomGuardrail,
log_guardrail_information,
)
from litellm.llms.custom_httpx.http_handler import ( from litellm.llms.custom_httpx.http_handler import (
get_async_httpx_client, get_async_httpx_client,
httpxSpecialProvider, httpxSpecialProvider,
@ -483,6 +486,7 @@ class EnkryptAIGuardrails(CustomGuardrail):
request_data=data, guardrail_name=self.guardrail_name request_data=data, guardrail_name=self.guardrail_name
) )
@log_guardrail_information
async def apply_guardrail( async def apply_guardrail(
self, self,
inputs: "GenericGuardrailAPIInputs", inputs: "GenericGuardrailAPIInputs",

View file

@ -10,7 +10,10 @@ from typing import TYPE_CHECKING, Any, Dict, Literal, Optional
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.exceptions import GuardrailRaisedException from litellm.exceptions import GuardrailRaisedException
from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.integrations.custom_guardrail import (
CustomGuardrail,
log_guardrail_information,
)
from litellm.llms.custom_httpx.http_handler import ( from litellm.llms.custom_httpx.http_handler import (
get_async_httpx_client, get_async_httpx_client,
httpxSpecialProvider, httpxSpecialProvider,
@ -150,6 +153,7 @@ class GenericGuardrailAPI(CustomGuardrail):
return result_metadata return result_metadata
@log_guardrail_information
async def apply_guardrail( async def apply_guardrail(
self, self,
inputs: GenericGuardrailAPIInputs, inputs: GenericGuardrailAPIInputs,

View file

@ -9,7 +9,8 @@ from fastapi import HTTPException
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.integrations.custom_guardrail import ( from litellm.integrations.custom_guardrail import (
CustomGuardrail, CustomGuardrail,
ModifyResponseException ModifyResponseException,
log_guardrail_information,
) )
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.litellm_core_utils.safe_json_loads import safe_json_loads from litellm.litellm_core_utils.safe_json_loads import safe_json_loads
@ -108,7 +109,9 @@ class GraySwanGuardrail(CustomGuardrail):
self.categories = categories self.categories = categories
self.policy_id = policy_id self.policy_id = policy_id
self.fail_open = True if fail_open is None else bool(fail_open) self.fail_open = True if fail_open is None else bool(fail_open)
self.guardrail_timeout = 30.0 if guardrail_timeout is None else float(guardrail_timeout) self.guardrail_timeout = (
30.0 if guardrail_timeout is None else float(guardrail_timeout)
)
# Streaming configuration # Streaming configuration
self.streaming_end_of_stream_only = streaming_end_of_stream_only self.streaming_end_of_stream_only = streaming_end_of_stream_only
@ -155,6 +158,7 @@ class GraySwanGuardrail(CustomGuardrail):
# Unified Guardrail Interface (works with ALL endpoints automatically) # Unified Guardrail Interface (works with ALL endpoints automatically)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@log_guardrail_information
async def apply_guardrail( async def apply_guardrail(
self, self,
inputs: GenericGuardrailAPIInputs, inputs: GenericGuardrailAPIInputs,
@ -208,7 +212,9 @@ class GraySwanGuardrail(CustomGuardrail):
messages = [{"role": role, "content": text} for text in texts] messages = [{"role": role, "content": text} for text in texts]
# Get dynamic params from request metadata # Get dynamic params from request metadata
dynamic_body = self.get_guardrail_dynamic_request_body_params(request_data) or {} dynamic_body = (
self.get_guardrail_dynamic_request_body_params(request_data) or {}
)
if dynamic_body: if dynamic_body:
verbose_proxy_logger.debug( verbose_proxy_logger.debug(
"Gray Swan Guardrail: dynamic extra_body=%s", safe_dumps(dynamic_body) "Gray Swan Guardrail: dynamic extra_body=%s", safe_dumps(dynamic_body)
@ -365,7 +371,10 @@ class GraySwanGuardrail(CustomGuardrail):
) )
# If hook_type is provided and in pre/during call, raise exception # If hook_type is provided and in pre/during call, raise exception
if hook_type in [GuardrailEventHooks.pre_call, GuardrailEventHooks.during_call]: if hook_type in [
GuardrailEventHooks.pre_call,
GuardrailEventHooks.during_call,
]:
# Raise ModifyResponseException to short-circuit LLM call # Raise ModifyResponseException to short-circuit LLM call
if data is None: if data is None:
data = {} data = {}
@ -540,7 +549,9 @@ class GraySwanGuardrail(CustomGuardrail):
if isinstance(litellm_metadata, dict) and litellm_metadata: if isinstance(litellm_metadata, dict) and litellm_metadata:
cleaned_litellm_metadata = dict(litellm_metadata) cleaned_litellm_metadata = dict(litellm_metadata)
# cleaned_litellm_metadata.pop("user_api_key_auth", None) # cleaned_litellm_metadata.pop("user_api_key_auth", None)
sanitized = safe_json_loads(safe_dumps(cleaned_litellm_metadata), default={}) sanitized = safe_json_loads(
safe_dumps(cleaned_litellm_metadata), default={}
)
if isinstance(sanitized, dict) and sanitized: if isinstance(sanitized, dict) and sanitized:
payload["litellm_metadata"] = sanitized payload["litellm_metadata"] = sanitized
@ -566,7 +577,9 @@ class GraySwanGuardrail(CustomGuardrail):
detection_info = detection_info[0] detection_info = detection_info[0]
# Extract fields from detection_info dict # Extract fields from detection_info dict
detection_dict: dict = detection_info if isinstance(detection_info, dict) else {} detection_dict: dict = (
detection_info if isinstance(detection_info, dict) else {}
)
violation_score = detection_dict.get("violation_score", 0.0) violation_score = detection_dict.get("violation_score", 0.0)
violated_rules = detection_dict.get("violated_rules", []) violated_rules = detection_dict.get("violated_rules", [])
mutation = detection_dict.get("mutation", False) mutation = detection_dict.get("mutation", False)
@ -582,7 +595,9 @@ class GraySwanGuardrail(CustomGuardrail):
if violated_rules: if violated_rules:
formatted_rules = self._format_violated_rules(violated_rules) formatted_rules = self._format_violated_rules(violated_rules)
if formatted_rules: if formatted_rules:
message_parts.append(f"It was violating the rule(s): {formatted_rules}.") message_parts.append(
f"It was violating the rule(s): {formatted_rules}."
)
if mutation: if mutation:
message_parts.append( message_parts.append(
@ -590,9 +605,7 @@ class GraySwanGuardrail(CustomGuardrail):
) )
if ipi: if ipi:
message_parts.append( message_parts.append("Indirect Prompt Injection was DETECTED.")
"Indirect Prompt Injection was DETECTED."
)
return "\n".join(message_parts) return "\n".join(message_parts)

View file

@ -10,7 +10,10 @@ from httpx import HTTPStatusError
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.integrations.custom_guardrail import (
CustomGuardrail,
log_guardrail_information,
)
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.custom_httpx.http_handler import ( from litellm.llms.custom_httpx.http_handler import (
get_async_httpx_client, get_async_httpx_client,
@ -110,6 +113,7 @@ class HiddenlayerGuardrail(CustomGuardrail):
) )
super().__init__(**kwargs) super().__init__(**kwargs)
@log_guardrail_information
async def apply_guardrail( async def apply_guardrail(
self, self,
inputs: GenericGuardrailAPIInputs, inputs: GenericGuardrailAPIInputs,

View file

@ -28,7 +28,10 @@ from fastapi import HTTPException
from litellm import Router from litellm import Router
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.integrations.custom_guardrail import (
CustomGuardrail,
log_guardrail_information,
)
from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy._types import UserAPIKeyAuth
from litellm.types.utils import ModelResponseStream from litellm.types.utils import ModelResponseStream
@ -50,6 +53,7 @@ from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter impor
ContentFilterDetection, ContentFilterDetection,
PatternDetection, PatternDetection,
) )
from .patterns import PATTERN_EXTRA_CONFIG, get_compiled_pattern from .patterns import PATTERN_EXTRA_CONFIG, get_compiled_pattern
MAX_KEYWORD_VALUE_GAP_WORDS = 1 MAX_KEYWORD_VALUE_GAP_WORDS = 1
@ -168,9 +172,9 @@ class ContentFilterGuardrail(CustomGuardrail):
self.image_model = image_model self.image_model = image_model
# Store loaded categories # Store loaded categories
self.loaded_categories: Dict[str, CategoryConfig] = {} self.loaded_categories: Dict[str, CategoryConfig] = {}
self.category_keywords: Dict[ self.category_keywords: Dict[str, Tuple[str, str, ContentFilterAction]] = (
str, Tuple[str, str, ContentFilterAction] {}
] = {} # keyword -> (category, severity, action) ) # keyword -> (category, severity, action)
# Load categories if provided # Load categories if provided
if categories: if categories:
@ -994,6 +998,7 @@ class ContentFilterGuardrail(CustomGuardrail):
masked_entity_count=masked_entity_count, masked_entity_count=masked_entity_count,
) )
@log_guardrail_information
async def apply_guardrail( async def apply_guardrail(
self, self,
inputs: "GenericGuardrailAPIInputs", inputs: "GenericGuardrailAPIInputs",

View file

@ -12,7 +12,10 @@ import httpx
from fastapi import HTTPException from fastapi import HTTPException
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.integrations.custom_guardrail import (
CustomGuardrail,
log_guardrail_information,
)
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.custom_httpx.http_handler import ( from litellm.llms.custom_httpx.http_handler import (
get_async_httpx_client, get_async_httpx_client,
@ -26,7 +29,11 @@ if TYPE_CHECKING:
class OnyxGuardrail(CustomGuardrail): class OnyxGuardrail(CustomGuardrail):
def __init__( def __init__(
self, api_base: Optional[str] = None, api_key: Optional[str] = None, timeout: Optional[float] = 10.0, **kwargs self,
api_base: Optional[str] = None,
api_key: Optional[str] = None,
timeout: Optional[float] = 10.0,
**kwargs,
): ):
timeout = timeout or int(os.getenv("ONYX_TIMEOUT", 10.0)) timeout = timeout or int(os.getenv("ONYX_TIMEOUT", 10.0))
self.async_handler = get_async_httpx_client( self.async_handler = get_async_httpx_client(
@ -79,6 +86,7 @@ class OnyxGuardrail(CustomGuardrail):
) )
return result return result
@log_guardrail_information
async def apply_guardrail( async def apply_guardrail(
self, self,
inputs: GenericGuardrailAPIInputs, inputs: GenericGuardrailAPIInputs,

View file

@ -58,7 +58,9 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail):
guardrail_name: str, guardrail_name: str,
api_key: Optional[str] = None, api_key: Optional[str] = None,
api_base: Optional[str] = None, api_base: Optional[str] = None,
model: Optional[Literal["omni-moderation-latest", "text-moderation-latest"]] = None, model: Optional[
Literal["omni-moderation-latest", "text-moderation-latest"]
] = None,
**kwargs, **kwargs,
): ):
"""Initialize OpenAI Moderation guardrail handler.""" """Initialize OpenAI Moderation guardrail handler."""
@ -83,10 +85,14 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail):
# Store configuration # Store configuration
self.api_key = api_key or self._get_api_key() self.api_key = api_key or self._get_api_key()
self.api_base = api_base or "https://api.openai.com/v1" self.api_base = api_base or "https://api.openai.com/v1"
self.model: Literal["omni-moderation-latest", "text-moderation-latest"] = model or "omni-moderation-latest" self.model: Literal["omni-moderation-latest", "text-moderation-latest"] = (
model or "omni-moderation-latest"
)
if not self.api_key: if not self.api_key:
raise ValueError("OpenAI Moderation: api_key is required. Set OPENAI_API_KEY environment variable or pass it in configuration.") raise ValueError(
"OpenAI Moderation: api_key is required. Set OPENAI_API_KEY environment variable or pass it in configuration."
)
verbose_proxy_logger.debug( verbose_proxy_logger.debug(
f"Initialized OpenAI Moderation Guardrail: {guardrail_name} with model: {self.model}" f"Initialized OpenAI Moderation Guardrail: {guardrail_name} with model: {self.model}"
@ -106,20 +112,13 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail):
or get_secret_str("OPENAI_API_KEY") or get_secret_str("OPENAI_API_KEY")
) )
async def async_make_request( async def async_make_request(self, input_text: str) -> "OpenAIModerationResponse":
self, input_text: str
) -> "OpenAIModerationResponse":
""" """
Make a request to the OpenAI Moderation API. Make a request to the OpenAI Moderation API.
""" """
request_body = { request_body = {"model": self.model, "input": input_text}
"model": self.model,
"input": input_text
}
verbose_proxy_logger.debug( verbose_proxy_logger.debug("OpenAI Moderation guard request: %s", request_body)
"OpenAI Moderation guard request: %s", request_body
)
response = await self.async_handler.post( response = await self.async_handler.post(
url=f"{self.api_base}/moderations", url=f"{self.api_base}/moderations",
@ -144,9 +143,12 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail):
) )
from litellm.types.llms.openai import OpenAIModerationResponse from litellm.types.llms.openai import OpenAIModerationResponse
return OpenAIModerationResponse(**response.json()) return OpenAIModerationResponse(**response.json())
def _check_moderation_result(self, moderation_response: "OpenAIModerationResponse") -> None: def _check_moderation_result(
self, moderation_response: "OpenAIModerationResponse"
) -> None:
""" """
Check if the moderation response indicates harmful content and raise exception if needed. Check if the moderation response indicates harmful content and raise exception if needed.
""" """
@ -169,7 +171,7 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail):
verbose_proxy_logger.warning( verbose_proxy_logger.warning(
"OpenAI Moderation: Content flagged for violations: %s", "OpenAI Moderation: Content flagged for violations: %s",
violation_details violation_details,
) )
raise HTTPException( raise HTTPException(
@ -180,6 +182,7 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail):
}, },
) )
@log_guardrail_information
async def apply_guardrail( async def apply_guardrail(
self, self,
inputs: GenericGuardrailAPIInputs, inputs: GenericGuardrailAPIInputs,
@ -233,7 +236,6 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail):
# Moderation doesn't modify content, just blocks - return inputs unchanged # Moderation doesn't modify content, just blocks - return inputs unchanged
return inputs return inputs
@log_guardrail_information @log_guardrail_information
async def async_post_call_streaming_iterator_hook( async def async_post_call_streaming_iterator_hook(
self, self,
@ -252,9 +254,7 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail):
from litellm.main import stream_chunk_builder from litellm.main import stream_chunk_builder
from litellm.types.utils import TextCompletionResponse from litellm.types.utils import TextCompletionResponse
verbose_proxy_logger.debug( verbose_proxy_logger.debug("OpenAI Moderation: Running streaming response scan")
"OpenAI Moderation: Running streaming response scan"
)
# Collect all chunks to process them together # Collect all chunks to process them together
all_chunks: List["ModelResponseStream"] = [] all_chunks: List["ModelResponseStream"] = []
@ -294,9 +294,7 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail):
self._check_moderation_result(moderation_response) self._check_moderation_result(moderation_response)
# If we reach here, content passed moderation - yield the original chunks # If we reach here, content passed moderation - yield the original chunks
mock_response = MockResponseIterator( mock_response = MockResponseIterator(model_response=assembled_model_response)
model_response=assembled_model_response
)
# Return the reconstructed stream # Return the reconstructed stream
async for chunk in mock_response: async for chunk in mock_response:
@ -306,30 +304,30 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail):
""" """
Extract text content from the model response for moderation. Extract text content from the model response for moderation.
""" """
if not hasattr(response, 'choices') or not response.choices: if not hasattr(response, "choices") or not response.choices:
return None return None
response_texts = [] response_texts = []
for choice in response.choices: for choice in response.choices:
try: try:
# Try to get content from message (chat completion) # Try to get content from message (chat completion)
message = getattr(choice, 'message', None) message = getattr(choice, "message", None)
if message: if message:
content = getattr(message, 'content', None) content = getattr(message, "content", None)
if content and isinstance(content, str): if content and isinstance(content, str):
response_texts.append(content) response_texts.append(content)
continue continue
# Try to get text (text completion) # Try to get text (text completion)
text = getattr(choice, 'text', None) text = getattr(choice, "text", None)
if text and isinstance(text, str): if text and isinstance(text, str):
response_texts.append(text) response_texts.append(text)
continue continue
# Try to get content from delta (streaming) # Try to get content from delta (streaming)
delta = getattr(choice, 'delta', None) delta = getattr(choice, "delta", None)
if delta: if delta:
content = getattr(delta, 'content', None) content = getattr(delta, "content", None)
if content and isinstance(content, str): if content and isinstance(content, str):
response_texts.append(content) response_texts.append(content)
continue continue

View file

@ -9,10 +9,10 @@
import asyncio import asyncio
import threading
import json import json
from datetime import datetime import threading
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import datetime
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
@ -39,7 +39,10 @@ if TYPE_CHECKING:
from litellm._uuid import uuid from litellm._uuid import uuid
from litellm.caching.caching import DualCache from litellm.caching.caching import DualCache
from litellm.exceptions import BlockedPiiEntityError from litellm.exceptions import BlockedPiiEntityError
from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.integrations.custom_guardrail import (
CustomGuardrail,
log_guardrail_information,
)
from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy._types import UserAPIKeyAuth
from litellm.types.guardrails import ( from litellm.types.guardrails import (
GuardrailEventHooks, GuardrailEventHooks,
@ -568,9 +571,9 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail):
if messages is None: if messages is None:
return data return data
tasks = [] tasks = []
task_mappings: List[ task_mappings: List[Tuple[int, Optional[int]]] = (
Tuple[int, Optional[int]] []
] = [] # Track (message_index, content_index) for each task ) # Track (message_index, content_index) for each task
for msg_idx, m in enumerate(messages): for msg_idx, m in enumerate(messages):
content = m.get("content", None) content = m.get("content", None)
@ -671,9 +674,9 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail):
): # /chat/completions requests ): # /chat/completions requests
messages: Optional[List] = kwargs.get("messages", None) messages: Optional[List] = kwargs.get("messages", None)
tasks = [] tasks = []
task_mappings: List[ task_mappings: List[Tuple[int, Optional[int]]] = (
Tuple[int, Optional[int]] []
] = [] # Track (message_index, content_index) for each task ) # Track (message_index, content_index) for each task
if messages is None: if messages is None:
return kwargs, result return kwargs, result
@ -792,11 +795,11 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail):
# Type narrowing: StreamingChoices doesn't have .message attribute # Type narrowing: StreamingChoices doesn't have .message attribute
if not hasattr(choice, "message"): if not hasattr(choice, "message"):
continue continue
content = getattr(choice.message, "content", None) content = getattr(choice.message, "content", None) # type: ignore
if content is None: if content is None:
continue continue
if isinstance(content, str): if isinstance(content, str):
choice.message.content = await self.check_pii( choice.message.content = await self.check_pii( # type: ignore
text=content, text=content,
output_parse_pii=False, output_parse_pii=False,
presidio_config=presidio_config, presidio_config=presidio_config,
@ -989,6 +992,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail):
except Exception: except Exception:
pass pass
@log_guardrail_information
async def apply_guardrail( async def apply_guardrail(
self, self,
inputs: "GenericGuardrailAPIInputs", inputs: "GenericGuardrailAPIInputs",

View file

@ -6,7 +6,10 @@ from typing import TYPE_CHECKING, Any, List, Literal, Optional, Type
from fastapi import HTTPException from fastapi import HTTPException
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.integrations.custom_guardrail import (
CustomGuardrail,
log_guardrail_information,
)
from litellm.llms.custom_httpx.http_handler import ( from litellm.llms.custom_httpx.http_handler import (
get_async_httpx_client, get_async_httpx_client,
httpxSpecialProvider, httpxSpecialProvider,
@ -67,6 +70,7 @@ class PromptSecurityGuardrail(CustomGuardrail):
super().__init__(**kwargs) super().__init__(**kwargs)
@log_guardrail_information
async def apply_guardrail( async def apply_guardrail(
self, self,
inputs: GenericGuardrailAPIInputs, inputs: GenericGuardrailAPIInputs,

View file

@ -12,10 +12,11 @@ from typing import Any, Dict, List, Literal, Optional, Type
from fastapi import HTTPException from fastapi import HTTPException
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.integrations.custom_guardrail import (
from litellm.litellm_core_utils.litellm_logging import ( CustomGuardrail,
Logging as LiteLLMLoggingObj, log_guardrail_information,
) )
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.custom_httpx.http_handler import ( from litellm.llms.custom_httpx.http_handler import (
get_async_httpx_client, get_async_httpx_client,
httpxSpecialProvider, httpxSpecialProvider,
@ -343,9 +344,7 @@ class QualifireGuardrail(CustomGuardrail):
) )
url = f"{self.qualifire_api_base}/api/evaluation/evaluate" url = f"{self.qualifire_api_base}/api/evaluation/evaluate"
verbose_proxy_logger.debug( verbose_proxy_logger.debug(f"Qualifire Guardrail: Making request to {url}")
f"Qualifire Guardrail: Making request to {url}"
)
# Make the API request # Make the API request
response = await self.async_handler.post( response = await self.async_handler.post(
@ -393,6 +392,7 @@ class QualifireGuardrail(CustomGuardrail):
verbose_proxy_logger.exception(f"Qualifire Guardrail error: {e}") verbose_proxy_logger.exception(f"Qualifire Guardrail error: {e}")
raise raise
@log_guardrail_information
async def apply_guardrail( async def apply_guardrail(
self, self,
inputs: GenericGuardrailAPIInputs, inputs: GenericGuardrailAPIInputs,

View file

@ -9,7 +9,10 @@ from typing import TYPE_CHECKING, Literal, Optional
from fastapi import HTTPException from fastapi import HTTPException
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.integrations.custom_guardrail import (
CustomGuardrail,
log_guardrail_information,
)
from litellm.llms.custom_httpx.http_handler import ( from litellm.llms.custom_httpx.http_handler import (
get_async_httpx_client, get_async_httpx_client,
httpxSpecialProvider, httpxSpecialProvider,
@ -70,6 +73,7 @@ class ZscalerAIGuard(CustomGuardrail):
return str(value).strip() return str(value).strip()
return "N/A" return "N/A"
@log_guardrail_information
async def apply_guardrail( async def apply_guardrail(
self, self,
inputs: "GenericGuardrailAPIInputs", inputs: "GenericGuardrailAPIInputs",
@ -154,9 +158,7 @@ class ZscalerAIGuard(CustomGuardrail):
zscaler_ai_guard_result zscaler_ai_guard_result
and zscaler_ai_guard_result.get("action") == "BLOCK" and zscaler_ai_guard_result.get("action") == "BLOCK"
): ):
blocking_info = zscaler_ai_guard_result.get( blocking_info = zscaler_ai_guard_result.get("zscaler_ai_guard_response")
"zscaler_ai_guard_response"
)
error_message = f"Content blocked by Zscaler AI Guard: {self.extract_blocking_info(blocking_info)}" error_message = f"Content blocked by Zscaler AI Guard: {self.extract_blocking_info(blocking_info)}"
raise Exception(error_message) raise Exception(error_message)
except Exception as e: except Exception as e:

View file

@ -0,0 +1,126 @@
"""
Test that all guardrail hooks with async def apply_guardrail use @log_guardrail_information decorator.
This ensures consistent logging and observability across all guardrail implementations.
"""
import ast
from pathlib import Path
from typing import List, Tuple
def find_apply_guardrail_methods(file_path: Path) -> List[Tuple[str, int, bool]]:
"""
Find all apply_guardrail methods and check if they have the decorator.
Returns:
List of tuples: (class_name, line_number, has_decorator)
"""
with open(file_path, "r") as f:
content = f.read()
try:
tree = ast.parse(content)
except SyntaxError:
return []
results = []
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
class_name = node.name
# Check if this class has apply_guardrail method
for item in node.body:
if (
isinstance(item, ast.AsyncFunctionDef)
and item.name == "apply_guardrail"
):
# Check if it has the log_guardrail_information decorator
has_decorator = False
for decorator in item.decorator_list:
if (
isinstance(decorator, ast.Name)
and decorator.id == "log_guardrail_information"
):
has_decorator = True
break
results.append((class_name, item.lineno, has_decorator))
return results
def test_guardrail_apply_decorator():
"""Test that all guardrail hooks with apply_guardrail have the decorator."""
# Path to the guardrail hooks directory
guardrail_hooks_dir = (
Path(__file__).parent.parent.parent
/ "litellm"
/ "proxy"
/ "guardrails"
/ "guardrail_hooks"
)
# Find all Python files in the guardrail hooks directory
python_files = list(guardrail_hooks_dir.rglob("*.py"))
# Track violations
violations = []
for python_file in python_files:
# Skip __init__.py files and test files
if python_file.name == "__init__.py" or python_file.name.startswith("test_"):
continue
# Skip base files and primitives
if python_file.name in ["base.py", "primitives.py", "patterns.py"]:
continue
# Skip bedrock_guardrails.py - it implements logging differently via
# add_standard_logging_guardrail_information_to_request_data calls
# in make_bedrock_api_request method instead of using the decorator
if python_file.name == "bedrock_guardrails.py":
continue
results = find_apply_guardrail_methods(python_file)
for class_name, line_num, has_decorator in results:
if not has_decorator:
relative_path = python_file.relative_to(
Path(__file__).parent.parent.parent
)
violations.append((relative_path, class_name, line_num))
# Assert no violations found
if violations:
print(
f"\nFound {len(violations)} guardrail hook(s) without @log_guardrail_information decorator:"
)
print(
"\nAll guardrail hooks must use @log_guardrail_information decorator on their apply_guardrail method."
)
print(
"This ensures consistent logging and observability across all guardrails.\n"
)
for file_path, class_name, line_num in violations:
print(f" - {file_path}:{line_num} ({class_name}.apply_guardrail)")
print("\nTo fix, add the decorator:")
print(
" from litellm.integrations.custom_guardrail import log_guardrail_information"
)
print(" ")
print(" @log_guardrail_information")
print(" async def apply_guardrail(self, ...):")
print(" ...")
raise AssertionError(
f"Found {len(violations)} guardrail hook(s) without @log_guardrail_information decorator"
)
if __name__ == "__main__":
test_guardrail_apply_decorator()
print("✓ All guardrail hooks have @log_guardrail_information decorator")

View file

@ -13159,6 +13159,21 @@
"type": "github", "type": "github",
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
"integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
} }
} }
} }

View file

@ -3,7 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --webpack", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",

View file

@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
import { Modal, Select, Switch, Collapse, Input } from "antd"; import { Modal, Select, Switch, Collapse, Input, Divider } from "antd";
import { Button, TextInput } from "@tremor/react"; import { Button, TextInput } from "@tremor/react";
import { import {
CodeOutlined, CodeOutlined,
@ -8,6 +8,8 @@ import {
CloseCircleOutlined, CloseCircleOutlined,
CaretRightOutlined, CaretRightOutlined,
SaveOutlined, SaveOutlined,
UsergroupAddOutlined,
ExportOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { createGuardrailCall, updateGuardrailCall, testCustomCodeGuardrail } from "../../networking"; import { createGuardrailCall, updateGuardrailCall, testCustomCodeGuardrail } from "../../networking";
import NotificationsManager from "../../molecules/notifications_manager"; import NotificationsManager from "../../molecules/notifications_manager";
@ -91,6 +93,7 @@ const CODE_TEMPLATES = {
}, },
}; };
// Available primitives organized by category // Available primitives organized by category
const PRIMITIVES = { const PRIMITIVES = {
"Return Values": [ "Return Values": [
@ -241,6 +244,8 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
// Handle template change // Handle template change
const handleTemplateChange = (templateKey: string) => { const handleTemplateChange = (templateKey: string) => {
setSelectedTemplate(templateKey); setSelectedTemplate(templateKey);
// Check if it's a standard template
setCode(CODE_TEMPLATES[templateKey as keyof typeof CODE_TEMPLATES].code); setCode(CODE_TEMPLATES[templateKey as keyof typeof CODE_TEMPLATES].code);
}; };
@ -486,12 +491,45 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
onChange={handleTemplateChange} onChange={handleTemplateChange}
className="w-full" className="w-full"
size="middle" size="middle"
dropdownRender={(menu) => (
<>
{menu}
<Divider style={{ margin: '8px 0' }} />
<div
style={{
padding: '8px 12px',
cursor: 'pointer',
color: '#1890ff',
fontSize: '12px',
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
onClick={(e) => {
e.preventDefault();
window.open('https://models.litellm.ai/guardrails', '_blank');
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#f0f0f0';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
<UsergroupAddOutlined />
<span>Browse Community templates</span>
<ExportOutlined style={{ fontSize: '10px' }} />
</div>
</>
)}
> >
{Object.entries(CODE_TEMPLATES).map(([key, template]) => ( <Select.OptGroup label="STANDARD">
<Select.Option key={key} value={key}> {Object.entries(CODE_TEMPLATES).map(([key, template]) => (
{template.name} <Select.Option key={key} value={key}>
</Select.Option> {template.name}
))} </Select.Option>
))}
</Select.OptGroup>
</Select> </Select>
</div> </div>
<div className="flex items-center gap-2 pt-5"> <div className="flex items-center gap-2 pt-5">
@ -632,6 +670,27 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
</div> </div>
</Panel> </Panel>
</Collapse> </Collapse>
{/* Contribution CTA Banner */}
<div className="mt-3 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg flex items-center justify-between flex-shrink-0">
<div className="flex items-center gap-3">
<div className="bg-blue-100 rounded-full p-2">
<UsergroupAddOutlined className="text-blue-600 text-lg" />
</div>
<div>
<div className="text-sm font-medium text-gray-900">Built a useful guardrail?</div>
<div className="text-xs text-gray-600">Share it with the community and help others build faster</div>
</div>
</div>
<Button
size="xs"
onClick={() => window.open('https://github.com/BerriAI/litellm-guardrails', '_blank')}
icon={ExportOutlined}
className="bg-blue-600 hover:bg-blue-700 text-white border-0"
>
Contribute Template
</Button>
</div>
</div> </div>
{/* Primitives Panel */} {/* Primitives Panel */}

View file

@ -14,7 +14,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "react-jsx", "jsx": "preserve",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {