fix(guardrails): filter Add-Guardrail mode dropdown per provider (#32712)

* fix(guardrails): filter Add-Guardrail mode dropdown per provider

The GET /guardrails/ui/add_guardrail_settings endpoint returned every
GuardrailEventHooks value in one flat supported_modes list, so the Admin
UI rendered pre_mcp_call as a selectable Mode for every guardrail. Saving
Content Filter or Tool Permission with pre_mcp_call then failed with a
400 because those guardrails' server-side supported_event_hooks list
excludes it.

Expose each guardrail's supported hooks as a get_supported_event_hooks
classmethod on CustomGuardrail (mirrors the existing get_config_model
pattern) and have the endpoint iterate guardrail_class_registry to build
a supported_modes_by_provider map. The UI Mode dropdown filters by that
map when the selected provider is known and falls back to the global
list otherwise. __init__ now sources its own supported_event_hooks list
from the classmethod so the two sides can't drift.

Also register BedrockGuardrail, ToolPermissionGuardrail, lakera,
lakera_v2, and presidio in guardrail_class_registry so they participate
in the map (they were previously only in guardrail_initializer_registry
and had no class-registry entry).

Behavior change: guardrails that previously had no supported_event_hooks
declared (aim, javelin, azure/text_moderation, cato_networks,
crowdstrike_aidr, headroom, hiddenlayer, lasso, noma, onyx,
prompt_security, qualifire, repelloai, zscaler_ai_guard, aporia_ai,
lakera_ai, lakera_ai_v2, mcp_jwt_signer, model_armor, presidio) now
validate the configured mode at instantiation. Existing configs where
the mode was silently a no-op will fail at proxy startup with a clear
validation error rather than running as a broken guardrail.

Resolves LIT-4226

* fix(guardrails): add LITELLM_STRICT_GUARDRAIL_MODES escape hatch, preserve current mode in edit form

Address Greptile P1 (startup break) and P2 (edit form UX):

LITELLM_STRICT_GUARDRAIL_MODES defaults to true (raise on unsupported
event_hook, unchanged behavior for the guardrails validated pre-PR).
Setting it to false logs a warning and continues, giving deployments an
opt-out while they fix configs that now surface as errors instead of
silently no-op'ing. Regression test covers both modes.

Edit form now surfaces the currently-saved mode even when it is not in
the filtered per-provider list, so a legacy row (e.g. content_filter
saved with pre_mcp_call before this fix) no longer disappears from the
dropdown; the option renders with a 'not supported by <provider>' note
so the user knows to pick another.

* fix(guardrails): correct audited hook lists, prune stale modes on provider switch, clean form lint

Audited every get_supported_event_hooks classmethod against the hooks
each guardrail's own tests exercise and its handler methods. Five were
too narrow and their tests caught it in CI: rubrik gains pre_call,
presidio gains during_call and pre_mcp_call, prompt_security, onyx and
qualifire gain during_call. The remaining classes match either their
original __init__ declarations or their exercised modes exactly.

Cursor review fixes: the Add form now drops selected modes the new
provider does not support when the user switches providers, so a
pre_mcp_call selection cannot ride along into a provider that rejects
it at save; the edit form handles list-shaped stored modes instead of
treating mode as always a string.

Extracted shared toModeArray and getSupportedModesForProvider helpers
into guardrail_info_helpers so both forms use one implementation, typed
the remaining any usages in both forms, removed nested ternaries, and
committed the ratcheted-down eslint metrics and pruned suppressions
This commit is contained in:
yucheng-berri 2026-07-11 14:51:27 -07:00 • committed by GitHub
parent f9ed4f8aea
commit 69c5839cc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 1222 additions and 684 deletions

View file

@ -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]],

View file

@ -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,

View file

@ -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(),

View file

@ -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,

View file

@ -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(

View file

@ -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"]

View file

@ -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,
]

View file

@ -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__(

View file

@ -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.

View file

@ -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

View file

@ -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,

View file

@ -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,
]

View file

@ -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(

View file

@ -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()

View file

@ -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,
]

View file

@ -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,
]

View file

@ -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,
]

View file

@ -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
# ------------------------------------------------------------------

View file

@ -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,
]

View file

@ -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:

View file

@ -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"

View file

@ -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,
]

View file

@ -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(

View file

@ -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

View file

@ -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(

View file

@ -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")

View file

@ -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,
]

View file

@ -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",
]

View file

@ -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
# ------------------------------------------------------------------

View file

@ -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 ---

View file

@ -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,

View file

@ -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
# ------------------------------------------------------------------

View file

@ -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)

