diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index ae82e7992ed..c8bfcabc64e 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._logging import verbose_logger 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 @@ from litellm.exceptions import ( _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 @@ class CustomGuardrail(CustomLogger): 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: @@ -303,6 +329,18 @@ class CustomGuardrail(CustomLogger): """ 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 2b54a411ec7..11809ee6361 100644 --- a/litellm/integrations/rubrik.py +++ b/litellm/integrations/rubrik.py @@ -7,7 +7,7 @@ import time 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.pre_call, GuardrailEventHooks.post_call] + def __init__( self, api_key: str | None = None, @@ -69,6 +73,7 @@ class RubrikLogger(CustomGuardrail, CustomBatchLogger): 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 d3a5d649f17..b6a2d8d9069 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 d22243cbe88..01eb61dad08 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 @@ from litellm.proxy.guardrails._content_utils import ( 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 be9c9cb1be7..daae74ae8e0 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/akto/akto.py +++ b/litellm/proxy/guardrails/guardrail_hooks/akto/akto.py @@ -11,7 +11,7 @@ import asyncio 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 @@ class AktoGuardrail(CustomGuardrail): 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 @@ class AktoGuardrail(CustomGuardrail): 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 ba9c8398152..dc3fc40625c 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 @@ if TYPE_CHECKING: 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 788fe5b05c7..befb1b7ae56 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 @@ from litellm.integrations.custom_guardrail import ( CustomGuardrail, log_guardrail_information, ) +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.utils import CallTypesLiteral from .base import AzureGuardrailBase @@ -47,19 +48,13 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai **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 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai ) 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 c8553926559..91f5df0e9b8 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 @@ from litellm.integrations.custom_guardrail import ( 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 @@ class AzureContentSafetyTextModerationGuardrail(AzureGuardrailBase, CustomGuardr 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 a45719d2eb8..44ae57f81db 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py +++ b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py @@ -166,14 +166,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): """ # 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 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): 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 ea66f416e15..cfb4a78fa6e 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 @@ class BlockCodeExecutionGuardrail(CustomGuardrail): _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 @@ class BlockCodeExecutionGuardrail(CustomGuardrail): 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 bf5a0a5f262..440618a2ffa 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 contextlib 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 @@ from litellm.proxy.guardrails._content_utils import ( 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 561e6ce5f2b..4c31b038172 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 @@ class CiscoAIDefenseGuardrail(_CiscoAIDefenseMcpMixin, CustomGuardrail): 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 @@ class CiscoAIDefenseGuardrail(_CiscoAIDefenseMcpMixin, CustomGuardrail): ) 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 eeb3623977e..fa71e7fc301 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 collections.abc import Mapping, Sequence from typing import ( TYPE_CHECKING, Annotated, + List, Literal, NamedTuple, Optional, @@ -31,6 +32,7 @@ from litellm.llms.custom_httpx.http_handler import ( 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 @@ class CrowdStrikeAIDRHandler(CustomGuardrail): "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 9021f023156..245b9806e71 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 @@ Example: block when response rejects the user (input_type response only): 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 @@ class CustomCodeGuardrail(CustomGuardrail): 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 @@ class CustomCodeGuardrail(CustomGuardrail): """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 b02f1030592..2db3c35866f 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/dynamoai/dynamoai.py +++ b/litellm/proxy/guardrails/guardrail_hooks/dynamoai/dynamoai.py @@ -73,12 +73,7 @@ class DynamoAIGuardrails(CustomGuardrail): 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 @@ class DynamoAIGuardrails(CustomGuardrail): ) 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 f9bb13ad64e..dad8e7dd973 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.py +++ b/litellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.py @@ -80,12 +80,7 @@ class EnkryptAIGuardrails(CustomGuardrail): 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 @@ class EnkryptAIGuardrails(CustomGuardrail): ) 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 dc519f56d1a..e29d6e56353 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 @@ class GenericGuardrailAPI(CustomGuardrail): 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 @@ class GenericGuardrailAPI(CustomGuardrail): ) 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 9805b1a9117..72409a61c30 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/grayswan/grayswan.py +++ b/litellm/proxy/guardrails/guardrail_hooks/grayswan/grayswan.py @@ -119,18 +119,20 @@ class GraySwanGuardrail(CustomGuardrail): streaming_sampling_rate, ) - supported_event_hooks = [ + super().__init__( + guardrail_name=guardrail_name, + 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, ] - super().__init__( - guardrail_name=guardrail_name, - supported_event_hooks=supported_event_hooks, - **kwargs, - ) - # ------------------------------------------------------------------ # 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 71c426a3367..a7c94a4742d 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 @@ class GuardrailsAI(CustomGuardrail): 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 @@ class GuardrailsAI(CustomGuardrail): ) 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 84d03fe7144..e6f76b67c3c 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py +++ b/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py @@ -4,7 +4,7 @@ import json 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 @@ class HeadroomGuardrail(CustomGuardrail): 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 287f108b070..1566c90ac0c 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py +++ b/litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py @@ -3,7 +3,7 @@ from uuid import uuid4 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 @@ from litellm.llms.custom_httpx.http_handler import ( 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 @@ class HiddenlayerGuardrail(CustomGuardrail): 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 27ba9f3467c..955ec0c0d35 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 @@ class IBMGuardrailDetector(CustomGuardrail): 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 @@ class IBMGuardrailDetector(CustomGuardrail): ) 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 4575504feb6..4da3e75d03e 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/javelin/javelin.py +++ b/litellm/proxy/guardrails/guardrail_hooks/javelin/javelin.py @@ -25,6 +25,12 @@ if TYPE_CHECKING: 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 @@ class JavelinGuardrail(CustomGuardrail): 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 3804d1cb93f..d3360bbe641 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._types import UserAPIKeyAuth 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 @@ INPUT_POSITIONING_MAP = { 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 @@ class lakeraAI_Moderation(CustomGuardrail): 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 e79a3e7b3d8..76603579d6c 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 @@ from litellm.types.utils import CallTypesLiteral, GuardrailStatus, ModelResponse 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 @@ class LakeraAIGuardrail(CustomGuardrail): 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 36fbd73c5bd..9c4cef2f06b 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 @@ class LassoGuardrail(CustomGuardrail): 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 ede36b23216..c3b66f8f388 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 @@ class ContentFilterGuardrail(CustomGuardrail): 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 @@ class ContentFilterGuardrail(CustomGuardrail): ) 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 5445425a1d1..a17ca07ae2e 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 fastapi import HTTPException 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 @@ class LLMAsAJudgeGuardrail(CustomGuardrail): 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 @@ class LLMAsAJudgeGuardrail(CustomGuardrail): 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 524d087cfb7..d084a7e088b 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 @@ class MCPEndUserPermissionGuardrail(CustomGuardrail): 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 46b1afb5db7..e70a4e1d8e7 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 @@ from litellm.integrations.custom_guardrail import ( 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 @@ class MCPJWTSigner(CustomGuardrail): 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 3aeed1a25bb..9b5c221b9c4 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 @@ class MCPSecurityGuardrail(CustomGuardrail): 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 c6471cfcf51..f0fdaff9c29 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 @@ class MicrosoftPurviewDLPGuardrail(PurviewGuardrailBase, CustomGuardrail): 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 @@ class MicrosoftPurviewDLPGuardrail(PurviewGuardrailBase, CustomGuardrail): 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 @@ class MicrosoftPurviewDLPGuardrail(PurviewGuardrailBase, CustomGuardrail): 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 8a4e79b31ab..3ca63a1e287 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 @@ class ModelArmorGuardrail(CustomGuardrail, VertexBase): 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 7e8a22a66a8..a467bb14eb6 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 @@ class NomaGuardrail(CustomGuardrail): 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 1cf3dcd9ac4..9cf0986c122 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 enum 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 @@ class NomaV2Guardrail(CustomGuardrail): 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 @@ class NomaV2Guardrail(CustomGuardrail): 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 b411d0fb9eb..606f0a4587d 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 @@ from litellm.llms.custom_httpx.http_handler import ( get_async_httpx_client, httpxSpecialProvider, ) +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.utils import GenericGuardrailAPIInputs, ModelResponse if TYPE_CHECKING: @@ -28,6 +29,14 @@ if TYPE_CHECKING: class OnyxGuardrail(CustomGuardrail): + @classmethod + def get_supported_event_hooks(cls) -> List[GuardrailEventHooks]: + return [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.during_call, + GuardrailEventHooks.post_call, + ] + def __init__( self, api_base: Optional[str] = None, @@ -35,6 +44,7 @@ class OnyxGuardrail(CustomGuardrail): 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 093ac693d5e..44016bfd3dc 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py +++ b/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py @@ -6,6 +6,7 @@ OpenAI Moderation Guardrail Integration for LiteLLM from typing import ( TYPE_CHECKING, Dict, + List, Literal, Optional, Type, @@ -64,17 +65,9 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail): **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 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail): ) 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 7986d4294a4..e92ac37ca77 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 3d3c5403993..d02d4b448e8 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/pangea/pangea.py +++ b/litellm/proxy/guardrails/guardrail_hooks/pangea/pangea.py @@ -89,15 +89,10 @@ class PangeaHandler(CustomGuardrail): 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 @@ class PangeaHandler(CustomGuardrail): ) 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 c522ffad35d..192c8c9bc77 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 @@ class PanwPrismaAirsHandler(CustomGuardrail): 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 @@ class PanwPrismaAirsHandler(CustomGuardrail): ) 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 f976839787b..1d884352eec 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/pillar/pillar.py +++ b/litellm/proxy/guardrails/guardrail_hooks/pillar/pillar.py @@ -269,18 +269,9 @@ class PillarGuardrail(CustomGuardrail): ) 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 @@ class PillarGuardrail(CustomGuardrail): ) 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 95876a55eab..a0c822964a0 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -67,6 +67,16 @@ 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.during_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.logging_only, + GuardrailEventHooks.pre_mcp_call, + ] + # Class variables or attributes def __init__( self, @@ -87,6 +97,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): 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 23f349e0b18..e60815dbcf7 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 @@ from litellm.llms.custom_httpx.http_handler import ( get_async_httpx_client, httpxSpecialProvider, ) +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.utils import GenericGuardrailAPIInputs if TYPE_CHECKING: @@ -26,6 +27,14 @@ class PromptSecurityGuardrailMissingSecrets(Exception): class PromptSecurityGuardrail(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, @@ -35,6 +44,7 @@ class PromptSecurityGuardrail(CustomGuardrail): 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 7012b0d8d5d..6603183efef 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/promptguard/promptguard.py +++ b/litellm/proxy/guardrails/guardrail_hooks/promptguard/promptguard.py @@ -81,11 +81,7 @@ class PromptGuardGuardrail(CustomGuardrail): 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 @@ class PromptGuardGuardrail(CustomGuardrail): 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 54f47cfbfa1..9c62c2915ff 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/qualifire/qualifire.py +++ b/litellm/proxy/guardrails/guardrail_hooks/qualifire/qualifire.py @@ -22,6 +22,7 @@ from litellm.llms.custom_httpx.http_handler import ( 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,14 @@ DEFAULT_QUALIFIRE_API_BASE = "https://proxy.qualifire.ai" class QualifireGuardrail(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, @@ -86,6 +95,7 @@ class QualifireGuardrail(CustomGuardrail): # 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 f8971ceb405..da1c74b37b6 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 @@ class RepelloAIGuardrail(CustomGuardrail): 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 3f802485a8e..3865251a48a 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 @@ class SemanticGuardrail(CustomGuardrail): ): 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 @@ class SemanticGuardrail(CustomGuardrail): 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 2171be235e5..58c57dfdac1 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py +++ b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py @@ -52,11 +52,7 @@ class ToolPermissionGuardrail(CustomGuardrail): **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 @@ class ToolPermissionGuardrail(CustomGuardrail): 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 9a8893734e1..9976a1c48b1 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 @@ class VigilGuardGuardrail(CustomGuardrail): 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 @@ class VigilGuardGuardrail(CustomGuardrail): 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 36c22753404..1b663c16d5b 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py +++ b/litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py @@ -119,13 +119,7 @@ class XecGuardGuardrail(CustomGuardrail): 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 @@ class XecGuardGuardrail(CustomGuardrail): 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 9c660620582..65338827e07 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 @@ from litellm.llms.custom_httpx.http_handler import ( get_async_httpx_client, httpxSpecialProvider, ) +from litellm.types.guardrails import GuardrailEventHooks from litellm.types.utils import GenericGuardrailAPIInputs if TYPE_CHECKING: @@ -27,6 +28,13 @@ GUARDRAIL_TIMEOUT = 5 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 @@ class ZscalerAIGuard(CustomGuardrail): 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 8962073fe7a..e9eee3a1a8a 100644 --- a/litellm/proxy/guardrails/guardrail_registry.py +++ b/litellm/proxy/guardrails/guardrail_registry.py @@ -13,12 +13,23 @@ from litellm._logging import verbose_proxy_logger 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_initializer_registry = { } 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 8d7d7311fad..c7e080b1363 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/e2e/logging/logging_client.py b/tests/e2e/logging/logging_client.py index 06b219fc6e2..6071e657bd3 100644 --- a/tests/e2e/logging/logging_client.py +++ b/tests/e2e/logging/logging_client.py @@ -21,7 +21,7 @@ from dataclasses import dataclass 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 @@ class LoggingClient: 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, diff --git a/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py b/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py index ce8f0802ae1..eb0a670291e 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 @@ from litellm.proxy.guardrails.guardrail_endpoints import ( delete_guardrail, get_guardrail_info, get_guardrail_submission, + get_guardrail_ui_settings, list_guardrail_submissions, list_guardrails_v2, patch_guardrail, @@ -2079,3 +2080,131 @@ 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() + + +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/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 2448348bbce..f328ecbdcfa 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -12,6 +12,67 @@ "count": 1 } }, + "src/app/(dashboard)/agents/_components/AgentsPanel.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/agents/_components/add_agent_form.tsx": { + "no-nested-ternary": { + "count": 3 + }, + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 2 + }, + "unused-imports/no-unused-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/agents/_components/agent_card_discovery.tsx": { + "react-hooks/refs": { + "count": 3 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/agents/_components/agent_cost_view.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/agents/_components/agent_form_fields.tsx": { + "no-nested-ternary": { + "count": 1 + } + }, + "src/app/(dashboard)/agents/_components/agent_info.tsx": { + "no-nested-ternary": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + } + }, + "src/app/(dashboard)/agents/_components/agent_virtual_keys.tsx": { + "no-nested-ternary": { + "count": 1 + } + }, + "src/app/(dashboard)/agents/_components/dynamic_agent_form_fields.tsx": { + "no-nested-ternary": { + "count": 2 + } + }, "src/app/(dashboard)/api-reference/_components/APIReferenceView.tsx": { "no-restricted-imports": { "count": 1 @@ -164,6 +225,136 @@ "count": 8 } }, + "src/app/(dashboard)/guardrails/_components/GuardrailTestPanel.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/GuardrailTestPlayground.tsx": { + "no-nested-ternary": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/GuardrailTestResults.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/GuardrailsPanel.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx": { + "no-nested-ternary": { + "count": 2 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + }, + "react/no-unescaped-entities": { + "count": 2 + } + }, + "src/app/(dashboard)/guardrails/_components/content_filter/CompetitorIntentConfiguration.tsx": { + "no-nested-ternary": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/content_filter/ContentCategoryConfiguration.tsx": { + "no-nested-ternary": { + "count": 3 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterDisplay.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterManager.tsx": { + "max-params": { + "count": 2 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.tsx": { + "no-nested-ternary": { + "count": 6 + }, + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/edit_guardrail_form.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "no-restricted-syntax": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/guardrail_info.tsx": { + "max-params": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 3 + } + }, + "src/app/(dashboard)/guardrails/_components/guardrail_optional_params.tsx": { + "no-nested-ternary": { + "count": 5 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/guardrail_provider_fields.tsx": { + "no-nested-ternary": { + "count": 5 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/guardrail_table.tsx": { + "no-nested-ternary": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails/_components/tool_permission/ToolPermissionRulesEditor.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/purity": { + "count": 1 + } + }, "src/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails.ts": { "no-restricted-syntax": { "count": 1 @@ -536,6 +727,134 @@ "count": 1 } }, + "src/app/(dashboard)/policies/_components/add_attachment_form.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/add_policy_form.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 2 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/ai_suggestion_modal.tsx": { + "no-nested-ternary": { + "count": 10 + }, + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/attachment_table.test.tsx": { + "react/display-name": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/attachment_table.tsx": { + "no-nested-ternary": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/guardrail_selection_modal.tsx": { + "no-nested-ternary": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/impact_popover.test.tsx": { + "react/display-name": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/impact_popover.tsx": { + "no-nested-ternary": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/index.test.tsx": { + "react/display-name": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/pipeline_flow_builder.tsx": { + "no-nested-ternary": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 2 + } + }, + "src/app/(dashboard)/policies/_components/policy_info.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/policy_table.test.tsx": { + "react/display-name": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/policy_table.tsx": { + "no-nested-ternary": { + "count": 2 + }, + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/policy_test_panel.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + } + }, + "src/app/(dashboard)/policies/_components/template_parameter_modal.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, "src/app/(dashboard)/projects/_components/ProjectDetailsPage.tsx": { "no-nested-ternary": { "count": 3 @@ -697,6 +1016,53 @@ "count": 1 } }, + "src/app/(dashboard)/skills/_components/ClaudeCodePluginsPanel.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/skills/_components/add_plugin_form.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/skills/_components/plugin_table.tsx": { + "no-nested-ternary": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/tag-management/_components/TagTable.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/tag-management/_components/components/CreateTagModal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/tag-management/_components/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/tag-management/_components/tag_info.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, "src/app/(dashboard)/transform-request/TransformRequestPanel.tsx": { "no-restricted-imports": { "count": 1 @@ -774,6 +1140,43 @@ "count": 1 } }, + "src/app/(dashboard)/vector-stores/_components/CreateVectorStore.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/vector-stores/_components/VectorStoreForm.tsx": { + "no-nested-ternary": { + "count": 2 + }, + "no-restricted-imports": { + "count": 1 + }, + "react/no-unescaped-entities": { + "count": 1 + } + }, + "src/app/(dashboard)/vector-stores/_components/VectorStoreTable.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/vector-stores/_components/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/vector-stores/_components/vector_store_info.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, "src/app/(dashboard)/workflows/WorkflowRuns.tsx": { "no-nested-ternary": { "count": 1 @@ -1261,67 +1664,6 @@ "count": 1 } }, - "src/app/(dashboard)/agents/_components/AgentsPanel.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/agents/_components/add_agent_form.tsx": { - "no-nested-ternary": { - "count": 3 - }, - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 2 - }, - "unused-imports/no-unused-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/agents/_components/agent_card_discovery.tsx": { - "react-hooks/refs": { - "count": 3 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/agents/_components/agent_cost_view.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/agents/_components/agent_form_fields.tsx": { - "no-nested-ternary": { - "count": 1 - } - }, - "src/app/(dashboard)/agents/_components/agent_info.tsx": { - "no-nested-ternary": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/immutability": { - "count": 1 - } - }, - "src/app/(dashboard)/agents/_components/agent_virtual_keys.tsx": { - "no-nested-ternary": { - "count": 1 - } - }, - "src/app/(dashboard)/agents/_components/dynamic_agent_form_fields.tsx": { - "no-nested-ternary": { - "count": 2 - } - }, "src/components/alerting/dynamic_form.tsx": { "no-nested-ternary": { "count": 4 @@ -1363,14 +1705,6 @@ "count": 2 } }, - "src/app/(dashboard)/skills/_components/ClaudeCodePluginsPanel.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, "src/components/claude_code_plugins/MakeSkillPublicForm.tsx": { "no-restricted-imports": { "count": 1 @@ -1379,19 +1713,6 @@ "count": 1 } }, - "src/app/(dashboard)/skills/_components/add_plugin_form.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/skills/_components/plugin_table.tsx": { - "no-nested-ternary": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, "src/components/cloudzero_export_modal.tsx": { "no-restricted-imports": { "count": 1 @@ -1534,139 +1855,6 @@ "count": 1 } }, - "src/app/(dashboard)/guardrails/_components/GuardrailsPanel.tsx": { - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/GuardrailTestPanel.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/GuardrailTestPlayground.tsx": { - "no-nested-ternary": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/GuardrailTestResults.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx": { - "no-nested-ternary": { - "count": 2 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx": { - "no-nested-ternary": { - "count": 4 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - }, - "react/no-unescaped-entities": { - "count": 2 - } - }, - "src/app/(dashboard)/guardrails/_components/content_filter/CompetitorIntentConfiguration.tsx": { - "no-nested-ternary": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/content_filter/ContentCategoryConfiguration.tsx": { - "no-nested-ternary": { - "count": 3 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterDisplay.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterManager.tsx": { - "max-params": { - "count": 2 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.tsx": { - "no-nested-ternary": { - "count": 6 - }, - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/edit_guardrail_form.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "no-restricted-syntax": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/guardrail_info.tsx": { - "max-params": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 3 - } - }, - "src/app/(dashboard)/guardrails/_components/guardrail_optional_params.tsx": { - "no-nested-ternary": { - "count": 5 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/guardrail_provider_fields.tsx": { - "no-nested-ternary": { - "count": 5 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/guardrail_table.tsx": { - "no-nested-ternary": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails/_components/tool_permission/ToolPermissionRulesEditor.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/purity": { - "count": 1 - } - }, "src/components/key_team_helpers/key_list.tsx": { "react-hooks/set-state-in-effect": { "count": 1 @@ -2080,134 +2268,6 @@ "count": 1 } }, - "src/app/(dashboard)/policies/_components/add_attachment_form.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/immutability": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/add_policy_form.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/immutability": { - "count": 2 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/ai_suggestion_modal.tsx": { - "no-nested-ternary": { - "count": 10 - }, - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/immutability": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/attachment_table.test.tsx": { - "react/display-name": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/attachment_table.tsx": { - "no-nested-ternary": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/guardrail_selection_modal.tsx": { - "no-nested-ternary": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/impact_popover.test.tsx": { - "react/display-name": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/impact_popover.tsx": { - "no-nested-ternary": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/index.test.tsx": { - "react/display-name": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/pipeline_flow_builder.tsx": { - "no-nested-ternary": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 2 - } - }, - "src/app/(dashboard)/policies/_components/policy_info.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/policy_table.test.tsx": { - "react/display-name": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/policy_table.tsx": { - "no-nested-ternary": { - "count": 2 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/policy_test_panel.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/immutability": { - "count": 1 - } - }, - "src/app/(dashboard)/policies/_components/template_parameter_modal.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/immutability": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, "src/components/price_data_reload.tsx": { "react-hooks/immutability": { "count": 2 @@ -2272,32 +2332,6 @@ "count": 1 } }, - "src/app/(dashboard)/tag-management/_components/TagTable.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/tag-management/_components/components/CreateTagModal.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/tag-management/_components/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/tag-management/_components/tag_info.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, "src/components/team/EditMembership.tsx": { "no-nested-ternary": { "count": 1 @@ -2388,43 +2422,6 @@ "count": 2 } }, - "src/app/(dashboard)/vector-stores/_components/CreateVectorStore.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/vector-stores/_components/VectorStoreForm.tsx": { - "no-nested-ternary": { - "count": 2 - }, - "no-restricted-imports": { - "count": 1 - }, - "react/no-unescaped-entities": { - "count": 1 - } - }, - "src/app/(dashboard)/vector-stores/_components/VectorStoreTable.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/vector-stores/_components/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/vector-stores/_components/vector_store_info.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, "src/components/view_logs/EvalViewer/EvalViewer.tsx": { "no-nested-ternary": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx index 9a45bf0be0d..202568b478d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx @@ -8,10 +8,12 @@ import { modelAvailableCall, } from "@/components/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"; @@ -60,6 +63,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[]; @@ -82,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; @@ -122,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(""); @@ -137,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) { @@ -168,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 @@ -189,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, @@ -205,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([ @@ -219,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, @@ -232,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 @@ -251,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") { @@ -385,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"); @@ -423,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: { @@ -467,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)", ); @@ -505,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, @@ -536,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); @@ -551,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 || "", @@ -652,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" }]} > - {guardrailSettings?.supported_modes?.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/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx index 3ac9fe4087a..e8c4810ca69 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/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) => {