From d860cd65748c0398f9d0f598f443ffd7d3da9215 Mon Sep 17 00:00:00 2001 From: yucheng-berri Date: Thu, 9 Jul 2026 19:52:07 -0700 Subject: [PATCH 1/4] fix(guardrails): filter Add-Guardrail mode dropdown per provider The GET /guardrails/ui/add_guardrail_settings endpoint returned every GuardrailEventHooks value in one flat supported_modes list, so the Admin UI rendered pre_mcp_call as a selectable Mode for every guardrail. Saving Content Filter or Tool Permission with pre_mcp_call then failed with a 400 because those guardrails' server-side supported_event_hooks list excludes it. Expose each guardrail's supported hooks as a get_supported_event_hooks classmethod on CustomGuardrail (mirrors the existing get_config_model pattern) and have the endpoint iterate guardrail_class_registry to build a supported_modes_by_provider map. The UI Mode dropdown filters by that map when the selected provider is known and falls back to the global list otherwise. __init__ now sources its own supported_event_hooks list from the classmethod so the two sides can't drift. Also register BedrockGuardrail, ToolPermissionGuardrail, lakera, lakera_v2, and presidio in guardrail_class_registry so they participate in the map (they were previously only in guardrail_initializer_registry and had no class-registry entry). Behavior change: guardrails that previously had no supported_event_hooks declared (aim, javelin, azure/text_moderation, cato_networks, crowdstrike_aidr, headroom, hiddenlayer, lasso, noma, onyx, prompt_security, qualifire, repelloai, zscaler_ai_guard, aporia_ai, lakera_ai, lakera_ai_v2, mcp_jwt_signer, model_armor, presidio) now validate the configured mode at instantiation. Existing configs where the mode was silently a no-op will fail at proxy startup with a clear validation error rather than running as a broken guardrail. Resolves LIT-4226 --- litellm/integrations/custom_guardrail.py | 12 +++ litellm/integrations/rubrik.py | 7 +- .../proxy/guardrails/guardrail_endpoints.py | 24 +++-- .../guardrails/guardrail_hooks/aim/aim.py | 12 ++- .../guardrails/guardrail_hooks/akto/akto.py | 14 ++- .../guardrail_hooks/aporia_ai/aporia_ai.py | 8 ++ .../guardrail_hooks/azure/prompt_shield.py | 16 +-- .../guardrail_hooks/azure/text_moderation.py | 9 ++ .../guardrail_hooks/bedrock_guardrails.py | 19 ++-- .../block_code_execution.py | 14 ++- .../cato_networks/cato_networks.py | 12 ++- .../cisco_ai_defense/cisco_ai_defense.py | 22 +++-- .../crowdstrike_aidr/crowdstrike_aidr.py | 10 ++ .../custom_code/custom_code_guardrail.py | 24 ++--- .../guardrail_hooks/dynamoai/dynamoai.py | 15 +-- .../guardrail_hooks/enkryptai/enkryptai.py | 15 +-- .../generic_guardrail_api.py | 17 ++-- .../guardrail_hooks/grayswan/grayswan.py | 16 +-- .../guardrails_ai/guardrails_ai.py | 15 +-- .../guardrail_hooks/headroom/headroom.py | 10 +- .../hiddenlayer/hiddenlayer.py | 11 ++- .../ibm_guardrails/ibm_detector.py | 15 +-- .../guardrail_hooks/javelin/javelin.py | 7 ++ .../guardrails/guardrail_hooks/lakera_ai.py | 9 ++ .../guardrail_hooks/lakera_ai_v2.py | 9 ++ .../guardrails/guardrail_hooks/lasso/lasso.py | 9 ++ .../litellm_content_filter/content_filter.py | 16 +-- .../llm_as_a_judge/__init__.py | 14 ++- .../mcp_end_user_permission.py | 11 ++- .../mcp_jwt_signer/mcp_jwt_signer.py | 6 ++ .../mcp_security/mcp_security_guardrail.py | 7 +- .../microsoft_purview/purview_dlp.py | 16 +-- .../model_armor/model_armor.py | 11 +++ .../guardrails/guardrail_hooks/noma/noma.py | 10 ++ .../guardrail_hooks/noma/noma_v2.py | 21 ++-- .../guardrails/guardrail_hooks/onyx/onyx.py | 11 ++- .../guardrail_hooks/openai/moderations.py | 19 ++-- .../guardrail_hooks/ovalix/ovalix.py | 7 ++ .../guardrail_hooks/pangea/pangea.py | 14 +-- .../panw_prisma_airs/panw_prisma_airs.py | 20 ++-- .../guardrail_hooks/pillar/pillar.py | 21 ++-- .../guardrails/guardrail_hooks/presidio.py | 9 ++ .../prompt_security/prompt_security.py | 9 ++ .../promptguard/promptguard.py | 13 ++- .../guardrail_hooks/qualifire/qualifire.py | 9 ++ .../guardrail_hooks/repelloai/repelloai.py | 10 +- .../semantic_guard/semantic_guard.py | 12 ++- .../guardrail_hooks/tool_permission.py | 13 ++- .../vigil_guard/vigil_guard.py | 13 ++- .../guardrail_hooks/xecguard/xecguard.py | 17 ++-- .../zscaler_ai_guard/zscaler_ai_guard.py | 11 ++- .../proxy/guardrails/guardrail_registry.py | 18 +++- litellm/types/guardrails.py | 1 + .../guardrails/test_guardrail_endpoints.py | 98 +++++++++++++++++++ .../guardrails/add_guardrail_form.tsx | 10 +- .../guardrails/edit_guardrail_form.tsx | 10 +- 56 files changed, 615 insertions(+), 193 deletions(-) diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index 59d37639098d..d5f1de87c648 100644 --- a/litellm/integrations/custom_guardrail.py +++ b/litellm/integrations/custom_guardrail.py @@ -303,6 +303,18 @@ def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: """ return None + @classmethod + def get_supported_event_hooks(cls) -> Optional[List[GuardrailEventHooks]]: + """ + Returns the event hooks this guardrail supports, for the UI to render. + + Subclasses should override to return their supported hooks list. When a + subclass returns None, the endpoint omits it from the per-provider map + and the UI is expected to fall back to the global `supported_modes` + list client-side. + """ + return None + def _validate_event_hook( self, event_hook: Optional[Union[GuardrailEventHooks, List[GuardrailEventHooks], Mode]], diff --git a/litellm/integrations/rubrik.py b/litellm/integrations/rubrik.py index 2b54a411ec74..07b77a4863a8 100644 --- a/litellm/integrations/rubrik.py +++ b/litellm/integrations/rubrik.py @@ -7,7 +7,7 @@ import urllib.parse import uuid from collections import Counter -from typing import TYPE_CHECKING, Any, Literal, Optional +from typing import TYPE_CHECKING, Any, List, Literal, Optional import httpx from litellm._logging import verbose_logger @@ -52,6 +52,10 @@ class _MalformedToolBlockingResponseError(Exception): class RubrikLogger(CustomGuardrail, CustomBatchLogger): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [GuardrailEventHooks.post_call] + def __init__( self, api_key: str | None = None, @@ -69,6 +73,7 @@ def __init__( kwargs["event_hook"] = kwargs.get("event_hook") or GuardrailEventHooks.post_call if kwargs.get("default_on") is None: kwargs["default_on"] = True + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__( flush_lock=self.flush_lock, **kwargs, diff --git a/litellm/proxy/guardrails/guardrail_endpoints.py b/litellm/proxy/guardrails/guardrail_endpoints.py index d3a5d649f17f..b6a2d8d9069d 100644 --- a/litellm/proxy/guardrails/guardrail_endpoints.py +++ b/litellm/proxy/guardrails/guardrail_endpoints.py @@ -1296,21 +1296,27 @@ async def get_guardrail_ui_settings(): get_available_content_categories, get_pattern_metadata, ) + from litellm.proxy.guardrails.guardrail_registry import guardrail_class_registry - # Convert the PII_ENTITY_CATEGORIES_MAP to the format expected by the UI - category_maps = [] - for category, entities in PII_ENTITY_CATEGORIES_MAP.items(): - category_maps.append( - { - "category": category.value, - "entities": [entity.value for entity in entities], - } - ) + category_maps = [ + { + "category": category.value, + "entities": [entity.value for entity in entities], + } + for category, entities in PII_ENTITY_CATEGORIES_MAP.items() + ] + + supported_modes_by_provider = { + provider: [hook.value for hook in hooks] + for provider, guardrail_class in guardrail_class_registry.items() + if (hooks := guardrail_class.get_supported_event_hooks()) is not None + } return GuardrailUIAddGuardrailSettings( supported_entities=[entity.value for entity in PiiEntityType], supported_actions=[action.value for action in PiiAction], supported_modes=[mode.value for mode in GuardrailEventHooks], + supported_modes_by_provider=supported_modes_by_provider, pii_entity_categories=category_maps, content_filter_settings={ "prebuilt_patterns": get_pattern_metadata(), diff --git a/litellm/proxy/guardrails/guardrail_hooks/aim/aim.py b/litellm/proxy/guardrails/guardrail_hooks/aim/aim.py index d22243cbe88b..01eb61dad084 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/aim/aim.py +++ b/litellm/proxy/guardrails/guardrail_hooks/aim/aim.py @@ -7,7 +7,7 @@ import asyncio import json import os -from typing import TYPE_CHECKING, Any, AsyncGenerator, Optional, Type, Union +from typing import TYPE_CHECKING, Any, AsyncGenerator, List, Optional, Type, Union from pydantic import BaseModel from websockets.asyncio.client import ClientConnection, connect @@ -26,6 +26,7 @@ build_inspection_messages, has_non_string_content, ) +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.utils import ( CallTypesLiteral, Choices, @@ -44,7 +45,16 @@ class AimGuardrailMissingSecrets(Exception): class AimGuardrail(CustomGuardrail): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + ] + def __init__(self, api_key: Optional[str] = None, api_base: Optional[str] = None, **kwargs): + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) ssl_verify = kwargs.pop("ssl_verify", None) self.async_handler = get_async_httpx_client( llm_provider=httpxSpecialProvider.GuardrailCallback, diff --git a/litellm/proxy/guardrails/guardrail_hooks/akto/akto.py b/litellm/proxy/guardrails/guardrail_hooks/akto/akto.py index be9c9cb1be7d..daae74ae8e04 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/akto/akto.py +++ b/litellm/proxy/guardrails/guardrail_hooks/akto/akto.py @@ -11,7 +11,7 @@ import json import os from datetime import datetime -from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Tuple, Type +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Type from fastapi import HTTPException @@ -52,6 +52,13 @@ def get_config_model() -> Type["GuardrailConfigModel"]: return AktoConfigModel + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + def __init__( self, akto_base_url: Optional[str] = None, @@ -90,10 +97,7 @@ def __init__( self.akto_account_id = akto_account_id or os.environ.get("AKTO_ACCOUNT_ID", "1000000") self.akto_vxlan_id = akto_vxlan_id or os.environ.get("AKTO_VXLAN_ID", "0") - kwargs["supported_event_hooks"] = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - ] + kwargs["supported_event_hooks"] = list(self.get_supported_event_hooks()) super().__init__(**kwargs) verbose_proxy_logger.debug( diff --git a/litellm/proxy/guardrails/guardrail_hooks/aporia_ai/aporia_ai.py b/litellm/proxy/guardrails/guardrail_hooks/aporia_ai/aporia_ai.py index ba9c83981527..dc3fc40625c7 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/aporia_ai/aporia_ai.py +++ b/litellm/proxy/guardrails/guardrail_hooks/aporia_ai/aporia_ai.py @@ -37,7 +37,15 @@ class AporiaGuardrail(CustomGuardrail): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + ] + def __init__(self, api_key: Optional[str] = None, api_base: Optional[str] = None, **kwargs): + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) self.async_handler = get_async_httpx_client(llm_provider=httpxSpecialProvider.GuardrailCallback) self.aporia_api_key = api_key or os.environ["APORIO_API_KEY"] self.aporia_api_base = api_base or os.environ["APORIO_API_BASE"] diff --git a/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py b/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py index 788fe5b05c70..befb1b7ae569 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py +++ b/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py @@ -12,6 +12,7 @@ CustomGuardrail, log_guardrail_information, ) +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.utils import CallTypesLiteral from .base import AzureGuardrailBase @@ -47,19 +48,13 @@ def __init__( **kwargs, ): """Initialize Azure Prompt Shield guardrail handler.""" - from litellm.types.guardrails import GuardrailEventHooks - - supported_event_hooks = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.during_call, - ] # AzureGuardrailBase.__init__ stores api_key, api_base, api_version, # async_handler and forwards the rest to CustomGuardrail. super().__init__( api_key=api_key, api_base=api_base, guardrail_name=guardrail_name, - supported_event_hooks=supported_event_hooks, + supported_event_hooks=list(self.get_supported_event_hooks()), **kwargs, ) @@ -149,3 +144,10 @@ def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: ) return AzurePromptShieldGuardrailConfigModel + + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/azure/text_moderation.py b/litellm/proxy/guardrails/guardrail_hooks/azure/text_moderation.py index c8553926559e..91f5df0e9b80 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/azure/text_moderation.py +++ b/litellm/proxy/guardrails/guardrail_hooks/azure/text_moderation.py @@ -13,6 +13,7 @@ log_guardrail_information, ) from litellm.proxy._types import UserAPIKeyAuth +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.utils import CallTypesLiteral from .base import AzureGuardrailBase @@ -42,6 +43,13 @@ class AzureContentSafetyTextModerationGuardrail(AzureGuardrailBase, CustomGuardr default_severity_threshold: int = 2 + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + def __init__( self, guardrail_name: str, @@ -56,6 +64,7 @@ def __init__( AzureTextModerationRequestBodyOptionalParams, ) + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) # AzureGuardrailBase.__init__ stores api_key, api_base, api_version, # async_handler and forwards the rest to CustomGuardrail. super().__init__( diff --git a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py index a45719d2eb81..44ae57f81db4 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py +++ b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py @@ -166,14 +166,7 @@ def __init__( """ # Set supported event hooks to include MCP hooks - if "supported_event_hooks" not in kwargs: - kwargs["supported_event_hooks"] = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.during_call, - GuardrailEventHooks.pre_mcp_call, - GuardrailEventHooks.during_mcp_call, - ] + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) BaseAWSLLM.__init__(self) @@ -184,6 +177,16 @@ def __init__( self.guardrailVersion, ) + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.pre_mcp_call, + GuardrailEventHooks.during_mcp_call, + ] + def _create_bedrock_input_content_request(self, messages: Optional[List[AllMessageValues]]) -> BedrockRequest: """ Create a bedrock request for the input content - the LLM request. diff --git a/litellm/proxy/guardrails/guardrail_hooks/block_code_execution/block_code_execution.py b/litellm/proxy/guardrails/guardrail_hooks/block_code_execution/block_code_execution.py index ea66f416e15a..cfb4a78fa6e2 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/block_code_execution/block_code_execution.py +++ b/litellm/proxy/guardrails/guardrail_hooks/block_code_execution/block_code_execution.py @@ -351,11 +351,7 @@ def __init__( _event_hook = GuardrailEventHooks(event_hook) super().__init__( guardrail_name=guardrail_name or "block_code_execution", - supported_event_hooks=[ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.during_call, - ], + supported_event_hooks=list(self.get_supported_event_hooks()), event_hook=_event_hook or [ GuardrailEventHooks.pre_call, @@ -378,6 +374,14 @@ def get_config_model() -> Optional[type[GuardrailConfigModel]]: return BlockCodeExecutionGuardrailConfigModel + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.during_call, + ] + def _find_blocks(self, text: str) -> List[Tuple[int, int, str, str, float, CodeBlockActionTaken]]: """ Find all fenced code blocks in text. Returns list of diff --git a/litellm/proxy/guardrails/guardrail_hooks/cato_networks/cato_networks.py b/litellm/proxy/guardrails/guardrail_hooks/cato_networks/cato_networks.py index bf5a0a5f262e..440618a2ffa1 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/cato_networks/cato_networks.py +++ b/litellm/proxy/guardrails/guardrail_hooks/cato_networks/cato_networks.py @@ -9,7 +9,7 @@ import json import os import ssl -from typing import TYPE_CHECKING, Any, AsyncGenerator, Optional, Type, Union +from typing import TYPE_CHECKING, Any, AsyncGenerator, List, Optional, Type, Union from fastapi import HTTPException from pydantic import BaseModel @@ -30,6 +30,7 @@ apply_redacted_messages_back, build_inspection_messages, ) +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.utils import ( CallTypesLiteral, Choices, @@ -49,7 +50,16 @@ class CatoNetworksGuardrailMissingSecrets(Exception): class CatoNetworksGuardrail(CustomGuardrail): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + ] + def __init__(self, api_key: Optional[str] = None, api_base: Optional[str] = None, **kwargs): + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) ssl_verify = kwargs.pop("ssl_verify", None) self.async_handler = get_async_httpx_client( llm_provider=httpxSpecialProvider.GuardrailCallback, diff --git a/litellm/proxy/guardrails/guardrail_hooks/cisco_ai_defense/cisco_ai_defense.py b/litellm/proxy/guardrails/guardrail_hooks/cisco_ai_defense/cisco_ai_defense.py index 561e6ce5f2b2..4c31b038172a 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/cisco_ai_defense/cisco_ai_defense.py +++ b/litellm/proxy/guardrails/guardrail_hooks/cisco_ai_defense/cisco_ai_defense.py @@ -224,18 +224,9 @@ def __init__( self.async_handler = get_async_httpx_client(llm_provider=httpxSpecialProvider.GuardrailCallback) # Register broadly; runtime filtering happens in ``_surface_matches``. - supported_event_hooks = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.during_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.logging_only, - GuardrailEventHooks.pre_mcp_call, - GuardrailEventHooks.during_mcp_call, - ] - super().__init__( guardrail_name=guardrail_name, - supported_event_hooks=supported_event_hooks, + supported_event_hooks=list(self.get_supported_event_hooks()), **kwargs, ) @@ -2133,3 +2124,14 @@ def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: ) return CiscoAIDefenseGuardrailConfigModel + + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.logging_only, + GuardrailEventHooks.pre_mcp_call, + GuardrailEventHooks.during_mcp_call, + ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py b/litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py index eeb3623977eb..fa71e7fc3018 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py +++ b/litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py @@ -4,6 +4,7 @@ from typing import ( TYPE_CHECKING, Annotated, + List, Literal, NamedTuple, Optional, @@ -31,6 +32,7 @@ from litellm.proxy.common_utils.callback_utils import ( add_guardrail_to_applied_guardrails_header, ) +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.llms.openai import AllMessageValues, OpenAIChatCompletionToolParam from litellm.types.utils import GenericGuardrailAPIInputs @@ -236,6 +238,13 @@ class CrowdStrikeAIDRHandler(CustomGuardrail): AI Guard service. """ + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + def __init__( self, guardrail_name: str, @@ -266,6 +275,7 @@ def __init__( "CrowdStrike AIDR API base URL is required. Set CS_AIDR_BASE_URL environment variable or pass it in litellm_params." ) + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) # Pass relevant kwargs to the parent class super().__init__(guardrail_name=guardrail_name, **kwargs) verbose_proxy_logger.debug( diff --git a/litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py b/litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py index 9021f0231561..245b9806e71f 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py +++ b/litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py @@ -36,7 +36,7 @@ async def apply_guardrail(inputs, request_data, input_type): import asyncio import threading -from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Type, cast +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Type, cast from fastapi import HTTPException @@ -121,18 +121,9 @@ def __init__( self._compile_lock = threading.Lock() self._compile_error: Optional[str] = None - supported_event_hooks = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.during_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.pre_mcp_call, - GuardrailEventHooks.during_mcp_call, - GuardrailEventHooks.logging_only, - ] - super().__init__( guardrail_name=guardrail_name, - supported_event_hooks=supported_event_hooks, + supported_event_hooks=list(self.get_supported_event_hooks()), **kwargs, ) @@ -144,6 +135,17 @@ def get_config_model() -> Optional[Type[GuardrailConfigModel]]: """Returns the config model for the UI.""" return CustomCodeGuardrailConfigModel + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.pre_mcp_call, + GuardrailEventHooks.during_mcp_call, + GuardrailEventHooks.logging_only, + ] + def _do_compile(self) -> None: """Internal compilation method without lock. Expected to run inside _compile_lock.""" exec_globals = build_sandbox_globals() diff --git a/litellm/proxy/guardrails/guardrail_hooks/dynamoai/dynamoai.py b/litellm/proxy/guardrails/guardrail_hooks/dynamoai/dynamoai.py index b02f10305921..2db3c35866f2 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/dynamoai/dynamoai.py +++ b/litellm/proxy/guardrails/guardrail_hooks/dynamoai/dynamoai.py @@ -73,12 +73,7 @@ def __init__( self.optional_params = kwargs # Set supported event hooks - if "supported_event_hooks" not in kwargs: - kwargs["supported_event_hooks"] = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.during_call, - ] + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(guardrail_name=guardrail_name, **kwargs) @@ -470,3 +465,11 @@ def get_config_model() -> Optional[Type[GuardrailConfigModel]]: ) return DynamoAIGuardrailConfigModel + + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.during_call, + ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.py b/litellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.py index f9bb13ad64e0..dad8e7dd9730 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.py +++ b/litellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.py @@ -80,12 +80,7 @@ def __init__( self.optional_params = kwargs # Set supported event hooks - if "supported_event_hooks" not in kwargs: - kwargs["supported_event_hooks"] = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.during_call, - ] + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(guardrail_name=guardrail_name, **kwargs) @@ -500,3 +495,11 @@ def get_config_model(): ) return EnkryptAIGuardrailConfigModel + + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.during_call, + ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py b/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py index dc519f56d1a9..e29d6e563535 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py +++ b/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py @@ -7,7 +7,7 @@ import fnmatch import os -from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Set +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Set import httpx @@ -222,12 +222,7 @@ def __init__( self.streaming_sampling_rate: int = 5 if streaming_sampling_rate is None else streaming_sampling_rate # Set supported event hooks - if "supported_event_hooks" not in kwargs: - kwargs["supported_event_hooks"] = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.during_call, - ] + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) @@ -490,3 +485,11 @@ def get_config_model() -> Optional[type["GuardrailConfigModel"]]: ) return GenericGuardrailAPIConfigModel + + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.during_call, + ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/grayswan/grayswan.py b/litellm/proxy/guardrails/guardrail_hooks/grayswan/grayswan.py index 9805b1a9117d..72409a61c30d 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/grayswan/grayswan.py +++ b/litellm/proxy/guardrails/guardrail_hooks/grayswan/grayswan.py @@ -119,18 +119,20 @@ def __init__( streaming_sampling_rate, ) - supported_event_hooks = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.during_call, - GuardrailEventHooks.post_call, - ] - super().__init__( guardrail_name=guardrail_name, - supported_event_hooks=supported_event_hooks, + supported_event_hooks=list(self.get_supported_event_hooks()), **kwargs, ) + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + ] + # ------------------------------------------------------------------ # Debug override to trace post_call issues # ------------------------------------------------------------------ diff --git a/litellm/proxy/guardrails/guardrail_hooks/guardrails_ai/guardrails_ai.py b/litellm/proxy/guardrails/guardrail_hooks/guardrails_ai/guardrails_ai.py index 71c426a33674..a7c94a4742d3 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/guardrails_ai/guardrails_ai.py +++ b/litellm/proxy/guardrails/guardrail_hooks/guardrails_ai/guardrails_ai.py @@ -74,12 +74,7 @@ def __init__( self.guardrails_ai_guard_name = guard_name self.optional_params = kwargs self.guardrails_ai_api_input_format = guardrails_ai_api_input_format - supported_event_hooks = [ - GuardrailEventHooks.post_call, - GuardrailEventHooks.pre_call, - GuardrailEventHooks.logging_only, - ] - super().__init__(supported_event_hooks=supported_event_hooks, **kwargs) + super().__init__(supported_event_hooks=list(self.get_supported_event_hooks()), **kwargs) async def make_guardrails_ai_api_request(self, llm_output: str, request_data: dict) -> GuardrailsAIResponse: from httpx import URL @@ -240,3 +235,11 @@ def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: ) return GuardrailsAIGuardrailConfigModel + + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.post_call, + GuardrailEventHooks.pre_call, + GuardrailEventHooks.logging_only, + ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py b/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py index 84d03fe71449..e6f76b67c3c9 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py +++ b/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py @@ -4,7 +4,7 @@ import re import time import uuid -from typing import TYPE_CHECKING, Any, Literal, Optional +from typing import TYPE_CHECKING, Any, List, Literal, Optional import httpx from fastapi import HTTPException @@ -208,6 +208,13 @@ def _build_responses_followup_items( class HeadroomGuardrail(CustomGuardrail): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + def __init__( self, api_base: str | None = None, @@ -237,6 +244,7 @@ def __init__( guardrail_name=guardrail_name, event_hook=event_hook, default_on=default_on, + supported_event_hooks=list(self.get_supported_event_hooks()), ) def _should_bypass(self, request_data: dict) -> bool: diff --git a/litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py b/litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py index 287f108b070d..1566c90ac0c2 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py +++ b/litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py @@ -3,7 +3,7 @@ import httpx import os -from typing import TYPE_CHECKING, Any, Literal, Optional, Type +from typing import TYPE_CHECKING, Any, List, Literal, Optional, Type from urllib.parse import urlparse import requests @@ -21,6 +21,7 @@ get_async_httpx_client, httpxSpecialProvider, ) +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.proxy.guardrails.guardrail_hooks.hiddenlayer import ( HiddenlayerAction, HiddenlayerMessages, @@ -63,6 +64,13 @@ def _get_jwt(auth_url, api_id, api_key): class HiddenlayerGuardrail(CustomGuardrail): """Custom guardrail wrapper for HiddenLayer's safety checks.""" + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + def __init__( self, api_id: Optional[str] = None, @@ -71,6 +79,7 @@ def __init__( auth_url: Optional[str] = None, **kwargs: Any, ) -> None: + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) self.hiddenlayer_client_id = api_id or os.getenv("HIDDENLAYER_CLIENT_ID") self.hiddenlayer_client_secret = api_key or os.getenv("HIDDENLAYER_CLIENT_SECRET") self.api_base = api_base or os.getenv("HIDDENLAYER_API_BASE") or "https://api.hiddenlayer.ai" diff --git a/litellm/proxy/guardrails/guardrail_hooks/ibm_guardrails/ibm_detector.py b/litellm/proxy/guardrails/guardrail_hooks/ibm_guardrails/ibm_detector.py index 27ba9f3467c7..955ec0c0d357 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/ibm_guardrails/ibm_detector.py +++ b/litellm/proxy/guardrails/guardrail_hooks/ibm_guardrails/ibm_detector.py @@ -86,12 +86,7 @@ def __init__( self.optional_params = kwargs # Set supported event hooks - if "supported_event_hooks" not in kwargs: - kwargs["supported_event_hooks"] = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.during_call, - ] + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(guardrail_name=guardrail_name, **kwargs) @@ -669,3 +664,11 @@ def get_config_model(): ) return IBMDetectorGuardrailConfigModel + + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.during_call, + ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/javelin/javelin.py b/litellm/proxy/guardrails/guardrail_hooks/javelin/javelin.py index 4575504feb6a..4da3e75d03e5 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/javelin/javelin.py +++ b/litellm/proxy/guardrails/guardrail_hooks/javelin/javelin.py @@ -25,6 +25,12 @@ class JavelinGuardrail(CustomGuardrail): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + ] + def __init__( self, api_key: Optional[str] = None, @@ -72,6 +78,7 @@ def __init__( self.api_version, ) + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(guardrail_name=guardrail_name, default_on=default_on, **kwargs) async def call_javelin_guard( diff --git a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai.py b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai.py index 3804d1cb93f8..d3360bbe6413 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai.py +++ b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai.py @@ -30,6 +30,7 @@ from litellm.proxy.guardrails.guardrail_helpers import should_proceed_based_on_metadata from litellm.secret_managers.main import get_secret from litellm.types.guardrails import ( + GuardrailEventHooks, GuardrailItem, LakeraCategoryThresholds, Role, @@ -46,6 +47,13 @@ class lakeraAI_Moderation(CustomGuardrail): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + ] + def __init__( self, moderation_check: Literal["pre_call", "in_parallel"] = "in_parallel", @@ -54,6 +62,7 @@ def __init__( api_key: Optional[str] = None, **kwargs, ): + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) self.async_handler = get_async_httpx_client(llm_provider=httpxSpecialProvider.GuardrailCallback) self.lakera_api_key = api_key or os.environ.get("LAKERA_API_KEY") or "" self.moderation_check = moderation_check diff --git a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py index e79a3e7b3d85..76603579d6c8 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py +++ b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py @@ -29,6 +29,14 @@ class LakeraAIGuardrail(CustomGuardrail): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + ] + def __init__( self, api_key: Optional[str] = None, @@ -68,6 +76,7 @@ def __init__( self.metadata: Optional[Dict] = metadata self.dev_info: Optional[bool] = dev_info self.on_flagged = on_flagged or "block" + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) async def call_v2_guard( diff --git a/litellm/proxy/guardrails/guardrail_hooks/lasso/lasso.py b/litellm/proxy/guardrails/guardrail_hooks/lasso/lasso.py index 36fbd73c5bdf..9c4cef2f06b1 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/lasso/lasso.py +++ b/litellm/proxy/guardrails/guardrail_hooks/lasso/lasso.py @@ -93,6 +93,14 @@ class LassoGuardrail(CustomGuardrail): through the Lasso Security API. """ + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + ] + def __init__( self, lasso_api_key: Optional[str] = None, @@ -103,6 +111,7 @@ def __init__( mask: Optional[bool] = False, **kwargs, ): + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) self.async_handler = get_async_httpx_client(llm_provider=httpxSpecialProvider.GuardrailCallback) self.lasso_api_key = lasso_api_key or api_key or os.environ.get("LASSO_API_KEY") self.user_id = user_id or os.environ.get("LASSO_USER_ID") diff --git a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py index ede36b23216d..c3b66f8f388f 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py +++ b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py @@ -179,12 +179,7 @@ def __init__( super().__init__( guardrail_name=guardrail_name, - supported_event_hooks=[ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.during_call, - GuardrailEventHooks.realtime_input_transcription, - ], + supported_event_hooks=list(self.get_supported_event_hooks()), event_hook=event_hook or GuardrailEventHooks.pre_call, default_on=default_on, **kwargs, @@ -1900,3 +1895,12 @@ def get_config_model(): ) return LitellmContentFilterGuardrailConfigModel + + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.realtime_input_transcription, + ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/llm_as_a_judge/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/llm_as_a_judge/__init__.py index 5445425a1d14..a17ca07ae2ec 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/llm_as_a_judge/__init__.py +++ b/litellm/proxy/guardrails/guardrail_hooks/llm_as_a_judge/__init__.py @@ -9,7 +9,7 @@ from litellm._logging import verbose_logger from litellm.integrations.custom_guardrail import CustomGuardrail -from litellm.types.guardrails import GuardrailEventHooks +from litellm.types.guardrails import GuardrailEventHooks, SupportedGuardrailIntegrations from litellm.types.utils import GenericGuardrailAPIInputs, GuardrailStatus if TYPE_CHECKING: @@ -105,7 +105,7 @@ def __init__( super().__init__( guardrail_name=guardrail_name, - supported_event_hooks=[GuardrailEventHooks.post_call], + supported_event_hooks=list(self.get_supported_event_hooks()), event_hook=_event_hook or GuardrailEventHooks.post_call, default_on=default_on, **kwargs, @@ -115,6 +115,10 @@ def __init__( self.overall_threshold = overall_threshold self.on_failure = on_failure + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [GuardrailEventHooks.post_call] + async def _run_judge( self, messages: List[Dict[str, Any]], @@ -267,7 +271,13 @@ def initialize_guardrail( return instance +guardrail_class_registry = { + SupportedGuardrailIntegrations.LLM_AS_A_JUDGE.value: LLMAsAJudgeGuardrail, +} + + __all__ = [ "LLMAsAJudgeGuardrail", + "guardrail_class_registry", "initialize_guardrail", ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/mcp_end_user_permission/mcp_end_user_permission.py b/litellm/proxy/guardrails/guardrail_hooks/mcp_end_user_permission/mcp_end_user_permission.py index 524d087cfb75..d084a7e088b1 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/mcp_end_user_permission/mcp_end_user_permission.py +++ b/litellm/proxy/guardrails/guardrail_hooks/mcp_end_user_permission/mcp_end_user_permission.py @@ -40,10 +40,7 @@ class MCPEndUserPermissionGuardrail(CustomGuardrail): """ def __init__(self, **kwargs): - if "supported_event_hooks" not in kwargs: - kwargs["supported_event_hooks"] = [ - GuardrailEventHooks.pre_call, - ] + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) verbose_proxy_logger.debug("MCP End User Permission Guardrail initialized") @@ -210,6 +207,12 @@ def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: return MCPEndUserPermissionGuardrailConfigModel + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + ] + # ------------------------------------------------------------------ # Private — tool name extraction # ------------------------------------------------------------------ diff --git a/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py b/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py index 46b1afb5db79..e70a4e1d8e7a 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py +++ b/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py @@ -87,6 +87,7 @@ log_guardrail_information, ) from litellm.proxy._types import UserAPIKeyAuth +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.utils import CallTypesLiteral # Module-level singleton for the JWKS discovery endpoint to access. @@ -211,6 +212,10 @@ class MCPJWTSigner(CustomGuardrail): DEFAULT_AUDIENCE = "mcp" SIGNING_KEY_ENV = "MCP_JWT_SIGNING_KEY" + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [GuardrailEventHooks.pre_mcp_call] + def __init__( self, # Core signing config @@ -240,6 +245,7 @@ def __init__( allowed_scopes: Optional[List[str]] = None, **kwargs: Any, ) -> None: + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) # --- Signing key setup --- diff --git a/litellm/proxy/guardrails/guardrail_hooks/mcp_security/mcp_security_guardrail.py b/litellm/proxy/guardrails/guardrail_hooks/mcp_security/mcp_security_guardrail.py index 3aeed1a25bb2..9b5c221b9c41 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/mcp_security/mcp_security_guardrail.py +++ b/litellm/proxy/guardrails/guardrail_hooks/mcp_security/mcp_security_guardrail.py @@ -27,11 +27,14 @@ def __init__( on_violation: Literal["block", "alert"] = "block", **kwargs, ): - if "supported_event_hooks" not in kwargs: - kwargs["supported_event_hooks"] = [GuardrailEventHooks.pre_call] + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) self.on_violation = on_violation + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [GuardrailEventHooks.pre_call] + @log_guardrail_information async def async_pre_call_hook( self, diff --git a/litellm/proxy/guardrails/guardrail_hooks/microsoft_purview/purview_dlp.py b/litellm/proxy/guardrails/guardrail_hooks/microsoft_purview/purview_dlp.py index c6471cfcf519..f0fdaff9c298 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/microsoft_purview/purview_dlp.py +++ b/litellm/proxy/guardrails/guardrail_hooks/microsoft_purview/purview_dlp.py @@ -75,12 +75,6 @@ def __init__( user_id_field: str = "user_id", **kwargs: Any, ): - supported_event_hooks = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.logging_only, - ] - super().__init__( tenant_id=tenant_id, client_id=client_id, @@ -88,7 +82,7 @@ def __init__( purview_app_name=purview_app_name, user_id_field=user_id_field, guardrail_name=guardrail_name, - supported_event_hooks=supported_event_hooks, + supported_event_hooks=list(self.get_supported_event_hooks()), **kwargs, ) self.guardrail_provider = "microsoft_purview" @@ -101,6 +95,14 @@ def __init__( def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: return None # Config model can be added later for UI support + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.logging_only, + ] + # ------------------------------------------------------------------ # Core DLP check # ------------------------------------------------------------------ diff --git a/litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py b/litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py index 8a4e79b31ab4..3ca63a1e287c 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py +++ b/litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py @@ -60,6 +60,16 @@ class ModelArmorGuardrail(CustomGuardrail, VertexBase): - Post-call sanitization (sanitizeModelResponse) """ + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.pre_mcp_call, + GuardrailEventHooks.during_mcp_call, + ] + def __init__( self, template_id: Optional[str] = None, @@ -76,6 +86,7 @@ def __init__( GuardrailEventHooks.during_call, GuardrailEventHooks.post_call, ] + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) # Initialize parent classes first super().__init__(**kwargs) diff --git a/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py b/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py index 7e8a22a66a8d..a467bb14eb6f 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py +++ b/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py @@ -104,6 +104,15 @@ class NomaGuardrail(CustomGuardrail): _DEFAULT_API_BASE = "https://api.noma.security/" _AIDR_ENDPOINT = "/ai-dr/v2/prompt/scan" + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.pre_mcp_call, + ] + def __init__( self, api_key: Optional[str] = None, @@ -147,6 +156,7 @@ def __init__( else: self.anonymize_input = anonymize_input + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) def _create_background_noma_check( diff --git a/litellm/proxy/guardrails/guardrail_hooks/noma/noma_v2.py b/litellm/proxy/guardrails/guardrail_hooks/noma/noma_v2.py index 1cf3dcd9ac4e..9cf0986c1223 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/noma/noma_v2.py +++ b/litellm/proxy/guardrails/guardrail_hooks/noma/noma_v2.py @@ -8,7 +8,7 @@ import json import os from datetime import datetime -from typing import TYPE_CHECKING, Any, Literal, Optional, Type, cast +from typing import TYPE_CHECKING, Any, List, Literal, Optional, Type, cast from urllib.parse import urlparse from litellm._logging import verbose_proxy_logger @@ -71,14 +71,7 @@ def __init__( if self._requires_api_key(api_base=self.api_base) and not self.api_key: raise ValueError("Noma v2 guardrail requires api_key when using Noma SaaS endpoint") - if "supported_event_hooks" not in kwargs: - kwargs["supported_event_hooks"] = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.during_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.pre_mcp_call, - GuardrailEventHooks.during_mcp_call, - ] + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) @@ -90,6 +83,16 @@ def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: return NomaV2GuardrailConfigModel + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.pre_mcp_call, + GuardrailEventHooks.during_mcp_call, + ] + def _get_authorization_header(self) -> str: if not self.api_key: return "" diff --git a/litellm/proxy/guardrails/guardrail_hooks/onyx/onyx.py b/litellm/proxy/guardrails/guardrail_hooks/onyx/onyx.py index b411d0fb9eb9..63354be9bb14 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/onyx/onyx.py +++ b/litellm/proxy/guardrails/guardrail_hooks/onyx/onyx.py @@ -6,7 +6,7 @@ # +-------------------------------------------------------------+ import os import uuid -from typing import TYPE_CHECKING, Any, Literal, Optional, Type +from typing import TYPE_CHECKING, Any, List, Literal, Optional, Type import httpx from fastapi import HTTPException @@ -21,6 +21,7 @@ get_async_httpx_client, httpxSpecialProvider, ) +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.utils import GenericGuardrailAPIInputs, ModelResponse if TYPE_CHECKING: @@ -28,6 +29,13 @@ class OnyxGuardrail(CustomGuardrail): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + def __init__( self, api_base: Optional[str] = None, @@ -35,6 +43,7 @@ def __init__( timeout: Optional[float] = 10.0, **kwargs, ): + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) timeout = timeout or int(os.getenv("ONYX_TIMEOUT", 10.0)) self.async_handler = get_async_httpx_client( llm_provider=httpxSpecialProvider.GuardrailCallback, diff --git a/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py b/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py index 093ac693d5ee..44016bfd3dc8 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py +++ b/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py @@ -6,6 +6,7 @@ from typing import ( TYPE_CHECKING, Dict, + List, Literal, Optional, Type, @@ -64,17 +65,9 @@ def __init__( **kwargs, ): """Initialize OpenAI Moderation guardrail handler.""" - from litellm.types.guardrails import GuardrailEventHooks - - # Initialize parent CustomGuardrail - supported_event_hooks = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.during_call, - GuardrailEventHooks.post_call, - ] super().__init__( guardrail_name=guardrail_name, - supported_event_hooks=supported_event_hooks, + supported_event_hooks=list(self.get_supported_event_hooks()), **kwargs, ) @@ -353,3 +346,11 @@ def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: ) return OpenAIModerationGuardrailConfigModel + + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/ovalix/ovalix.py b/litellm/proxy/guardrails/guardrail_hooks/ovalix/ovalix.py index 7986d4294a4e..e92ac37ca77f 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/ovalix/ovalix.py +++ b/litellm/proxy/guardrails/guardrail_hooks/ovalix/ovalix.py @@ -66,6 +66,13 @@ class OvalixGuardrail(CustomGuardrail): Monolith backend. """ + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + def __init__( self, tracker_api_base: Optional[str] = None, diff --git a/litellm/proxy/guardrails/guardrail_hooks/pangea/pangea.py b/litellm/proxy/guardrails/guardrail_hooks/pangea/pangea.py index 3d3c5403993e..d02d4b448e81 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/pangea/pangea.py +++ b/litellm/proxy/guardrails/guardrail_hooks/pangea/pangea.py @@ -89,15 +89,10 @@ def __init__( self.pangea_input_recipe = pangea_input_recipe self.pangea_output_recipe = pangea_output_recipe - supported_event_hooks = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - ] - # Pass relevant kwargs to the parent class super().__init__( guardrail_name=guardrail_name, - supported_event_hooks=supported_event_hooks, + supported_event_hooks=list(self.get_supported_event_hooks()), **kwargs, ) verbose_proxy_logger.debug( @@ -317,3 +312,10 @@ def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: ) return PangeaGuardrailConfigModel + + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py b/litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py index c522ffad35d7..192c8c9bc771 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py +++ b/litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py @@ -94,14 +94,7 @@ def __init__( super().__init__( guardrail_name=guardrail_name, default_on=default_on, - supported_event_hooks=[ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.during_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.logging_only, - GuardrailEventHooks.pre_mcp_call, - GuardrailEventHooks.during_mcp_call, - ], + supported_event_hooks=list(self.get_supported_event_hooks()), mask_request_content=_mask_request_content, mask_response_content=_mask_response_content, violation_message_template=violation_message_template, @@ -1854,3 +1847,14 @@ def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: ) return PanwPrismaAirsGuardrailConfigModel + + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.logging_only, + GuardrailEventHooks.pre_mcp_call, + GuardrailEventHooks.during_mcp_call, + ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/pillar/pillar.py b/litellm/proxy/guardrails/guardrail_hooks/pillar/pillar.py index f976839787ba..1d884352eec5 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/pillar/pillar.py +++ b/litellm/proxy/guardrails/guardrail_hooks/pillar/pillar.py @@ -269,18 +269,9 @@ def __init__( ) self.timeout = self.DEFAULT_TIMEOUT - # Define supported event hooks - supported_event_hooks = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.during_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.pre_mcp_call, - GuardrailEventHooks.during_mcp_call, - ] - super().__init__( guardrail_name=guardrail_name, - supported_event_hooks=supported_event_hooks, + supported_event_hooks=list(self.get_supported_event_hooks()), **kwargs, ) @@ -830,3 +821,13 @@ def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: ) return PillarGuardrailConfigModel + + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.pre_mcp_call, + GuardrailEventHooks.during_mcp_call, + ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index 95876a55eab2..b4116742a43c 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -67,6 +67,14 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): user_api_key_cache = None ad_hoc_recognizers = None + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.logging_only, + ] + # Class variables or attributes def __init__( self, @@ -87,6 +95,7 @@ def __init__( if logging_only is True: self.logging_only = True kwargs["event_hook"] = GuardrailEventHooks.logging_only + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) self.guardrail_provider = "presidio" self.pii_tokens: dict = {} # mapping of PII token to original text - only used with Presidio `replace` operation diff --git a/litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py b/litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py index 23f349e0b187..4c7098c9c744 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py +++ b/litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py @@ -14,6 +14,7 @@ get_async_httpx_client, httpxSpecialProvider, ) +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.utils import GenericGuardrailAPIInputs if TYPE_CHECKING: @@ -26,6 +27,13 @@ class PromptSecurityGuardrailMissingSecrets(Exception): class PromptSecurityGuardrail(CustomGuardrail): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + def __init__( self, api_key: Optional[str] = None, @@ -35,6 +43,7 @@ def __init__( check_tool_results: Optional[bool] = None, **kwargs, ): + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) self.async_handler = get_async_httpx_client(llm_provider=httpxSpecialProvider.GuardrailCallback) self.api_key = api_key or os.environ.get("PROMPT_SECURITY_API_KEY") self.api_base = api_base or os.environ.get("PROMPT_SECURITY_API_BASE") diff --git a/litellm/proxy/guardrails/guardrail_hooks/promptguard/promptguard.py b/litellm/proxy/guardrails/guardrail_hooks/promptguard/promptguard.py index 7012b0d8d5d0..6603183efef0 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/promptguard/promptguard.py +++ b/litellm/proxy/guardrails/guardrail_hooks/promptguard/promptguard.py @@ -81,11 +81,7 @@ def __init__( llm_provider=httpxSpecialProvider.GuardrailCallback, ) - if "supported_event_hooks" not in kwargs: - kwargs["supported_event_hooks"] = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - ] + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) @@ -97,6 +93,13 @@ def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: return PromptGuardConfigModel + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + @log_guardrail_information async def apply_guardrail( self, diff --git a/litellm/proxy/guardrails/guardrail_hooks/qualifire/qualifire.py b/litellm/proxy/guardrails/guardrail_hooks/qualifire/qualifire.py index 54f47cfbfa1d..30eb9a2e4a7c 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/qualifire/qualifire.py +++ b/litellm/proxy/guardrails/guardrail_hooks/qualifire/qualifire.py @@ -22,6 +22,7 @@ httpxSpecialProvider, ) from litellm.secret_managers.main import get_secret_str +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.llms.openai import AllMessageValues from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel from litellm.types.utils import GenericGuardrailAPIInputs @@ -31,6 +32,13 @@ class QualifireGuardrail(CustomGuardrail): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + def __init__( self, api_key: Optional[str] = None, @@ -86,6 +94,7 @@ def __init__( # Initialize async HTTP client for direct API calls self.async_handler = get_async_httpx_client(llm_provider=httpxSpecialProvider.GuardrailCallback) + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) def _has_any_check_enabled(self) -> bool: diff --git a/litellm/proxy/guardrails/guardrail_hooks/repelloai/repelloai.py b/litellm/proxy/guardrails/guardrail_hooks/repelloai/repelloai.py index f8971ceb4056..da1c74b37b61 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/repelloai/repelloai.py +++ b/litellm/proxy/guardrails/guardrail_hooks/repelloai/repelloai.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import datetime -from typing import AsyncGenerator, Literal +from typing import AsyncGenerator, List, Literal from pydantic import TypeAdapter, ValidationError from pydantic import BaseModel @@ -64,6 +64,13 @@ def _is_object_list(value: object) -> TypeGuard[list[object]]: # guard-ok: isin class RepelloAIGuardrail(CustomGuardrail): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + @staticmethod def _get_field(obj: object, key: str) -> object: if _is_object_dict(obj): @@ -169,6 +176,7 @@ def __init__( guardrail_name=guardrail_name, event_hook=event_hook, default_on=default_on, + supported_event_hooks=list(self.get_supported_event_hooks()), ) async def _call_analyze( diff --git a/litellm/proxy/guardrails/guardrail_hooks/semantic_guard/semantic_guard.py b/litellm/proxy/guardrails/guardrail_hooks/semantic_guard/semantic_guard.py index 3f802485a8e6..3865251a48a5 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/semantic_guard/semantic_guard.py +++ b/litellm/proxy/guardrails/guardrail_hooks/semantic_guard/semantic_guard.py @@ -58,10 +58,7 @@ def __init__( ): super().__init__( guardrail_name=guardrail_name, - supported_event_hooks=[ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - ], + supported_event_hooks=list(self.get_supported_event_hooks()), event_hook=event_hook or GuardrailEventHooks.pre_call, default_on=default_on, **kwargs, @@ -96,6 +93,13 @@ def __init__( f"embedding_model={embedding_model}, threshold={similarity_threshold}" ) + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + @log_guardrail_information async def async_pre_call_hook( self, diff --git a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py index 2171be235e5c..58c57dfdac1b 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py +++ b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py @@ -52,11 +52,7 @@ def __init__( **kwargs: Additional arguments passed to CustomGuardrail """ # Set supported event hooks - this guardrail only works on post_call - if "supported_event_hooks" not in kwargs: - kwargs["supported_event_hooks"] = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - ] + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) @@ -179,6 +175,13 @@ def get_config_model(): return ToolPermissionGuardrailConfigModel + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + def _matches_regex(self, pattern: Optional[re.Pattern], value: Optional[str]) -> bool: if pattern is None: return True diff --git a/litellm/proxy/guardrails/guardrail_hooks/vigil_guard/vigil_guard.py b/litellm/proxy/guardrails/guardrail_hooks/vigil_guard/vigil_guard.py index 9a8893734e19..9976a1c48b15 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/vigil_guard/vigil_guard.py +++ b/litellm/proxy/guardrails/guardrail_hooks/vigil_guard/vigil_guard.py @@ -116,11 +116,7 @@ def __init__( llm_provider=httpxSpecialProvider.GuardrailCallback, ) - if "supported_event_hooks" not in kwargs: - kwargs["supported_event_hooks"] = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.post_call, - ] + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) @@ -132,6 +128,13 @@ def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: return VigilGuardGuardrailConfigModel + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + @log_guardrail_information async def apply_guardrail( self, diff --git a/litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py b/litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py index 36c22753404f..1b663c16d5b8 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py +++ b/litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py @@ -119,13 +119,7 @@ def __init__( llm_provider=httpxSpecialProvider.GuardrailCallback, ) - if "supported_event_hooks" not in kwargs: - kwargs["supported_event_hooks"] = [ - GuardrailEventHooks.pre_call, - GuardrailEventHooks.during_call, - GuardrailEventHooks.post_call, - GuardrailEventHooks.logging_only, - ] + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) super().__init__(**kwargs) @@ -137,6 +131,15 @@ def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: return XecGuardConfigModel + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.logging_only, + ] + @log_guardrail_information async def apply_guardrail( self, diff --git a/litellm/proxy/guardrails/guardrail_hooks/zscaler_ai_guard/zscaler_ai_guard.py b/litellm/proxy/guardrails/guardrail_hooks/zscaler_ai_guard/zscaler_ai_guard.py index 9c6606205823..65338827e07f 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/zscaler_ai_guard/zscaler_ai_guard.py +++ b/litellm/proxy/guardrails/guardrail_hooks/zscaler_ai_guard/zscaler_ai_guard.py @@ -4,7 +4,7 @@ # # +-------------------------------------------------------------+ import os -from typing import TYPE_CHECKING, Literal, Optional +from typing import TYPE_CHECKING, List, Literal, Optional from fastapi import HTTPException @@ -17,6 +17,7 @@ get_async_httpx_client, httpxSpecialProvider, ) +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.utils import GenericGuardrailAPIInputs if TYPE_CHECKING: @@ -27,6 +28,13 @@ class ZscalerAIGuard(CustomGuardrail): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + ] + def __init__( self, api_key: Optional[str] = None, @@ -37,6 +45,7 @@ def __init__( send_user_api_key_team_id: Optional[bool] = None, **kwargs, ): + kwargs.setdefault("supported_event_hooks", list(self.get_supported_event_hooks())) self.optional_params = kwargs self.zscaler_ai_guard_url = api_base or os.getenv( "ZSCALER_AI_GUARD_URL", diff --git a/litellm/proxy/guardrails/guardrail_registry.py b/litellm/proxy/guardrails/guardrail_registry.py index 8962073fe7a9..e9eee3a1a8ac 100644 --- a/litellm/proxy/guardrails/guardrail_registry.py +++ b/litellm/proxy/guardrails/guardrail_registry.py @@ -13,12 +13,23 @@ from litellm._uuid import uuid from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.litellm_core_utils.safe_json_dumps import safe_dumps +from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import ( + BedrockGuardrail, +) from litellm.proxy.guardrails.guardrail_hooks.grayswan import ( GraySwanGuardrail, ) from litellm.proxy.guardrails.guardrail_hooks.grayswan import ( initialize_guardrail as initialize_grayswan, ) +from litellm.proxy.guardrails.guardrail_hooks.lakera_ai import lakeraAI_Moderation +from litellm.proxy.guardrails.guardrail_hooks.lakera_ai_v2 import LakeraAIGuardrail +from litellm.proxy.guardrails.guardrail_hooks.presidio import ( + _OPTIONAL_PresidioPIIMasking, +) +from litellm.proxy.guardrails.guardrail_hooks.tool_permission import ( + ToolPermissionGuardrail, +) from litellm.proxy.types_utils.utils import get_instance_fn from litellm.proxy.utils import PrismaClient from litellm.repositories.table_repositories import GuardrailsRepository @@ -55,7 +66,12 @@ } guardrail_class_registry: Dict[str, Type[CustomGuardrail]] = { - SupportedGuardrailIntegrations.GRAYSWAN.value: GraySwanGuardrail + SupportedGuardrailIntegrations.BEDROCK.value: BedrockGuardrail, + SupportedGuardrailIntegrations.GRAYSWAN.value: GraySwanGuardrail, + SupportedGuardrailIntegrations.LAKERA.value: lakeraAI_Moderation, + SupportedGuardrailIntegrations.LAKERA_V2.value: LakeraAIGuardrail, + SupportedGuardrailIntegrations.PRESIDIO.value: _OPTIONAL_PresidioPIIMasking, + SupportedGuardrailIntegrations.TOOL_PERMISSION.value: ToolPermissionGuardrail, } diff --git a/litellm/types/guardrails.py b/litellm/types/guardrails.py index 8d7d7311fad3..c7e080b13633 100644 --- a/litellm/types/guardrails.py +++ b/litellm/types/guardrails.py @@ -904,6 +904,7 @@ class GuardrailUIAddGuardrailSettings(BaseModel): supported_entities: List[str] supported_actions: List[str] supported_modes: List[str] + supported_modes_by_provider: Dict[str, List[str]] pii_entity_categories: List[PiiEntityCategoryMap] content_filter_settings: Optional[Dict[str, Any]] = None diff --git a/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py b/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py index ce8f0802ae1c..09d4f127d751 100644 --- a/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py +++ b/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py @@ -25,6 +25,7 @@ delete_guardrail, get_guardrail_info, get_guardrail_submission, + get_guardrail_ui_settings, list_guardrail_submissions, list_guardrails_v2, patch_guardrail, @@ -2079,3 +2080,100 @@ async def test_list_submissions_summary_counts_unaffected_by_filters(mocker): assert result.summary.total == 2 # unfiltered assert result.summary.pending_review == 1 assert result.summary.active == 1 + + +@pytest.mark.asyncio +async def test_get_guardrail_ui_settings_returns_per_provider_supported_modes(): + """ + Regression test for LIT-4226. The Admin UI used to render `pre_mcp_call` as a + selectable mode for every guardrail because the settings endpoint returned a + single global `supported_modes` list. The proxy then rejected the save because + Content Filter and Tool Permission do not accept `pre_mcp_call`. The endpoint + must now return per-provider modes so the UI can filter its dropdown. + """ + result = await get_guardrail_ui_settings() + + modes_by_provider = result.supported_modes_by_provider + + # Guardrails from the bug report: neither accepts pre_mcp_call, and the + # settings endpoint must reflect that so the UI can hide it. + assert "pre_mcp_call" not in modes_by_provider["litellm_content_filter"] + assert modes_by_provider["tool_permission"] == ["pre_call", "post_call"] + + # MCP-capable guardrails must still advertise the MCP hooks so users who + # picked one of them can actually configure pre_mcp_call / during_mcp_call. + for provider in ("bedrock", "panw_prisma_airs", "cisco_ai_defense", "custom_code", "pillar"): + assert "pre_mcp_call" in modes_by_provider[provider], provider + assert "during_mcp_call" in modes_by_provider[provider], provider + + # The union list stays exhaustive for legacy clients that ignore the + # per-provider map; it must cover every declared GuardrailEventHooks value. + from litellm.types.guardrails import GuardrailEventHooks + + assert set(result.supported_modes) == {m.value for m in GuardrailEventHooks} + + +@pytest.mark.asyncio +async def test_ui_settings_map_matches_runtime_supported_event_hooks(): + """ + Regression guard against the two-copy-of-the-list drift risk. The map the + UI reads must agree with what CustomGuardrail._validate_event_hook accepts + at save time, otherwise the bug in LIT-4226 comes back one classname at a + time as future guardrails drift. + """ + from litellm.proxy.guardrails.guardrail_registry import guardrail_class_registry + + result = await get_guardrail_ui_settings() + + for provider, guardrail_class in guardrail_class_registry.items(): + declared = guardrail_class.get_supported_event_hooks() + if declared is None: + assert ( + provider not in result.supported_modes_by_provider + ), f"{provider} returned None from classmethod but appears in map" + continue + + assert provider in result.supported_modes_by_provider, provider + assert result.supported_modes_by_provider[provider] == [ + hook.value for hook in declared + ], provider + + +def test_content_filter_runtime_rejects_pre_mcp_call(): + """ + Locks the runtime side of the LIT-4226 contract: the ContentFilterGuardrail + validator must reject pre_mcp_call at construction. If someone widens the + UI classmethod but forgets to widen the runtime supported_event_hooks (or + vice versa), the two-lists-must-agree test above catches the drift and this + test catches the specific bug the ticket reported. + """ + from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import ( + ContentFilterGuardrail, + ) + from litellm.types.guardrails import GuardrailEventHooks + + with pytest.raises(ValueError, match="not in the supported event hooks"): + ContentFilterGuardrail( + guardrail_name="lit4226-runtime-check", + event_hook=GuardrailEventHooks.pre_mcp_call, + ) + + +def test_model_armor_runtime_supported_event_hooks_match_classmethod(): + """ + Regression for the drift Round 2 caught: the ModelArmorGuardrail classmethod + declared its supported hooks for the UI, but __init__ did not seed the + runtime instance's `supported_event_hooks` from that classmethod, so the + runtime validator accepted any hook (including nonsense like logging_only) + while the UI hid them. Ensures the two sides agree at instantiation time. + """ + from litellm.proxy.guardrails.guardrail_hooks.model_armor.model_armor import ( + ModelArmorGuardrail, + ) + + instance = ModelArmorGuardrail( + guardrail_name="lit4226-model-armor-drift", + template_id="t", + project_id="p", + ) + assert instance.supported_event_hooks == ModelArmorGuardrail.get_supported_event_hooks() diff --git a/ui/litellm-dashboard/src/components/guardrails/add_guardrail_form.tsx b/ui/litellm-dashboard/src/components/guardrails/add_guardrail_form.tsx index 847c3557280d..49269144c7d0 100644 --- a/ui/litellm-dashboard/src/components/guardrails/add_guardrail_form.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/add_guardrail_form.tsx @@ -60,6 +60,7 @@ interface GuardrailSettings { supported_entities: string[]; supported_actions: string[]; supported_modes: string[]; + supported_modes_by_provider?: Record; pii_entity_categories: Array<{ category: string; entities: string[]; @@ -732,7 +733,14 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a rules={[{ required: true, message: "Please select a mode" }]} > - {guardrailSettings?.supported_modes?.map((mode) => ( + {(() => { + const providerKey = selectedProvider ? guardrail_provider_map[selectedProvider]?.toLowerCase() : null; + const perProvider = + providerKey && guardrailSettings?.supported_modes_by_provider + ? guardrailSettings.supported_modes_by_provider[providerKey] + : undefined; + return perProvider ?? guardrailSettings?.supported_modes; + })()?.map((mode) => ( From 4870a9d738004fd42f5898863a471770dafb0de0 Mon Sep 17 00:00:00 2001 From: yucheng-berri Date: Thu, 9 Jul 2026 20:16:11 -0700 Subject: [PATCH 2/4] fix(guardrails): add LITELLM_STRICT_GUARDRAIL_MODES escape hatch, preserve current mode in edit form Address Greptile P1 (startup break) and P2 (edit form UX): LITELLM_STRICT_GUARDRAIL_MODES defaults to true (raise on unsupported event_hook, unchanged behavior for the guardrails validated pre-PR). Setting it to false logs a warning and continues, giving deployments an opt-out while they fix configs that now surface as errors instead of silently no-op'ing. Regression test covers both modes. Edit form now surfaces the currently-saved mode even when it is not in the filtered per-provider list, so a legacy row (e.g. content_filter saved with pre_mcp_call before this fix) no longer disappears from the dropdown; the option renders with a 'not supported by ' note so the user knows to pick another. --- litellm/integrations/custom_guardrail.py | 28 ++++++++++++++++- .../guardrails/test_guardrail_endpoints.py | 31 +++++++++++++++++++ .../guardrails/edit_guardrail_form.tsx | 22 ++++++------- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index d5f1de87c648..46d787c8b118 100644 --- a/litellm/integrations/custom_guardrail.py +++ b/litellm/integrations/custom_guardrail.py @@ -1,3 +1,4 @@ +import os import secrets from datetime import datetime from typing import ( @@ -17,6 +18,7 @@ from litellm.litellm_core_utils.core_helpers import redact_nested_match_and_regex_keys from litellm.caching import DualCache from litellm.integrations.custom_logger import CustomLogger +from litellm.secret_managers.main import str_to_bool from litellm.types.guardrails import ( DynamicGuardrailParams, GuardrailEventHooks, @@ -59,6 +61,20 @@ _PRE_CALL_EXECUTED_TOKEN = secrets.token_hex(16) +def _strict_guardrail_modes_enabled() -> bool: + """Whether guardrail-mode validation raises (default) or logs a warning. + + Set `LITELLM_STRICT_GUARDRAIL_MODES=false` to keep the pre-LIT-4226 behavior + for guardrails whose supported_event_hooks list newly includes their + configured mode: log the mismatch and continue instead of raising at boot. + """ + raw = os.environ.get("LITELLM_STRICT_GUARDRAIL_MODES") + if raw is None: + return True + parsed = str_to_bool(raw) + return True if parsed is None else parsed + + def get_session_id_from_request_data(request_data: Dict[str, Any]) -> Optional[str]: """Extract session_id from request data (litellm_session_id or metadata).""" session_id = request_data.get("litellm_session_id") @@ -132,7 +148,17 @@ def __init__( if supported_event_hooks: ## validate event_hook is in supported_event_hooks - self._validate_event_hook(event_hook, supported_event_hooks) + try: + self._validate_event_hook(event_hook, supported_event_hooks) + except ValueError as validation_error: + if _strict_guardrail_modes_enabled(): + raise + verbose_logger.warning( + "%s. LITELLM_STRICT_GUARDRAIL_MODES=false; continuing " + "with unsupported event_hook. Set the env var to true " + "(default) to enforce validation and fail at startup.", + validation_error, + ) super().__init__(**kwargs) def render_violation_message(self, default: str, context: Optional[Dict[str, Any]] = None) -> str: diff --git a/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py b/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py index 09d4f127d751..eb0a670291eb 100644 --- a/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py +++ b/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py @@ -2177,3 +2177,34 @@ def test_model_armor_runtime_supported_event_hooks_match_classmethod(): project_id="p", ) assert instance.supported_event_hooks == ModelArmorGuardrail.get_supported_event_hooks() + + +def test_strict_guardrail_modes_flag_controls_raise_vs_warn(monkeypatch, caplog): + """ + Escape hatch for the boot-time behavior change. Deployments upgrading from + a build where a guardrail previously silently no-op'd on an unsupported + mode should be able to set LITELLM_STRICT_GUARDRAIL_MODES=false and boot + with a warning instead of a hard failure while they fix their config. + """ + import logging + + from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import ( + ContentFilterGuardrail, + ) + from litellm.types.guardrails import GuardrailEventHooks + + monkeypatch.delenv("LITELLM_STRICT_GUARDRAIL_MODES", raising=False) + with pytest.raises(ValueError, match="not in the supported event hooks"): + ContentFilterGuardrail( + guardrail_name="lit4226-strict-default", + event_hook=GuardrailEventHooks.pre_mcp_call, + ) + + monkeypatch.setenv("LITELLM_STRICT_GUARDRAIL_MODES", "false") + with caplog.at_level(logging.WARNING): + instance = ContentFilterGuardrail( + guardrail_name="lit4226-strict-off", + event_hook=GuardrailEventHooks.pre_mcp_call, + ) + assert instance is not None + assert any("not in the supported event hooks" in rec.message for rec in caplog.records) diff --git a/ui/litellm-dashboard/src/components/guardrails/edit_guardrail_form.tsx b/ui/litellm-dashboard/src/components/guardrails/edit_guardrail_form.tsx index 6a76b874c2ce..5ca52ac8af55 100644 --- a/ui/litellm-dashboard/src/components/guardrails/edit_guardrail_form.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/edit_guardrail_form.tsx @@ -426,17 +426,17 @@ const EditGuardrailForm: React.FC = ({ providerKey && guardrailSettings?.supported_modes_by_provider ? guardrailSettings.supported_modes_by_provider[providerKey] : undefined; - return perProvider ?? guardrailSettings?.supported_modes; - })()?.map((mode) => ( - - )) || ( - <> - - - - )} + const modes = perProvider ?? guardrailSettings?.supported_modes ?? ["pre_call", "post_call"]; + const currentMode = initialValues?.mode; + const merged = currentMode && !modes.includes(currentMode) ? [currentMode, ...modes] : modes; + return merged.map((mode) => ( + + )); + })()} From 727389fa64b7b28f163faedd67f69b0fdccfdcdd Mon Sep 17 00:00:00 2001 From: yucheng-berri Date: Fri, 10 Jul 2026 01:18:48 -0700 Subject: [PATCH 3/4] fix(guardrails): correct audited hook lists, prune stale modes on provider switch, clean form lint Audited every get_supported_event_hooks classmethod against the hooks each guardrail's own tests exercise and its handler methods. Five were too narrow and their tests caught it in CI: rubrik gains pre_call, presidio gains during_call and pre_mcp_call, prompt_security, onyx and qualifire gain during_call. The remaining classes match either their original __init__ declarations or their exercised modes exactly. Cursor review fixes: the Add form now drops selected modes the new provider does not support when the user switches providers, so a pre_mcp_call selection cannot ride along into a provider that rejects it at save; the edit form handles list-shaped stored modes instead of treating mode as always a string. Extracted shared toModeArray and getSupportedModesForProvider helpers into guardrail_info_helpers so both forms use one implementation, typed the remaining any usages in both forms, removed nested ternaries, and committed the ratcheted-down eslint metrics and pruned suppressions --- litellm/integrations/rubrik.py | 2 +- .../guardrails/guardrail_hooks/onyx/onyx.py | 1 + .../guardrails/guardrail_hooks/presidio.py | 2 + .../prompt_security/prompt_security.py | 1 + .../guardrail_hooks/qualifire/qualifire.py | 1 + ui/litellm-dashboard/eslint-metrics.json | 6 +- ui/litellm-dashboard/eslint-suppressions.json | 3 - .../guardrails/add_guardrail_form.tsx | 176 +++++++++++------- .../guardrails/edit_guardrail_form.tsx | 32 ++-- .../guardrails/guardrail_info_helpers.tsx | 20 ++ 10 files changed, 151 insertions(+), 93 deletions(-) diff --git a/litellm/integrations/rubrik.py b/litellm/integrations/rubrik.py index 07b77a4863a8..11809ee63618 100644 --- a/litellm/integrations/rubrik.py +++ b/litellm/integrations/rubrik.py @@ -54,7 +54,7 @@ class _MalformedToolBlockingResponseError(Exception): class RubrikLogger(CustomGuardrail, CustomBatchLogger): @classmethod def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: - return [GuardrailEventHooks.post_call] + return [GuardrailEventHooks.pre_call, GuardrailEventHooks.post_call] def __init__( self, diff --git a/litellm/proxy/guardrails/guardrail_hooks/onyx/onyx.py b/litellm/proxy/guardrails/guardrail_hooks/onyx/onyx.py index 63354be9bb14..606f0a4587d4 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/onyx/onyx.py +++ b/litellm/proxy/guardrails/guardrail_hooks/onyx/onyx.py @@ -33,6 +33,7 @@ class OnyxGuardrail(CustomGuardrail): def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: return [ GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, GuardrailEventHooks.post_call, ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index b4116742a43c..a0c822964a05 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -71,8 +71,10 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: return [ GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, GuardrailEventHooks.post_call, GuardrailEventHooks.logging_only, + GuardrailEventHooks.pre_mcp_call, ] # Class variables or attributes diff --git a/litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py b/litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py index 4c7098c9c744..e60815dbcf7d 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py +++ b/litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py @@ -31,6 +31,7 @@ class PromptSecurityGuardrail(CustomGuardrail): def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: return [ GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, GuardrailEventHooks.post_call, ] diff --git a/litellm/proxy/guardrails/guardrail_hooks/qualifire/qualifire.py b/litellm/proxy/guardrails/guardrail_hooks/qualifire/qualifire.py index 30eb9a2e4a7c..9c62c2915ff4 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/qualifire/qualifire.py +++ b/litellm/proxy/guardrails/guardrail_hooks/qualifire/qualifire.py @@ -36,6 +36,7 @@ class QualifireGuardrail(CustomGuardrail): def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: return [ GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, GuardrailEventHooks.post_call, ] diff --git a/ui/litellm-dashboard/eslint-metrics.json b/ui/litellm-dashboard/eslint-metrics.json index 2e204c63a484..dca4d6d6e52f 100644 --- a/ui/litellm-dashboard/eslint-metrics.json +++ b/ui/litellm-dashboard/eslint-metrics.json @@ -1,8 +1,8 @@ { - "@typescript-eslint/no-explicit-any": 1978, + "@typescript-eslint/no-explicit-any": 1958, "complexity": 129, - "local/no-large-inline-object-arg": 512, - "local/no-long-condition-chain": 233, + "local/no-large-inline-object-arg": 509, + "local/no-long-condition-chain": 231, "max-depth": 59, "no-console": 15 } diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 32ab92cbcc92..695f126762e0 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1563,9 +1563,6 @@ } }, "src/components/guardrails/add_guardrail_form.tsx": { - "no-nested-ternary": { - "count": 4 - }, "react-hooks/set-state-in-effect": { "count": 1 }, diff --git a/ui/litellm-dashboard/src/components/guardrails/add_guardrail_form.tsx b/ui/litellm-dashboard/src/components/guardrails/add_guardrail_form.tsx index 49269144c7d0..c785c865b345 100644 --- a/ui/litellm-dashboard/src/components/guardrails/add_guardrail_form.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/add_guardrail_form.tsx @@ -8,10 +8,12 @@ import { modelAvailableCall, } from "../networking"; import ContentFilterConfiguration from "./content_filter/ContentFilterConfiguration"; +import { type CompetitorIntentConfig } from "./content_filter/CompetitorIntentConfiguration"; import { choiceToSkipSystemForCreate, choiceToSkipToolForCreate, getGuardrailProviders, + getSupportedModesForProvider, guardrail_provider_map, guardrailLogoMap, populateGuardrailProviderMap, @@ -19,6 +21,7 @@ import { shouldRenderContentFilterConfigSettings, shouldRenderLLMJudgeFields, shouldRenderPIIConfigSettings, + toModeArray, } from "./guardrail_info_helpers"; import { resolveLogoSrc } from "@/lib/assetPaths"; import GuardrailOptionalParams from "./guardrail_optional_params"; @@ -83,13 +86,55 @@ interface GuardrailSettings { }; } -interface LiteLLMParams { - guardrail: string; - mode: string; - default_on: boolean; - [key: string]: any; // Allow additional properties for specific guardrails +interface ContentFilterPattern { + id: string; + type: "prebuilt" | "custom"; + name: string; + display_name?: string; + pattern?: string; + action: "BLOCK" | "MASK"; +} + +interface ContentFilterBlockedWord { + id: string; + keyword: string; + action: "BLOCK" | "MASK"; + description?: string; +} + +interface SelectedContentCategory { + id: string; + category: string; + display_name: string; + action: "BLOCK" | "MASK"; + severity_threshold: "high" | "medium" | "low"; +} + +interface JudgeCriterion { + name: string; + weight: number | string; + description?: string; } +const createEmptyToolPermissionConfig = (): ToolPermissionConfig => ({ + rules: [], + default_action: "deny", + on_disallowed_action: "block", + violation_message_template: "", +}); + +const getStepIndicatorStyle = (isDone: boolean, isCurrent: boolean): React.CSSProperties => { + if (isDone) return { background: "#4f46e5", color: "#fff", border: "none" }; + if (isCurrent) return { background: "#fff", color: "#4f46e5", border: "2px solid #4f46e5" }; + return { background: "#f8fafc", color: "#94a3b8", border: "1px solid #e2e8f0" }; +}; + +const getStepTitleColor = (isDone: boolean, isCurrent: boolean): string => { + if (isCurrent) return "#1e293b"; + if (isDone) return "#4f46e5"; + return "#94a3b8"; +}; + // Mapping of provider -> list of param descriptors interface ProviderParam { param: string; @@ -123,12 +168,12 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a const [categorySpecificThresholds, setCategorySpecificThresholds] = useState<{ [key: string]: number }>({}); // Content Filter state - const [selectedPatterns, setSelectedPatterns] = useState([]); - const [blockedWords, setBlockedWords] = useState([]); - const [selectedContentCategories, setSelectedContentCategories] = useState([]); + const [selectedPatterns, setSelectedPatterns] = useState([]); + const [blockedWords, setBlockedWords] = useState([]); + const [selectedContentCategories, setSelectedContentCategories] = useState([]); const [pendingCategorySelection, setPendingCategorySelection] = useState(""); const [competitorIntentEnabled, setCompetitorIntentEnabled] = useState(false); - const [competitorIntentConfig, setCompetitorIntentConfig] = useState(null); + const [competitorIntentConfig, setCompetitorIntentConfig] = useState(null); // Endpoint Settings state (step 5) const [selectedEndpointType, setSelectedEndpointType] = useState(""); @@ -138,12 +183,9 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a const [endpointSettingsOpen, setEndpointSettingsOpen] = useState(false); const [availableModels, setAvailableModels] = useState([]); - const [toolPermissionConfig, setToolPermissionConfig] = useState({ - rules: [], - default_action: "deny", - on_disallowed_action: "block", - violation_message_template: "", - }); + const [toolPermissionConfig, setToolPermissionConfig] = useState( + createEmptyToolPermissionConfig, + ); const isToolPermissionProvider = useMemo(() => { if (!selectedProvider) { @@ -169,7 +211,7 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a setGuardrailSettings(uiSettings); setProviderParams(providerParamsResp); if (modelsResp?.data) { - setAvailableModels(modelsResp.data.map((m: any) => m.id)); + setAvailableModels(modelsResp.data.map((m: { id: string }) => m.id)); } // Populate dynamic providers from API response @@ -190,7 +232,7 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a // Set provider setSelectedProvider(preset.provider); - const baseValues: Record = { + const baseValues: Record = { provider: preset.provider, guardrail_name: preset.guardrailNameSuggestion, mode: preset.mode, @@ -206,7 +248,7 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a // Pre-select content category if specified if (preset.categoryName && guardrailSettings.content_filter_settings?.content_categories) { const category = guardrailSettings.content_filter_settings.content_categories.find( - (c: any) => c.name === preset.categoryName, + (c) => c.name === preset.categoryName, ); if (category) { setSelectedContentCategories([ @@ -220,12 +262,12 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a ]); } } - }, [preset, visible, guardrailSettings]); + }, [preset, visible, guardrailSettings, form]); const handleProviderChange = (value: string) => { setSelectedProvider(value); // Reset form fields that are provider-specific - const resetValues: Record = { + const resetValues: Record = { config: undefined, presidio_analyzer_api_base: undefined, presidio_anonymizer_api_base: undefined, @@ -233,6 +275,21 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a if (value === "BlockCodeExecution") { resetValues.confidence_threshold = 0.5; } + + // Drop selected modes the new provider does not support + const newProviderKey = guardrail_provider_map[value]?.toLowerCase(); + const newProviderModes = + newProviderKey && guardrailSettings?.supported_modes_by_provider + ? guardrailSettings.supported_modes_by_provider[newProviderKey] + : undefined; + if (newProviderModes) { + const selectedModes = toModeArray(form.getFieldValue("mode")); + const keptModes = selectedModes.filter((m) => newProviderModes.includes(m)); + if (keptModes.length !== selectedModes.length) { + resetValues.mode = keptModes.length > 0 ? keptModes : undefined; + } + } + form.setFieldsValue(resetValues); // Reset PII selections when changing provider @@ -252,12 +309,7 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a setCompetitorIntentEnabled(false); setCompetitorIntentConfig(null); - setToolPermissionConfig({ - rules: [], - default_action: "deny", - on_disallowed_action: "block", - violation_message_template: "", - }); + setToolPermissionConfig(createEmptyToolPermissionConfig()); // Default LLM-as-a-Judge to post_call mode if (value === "LlmAsAJudge") { @@ -386,12 +438,7 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a setBlockedWords([]); setSelectedContentCategories([]); setPendingCategorySelection(""); - setToolPermissionConfig({ - rules: [], - default_action: "deny", - on_disallowed_action: "block", - violation_message_template: "", - }); + setToolPermissionConfig(createEmptyToolPermissionConfig()); setSelectedEndpointType(""); setEndSessionAfterNFails(undefined); setOnViolation("warn"); @@ -424,9 +471,9 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a guardrail: string; mode: string; default_on: boolean; - [key: string]: any; // Allow dynamic properties + [key: string]: unknown; // Allow dynamic properties }; - guardrail_info: any; + guardrail_info: Record; } = { guardrail_name: values.guardrail_name, litellm_params: { @@ -468,13 +515,10 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a // For Content Filter, add patterns, blocked words, categories, and optionally competitor intent if (shouldRenderContentFilterConfigSettings(values.provider)) { // Validate that at least one content filter setting is configured - const hasCompetitorIntent = competitorIntentEnabled && competitorIntentConfig?.brand_self?.length > 0; - if ( - selectedPatterns.length === 0 && - blockedWords.length === 0 && - selectedContentCategories.length === 0 && - !hasCompetitorIntent - ) { + const hasCompetitorIntent = competitorIntentEnabled && (competitorIntentConfig?.brand_self?.length ?? 0) > 0; + const hasContentFilterSelections = + selectedPatterns.length > 0 || blockedWords.length > 0 || selectedContentCategories.length > 0; + if (!hasContentFilterSelections && !hasCompetitorIntent) { NotificationsManager.fromBackend( "Please configure at least one content filter setting (category, pattern, keyword, or competitor intent)", ); @@ -506,14 +550,15 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a severity_threshold: c.severity_threshold || "medium", })); } - if (competitorIntentEnabled && competitorIntentConfig?.brand_self?.length > 0) { + if (hasCompetitorIntent && competitorIntentConfig) { guardrailData.litellm_params.competitor_intent_config = { competitor_intent_type: competitorIntentConfig.competitor_intent_type ?? "airline", brand_self: competitorIntentConfig.brand_self, - locations: competitorIntentConfig.locations?.length > 0 ? competitorIntentConfig.locations : undefined, + locations: + (competitorIntentConfig.locations?.length ?? 0) > 0 ? competitorIntentConfig.locations : undefined, competitors: competitorIntentConfig.competitor_intent_type === "generic" && - competitorIntentConfig.competitors?.length > 0 + (competitorIntentConfig.competitors?.length ?? 0) > 0 ? competitorIntentConfig.competitors : undefined, policy: competitorIntentConfig.policy, @@ -537,13 +582,13 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a } if (guardrailProvider === "llm_as_a_judge") { - const criteria: any[] = values.criteria || []; + const criteria: JudgeCriterion[] = values.criteria || []; if (criteria.length === 0) { NotificationsManager.fromBackend("Add at least one evaluation criterion"); setLoading(false); return; } - const weightTotal = criteria.reduce((sum: number, c: any) => sum + (Number(c?.weight) || 0), 0); + const weightTotal = criteria.reduce((sum, c) => sum + (Number(c?.weight) || 0), 0); if (weightTotal !== 100) { NotificationsManager.fromBackend(`Criterion weights must sum to 100% (currently ${weightTotal}%)`); setLoading(false); @@ -552,7 +597,7 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a guardrailData.litellm_params.judge_model = values.judge_model; guardrailData.litellm_params.overall_threshold = values.overall_threshold ?? 80; guardrailData.litellm_params.on_failure = values.on_failure ?? "block"; - guardrailData.litellm_params.criteria = criteria.map((c: any) => ({ + guardrailData.litellm_params.criteria = criteria.map((c) => ({ name: c.name, weight: Number(c.weight), description: c.description || "", @@ -653,6 +698,10 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a }; const renderBasicInfo = () => { + const showProviderFields = + !isToolPermissionProvider && + !shouldRenderContentFilterConfigSettings(selectedProvider) && + !shouldRenderLLMJudgeFields(selectedProvider); return ( <> = ({ visible, onClose, a rules={[{ required: true, message: "Please select a mode" }]} > {(() => { - const providerKey = selectedProvider ? guardrail_provider_map[selectedProvider]?.toLowerCase() : null; - const perProvider = - providerKey && guardrailSettings?.supported_modes_by_provider - ? guardrailSettings.supported_modes_by_provider[providerKey] - : undefined; - const modes = perProvider ?? guardrailSettings?.supported_modes ?? ["pre_call", "post_call"]; - const currentMode = initialValues?.mode; - const merged = currentMode && !modes.includes(currentMode) ? [currentMode, ...modes] : modes; - return merged.map((mode) => ( + const modes = getSupportedModesForProvider(guardrailSettings, selectedProvider) ?? [ + "pre_call", + "post_call", + ]; + const currentModes = toModeArray(initialValues?.mode); + const unsupportedCurrent = currentModes.filter((m) => !modes.includes(m)); + return [...unsupportedCurrent, ...modes].map((mode) => ( diff --git a/ui/litellm-dashboard/src/components/guardrails/guardrail_info_helpers.tsx b/ui/litellm-dashboard/src/components/guardrails/guardrail_info_helpers.tsx index 3ac9fe4087a5..e8c4810ca693 100644 --- a/ui/litellm-dashboard/src/components/guardrails/guardrail_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/guardrail_info_helpers.tsx @@ -76,6 +76,26 @@ export const populateGuardrailProviderMap = (providerParamsResponse: Record { + if (Array.isArray(raw)) return raw.filter((m): m is string => typeof m === "string"); + if (typeof raw === "string") return [raw]; + return []; +}; + +// Resolves the supported modes for the selected provider, falling back to the global list +export const getSupportedModesForProvider = ( + settings: { supported_modes?: string[]; supported_modes_by_provider?: Record } | null, + selectedProvider: string | null, +): string[] | undefined => { + const providerKey = selectedProvider ? guardrail_provider_map[selectedProvider]?.toLowerCase() : null; + const perProvider = + providerKey && settings?.supported_modes_by_provider + ? settings.supported_modes_by_provider[providerKey] + : undefined; + return perProvider ?? settings?.supported_modes; +}; + // Decides if we should render the PII config settings for a given provider // For now we only support PII config settings for Presidio PII export const shouldRenderPIIConfigSettings = (provider: string | null) => { From 8748738794566095954b9805e412ce4d5f641757 Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Sat, 11 Jul 2026 14:36:20 -0700 Subject: [PATCH 4/4] fix(e2e): satisfy the lint-e2e-basedpyright gate in logging_client The gate landed on the base branch failing against its own file. The LangfuseListParams call sites now use the pydantic alias names the synthesized constructor exposes (traceId, fromStartTime), and completion_response_id validates the body with a TypeAdapter instead of json.loads returning Any. Runtime behavior is unchanged; verified the id-extraction edge cases and the params field mapping by hand --- tests/e2e/logging/logging_client.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/e2e/logging/logging_client.py b/tests/e2e/logging/logging_client.py index 06b219fc6e21..6071e657bd37 100644 --- a/tests/e2e/logging/logging_client.py +++ b/tests/e2e/logging/logging_client.py @@ -21,7 +21,7 @@ from typing import Literal import pytest -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, JsonValue, TypeAdapter, ValidationError from e2e_config import POLL_INTERVAL, POLL_TIMEOUT from e2e_gateway import Gateway, build_gateway @@ -200,15 +200,16 @@ def costs_agree(expected: float, actual: float, *, rel_tol: float = 0.05) -> boo return abs(expected - actual) <= max(1e-9, abs(expected) * rel_tol) +_COMPLETION_BODY_ADAPTER: TypeAdapter[dict[str, JsonValue]] = TypeAdapter(dict[str, JsonValue]) + + def completion_response_id(body: str) -> str | None: """SpendLogs.request_id is the chat completion body id, not x-litellm-call-id.""" if not body or body == "": return None try: - parsed = json.loads(body) - except json.JSONDecodeError: - return None - if not isinstance(parsed, dict): + parsed = _COMPLETION_BODY_ADAPTER.validate_json(body) + except ValidationError: return None raw = parsed.get("id") return raw if isinstance(raw, str) and raw else None @@ -499,9 +500,9 @@ def list_langfuse_observations( headers=creds.auth_headers, params=LangfuseListParams( limit=100, - trace_id=trace_id, + traceId=trace_id, name=name, - from_start_time=from_start_time, + fromStartTime=from_start_time, ), response_type=LangfuseObservationList, timeout=30.0,