View file

@ -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(

View file

@ -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 ""

View file

@ -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,

View file

@ -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,
]

View file

@ -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,

View file

@ -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,
]

View file

@ -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,
]

View file

@ -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,
]

View file

@ -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

View file

@ -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")

View file

@ -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,

View file

@ -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:

View file

@ -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(

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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",

View file

@ -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,
}

View file

@ -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

View file

@ -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 == "<streamed>":
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,

View file

@ -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)

View file

@ -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

View file

@ -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<string, string[]>;
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<AddGuardrailFormProps> = ({ visible, onClose, a
const [categorySpecificThresholds, setCategorySpecificThresholds] = useState<{ [key: string]: number }>({});
// Content Filter state
const [selectedPatterns, setSelectedPatterns] = useState<any[]>([]);
const [blockedWords, setBlockedWords] = useState<any[]>([]);
const [selectedContentCategories, setSelectedContentCategories] = useState<any[]>([]);
const [selectedPatterns, setSelectedPatterns] = useState<ContentFilterPattern[]>([]);
const [blockedWords, setBlockedWords] = useState<ContentFilterBlockedWord[]>([]);
const [selectedContentCategories, setSelectedContentCategories] = useState<SelectedContentCategory[]>([]);
const [pendingCategorySelection, setPendingCategorySelection] = useState<string>("");
const [competitorIntentEnabled, setCompetitorIntentEnabled] = useState(false);
const [competitorIntentConfig, setCompetitorIntentConfig] = useState<any>(null);
const [competitorIntentConfig, setCompetitorIntentConfig] = useState<CompetitorIntentConfig | null>(null);
// Endpoint Settings state (step 5)
const [selectedEndpointType, setSelectedEndpointType] = useState<string>("");
@ -137,12 +183,9 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
const [endpointSettingsOpen, setEndpointSettingsOpen] = useState<boolean>(false);
const [availableModels, setAvailableModels] = useState<string[]>([]);
const [toolPermissionConfig, setToolPermissionConfig] = useState<ToolPermissionConfig>({
rules: [],
default_action: "deny",
on_disallowed_action: "block",
violation_message_template: "",
});
const [toolPermissionConfig, setToolPermissionConfig] = useState<ToolPermissionConfig>(
createEmptyToolPermissionConfig,
);
const isToolPermissionProvider = useMemo(() => {
if (!selectedProvider) {
@ -168,7 +211,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ 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<AddGuardrailFormProps> = ({ visible, onClose, a
// Set provider
setSelectedProvider(preset.provider);
const baseValues: Record<string, any> = {
const baseValues: Record<string, unknown> = {
provider: preset.provider,
guardrail_name: preset.guardrailNameSuggestion,
mode: preset.mode,
@ -205,7 +248,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ 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<AddGuardrailFormProps> = ({ 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<string, any> = {
const resetValues: Record<string, unknown> = {
config: undefined,
presidio_analyzer_api_base: undefined,
presidio_anonymizer_api_base: undefined,
@ -232,6 +275,21 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ 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<AddGuardrailFormProps> = ({ 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<AddGuardrailFormProps> = ({ 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<AddGuardrailFormProps> = ({ 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<string, unknown>;
} = {
guardrail_name: values.guardrail_name,
litellm_params: {
@ -467,13 +515,10 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ 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<AddGuardrailFormProps> = ({ 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<AddGuardrailFormProps> = ({ 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<AddGuardrailFormProps> = ({ 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<AddGuardrailFormProps> = ({ visible, onClose, a
};
const renderBasicInfo = () => {
const showProviderFields =
!isToolPermissionProvider &&
!shouldRenderContentFilterConfigSettings(selectedProvider) &&
!shouldRenderLLMJudgeFields(selectedProvider);
return (
<>
<Form.Item
@ -732,7 +782,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
rules={[{ required: true, message: "Please select a mode" }]}
>
<Select optionLabelProp="label" mode="multiple">
{guardrailSettings?.supported_modes?.map((mode) => (
{getSupportedModesForProvider(guardrailSettings, selectedProvider)?.map((mode) => (
<Option key={mode} value={mode} label={mode}>
<div>
<div>
@ -823,15 +873,13 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
</Form.Item>
{/* Use the GuardrailProviderFields component to render provider-specific fields */}
{!isToolPermissionProvider &&
!shouldRenderContentFilterConfigSettings(selectedProvider) &&
!shouldRenderLLMJudgeFields(selectedProvider) && (
<GuardrailProviderFields
selectedProvider={selectedProvider}
accessToken={accessToken}
providerParams={providerParams}
/>
)}
{showProviderFields && (
<GuardrailProviderFields
selectedProvider={selectedProvider}
accessToken={accessToken}
providerParams={providerParams}
/>
)}
</>
);
};
@ -1179,11 +1227,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
<div className="flex flex-col items-center shrink-0" style={{ width: 24 }}>
<div
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium shrink-0"
style={{
background: isDone ? "#4f46e5" : isCurrent ? "#fff" : "#f8fafc",
color: isDone ? "#fff" : isCurrent ? "#4f46e5" : "#94a3b8",
border: isCurrent ? "2px solid #4f46e5" : isDone ? "none" : "1px solid #e2e8f0",
}}
style={getStepIndicatorStyle(isDone, isCurrent)}
>
{isDone ? "\u2713" : index + 1}
</div>
@ -1213,7 +1257,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
className="text-sm"
style={{
fontWeight: isCurrent ? 600 : 500,
color: isCurrent ? "#1e293b" : isDone ? "#4f46e5" : "#94a3b8",
color: getStepTitleColor(isDone, isCurrent),
}}
>
{step.title}

View file

@ -5,6 +5,8 @@ import {
guardrail_provider_map,
guardrailLogoMap,
getGuardrailProviders,
getSupportedModesForProvider,
toModeArray,
type SkipSystemMessageChoice,
type SkipToolMessageChoice,
} from "./guardrail_info_helpers";
@ -23,7 +25,7 @@ interface EditGuardrailFormProps {
onSuccess: () => void;
guardrailId: string;
/** Full stored params merged into PUT so optional fields (e.g. content filter) are preserved. */
fullLitellmParams?: Record<string, any> | null;
fullLitellmParams?: Record<string, unknown> | null;
initialValues: {
guardrail_name: string;
provider: string;
@ -32,7 +34,7 @@ interface EditGuardrailFormProps {
pii_entities_config?: { [key: string]: string };
skip_system_message_choice?: SkipSystemMessageChoice;
skip_tool_message_choice?: SkipToolMessageChoice;
[key: string]: any;
[key: string]: unknown;
};
}
@ -40,6 +42,7 @@ interface GuardrailSettings {
supported_entities: string[];
supported_actions: string[];
supported_modes: string[];
supported_modes_by_provider?: Record<string, string[]>;
pii_entity_categories: Array<{
category: string;
entities: string[];
@ -125,7 +128,7 @@ const EditGuardrailForm: React.FC<EditGuardrailFormProps> = ({
// Get the guardrail provider value from the map
const guardrailProvider = guardrail_provider_map[values.provider];
const litellm_params: Record<string, any> =
const litellm_params: Record<string, unknown> =
fullLitellmParams && typeof fullLitellmParams === "object" ? { ...fullLitellmParams } : {};
litellm_params.guardrail = guardrailProvider;
@ -150,7 +153,7 @@ const EditGuardrailForm: React.FC<EditGuardrailFormProps> = ({
delete litellm_params.skip_tool_message_in_guardrail;
}
let guardrail_info: any = {};
let guardrail_info: Record<string, unknown> = {};
// For Presidio PII, add the entity and action configurations
if (values.provider === "PresidioPII" && selectedEntities.length > 0) {
@ -189,8 +192,8 @@ const EditGuardrailForm: React.FC<EditGuardrailFormProps> = ({
guardrail_id: string;
guardrail: {
guardrail_name: string;
litellm_params: Record<string, any>;
guardrail_info: any;
litellm_params: Record<string, unknown>;
guardrail_info: Record<string, unknown>;
};
} = {
guardrail_id: guardrailId,
@ -419,16 +422,21 @@ const EditGuardrailForm: React.FC<EditGuardrailFormProps> = ({
rules={[{ required: true, message: "Please select a mode" }]}
>
<Select>
{guardrailSettings?.supported_modes?.map((mode) => (
<Option key={mode} value={mode}>
{mode}
</Option>
)) || (
<>
<Option value="pre_call">pre_call</Option>
<Option value="post_call">post_call</Option>
</>
)}
{(() => {
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) => (
<Option key={mode} value={mode}>
{unsupportedCurrent.includes(mode)
? `${mode} (not supported by ${selectedProvider}, pick another)`
: mode}
</Option>
));
})()}
</Select>
</Form.Item>

View file

@ -76,6 +76,26 @@ export const populateGuardrailProviderMap = (providerParamsResponse: Record<stri
});
};
// Normalizes a form "mode" value (string, string[], or empty) into a string array
export const toModeArray = (raw: unknown): string[] => {
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<string, string[]> } | 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) => {