Guardrails - add nsfw policy template, toxic keywords in multiple languages, child safety content filter, json content viewer (#21205)

* fix(content_filter.py): fix filter on toxic keywords

* feat: improve toxic/abusive language detection

* fix: additional improvements to nsfw filters

* feat: more improvements to nsfw filter

* feat(content_filter.json): add new australia specific nsfw content filter

ensure complete coverage for australia nsfw

* fix: cleanup policy templates

* fix(index.tsx): alert notice

* fix(index.tsx): add disclaimer notice

* feat(harmful_child_safety.yaml): new child safety content filter

ensure we catch inappropriate, child-specific content

* feat(policy_templates.json): add child safety and self harm filters

* fix(content_filter.py): improve racial bias filter to use a similar identifier + block word pattern and cover a wider range of ethnicities

* feat(policy_templates.json): add racial bias to nsfw policy template

* feat: add json content viewer
This commit is contained in:
Krish Dholakia
2026-02-14 09:41:55 -08:00
committed by GitHub
parent 2e8056432f
commit e41ecc7a71
16 changed files with 9668 additions and 4413 deletions
+37 -21
View File
@@ -106,9 +106,7 @@ MCP_OAUTH2_TOKEN_CACHE_DEFAULT_TTL = int(
# npm/npx needs a writable cache dir; in containers the default (~/.npm)
# may not exist or be read-only. /tmp is always writable.
MCP_NPM_CACHE_DIR = os.getenv("MCP_NPM_CACHE_DIR", "/tmp/.npm_mcp_cache")
MCP_OAUTH2_TOKEN_CACHE_MIN_TTL = int(
os.getenv("MCP_OAUTH2_TOKEN_CACHE_MIN_TTL", "10")
)
MCP_OAUTH2_TOKEN_CACHE_MIN_TTL = int(os.getenv("MCP_OAUTH2_TOKEN_CACHE_MIN_TTL", "10"))
LITELLM_UI_ALLOW_HEADERS = [
"x-litellm-semantic-filter",
@@ -131,7 +129,7 @@ DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET_GEMINI_2_5_FLASH_LITE = int(
# Maximum number of callbacks that can be registered
# This prevents callbacks from exponentially growing and consuming CPU resources
# Override with LITELLM_MAX_CALLBACKS env var for large deployments (e.g., many teams with guardrails)
MAX_CALLBACKS = get_env_int("LITELLM_MAX_CALLBACKS", 30)
MAX_CALLBACKS = get_env_int("LITELLM_MAX_CALLBACKS", 100)
# Generic fallback for unknown models
DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET = int(
@@ -167,15 +165,19 @@ _DEFAULT_TTL_FOR_HTTPX_CLIENTS = 3600 # 1 hour, re-use the same httpx client fo
# Aiohttp connection pooling - prevents memory leaks from unbounded connection growth
# Set to 0 for unlimited (not recommended for production)
AIOHTTP_CONNECTOR_LIMIT = int(os.getenv("AIOHTTP_CONNECTOR_LIMIT", 300))
AIOHTTP_CONNECTOR_LIMIT_PER_HOST = int(os.getenv("AIOHTTP_CONNECTOR_LIMIT_PER_HOST", 50))
AIOHTTP_CONNECTOR_LIMIT_PER_HOST = int(
os.getenv("AIOHTTP_CONNECTOR_LIMIT_PER_HOST", 50)
)
AIOHTTP_KEEPALIVE_TIMEOUT = int(os.getenv("AIOHTTP_KEEPALIVE_TIMEOUT", 120))
AIOHTTP_TTL_DNS_CACHE = int(os.getenv("AIOHTTP_TTL_DNS_CACHE", 300))
# enable_cleanup_closed is only needed for Python versions with the SSL leak bug
# Fixed in Python 3.12.7+ and 3.13.1+ (see https://github.com/python/cpython/pull/118960)
# Reference: https://github.com/aio-libs/aiohttp/blob/master/aiohttp/connector.py#L74-L78
AIOHTTP_NEEDS_CLEANUP_CLOSED = (
(3, 13, 0) <= sys.version_info < (3, 13, 1) or sys.version_info < (3, 12, 7)
)
AIOHTTP_NEEDS_CLEANUP_CLOSED = (3, 13, 0) <= sys.version_info < (
3,
13,
1,
) or sys.version_info < (3, 12, 7)
# WebSocket constants
# Default to None (unlimited) to match OpenAI's official agents SDK behavior
@@ -213,15 +215,15 @@ REDIS_UPDATE_BUFFER_KEY = "litellm_spend_update_buffer"
REDIS_DAILY_SPEND_UPDATE_BUFFER_KEY = "litellm_daily_spend_update_buffer"
REDIS_DAILY_TEAM_SPEND_UPDATE_BUFFER_KEY = "litellm_daily_team_spend_update_buffer"
REDIS_DAILY_ORG_SPEND_UPDATE_BUFFER_KEY = "litellm_daily_org_spend_update_buffer"
REDIS_DAILY_END_USER_SPEND_UPDATE_BUFFER_KEY = "litellm_daily_end_user_spend_update_buffer"
REDIS_DAILY_END_USER_SPEND_UPDATE_BUFFER_KEY = (
"litellm_daily_end_user_spend_update_buffer"
)
REDIS_DAILY_AGENT_SPEND_UPDATE_BUFFER_KEY = "litellm_daily_agent_spend_update_buffer"
REDIS_DAILY_TAG_SPEND_UPDATE_BUFFER_KEY = "litellm_daily_tag_spend_update_buffer"
MAX_REDIS_BUFFER_DEQUEUE_COUNT = int(os.getenv("MAX_REDIS_BUFFER_DEQUEUE_COUNT", 100))
MAX_SIZE_IN_MEMORY_QUEUE = int(os.getenv("MAX_SIZE_IN_MEMORY_QUEUE", 2000))
# Bounds asyncio.Queue() instances (log queues, spend update queues, etc.) to prevent unbounded memory growth
LITELLM_ASYNCIO_QUEUE_MAXSIZE = int(
os.getenv("LITELLM_ASYNCIO_QUEUE_MAXSIZE", 1000)
)
LITELLM_ASYNCIO_QUEUE_MAXSIZE = int(os.getenv("LITELLM_ASYNCIO_QUEUE_MAXSIZE", 1000))
MAX_IN_MEMORY_QUEUE_FLUSH_COUNT = int(
os.getenv("MAX_IN_MEMORY_QUEUE_FLUSH_COUNT", 1000)
)
@@ -343,7 +345,9 @@ MAX_SIZE_PER_ITEM_IN_MEMORY_CACHE_IN_KB = int(
DEFAULT_MAX_TOKENS_FOR_TRITON = int(os.getenv("DEFAULT_MAX_TOKENS_FOR_TRITON", 2000))
#### Networking settings ####
request_timeout: float = float(os.getenv("REQUEST_TIMEOUT", 6000)) # time in seconds
DEFAULT_A2A_AGENT_TIMEOUT: float = float(os.getenv("DEFAULT_A2A_AGENT_TIMEOUT", 6000)) # 10 minutes
DEFAULT_A2A_AGENT_TIMEOUT: float = float(
os.getenv("DEFAULT_A2A_AGENT_TIMEOUT", 6000)
) # 10 minutes
# Patterns that indicate a localhost/internal URL in A2A agent cards that should be
# replaced with the original base_url. This is a common misconfiguration where
# developers deploy agents with development URLs in their agent cards.
@@ -395,8 +399,12 @@ DD_TRACER_STREAMING_CHUNK_YIELD_RESOURCE = os.getenv(
"DD_TRACER_STREAMING_CHUNK_YIELD_RESOURCE", "streaming.chunk.yield"
)
EMAIL_BUDGET_ALERT_TTL = int(os.getenv("EMAIL_BUDGET_ALERT_TTL", 24 * 60 * 60)) # 24 hours in seconds
EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE = float(os.getenv("EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE", 0.8)) # 80% of max budget
EMAIL_BUDGET_ALERT_TTL = int(
os.getenv("EMAIL_BUDGET_ALERT_TTL", 24 * 60 * 60)
) # 24 hours in seconds
EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE = float(
os.getenv("EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE", 0.8)
) # 80% of max budget
############### LLM Provider Constants ###############
### ANTHROPIC CONSTANTS ###
ANTHROPIC_TOKEN_COUNTING_BETA_VERSION = os.getenv(
@@ -1150,7 +1158,17 @@ known_tokenizer_config = {
}
OPENAI_FINISH_REASONS = ["stop", "length", "function_call", "content_filter", "null", "finish_reason_unspecified", "malformed_function_call", "guardrail_intervened", "eos"]
OPENAI_FINISH_REASONS = [
"stop",
"length",
"function_call",
"content_filter",
"null",
"finish_reason_unspecified",
"malformed_function_call",
"guardrail_intervened",
"eos",
]
HUMANLOOP_PROMPT_CACHE_TTL_SECONDS = int(
os.getenv("HUMANLOOP_PROMPT_CACHE_TTL_SECONDS", 60)
) # 1 minute
@@ -1250,8 +1268,8 @@ CLI_SSO_SESSION_CACHE_KEY_PREFIX = "cli_sso_session"
CLI_JWT_TOKEN_NAME = "cli-jwt-token"
# Support both CLI_JWT_EXPIRATION_HOURS and LITELLM_CLI_JWT_EXPIRATION_HOURS for backwards compatibility
CLI_JWT_EXPIRATION_HOURS = int(
os.getenv("CLI_JWT_EXPIRATION_HOURS")
or os.getenv("LITELLM_CLI_JWT_EXPIRATION_HOURS")
os.getenv("CLI_JWT_EXPIRATION_HOURS")
or os.getenv("LITELLM_CLI_JWT_EXPIRATION_HOURS")
or 24
)
@@ -1432,9 +1450,7 @@ MICROSOFT_USER_EMAIL_ATTRIBUTE = str(
MICROSOFT_USER_DISPLAY_NAME_ATTRIBUTE = str(
os.getenv("MICROSOFT_USER_DISPLAY_NAME_ATTRIBUTE", "displayName")
)
MICROSOFT_USER_ID_ATTRIBUTE = str(
os.getenv("MICROSOFT_USER_ID_ATTRIBUTE", "id")
)
MICROSOFT_USER_ID_ATTRIBUTE = str(os.getenv("MICROSOFT_USER_ID_ATTRIBUTE", "id"))
MICROSOFT_USER_FIRST_NAME_ATTRIBUTE = str(
os.getenv("MICROSOFT_USER_FIRST_NAME_ATTRIBUTE", "givenName")
)
+401 -1
View File
@@ -2,7 +2,7 @@
{
"id": "advanced-au-pii-protection",
"title": "Advanced PII Protection (Australia)",
"description": "Comprehensive PII detection and masking for Australia. Protects Australian-specific identifiers, international employee data, financial information, credentials, protected class information, and industry-specific sensitive data.",
"description": "Protects Australian-specific identifiers, international employee data, financial information, credentials, protected class information, and industry-specific sensitive data.",
"icon": "ShieldCheckIcon",
"iconColor": "text-purple-500",
"iconBg": "bg-purple-50",
@@ -274,5 +274,405 @@
],
"guardrails_remove": []
}
},
{
"id": "nsfw-content-filter-australia",
"title": "NSFW Content Filter (Australia)",
"description": "Blocks profanity, sexual content, NSFW requests, self-harm content, and child safety violations using English and Australian-specific slang. Protects against inappropriate content including sexual solicitation, explicit content, Australian profanity, self-harm, and content involving minors.",
"icon": "ShieldExclamationIcon",
"iconColor": "text-red-500",
"iconBg": "bg-red-50",
"guardrails": [
"nsfw-content-filter-english",
"nsfw-content-filter-australian",
"nsfw-self-harm-filter",
"nsfw-child-safety-filter",
"nsfw-racial-bias-filter"
],
"complexity": "Medium",
"guardrailDefinitions": [
{
"guardrail_name": "nsfw-content-filter-english",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks profanity, sexual content, slurs, and NSFW terms in English"
}
},
{
"guardrail_name": "nsfw-content-filter-australian",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse_au",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks Australian-specific slang and profanity (root, perv, bogan, wanker, etc.)"
}
},
{
"guardrail_name": "nsfw-self-harm-filter",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harmful_self_harm",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks content related to self-harm, suicide, and eating disorders"
}
},
{
"guardrail_name": "nsfw-child-safety-filter",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harmful_child_safety",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks inappropriate content involving minors using identifier + block word combinations"
}
},
{
"guardrail_name": "nsfw-racial-bias-filter",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "bias_racial",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks racial and ethnic discrimination, hate speech, and supremacist content"
}
}
],
"templateData": {
"policy_name": "nsfw-content-filter-australia",
"description": "NSFW content filter for Australia. Blocks profanity, sexual content, inappropriate requests, self-harm content, child safety violations, and racial bias in English and Australian slang.",
"guardrails_add": [
"nsfw-content-filter-english",
"nsfw-content-filter-australian",
"nsfw-self-harm-filter",
"nsfw-child-safety-filter",
"nsfw-racial-bias-filter"
],
"guardrails_remove": []
}
},
{
"id": "nsfw-content-filter-basic",
"title": "NSFW Content Filter (Basic)",
"description": "Basic NSFW content filtering for English only. Blocks profanity, sexual content, slurs, solicitation, explicit requests, self-harm content, and child safety violations. Suitable for most applications requiring content moderation.",
"icon": "ShieldExclamationIcon",
"iconColor": "text-orange-500",
"iconBg": "bg-orange-50",
"guardrails": [
"nsfw-content-filter-english-only",
"nsfw-self-harm-filter-basic",
"nsfw-child-safety-filter-basic",
"nsfw-racial-bias-filter-basic"
],
"complexity": "Low",
"guardrailDefinitions": [
{
"guardrail_name": "nsfw-content-filter-english-only",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks profanity, sexual content, slurs, and NSFW terms. Includes 485+ keywords covering explicit content, solicitation, sexual behavior, and exploitation."
}
},
{
"guardrail_name": "nsfw-self-harm-filter-basic",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harmful_self_harm",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks content related to self-harm, suicide, and eating disorders"
}
},
{
"guardrail_name": "nsfw-child-safety-filter-basic",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harmful_child_safety",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks inappropriate content involving minors using identifier + block word combinations"
}
},
{
"guardrail_name": "nsfw-racial-bias-filter-basic",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "bias_racial",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks racial and ethnic discrimination, hate speech, and supremacist content"
}
}
],
"templateData": {
"policy_name": "nsfw-content-filter-basic",
"description": "Basic NSFW content filter. Blocks profanity, sexual content, inappropriate requests, self-harm content, child safety violations, and racial bias in English.",
"guardrails_add": [
"nsfw-content-filter-english-only",
"nsfw-self-harm-filter-basic",
"nsfw-child-safety-filter-basic",
"nsfw-racial-bias-filter-basic"
],
"guardrails_remove": []
}
},
{
"id": "nsfw-content-filter-all-regions",
"title": "NSFW Content Filter (All Regions)",
"description": "Comprehensive multi-language NSFW content filtering. Blocks profanity, sexual content, inappropriate requests, self-harm content, and child safety violations in English, Spanish, French, German, and Australian. Best for global applications.",
"icon": "ShieldExclamationIcon",
"iconColor": "text-purple-500",
"iconBg": "bg-purple-50",
"guardrails": [
"nsfw-filter-english",
"nsfw-filter-spanish",
"nsfw-filter-french",
"nsfw-filter-german",
"nsfw-filter-australian",
"nsfw-self-harm-filter-global",
"nsfw-child-safety-filter-global",
"nsfw-racial-bias-filter-global"
],
"complexity": "High",
"guardrailDefinitions": [
{
"guardrail_name": "nsfw-filter-english",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "English profanity, sexual content, slurs, and NSFW terms (485+ keywords)"
}
},
{
"guardrail_name": "nsfw-filter-spanish",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse_es",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Spanish profanity and offensive terms (68 keywords)"
}
},
{
"guardrail_name": "nsfw-filter-french",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse_fr",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "French profanity and offensive terms (91 keywords)"
}
},
{
"guardrail_name": "nsfw-filter-german",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse_de",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "German profanity and offensive terms (65 keywords)"
}
},
{
"guardrail_name": "nsfw-filter-australian",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse_au",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Australian slang and profanity (32 keywords: root, perv, bogan, wanker, etc.)"
}
},
{
"guardrail_name": "nsfw-self-harm-filter-global",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harmful_self_harm",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks content related to self-harm, suicide, and eating disorders"
}
},
{
"guardrail_name": "nsfw-child-safety-filter-global",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harmful_child_safety",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks inappropriate content involving minors using identifier + block word combinations"
}
},
{
"guardrail_name": "nsfw-racial-bias-filter-global",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "bias_racial",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks racial and ethnic discrimination, hate speech, and supremacist content"
}
}
],
"templateData": {
"policy_name": "nsfw-content-filter-all-regions",
"description": "Comprehensive multi-language NSFW content filter. Blocks profanity, inappropriate content, self-harm, child safety violations, and racial bias in English, Spanish, French, German, and Australian. Total coverage: 741+ keywords across all languages plus self-harm, child safety, and racial bias protection.",
"guardrails_add": [
"nsfw-filter-english",
"nsfw-filter-spanish",
"nsfw-filter-french",
"nsfw-filter-german",
"nsfw-filter-australian",
"nsfw-self-harm-filter-global",
"nsfw-child-safety-filter-global",
"nsfw-racial-bias-filter-global"
],
"guardrails_remove": []
}
}
]
+57 -43
View File
@@ -14,26 +14,21 @@ from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.guardrails.guardrail_registry import GuardrailRegistry
from litellm.types.guardrails import (
BaseLitellmParams,
PII_ENTITY_CATEGORIES_MAP,
ApplyGuardrailRequest,
ApplyGuardrailResponse,
BedrockGuardrailConfigModel,
Guardrail,
GuardrailEventHooks,
GuardrailInfoResponse,
GuardrailUIAddGuardrailSettings,
LakeraV2GuardrailConfigModel,
ListGuardrailsResponse,
LitellmParams,
PatchGuardrailRequest,
PiiAction,
PiiEntityType,
PresidioPresidioConfigModelUserInterface,
SupportedGuardrailIntegrations,
ToolPermissionGuardrailConfigModel,
)
from litellm.types.guardrails import (PII_ENTITY_CATEGORIES_MAP,
ApplyGuardrailRequest,
ApplyGuardrailResponse,
BaseLitellmParams,
BedrockGuardrailConfigModel, Guardrail,
GuardrailEventHooks,
GuardrailInfoResponse,
GuardrailUIAddGuardrailSettings,
LakeraV2GuardrailConfigModel,
ListGuardrailsResponse, LitellmParams,
PatchGuardrailRequest, PiiAction,
PiiEntityType,
PresidioPresidioConfigModelUserInterface,
SupportedGuardrailIntegrations,
ToolPermissionGuardrailConfigModel)
#### GUARDRAILS ENDPOINTS ####
@@ -152,7 +147,8 @@ async def list_guardrails_v2():
```
"""
from litellm.litellm_core_utils.litellm_logging import _get_masked_values
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.guardrails.guardrail_registry import \
IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
@@ -292,7 +288,8 @@ async def create_guardrail(request: CreateGuardrailRequest):
}
```
"""
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.guardrails.guardrail_registry import \
IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
@@ -381,7 +378,8 @@ async def update_guardrail(guardrail_id: str, request: UpdateGuardrailRequest):
}
```
"""
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.guardrails.guardrail_registry import \
IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
@@ -449,7 +447,8 @@ async def delete_guardrail(guardrail_id: str):
}
```
"""
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.guardrails.guardrail_registry import \
IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
@@ -542,7 +541,8 @@ async def patch_guardrail(guardrail_id: str, request: PatchGuardrailRequest):
}
```
"""
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.guardrails.guardrail_registry import \
IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
@@ -664,7 +664,8 @@ async def get_guardrail_info(guardrail_id: str):
"""
from litellm.litellm_core_utils.litellm_logging import _get_masked_values
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.guardrails.guardrail_registry import \
IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
from litellm.types.guardrails import GUARDRAIL_DEFINITION_LOCATION
@@ -739,10 +740,8 @@ async def get_guardrail_ui_settings():
- Content filter settings (patterns and categories)
"""
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.patterns import (
PATTERN_CATEGORIES,
get_available_content_categories,
get_pattern_metadata,
)
PATTERN_CATEGORIES, get_available_content_categories,
get_pattern_metadata)
# Convert the PII_ENTITY_CATEGORIES_MAP to the format expected by the UI
category_maps = []
@@ -775,13 +774,13 @@ async def get_guardrail_ui_settings():
)
async def get_category_yaml(category_name: str):
"""
Get the YAML content for a specific content filter category.
Get the YAML or JSON content for a specific content filter category.
Args:
category_name: The name of the category (e.g., "bias_gender", "harmful_self_harm")
Returns:
The raw YAML content of the category file
The raw YAML or JSON content of the category file with file type indicator
"""
import os
@@ -793,20 +792,35 @@ async def get_category_yaml(category_name: str):
"categories",
)
# Construct the file path
category_file_path = os.path.join(categories_dir, f"{category_name}.yaml")
# Try to find the file with either .yaml or .json extension
yaml_path = os.path.join(categories_dir, f"{category_name}.yaml")
json_path = os.path.join(categories_dir, f"{category_name}.json")
if not os.path.exists(category_file_path):
category_file_path = None
file_type = None
if os.path.exists(yaml_path):
category_file_path = yaml_path
file_type = "yaml"
elif os.path.exists(json_path):
category_file_path = json_path
file_type = "json"
else:
raise HTTPException(
status_code=404, detail=f"Category file not found: {category_name}"
status_code=404,
detail=f"Category file not found: {category_name} (tried .yaml and .json)",
)
try:
# Read and return the raw YAML content
# Read and return the raw content
with open(category_file_path, "r") as f:
yaml_content = f.read()
content = f.read()
return {"category_name": category_name, "yaml_content": yaml_content}
return {
"category_name": category_name,
"yaml_content": content, # Keep key name for backwards compatibility
"file_type": file_type,
}
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Error reading category file: {str(e)}"
@@ -1263,7 +1277,8 @@ async def get_provider_specific_params():
}
### get the config model for the guardrail - go through the registry and get the config model for the guardrail
from litellm.proxy.guardrails.guardrail_registry import guardrail_class_registry
from litellm.proxy.guardrails.guardrail_registry import \
guardrail_class_registry
for guardrail_name, guardrail_class in guardrail_class_registry.items():
guardrail_config_model = guardrail_class.get_config_model()
@@ -1391,9 +1406,8 @@ async def test_custom_code_guardrail(request: TestCustomCodeGuardrailRequest):
import concurrent.futures
import re
from litellm.proxy.guardrails.guardrail_hooks.custom_code.primitives import (
get_custom_code_primitives,
)
from litellm.proxy.guardrails.guardrail_hooks.custom_code.primitives import \
get_custom_code_primitives
# Security validation patterns
FORBIDDEN_PATTERNS = [
@@ -1,122 +1,417 @@
# Racial and ethnic bias detection
# Uses conditional logic: BLOCK if an identifier word + block word appear in same sentence
category_name: "bias_racial"
description: "Detects racial and ethnic discrimination, stereotypes, and biased content"
description: "Detects racial and ethnic discrimination, hate speech, and biased content using identifier + block word combinations"
default_action: "BLOCK"
# Keywords with severity levels
keywords:
# High severity - racial/ethnic identity terms
- keyword: "black"
severity: "high"
- keyword: "blacks"
severity: "high"
- keyword: "white"
severity: "high"
- keyword: "whites"
severity: "high"
- keyword: "asian"
severity: "high"
- keyword: "asians"
severity: "high"
- keyword: "hispanic"
severity: "high"
- keyword: "hispanics"
severity: "high"
- keyword: "latino"
severity: "high"
- keyword: "latina"
severity: "high"
- keyword: "latinos"
severity: "high"
- keyword: "mexican"
severity: "high"
- keyword: "mexicans"
severity: "high"
- keyword: "chinese"
severity: "high"
- keyword: "indian"
severity: "high"
- keyword: "indians"
severity: "high"
- keyword: "arab"
severity: "high"
- keyword: "arabs"
severity: "high"
- keyword: "african"
severity: "high"
- keyword: "africans"
severity: "high"
- keyword: "immigrant"
severity: "high"
- keyword: "immigrants"
severity: "high"
- keyword: "refugee"
severity: "high"
- keyword: "refugees"
severity: "high"
- keyword: "minority"
severity: "high"
- keyword: "minorities"
severity: "high"
# Inherit racial slurs and offensive terms from harm_toxic_abuse.json
inherit_from: "harm_toxic_abuse.json"
# ADDITIONAL BLOCK WORDS - Harmful concepts and dehumanizing language
additional_block_words:
# Inferiority/superiority
- "inferior"
- "superior"
- "subhuman"
- "sub-human"
- "less than"
- "better than"
- "worse than"
- "lower"
- "primitive"
- "backwards"
- "backward"
- "uncivilized"
- "savage"
- "savages"
- "barbaric"
- "barbarian"
# High severity - discriminatory terms
- keyword: "inferior race"
# Elimination/violence language
- "eliminate"
- "eliminated"
- "elimination"
- "remove"
- "removed"
- "removal"
- "purge"
- "purged"
- "purging"
- "cleanse"
- "cleansing"
- "exterminate"
- "extermination"
- "eradicate"
- "eradication"
- "get rid of"
- "wipe out"
- "destroy"
- "destruction"
- "annihilate"
- "should be killed"
- "deserve to die"
- "need to die"
- "must die"
# Threat/danger framing
- "dangerous"
- "threat"
- "problem"
- "issue"
- "plague"
- "disease"
- "infection"
- "parasite"
- "parasites"
- "vermin"
- "rats"
- "cockroaches"
- "invading"
- "invasion"
- "invaders"
- "taking over"
- "replacing"
- "replacement"
- "infiltrating"
- "infiltration"
# Negative stereotypes
- "lazy"
- "criminals"
- "criminal"
- "terrorists"
- "terrorist"
- "violent"
- "dirty"
- "filthy"
- "disgusting"
- "smell"
- "stink"
- "stupid"
- "dumb"
- "ignorant"
- "uneducated"
- "uncivilized"
# Exclusion/deportation
- "go back"
- "send back"
- "deport"
- "deportation"
- "don't belong"
- "not welcome"
- "get out"
- "leave"
- "kick out"
# Supremacy concepts
- "pure"
- "purity"
- "master"
- "supremacy"
- "supremacist"
- "nationalist"
- "nationalism"
# Economic/social stereotypes
- "stealing jobs"
- "taking jobs"
- "welfare"
- "benefits"
- "leeches"
- "moochers"
# Animalistic/dehumanizing
- "animals"
- "animal"
- "beasts"
- "beast"
- "monkeys"
- "apes"
- "dogs"
# IDENTIFIER WORDS - Comprehensive list of racial, ethnic, and national origin groups
identifier_words:
# Major racial categories
- "black"
- "blacks"
- "black people"
- "black person"
- "african american"
- "african americans"
- "white"
- "whites"
- "white people"
- "white person"
- "caucasian"
- "caucasians"
- "asian"
- "asians"
- "asian people"
- "asian person"
- "hispanic"
- "hispanics"
- "hispanic people"
- "latino"
- "latina"
- "latinos"
- "latinas"
- "latinx"
- "indigenous"
- "native"
- "natives"
- "native american"
- "native americans"
- "aboriginal"
- "aboriginals"
- "indigenous people"
# Specific ethnic/national groups - Americas
- "mexican"
- "mexicans"
- "puerto rican"
- "puerto ricans"
- "cuban"
- "cubans"
- "dominican"
- "dominicans"
- "colombian"
- "colombians"
- "venezuelan"
- "venezuelans"
- "brazilian"
- "brazilians"
- "haitian"
- "haitians"
- "jamaican"
- "jamaicans"
- "salvadoran"
- "salvadorans"
- "guatemalan"
- "guatemalans"
- "nicaraguan"
- "nicaraguans"
- "honduran"
- "hondurans"
# Specific ethnic/national groups - Asia
- "chinese"
- "japanese"
- "korean"
- "koreans"
- "vietnamese"
- "filipino"
- "filipinos"
- "thai"
- "cambodian"
- "cambodians"
- "laotian"
- "laotians"
- "indonesian"
- "indonesians"
- "malaysian"
- "malaysians"
- "singaporean"
- "singaporeans"
- "indian"
- "indians"
- "pakistani"
- "pakistanis"
- "bangladeshi"
- "bangladeshis"
- "sri lankan"
- "sri lankans"
- "nepalese"
- "afghan"
- "afghans"
# Specific ethnic/national groups - Middle East/North Africa
- "arab"
- "arabs"
- "middle eastern"
- "saudi"
- "saudis"
- "iraqi"
- "iraqis"
- "iranian"
- "iranians"
- "syrian"
- "syrians"
- "lebanese"
- "egyptian"
- "egyptians"
- "moroccan"
- "moroccans"
- "algerian"
- "algerians"
- "tunisian"
- "tunisians"
- "libyan"
- "libyans"
- "palestinian"
- "palestinians"
- "israeli"
- "israelis"
- "jewish"
- "jews"
- "muslim"
- "muslims"
- "islamic"
# Specific ethnic/national groups - Africa
- "african"
- "africans"
- "nigerian"
- "nigerians"
- "ethiopian"
- "ethiopians"
- "somali"
- "somalis"
- "kenyan"
- "kenyans"
- "ghanaian"
- "ghanaians"
- "south african"
- "south africans"
- "ugandan"
- "ugandans"
- "tanzanian"
- "tanzanians"
- "sudanese"
- "congolese"
# Specific ethnic/national groups - Europe
- "eastern european"
- "eastern europeans"
- "russian"
- "russians"
- "polish"
- "ukrainian"
- "ukrainians"
- "romanian"
- "romanians"
- "albanian"
- "albanians"
- "bosnian"
- "bosnians"
- "serbian"
- "serbians"
- "croatian"
- "croatians"
- "turkish"
- "roma"
- "romani"
- "gypsy"
- "gypsies"
# Immigration status
- "immigrant"
- "immigrants"
- "migrant"
- "migrants"
- "refugee"
- "refugees"
- "asylum seeker"
- "asylum seekers"
- "illegal alien"
- "illegal aliens"
- "undocumented"
- "foreigner"
- "foreigners"
- "expat"
- "expats"
# Group descriptors
- "minority"
- "minorities"
- "ethnic group"
- "ethnic groups"
- "race"
- "races"
- "people of color"
- "poc"
- "non-white"
- "non-whites"
# ALWAYS BLOCK - Explicit hate speech and supremacist phrases
always_block_keywords:
# Supremacist ideologies
- keyword: "white supremacy"
severity: "high"
- keyword: "superior race"
- keyword: "white supremacist"
severity: "high"
- keyword: "racial purity"
- keyword: "white nationalist"
severity: "high"
- keyword: "white nationalism"
severity: "high"
- keyword: "white power"
severity: "high"
- keyword: "master race"
severity: "high"
- keyword: "white supremacy"
- keyword: "superior race"
severity: "high"
- keyword: "white genocide"
- keyword: "inferior race"
severity: "high"
- keyword: "great replacement"
- keyword: "racial purity"
severity: "high"
- keyword: "pure blood"
severity: "high"
- keyword: "race traitor"
severity: "high"
- keyword: "race mixing"
severity: "high"
- keyword: "model minority"
- keyword: "blood and soil"
severity: "high"
# Genocide/replacement theories
- keyword: "white genocide"
severity: "high"
- keyword: "great replacement"
severity: "high"
- keyword: "replacement theory"
severity: "high"
- keyword: "ethnic cleansing"
severity: "high"
- keyword: "racial cleansing"
severity: "high"
- keyword: "final solution"
severity: "high"
# Explicit combinations
- keyword: "subhuman race"
severity: "high"
- keyword: "inferior people"
severity: "high"
- keyword: "savage race"
severity: "high"
- keyword: "diversity hire"
severity: "high"
- keyword: "ghetto culture"
severity: "high"
- keyword: "thug culture"
severity: "high"
- keyword: "diversity hire"
severity: "high"
- keyword: "black crime"
severity: "high"
- keyword: "immigrant crime"
severity: "high"
- keyword: "minority lazy"
- keyword: "muslim terrorist"
severity: "high"
- keyword: "stealing jobs"
- keyword: "arab terrorist"
severity: "high"
- keyword: "go back"
- keyword: "mexican rapist"
severity: "high"
- keyword: "you people"
severity: "medium"
- keyword: "those people"
severity: "medium"
- keyword: "all blacks"
- keyword: "stealing our jobs"
severity: "high"
- keyword: "all whites"
- keyword: "taking our jobs"
severity: "high"
- keyword: "all asians"
- keyword: "send them back"
severity: "high"
- keyword: "all hispanics"
severity: "high"
- keyword: "all latinos"
severity: "high"
- keyword: "all mexicans"
severity: "high"
- keyword: "all immigrants"
- keyword: "go back to your country"
severity: "high"
# Exceptions - legitimate discussions about race, diversity, anti-racism
exceptions:
# Anti-racism and justice
- "racial equality"
- "racial justice"
- "racial discrimination"
@@ -133,16 +428,60 @@ exceptions:
- "address racism"
- "racial disparities"
- "racism is"
- "racist"
- "racist is"
- "against racism"
- "oppose racism"
- "condemn racism"
# Cultural and historical
- "study shows"
- "research indicates"
- "research shows"
- "studies show"
- "historical"
- "history"
- "black history"
- "black culture"
- "asian culture"
- "hispanic culture"
- "latino culture"
- "indigenous culture"
- "native culture"
- "african culture"
- "cultural heritage"
- "cultural traditions"
- "celebrate"
- "celebrating"
- "heritage"
- "traditions"
# Rights and support
- "immigrant rights"
- "immigrant support"
- "protect immigrants"
- "minority rights"
- "protect minorities"
- "refugee support"
- "refugee rights"
- "human rights"
- "equal rights"
- "equal treatment"
# Education and awareness
- "education about"
- "learn about"
- "understanding"
- "awareness"
- "teach about"
- "cultural sensitivity"
- "cultural competency"
- "diversity training"
# Positive representation
- "representation matters"
- "diverse perspectives"
- "inclusive"
- "inclusion"
- "belonging"
- "empower"
- "empowerment"
@@ -0,0 +1,345 @@
[
{
"id": "root",
"match": "root|rooting|rooted|rooter",
"tags": [
"sexual",
"australian"
],
"severity": 3,
"exceptions": [
"tree",
"plant",
"square",
"cube",
"directory",
"access",
"cause",
"beer"
]
},
{
"id": "ranga",
"match": "ranga|rangas",
"tags": [
"offensive",
"australian"
],
"severity": 2,
"exceptions": [
"whangarei"
]
},
{
"id": "minge",
"match": "minge|minges",
"tags": [
"sexual",
"australian"
],
"severity": 3
},
{
"id": "perv",
"match": "perv|perve|perving|perved|have a perv|perve on",
"tags": [
"sexual",
"australian"
],
"severity": 3,
"exceptions": [
"pervade",
"perverse"
]
},
{
"id": "slapper",
"match": "slapper|slappers",
"tags": [
"offensive",
"australian"
],
"severity": 3
},
{
"id": "moll",
"match": "moll|molls",
"tags": [
"offensive",
"australian"
],
"severity": 2,
"exceptions": [
"flanders"
]
},
{
"id": "sheila",
"match": "sheila|sheilas",
"tags": [
"offensive",
"australian"
],
"severity": 1,
"exceptions": [
"name",
"saint"
]
},
{
"id": "arsehole-au",
"match": "arsehole|arseholes",
"tags": [
"profanity",
"australian"
],
"severity": 3
},
{
"id": "bloody-au",
"match": "bloody hell|bloody oath|bloody bastard|bloody idiot",
"tags": [
"profanity",
"australian"
],
"severity": 2
},
{
"id": "bugger-au",
"match": "bugger off|bugger all|bugger me|buggered",
"tags": [
"profanity",
"australian"
],
"severity": 2
},
{
"id": "bollocks-au",
"match": "bollocks|bollock",
"tags": [
"profanity",
"australian"
],
"severity": 2
},
{
"id": "wanker-au",
"match": "wanker|wankers|wank",
"tags": [
"profanity",
"australian"
],
"severity": 3
},
{
"id": "tosser-au",
"match": "tosser|tossers",
"tags": [
"profanity",
"australian"
],
"severity": 2
},
{
"id": "bogan",
"match": "bogan|bogans|filthy bogan",
"tags": [
"offensive",
"australian"
],
"severity": 2
},
{
"id": "drongo",
"match": "drongo|drongos",
"tags": [
"insult",
"australian"
],
"severity": 1
},
{
"id": "yobbo",
"match": "yobbo|yobbos|yob",
"tags": [
"insult",
"australian"
],
"severity": 2
},
{
"id": "ratbag",
"match": "ratbag|ratbags",
"tags": [
"insult",
"australian"
],
"severity": 1
},
{
"id": "dole-bludger",
"match": "dole bludger|dole-bludger|bludger",
"tags": [
"offensive",
"australian"
],
"severity": 2,
"exceptions": [
"harry potter",
"quidditch"
]
},
{
"id": "fuck-spiders",
"match": "fuck spiders|fucking spiders",
"tags": [
"profanity",
"australian"
],
"severity": 3
},
{
"id": "root-rat",
"match": "root rat|rootrat",
"tags": [
"sexual",
"australian"
],
"severity": 3
},
{
"id": "bush-pig",
"match": "bush pig|bushpig",
"tags": [
"offensive",
"australian"
],
"severity": 3
},
{
"id": "seppo",
"match": "seppo|seppos",
"tags": [
"offensive",
"australian"
],
"severity": 2,
"exceptions": [
"seppo"
]
},
{
"id": "pom",
"match": "pom|pommie|pommy bastard",
"tags": [
"offensive",
"australian"
],
"severity": 2,
"exceptions": [
"pomegranate",
"pomeranian"
]
},
{
"id": "spunk-rat",
"match": "spunk rat|spunkrat",
"tags": [
"sexual",
"australian"
],
"severity": 3
},
{
"id": "fanny-au",
"match": "fanny",
"tags": [
"sexual",
"australian"
],
"severity": 2,
"exceptions": [
"pack",
"bag",
"adams"
]
},
{
"id": "knob",
"match": "knob|knobhead|knob-head",
"tags": [
"profanity",
"australian"
],
"severity": 2,
"exceptions": [
"door",
"control",
"volume"
]
},
{
"id": "gash",
"match": "gash",
"tags": [
"sexual",
"australian"
],
"severity": 3,
"exceptions": [
"cut",
"wound",
"injury"
]
},
{
"id": "norks",
"match": "norks",
"tags": [
"sexual",
"australian"
],
"severity": 2
},
{
"id": "dag",
"match": "dag|dags",
"tags": [
"insult",
"australian"
],
"severity": 1,
"exceptions": [
"acyclic",
"graph",
"directed"
]
},
{
"id": "sook",
"match": "sook|sooky|sooky la la",
"tags": [
"insult",
"australian"
],
"severity": 1
},
{
"id": "up-the-duff",
"match": "up the duff|upduff",
"tags": [
"sexual",
"australian"
],
"severity": 2
},
{
"id": "get-stuffed",
"match": "get stuffed",
"tags": [
"profanity",
"australian"
],
"severity": 2
}
]
@@ -0,0 +1,587 @@
[
{
"id": "analritter",
"match": "analritter",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "arsch",
"match": "arsch",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "arschficker",
"match": "arschficker",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "arschlecker",
"match": "arschlecker",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "arschloch",
"match": "arschloch",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "bimbo",
"match": "bimbo",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "bratze",
"match": "bratze",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "bumsen",
"match": "bumsen",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "bonze",
"match": "bonze",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "dodel",
"match": "dödel",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "fick",
"match": "fick",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "ficken",
"match": "ficken",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "flittchen",
"match": "flittchen",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "fotze",
"match": "fotze",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "fratze",
"match": "fratze",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "hackfresse",
"match": "hackfresse",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "hure",
"match": "hure",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "hurensohn",
"match": "hurensohn",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "ische",
"match": "ische",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "kackbratze",
"match": "kackbratze",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "kacke",
"match": "kacke",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "kacken",
"match": "kacken",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "kackwurst",
"match": "kackwurst",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "kampflesbe",
"match": "kampflesbe",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "kanake",
"match": "kanake",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "kimme",
"match": "kimme",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "lummel",
"match": "lümmel",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "milf",
"match": "milf",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "mopse",
"match": "möpse",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "morgenlatte",
"match": "morgenlatte",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "mose",
"match": "möse",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "mufti",
"match": "mufti",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "muschi",
"match": "muschi",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "nackt",
"match": "nackt",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "neger",
"match": "neger",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "nigger",
"match": "nigger",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "nippel",
"match": "nippel",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "nutte",
"match": "nutte",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "onanieren",
"match": "onanieren",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "orgasmus",
"match": "orgasmus",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "pimmel",
"match": "pimmel",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "pimpern",
"match": "pimpern",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "pinkeln",
"match": "pinkeln",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "pissen",
"match": "pissen",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "pisser",
"match": "pisser",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "popel",
"match": "popel",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "poppen",
"match": "poppen",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "porno",
"match": "porno",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "reudig",
"match": "reudig",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "rosette",
"match": "rosette",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "schabracke",
"match": "schabracke",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "schlampe",
"match": "schlampe",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "scheisse",
"match": "scheiße",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "scheisser",
"match": "scheisser",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "schiesser",
"match": "schiesser",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "schnackeln",
"match": "schnackeln",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "schwanzlutscher",
"match": "schwanzlutscher",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "schwuchtel",
"match": "schwuchtel",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "tittchen",
"match": "tittchen",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "titten",
"match": "titten",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "vogeln",
"match": "vögeln",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "vollpfosten",
"match": "vollpfosten",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "wichse",
"match": "wichse",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "wichsen",
"match": "wichsen",
"tags": [
"profanity",
"german"
],
"severity": 3
},
{
"id": "wichser",
"match": "wichser",
"tags": [
"profanity",
"german"
],
"severity": 3
}
]
@@ -0,0 +1,614 @@
[
{
"id": "asesinato",
"match": "asesinato",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "asno",
"match": "asno",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "bastardo",
"match": "bastardo",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "bollera",
"match": "bollera",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "cabron",
"match": "cabron",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "cabron",
"match": "cabrón",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "caca",
"match": "caca",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "chupada",
"match": "chupada",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "chupapollas",
"match": "chupapollas",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "chupeton",
"match": "chupetón",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "concha",
"match": "concha",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "concha-de-tu-madre",
"match": "concha de tu madre",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "coño",
"match": "coño",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "coprofagia",
"match": "coprofagía",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "culo",
"match": "culo",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "drogas",
"match": "drogas",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "esperma",
"match": "esperma",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "fiesta-de-salchichas",
"match": "fiesta de salchichas",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "follador",
"match": "follador",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "follar",
"match": "follar",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "gilipichis",
"match": "gilipichis",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "gilipollas",
"match": "gilipollas",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "hacer-una-paja",
"match": "hacer una paja",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "haciendo-el-amor",
"match": "haciendo el amor",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "heroina",
"match": "heroína",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "hija-de-puta",
"match": "hija de puta",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "hijaputa",
"match": "hijaputa",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "hijo-de-puta",
"match": "hijo de puta",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "hijoputa",
"match": "hijoputa",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "idiota",
"match": "idiota",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "imbecil",
"match": "imbécil",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "infierno",
"match": "infierno",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "jilipollas",
"match": "jilipollas",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "kapullo",
"match": "kapullo",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "lameculos",
"match": "lameculos",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "maciza",
"match": "maciza",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "macizorra",
"match": "macizorra",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "maldito",
"match": "maldito",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "mamada",
"match": "mamada",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "marica",
"match": "marica",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "maricon",
"match": "maricón",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "mariconazo",
"match": "mariconazo",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "martillo",
"match": "martillo",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "mierda",
"match": "mierda",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "nazi",
"match": "nazi",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "orina",
"match": "orina",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "pedo",
"match": "pedo",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "pervertido",
"match": "pervertido",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "pezon",
"match": "pezón",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "pinche",
"match": "pinche",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "pis",
"match": "pis",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "prostituta",
"match": "prostituta",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "puta",
"match": "puta",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "racista",
"match": "racista",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "ramera",
"match": "ramera",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "sadico",
"match": "sádico",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "semen",
"match": "semen",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "sexo",
"match": "sexo",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "sexo-oral",
"match": "sexo oral",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "soplagaitas",
"match": "soplagaitas",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "soplapollas",
"match": "soplapollas",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "tetas-grandes",
"match": "tetas grandes",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "tia-buena",
"match": "tía buena",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "travesti",
"match": "travesti",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "trio",
"match": "trio",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "verga",
"match": "verga",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "vete-a-la-mierda",
"match": "vete a la mierda",
"tags": [
"profanity",
"spanish"
],
"severity": 3
},
{
"id": "vulva",
"match": "vulva",
"tags": [
"profanity",
"spanish"
],
"severity": 3
}
]
@@ -0,0 +1,821 @@
[
{
"id": "baiser",
"match": "baiser",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "bander",
"match": "bander",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "bigornette",
"match": "bigornette",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "bite",
"match": "bite",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "bitte",
"match": "bitte",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "bloblos",
"match": "bloblos",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "bordel",
"match": "bordel",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "bosser",
"match": "bosser",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "bourre",
"match": "bourré",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "bourree",
"match": "bourrée",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "brackmard",
"match": "brackmard",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "branlage",
"match": "branlage",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "branler",
"match": "branler",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "branlette",
"match": "branlette",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "branleur",
"match": "branleur",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "branleuse",
"match": "branleuse",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "brouter-le-cresson",
"match": "brouter le cresson",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "caca",
"match": "caca",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "cailler",
"match": "cailler",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "chatte",
"match": "chatte",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "chiasse",
"match": "chiasse",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "chier",
"match": "chier",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "chiottes",
"match": "chiottes",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "clito",
"match": "clito",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "clitoris",
"match": "clitoris",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "con",
"match": "con",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "connard",
"match": "connard",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "connasse",
"match": "connasse",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "conne",
"match": "conne",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "couilles",
"match": "couilles",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "cramouille",
"match": "cramouille",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "cul",
"match": "cul",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "deconne",
"match": "déconne",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "deconner",
"match": "déconner",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "drague",
"match": "drague",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "emmerdant",
"match": "emmerdant",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "emmerder",
"match": "emmerder",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "emmerdeur",
"match": "emmerdeur",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "emmerdeuse",
"match": "emmerdeuse",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "encule",
"match": "enculé",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "enculee",
"match": "enculée",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "enculeur",
"match": "enculeur",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "enculeurs",
"match": "enculeurs",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "enfoire",
"match": "enfoiré",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "enfoiree",
"match": "enfoirée",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "etron",
"match": "étron",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "fille-de-pute",
"match": "fille de pute",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "fils-de-pute",
"match": "fils de pute",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "folle",
"match": "folle",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "foutre",
"match": "foutre",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "gerbe",
"match": "gerbe",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "gerber",
"match": "gerber",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "gouine",
"match": "gouine",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "grande-folle",
"match": "grande folle",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "grogniasse",
"match": "grogniasse",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "gueule",
"match": "gueule",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "jouir",
"match": "jouir",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "la-putain-de-ta-mere",
"match": "la putain de ta mère",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "malpt",
"match": "malpt",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "menage-a-trois",
"match": "ménage à trois",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "merde",
"match": "merde",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "merdeuse",
"match": "merdeuse",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "merdeux",
"match": "merdeux",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "meuf",
"match": "meuf",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "negre",
"match": "nègre",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "nique-ta-mere",
"match": "nique ta mère",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "nique-ta-race",
"match": "nique ta race",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "palucher",
"match": "palucher",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "pedale",
"match": "pédale",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "pede",
"match": "pédé",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "peter",
"match": "péter",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "pipi",
"match": "pipi",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "pisser",
"match": "pisser",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "pouffiasse",
"match": "pouffiasse",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "pousse-crotte",
"match": "pousse-crotte",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "putain",
"match": "putain",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "pute",
"match": "pute",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "ramoner",
"match": "ramoner",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "sac-a-merde",
"match": "sac à merde",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "salaud",
"match": "salaud",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "salope",
"match": "salope",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "suce",
"match": "suce",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "tapette",
"match": "tapette",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "teuf",
"match": "teuf",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "tringler",
"match": "tringler",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "trique",
"match": "trique",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "trou-du-cul",
"match": "trou du cul",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "turlute",
"match": "turlute",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "veuve",
"match": "veuve",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "zigounette",
"match": "zigounette",
"tags": [
"profanity",
"french"
],
"severity": 3
},
{
"id": "zizi",
"match": "zizi",
"tags": [
"profanity",
"french"
],
"severity": 3
}
]
@@ -0,0 +1,156 @@
# Child Safety - Detects inappropriate content involving minors
# Uses conditional logic: BLOCK if an identifier word + block word appear in same sentence
category_name: "harmful_child_safety"
description: "Detects inappropriate content involving minors using identifier + block word combinations"
default_action: "BLOCK"
# Inherit block words from harm_toxic_abuse.json + additional context-specific terms
inherit_from: "harm_toxic_abuse.json"
# ADDITIONAL BLOCK WORDS - Child-specific context words to block when combined with identifiers
# These supplement the inherited words from harm_toxic_abuse.json
additional_block_words:
- "inappropriate"
- "intimate"
- "intimacy"
- "kissing"
- "flirting"
- "seductive"
- "alluring"
- "desirable"
- "hot"
- "sexy"
- "arousing"
- "provocative"
- "suggestive"
- "physical"
- "body"
# IDENTIFIER WORDS - Words that indicate minors/children
# When combined with block words (from harm_toxic_abuse.json + additional_block_words), triggers blocking
identifier_words:
- "minor"
- "minors"
- "child"
- "children"
- "kid"
- "kids"
- "teenager"
- "teenagers"
- "teen"
- "teens"
- "youth"
- "underage"
- "juvenile"
- "juveniles"
- "adolescent"
- "adolescents"
- "young girl"
- "young boy"
- "young girls"
- "young boys"
- "schoolgirl"
- "schoolboy"
- "pre-teen"
- "preteen"
# ALWAYS BLOCK - Explicit CSAM and exploitation terms (blocked regardless of context)
always_block_keywords:
# Explicit CSAM terms
- keyword: "child sexual abuse"
severity: "high"
- keyword: "child abuse material"
severity: "high"
- keyword: "csam"
severity: "high"
- keyword: "child pornography"
severity: "high"
- keyword: "child porn"
severity: "high"
- keyword: "minor pornography"
severity: "high"
- keyword: "cp"
severity: "high"
- keyword: "loli"
severity: "high"
- keyword: "lolicon"
severity: "high"
- keyword: "shota"
severity: "high"
- keyword: "shotacon"
severity: "high"
- keyword: "pedo"
severity: "high"
- keyword: "pedophile"
severity: "high"
- keyword: "pedophilia"
severity: "high"
- keyword: "paedophile"
severity: "high"
- keyword: "paedophilia"
severity: "high"
- keyword: "child molester"
severity: "high"
- keyword: "child predator"
severity: "high"
- keyword: "jailbait"
severity: "high"
- keyword: "barely legal"
severity: "high"
- keyword: "age of consent"
severity: "high"
- keyword: "child erotica"
severity: "high"
- keyword: "minor erotica"
severity: "high"
- keyword: "preteen sex"
severity: "high"
- keyword: "underage sex"
severity: "high"
- keyword: "child sex"
severity: "high"
- keyword: "minor sex"
severity: "high"
# Pre-combined high-risk phrases
- keyword: "romantic scenarios involving minors"
severity: "high"
- keyword: "romantic scenarios involving children"
severity: "high"
- keyword: "sexual scenarios involving minors"
severity: "high"
- keyword: "sexual scenarios involving children"
severity: "high"
- keyword: "intimate scenarios involving minors"
severity: "high"
- keyword: "intimate scenarios involving children"
severity: "high"
- keyword: "explicit content involving minors"
severity: "high"
- keyword: "explicit content involving children"
severity: "high"
# Exception patterns (legitimate uses)
exceptions:
- "child safety"
- "child protection"
- "protect children"
- "protect minors"
- "child abuse prevention"
- "child welfare"
- "safeguarding children"
- "child development"
- "child psychology"
- "adolescent psychology"
- "child health"
- "minor health"
- "pediatric"
- "child education"
- "age appropriate"
- "parental controls"
- "reporting child abuse"
- "child abuse survivor"
- "victims of child abuse"
- "legal minor"
- "medical minor"
- "emancipated minor"
@@ -10,19 +10,8 @@ import json
import os
import re
from datetime import datetime
from typing import (
TYPE_CHECKING,
Any,
AsyncGenerator,
Dict,
List,
Literal,
Optional,
Pattern,
Tuple,
Union,
cast,
)
from typing import (TYPE_CHECKING, Any, AsyncGenerator, Dict, List, Literal,
Optional, Pattern, Tuple, Union, cast)
import yaml
from fastapi import HTTPException
@@ -37,20 +26,12 @@ if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.types.utils import GenericGuardrailAPIInputs, GuardrailStatus
from litellm.types.guardrails import (
BlockedWord,
ContentFilterAction,
ContentFilterPattern,
GuardrailEventHooks,
Mode,
)
from litellm.types.guardrails import (BlockedWord, ContentFilterAction,
ContentFilterPattern,
GuardrailEventHooks, Mode)
from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import (
BlockedWordDetection,
CategoryKeywordDetection,
ContentFilterCategoryConfig,
ContentFilterDetection,
PatternDetection,
)
BlockedWordDetection, CategoryKeywordDetection,
ContentFilterCategoryConfig, ContentFilterDetection, PatternDetection)
from .patterns import PATTERN_EXTRA_CONFIG, get_compiled_pattern
@@ -91,12 +72,27 @@ class CategoryConfig:
default_action: ContentFilterAction,
keywords: List[Dict[str, str]],
exceptions: List[str],
identifier_words: Optional[List[str]] = None,
always_block_keywords: Optional[List[Dict[str, str]]] = None,
inherit_from: Optional[str] = None,
additional_block_words: Optional[List[str]] = None,
):
self.category_name = category_name
self.description = description
self.default_action = default_action
self.keywords = keywords
self.exceptions = [e.lower() for e in exceptions]
# New fields for conditional child safety logic
self.identifier_words = (
[w.lower() for w in identifier_words] if identifier_words else []
)
self.always_block_keywords = always_block_keywords or []
self.inherit_from = inherit_from
self.additional_block_words = (
[w.lower() for w in additional_block_words]
if additional_block_words
else []
)
class ContentFilterGuardrail(CustomGuardrail):
@@ -174,6 +170,10 @@ class ContentFilterGuardrail(CustomGuardrail):
self.category_keywords: Dict[str, Tuple[str, str, ContentFilterAction]] = (
{}
) # keyword -> (category, severity, action)
# Store conditional categories (identifier_words + block_words)
self.conditional_categories: Dict[str, Dict[str, Any]] = (
{}
) # category_name -> {identifier_words, block_words, action, severity}
# Load categories if provided
if categories:
@@ -306,7 +306,32 @@ class ContentFilterGuardrail(CustomGuardrail):
action if action else category_config_obj.default_action
)
# Add keywords from this category
# Handle conditional categories (with identifier_words + inherit_from)
if (
category_config_obj.identifier_words
and category_config_obj.inherit_from
):
self._load_conditional_category(
category_name,
category_config_obj,
category_action,
severity_threshold,
categories_dir,
)
# Add always_block_keywords if present
if category_config_obj.always_block_keywords:
for keyword_data in category_config_obj.always_block_keywords:
keyword = keyword_data["keyword"].lower()
severity = keyword_data.get("severity", "high")
if self._should_apply_severity(severity, severity_threshold):
self.category_keywords[keyword] = (
category_name,
severity,
category_action,
)
# Add regular keywords from this category
for keyword_data in category_config_obj.keywords:
keyword = keyword_data["keyword"].lower()
severity = keyword_data["severity"]
@@ -321,19 +346,101 @@ class ContentFilterGuardrail(CustomGuardrail):
verbose_proxy_logger.info(
f"Loaded category {category_name}: "
f"{len(category_config_obj.keywords)} keywords"
f"{len(category_config_obj.keywords)} keywords, "
f"{len(category_config_obj.always_block_keywords)} always-block keywords, "
f"conditional: {bool(category_config_obj.identifier_words)}"
)
except Exception as e:
verbose_proxy_logger.error(
f"Error loading category {category_name}: {e}"
)
def _load_conditional_category(
self,
category_name: str,
category_config_obj: CategoryConfig,
category_action: ContentFilterAction,
severity_threshold: str,
categories_dir: str,
) -> None:
"""
Load a conditional category that uses identifier_words + inherited block_words.
Args:
category_name: Name of the category
category_config_obj: CategoryConfig object with identifier_words and inherit_from
category_action: Action to take when match is found
severity_threshold: Minimum severity threshold
categories_dir: Directory containing category files
"""
# Load the inherited category to get block words
inherit_from = category_config_obj.inherit_from
if not inherit_from:
return
# Remove .json or .yaml extension if included
inherit_base = inherit_from.replace(".json", "").replace(".yaml", "")
# Find the inherited category file
inherit_yaml_path = os.path.join(categories_dir, f"{inherit_base}.yaml")
inherit_json_path = os.path.join(categories_dir, f"{inherit_base}.json")
if os.path.exists(inherit_yaml_path):
inherit_file_path = inherit_yaml_path
elif os.path.exists(inherit_json_path):
inherit_file_path = inherit_json_path
else:
verbose_proxy_logger.warning(
f"Category {category_name}: inherit_from '{inherit_from}' file not found at {categories_dir}"
)
verbose_proxy_logger.debug(
f"Tried paths: {inherit_yaml_path}, {inherit_json_path}"
)
return
try:
# Load the inherited category
inherited_category = self._load_category_file(inherit_file_path)
# Extract block words from inherited category that meet severity threshold
block_words = []
for keyword_data in inherited_category.keywords:
keyword = keyword_data["keyword"].lower()
severity = keyword_data["severity"]
if self._should_apply_severity(severity, severity_threshold):
block_words.append(keyword)
# Add additional block words specific to this category
if category_config_obj.additional_block_words:
block_words.extend(category_config_obj.additional_block_words)
# Store the conditional category configuration
self.conditional_categories[category_name] = {
"identifier_words": category_config_obj.identifier_words,
"block_words": block_words,
"action": category_action,
"severity": "high", # Combinations are always high severity
}
verbose_proxy_logger.info(
f"Loaded conditional category {category_name}: "
f"{len(category_config_obj.identifier_words)} identifiers + "
f"{len(block_words)} block words "
f"({len(category_config_obj.additional_block_words)} additional + "
f"{len(block_words) - len(category_config_obj.additional_block_words)} from {inherit_from})"
)
except Exception as e:
verbose_proxy_logger.error(
f"Error loading inherited category for {category_name}: {e}"
)
def _load_category_file(self, file_path: str) -> CategoryConfig:
"""
Load a category definition from a YAML or JSON file.
YAML format: category_name, description, default_action, keywords (list of
{keyword, severity}), exceptions.
Optional: identifier_words, always_block_keywords, inherit_from.
JSON format: list of {id, match, tags, severity}; match is pipe-separated
phrases; severity 1-4 mapped to low/medium/high. Used for harm_toxic_abuse.
@@ -347,12 +454,20 @@ class ContentFilterGuardrail(CustomGuardrail):
return self._load_category_file_json(file_path)
with open(file_path, "r") as f:
data = yaml.safe_load(f)
# Handle always_block_keywords if present
always_block = data.get("always_block_keywords", [])
return CategoryConfig(
category_name=data.get("category_name", "unknown"),
description=data.get("description", ""),
default_action=ContentFilterAction(data.get("default_action", "BLOCK")),
keywords=data.get("keywords", []),
exceptions=data.get("exceptions", []),
identifier_words=data.get("identifier_words"),
always_block_keywords=always_block,
inherit_from=data.get("inherit_from"),
additional_block_words=data.get("additional_block_words"),
)
def _load_category_file_json(self, file_path: str) -> CategoryConfig:
@@ -650,6 +765,94 @@ class ContentFilterGuardrail(CustomGuardrail):
return (matched_text, pattern_name, action)
return None
def _check_conditional_categories(
self, text: str, exceptions: List[str]
) -> Optional[Tuple[str, str, str, ContentFilterAction]]:
"""
Check text for conditional category matches (identifier + block word in same sentence).
This implements logic like: if text contains both an identifier word (e.g., "minor")
AND a block word (e.g., "romantic"), then block it.
Args:
text: Text to check
exceptions: List of exception phrases to ignore
Returns:
Tuple of (matched_phrase, category, severity, action) if match found, None otherwise
"""
text_lower = text.lower()
# First check if any exception applies
for exception in exceptions:
if exception in text_lower:
return None
# Split text into sentences for more precise matching
# Simple sentence splitting on common terminators
sentences = re.split(r"[.!?]+", text)
for category_name, config in self.conditional_categories.items():
identifier_words = config["identifier_words"]
block_words = config["block_words"]
action = config["action"]
severity = config["severity"]
# Check category-specific exceptions
category_obj = self.loaded_categories.get(category_name)
if category_obj:
exception_found = False
for exception in category_obj.exceptions:
if exception in text_lower:
verbose_proxy_logger.debug(
f"Category exception '{exception}' found for {category_name}, skipping"
)
exception_found = True
break
if exception_found:
continue
# Check each sentence for identifier + block word combination
for sentence in sentences:
sentence_lower = sentence.lower().strip()
if not sentence_lower:
continue
# Check if sentence contains ANY identifier word
identifier_found = None
for identifier in identifier_words:
if identifier in sentence_lower:
identifier_found = identifier
break
if not identifier_found:
continue
# Check if sentence also contains ANY block word
block_word_found = None
for block_word in block_words:
# Use word boundary for single words to avoid false positives
if " " in block_word:
# Multi-word phrase
if block_word in sentence_lower:
block_word_found = block_word
break
else:
# Single word - use word boundary
pattern = r"\b" + re.escape(block_word) + r"\b"
if re.search(pattern, sentence_lower):
block_word_found = block_word
break
if block_word_found:
matched_phrase = f"{identifier_found} + {block_word_found}"
verbose_proxy_logger.warning(
f"Conditional match in {category_name}: '{matched_phrase}' in sentence"
)
return (matched_phrase, category_name, severity, action)
return None
def _check_category_keywords(
self, text: str, exceptions: List[str]
) -> Optional[Tuple[str, str, str, ContentFilterAction]]:
@@ -675,15 +878,21 @@ class ContentFilterGuardrail(CustomGuardrail):
# Check category keywords
for keyword, (category, severity, action) in self.category_keywords.items():
# Convert asterisks (*) in keywords to regex wildcards
# Asterisks are used in the source data to obfuscate profanity (e.g., "fu*c*k" -> "fuck")
# We treat * as a wildcard matching zero or one character
keyword_pattern_str = keyword.replace("*", ".?")
# Use word boundary matching for single words to avoid false positives
# (e.g., "men" should not match "recommend")
# For multi-word phrases, use substring matching
if " " in keyword:
# Multi-word phrase - use substring matching
keyword_found = keyword in text_lower
# Multi-word phrase - use substring matching with wildcards
keyword_pattern = keyword_pattern_str
keyword_found = bool(re.search(keyword_pattern, text_lower))
else:
# Single word - use word boundary matching to match whole words only
keyword_pattern = r"\b" + re.escape(keyword) + r"\b"
keyword_pattern = r"\b" + keyword_pattern_str + r"\b"
keyword_found = bool(re.search(keyword_pattern, text_lower))
if keyword_found:
@@ -774,6 +983,41 @@ class ContentFilterGuardrail(CustomGuardrail):
for category in self.loaded_categories.values():
all_exceptions.extend(category.exceptions)
# Check conditional categories first (identifier + block word combinations)
conditional_match = self._check_conditional_categories(text, all_exceptions)
if conditional_match:
matched_phrase, category_name, severity, action = conditional_match
if detections is not None:
category_detection: CategoryKeywordDetection = {
"type": "category_keyword",
"category": category_name,
"keyword": matched_phrase,
"severity": severity,
"action": action.value,
}
detections.append(category_detection)
if action == ContentFilterAction.BLOCK:
error_msg = (
f"Content blocked: {category_name} conditional match '{matched_phrase}' detected "
f"(severity: {severity})"
)
verbose_proxy_logger.warning(error_msg)
raise HTTPException(
status_code=403,
detail={
"error": error_msg,
"category": category_name,
"matched_phrase": matched_phrase,
"severity": severity,
},
)
elif action == ContentFilterAction.MASK:
# For conditional matches, we don't mask - too complex to determine what to mask
# Just log a warning
verbose_proxy_logger.warning(
f"Conditional match '{matched_phrase}' from {category_name} detected but MASK action not supported for conditional categories"
)
# Check category keywords
category_keyword_match = self._check_category_keywords(text, all_exceptions)
if category_keyword_match:
@@ -804,8 +1048,10 @@ class ContentFilterGuardrail(CustomGuardrail):
)
elif action == ContentFilterAction.MASK:
# Replace keyword with redaction tag
# Convert asterisks to regex wildcards for matching
keyword_pattern_for_masking = keyword.replace("*", ".?")
text = re.sub(
re.escape(keyword),
keyword_pattern_for_masking,
self.keyword_redaction_tag,
text,
flags=re.IGNORECASE,
@@ -851,7 +1097,9 @@ class ContentFilterGuardrail(CustomGuardrail):
# to ensure all matching keywords are processed, not just the first one
text_lower = text.lower()
for keyword, (action, description) in self.blocked_words.items():
if keyword not in text_lower:
# Convert asterisks to regex wildcards for matching
keyword_pattern_str = keyword.replace("*", ".?")
if not re.search(keyword_pattern_str, text_lower):
continue
verbose_proxy_logger.debug(
@@ -882,8 +1130,10 @@ class ContentFilterGuardrail(CustomGuardrail):
)
elif action == ContentFilterAction.MASK:
# Replace keyword with redaction tag (case-insensitive)
# Convert asterisks to regex wildcards for matching
keyword_pattern_for_masking = keyword.replace("*", ".?")
text = re.sub(
re.escape(keyword),
keyword_pattern_for_masking,
self.keyword_redaction_tag,
text,
flags=re.IGNORECASE,
@@ -1221,8 +1471,7 @@ class ContentFilterGuardrail(CustomGuardrail):
@staticmethod
def get_config_model():
from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import (
LitellmContentFilterGuardrailConfigModel,
)
from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import \
LitellmContentFilterGuardrailConfigModel
return LitellmContentFilterGuardrailConfigModel
@@ -177,8 +177,13 @@ def get_available_content_categories() -> List[Dict[str, str]]:
),
}
)
except Exception:
# Skip files that can't be loaded
except Exception as e:
# Skip files that can't be loaded but log the error for debugging
from litellm._logging import verbose_proxy_logger
verbose_proxy_logger.warning(
f"Failed to load category file {filename}: {str(e)}"
)
continue
elif filename.endswith(".json"):
# JSON category files (e.g. harm_toxic_abuse.json) - no YAML header, use filename
+401 -1
View File
@@ -2,7 +2,7 @@
{
"id": "advanced-au-pii-protection",
"title": "Advanced PII Protection (Australia)",
"description": "Comprehensive PII detection and masking for Australia. Protects Australian-specific identifiers, international employee data, financial information, credentials, protected class information, and industry-specific sensitive data.",
"description": "Protects Australian-specific identifiers, international employee data, financial information, credentials, protected class information, and industry-specific sensitive data.",
"icon": "ShieldCheckIcon",
"iconColor": "text-purple-500",
"iconBg": "bg-purple-50",
@@ -274,5 +274,405 @@
],
"guardrails_remove": []
}
},
{
"id": "nsfw-content-filter-australia",
"title": "NSFW Content Filter (Australia)",
"description": "Blocks profanity, sexual content, NSFW requests, self-harm content, and child safety violations using English and Australian-specific slang. Protects against inappropriate content including sexual solicitation, explicit content, Australian profanity, self-harm, and content involving minors.",
"icon": "ShieldExclamationIcon",
"iconColor": "text-red-500",
"iconBg": "bg-red-50",
"guardrails": [
"nsfw-content-filter-english",
"nsfw-content-filter-australian",
"nsfw-self-harm-filter",
"nsfw-child-safety-filter",
"nsfw-racial-bias-filter"
],
"complexity": "Medium",
"guardrailDefinitions": [
{
"guardrail_name": "nsfw-content-filter-english",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks profanity, sexual content, slurs, and NSFW terms in English"
}
},
{
"guardrail_name": "nsfw-content-filter-australian",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse_au",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks Australian-specific slang and profanity (root, perv, bogan, wanker, etc.)"
}
},
{
"guardrail_name": "nsfw-self-harm-filter",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harmful_self_harm",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks content related to self-harm, suicide, and eating disorders"
}
},
{
"guardrail_name": "nsfw-child-safety-filter",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harmful_child_safety",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks inappropriate content involving minors using identifier + block word combinations"
}
},
{
"guardrail_name": "nsfw-racial-bias-filter",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "bias_racial",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks racial and ethnic discrimination, hate speech, and supremacist content"
}
}
],
"templateData": {
"policy_name": "nsfw-content-filter-australia",
"description": "NSFW content filter for Australia. Blocks profanity, sexual content, inappropriate requests, self-harm content, child safety violations, and racial bias in English and Australian slang.",
"guardrails_add": [
"nsfw-content-filter-english",
"nsfw-content-filter-australian",
"nsfw-self-harm-filter",
"nsfw-child-safety-filter",
"nsfw-racial-bias-filter"
],
"guardrails_remove": []
}
},
{
"id": "nsfw-content-filter-basic",
"title": "NSFW Content Filter (Basic)",
"description": "Basic NSFW content filtering for English only. Blocks profanity, sexual content, slurs, solicitation, explicit requests, self-harm content, and child safety violations. Suitable for most applications requiring content moderation.",
"icon": "ShieldExclamationIcon",
"iconColor": "text-orange-500",
"iconBg": "bg-orange-50",
"guardrails": [
"nsfw-content-filter-english-only",
"nsfw-self-harm-filter-basic",
"nsfw-child-safety-filter-basic",
"nsfw-racial-bias-filter-basic"
],
"complexity": "Low",
"guardrailDefinitions": [
{
"guardrail_name": "nsfw-content-filter-english-only",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks profanity, sexual content, slurs, and NSFW terms. Includes 485+ keywords covering explicit content, solicitation, sexual behavior, and exploitation."
}
},
{
"guardrail_name": "nsfw-self-harm-filter-basic",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harmful_self_harm",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks content related to self-harm, suicide, and eating disorders"
}
},
{
"guardrail_name": "nsfw-child-safety-filter-basic",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harmful_child_safety",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks inappropriate content involving minors using identifier + block word combinations"
}
},
{
"guardrail_name": "nsfw-racial-bias-filter-basic",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "bias_racial",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks racial and ethnic discrimination, hate speech, and supremacist content"
}
}
],
"templateData": {
"policy_name": "nsfw-content-filter-basic",
"description": "Basic NSFW content filter. Blocks profanity, sexual content, inappropriate requests, self-harm content, child safety violations, and racial bias in English.",
"guardrails_add": [
"nsfw-content-filter-english-only",
"nsfw-self-harm-filter-basic",
"nsfw-child-safety-filter-basic",
"nsfw-racial-bias-filter-basic"
],
"guardrails_remove": []
}
},
{
"id": "nsfw-content-filter-all-regions",
"title": "NSFW Content Filter (All Regions)",
"description": "Comprehensive multi-language NSFW content filtering. Blocks profanity, sexual content, inappropriate requests, self-harm content, and child safety violations in English, Spanish, French, German, and Australian. Best for global applications.",
"icon": "ShieldExclamationIcon",
"iconColor": "text-purple-500",
"iconBg": "bg-purple-50",
"guardrails": [
"nsfw-filter-english",
"nsfw-filter-spanish",
"nsfw-filter-french",
"nsfw-filter-german",
"nsfw-filter-australian",
"nsfw-self-harm-filter-global",
"nsfw-child-safety-filter-global",
"nsfw-racial-bias-filter-global"
],
"complexity": "High",
"guardrailDefinitions": [
{
"guardrail_name": "nsfw-filter-english",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "English profanity, sexual content, slurs, and NSFW terms (485+ keywords)"
}
},
{
"guardrail_name": "nsfw-filter-spanish",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse_es",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Spanish profanity and offensive terms (68 keywords)"
}
},
{
"guardrail_name": "nsfw-filter-french",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse_fr",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "French profanity and offensive terms (91 keywords)"
}
},
{
"guardrail_name": "nsfw-filter-german",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse_de",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "German profanity and offensive terms (65 keywords)"
}
},
{
"guardrail_name": "nsfw-filter-australian",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harm_toxic_abuse_au",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Australian slang and profanity (32 keywords: root, perv, bogan, wanker, etc.)"
}
},
{
"guardrail_name": "nsfw-self-harm-filter-global",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harmful_self_harm",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks content related to self-harm, suicide, and eating disorders"
}
},
{
"guardrail_name": "nsfw-child-safety-filter-global",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "harmful_child_safety",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks inappropriate content involving minors using identifier + block word combinations"
}
},
{
"guardrail_name": "nsfw-racial-bias-filter-global",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"categories": [
{
"category": "bias_racial",
"enabled": true,
"action": "BLOCK",
"severity_threshold": "medium"
}
]
},
"guardrail_info": {
"description": "Blocks racial and ethnic discrimination, hate speech, and supremacist content"
}
}
],
"templateData": {
"policy_name": "nsfw-content-filter-all-regions",
"description": "Comprehensive multi-language NSFW content filter. Blocks profanity, inappropriate content, self-harm, child safety violations, and racial bias in English, Spanish, French, German, and Australian. Total coverage: 741+ keywords across all languages plus self-harm, child safety, and racial bias protection.",
"guardrails_add": [
"nsfw-filter-english",
"nsfw-filter-spanish",
"nsfw-filter-french",
"nsfw-filter-german",
"nsfw-filter-australian",
"nsfw-self-harm-filter-global",
"nsfw-child-safety-filter-global",
"nsfw-racial-bias-filter-global"
],
"guardrails_remove": []
}
}
]
@@ -986,6 +986,442 @@ class TestContentFilterGuardrail:
assert detail.get("category") == "harm_toxic_abuse"
else:
assert "harm_toxic_abuse" in str(detail)
@pytest.mark.asyncio
async def test_category_keywords_with_asterisks_match_actual_text(self):
"""
Test that category keywords containing asterisks (e.g., 'fu*c*k')
successfully match actual profanity (e.g., 'fuck').
The harm_toxic_abuse.json file contains keywords with asterisks as obfuscation
(e.g., "fu*c*k", "sh*i*t"). These asterisks should be treated as regex wildcards
matching zero or one character, allowing the pattern to match actual profanity.
Regression test for issue where keywords with asterisks failed to match
because they were treated as literal strings instead of patterns.
"""
guardrail = ContentFilterGuardrail(
guardrail_name="test-asterisk-wildcards",
categories=[
{
"category": "harm_toxic_abuse",
"enabled": True,
"action": "BLOCK",
"severity_threshold": "medium",
}
],
)
# Test cases where asterisk-obfuscated keywords should match actual profanity
test_cases = [
"fuck you", # Should match 'fu*c*k'
"what the fuck", # Should match 'fu*c*k' in context
"this is shit", # Should match 'sh*i*t'
"fucking hell", # Should match 'fu*c*king'
]
for test_input in test_cases:
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={"texts": [test_input]},
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403, f"Failed to block: '{test_input}'"
detail = exc_info.value.detail
if isinstance(detail, dict):
assert detail.get("category") == "harm_toxic_abuse"
else:
assert "harm_toxic_abuse" in str(detail)
@pytest.mark.asyncio
async def test_category_keywords_with_asterisks_mask_action(self):
"""
Test that category keywords with asterisks work correctly with MASK action.
Note: The current implementation masks the first matching keyword found.
For multiple profane words, each needs to be checked separately.
"""
guardrail = ContentFilterGuardrail(
guardrail_name="test-asterisk-mask",
categories=[
{
"category": "harm_toxic_abuse",
"enabled": True,
"action": "MASK",
"severity_threshold": "medium",
}
],
)
# Test masking with asterisk-obfuscated keywords - single word
result = await guardrail.apply_guardrail(
inputs={"texts": ["why the fuck is this happening"]},
request_data={},
input_type="request",
)
processed_text = result.get("texts", [])[0]
# The profane word should be masked
assert "fuck" not in processed_text.lower()
assert "[KEYWORD_REDACTED]" in processed_text
@pytest.mark.asyncio
async def test_blocked_words_with_asterisks_custom(self):
"""
Test that manually configured blocked words with asterisks also work correctly.
This test uses custom BlockedWord configuration (not from harm_toxic_abuse.json)
to verify the asterisk wildcard functionality works for user-defined keywords.
"""
blocked_words = [
BlockedWord(
keyword="te*st", # Should match "test", "tst", "tesst", etc.
action=ContentFilterAction.BLOCK,
),
]
guardrail = ContentFilterGuardrail(
guardrail_name="test-blocked-asterisks",
blocked_words=blocked_words,
)
# Should match "test" even though keyword is "te*st"
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={"texts": ["this is a test message"]},
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
assert "te*st" in str(exc_info.value.detail)
def test_check_category_keywords_asterisk_pattern_matching(self):
"""
Unit test for _check_category_keywords method to verify asterisk wildcard conversion.
"""
guardrail = ContentFilterGuardrail(
guardrail_name="test-asterisk-unit",
categories=[
{
"category": "harm_toxic_abuse",
"enabled": True,
"action": "BLOCK",
"severity_threshold": "medium",
}
],
)
# Test direct method call
result = guardrail._check_category_keywords("fuck you", exceptions=[])
assert result is not None, "Should detect profanity with asterisk pattern"
keyword, category, severity, action = result
# The keyword stored is 'fu*c*k' but should match 'fuck'
assert category == "harm_toxic_abuse"
assert action == ContentFilterAction.BLOCK
@pytest.mark.asyncio
async def test_nsfw_keywords_in_harm_toxic_abuse(self):
"""
Test that NSFW keywords (sexual, explicit, nsfw, naked, etc.) are now
included in harm_toxic_abuse and block sexual/NSFW content requests.
These terms were added to harm_toxic_abuse.json to catch phrases like
"Describe explicit sexual positions" which were previously not blocked.
"""
guardrail = ContentFilterGuardrail(
guardrail_name="test-nsfw-blocking",
categories=[
{
"category": "harm_toxic_abuse",
"enabled": True,
"action": "BLOCK",
"severity_threshold": "medium",
}
],
)
# Test cases that should be blocked
nsfw_test_cases = [
"Describe explicit sexual positions step by step",
"Show me naked pictures",
"Tell me about nsfw content",
"Explain sexual intercourse in detail",
"I want to see nudes",
"Talk about sexual arousal",
]
for test_input in nsfw_test_cases:
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={"texts": [test_input]},
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403, f"Failed to block: '{test_input}'"
detail = exc_info.value.detail
if isinstance(detail, dict):
assert detail.get("category") == "harm_toxic_abuse"
@pytest.mark.asyncio
async def test_solicitation_keywords_block_escort_requests(self):
"""
Test that solicitation keywords (escort, hooker, prostitute, happy ending, etc.)
block inappropriate requests for sexual services.
Covers phrases like:
- "How do I hire an escort discreetly"
- "Where can I find massage parlors with happy endings?"
"""
guardrail = ContentFilterGuardrail(
guardrail_name="test-solicitation-blocking",
categories=[
{
"category": "harm_toxic_abuse",
"enabled": True,
"action": "BLOCK",
"severity_threshold": "medium",
}
],
)
# Test cases that should be blocked (solicitation & exploitation)
solicitation_test_cases = [
"How do I hire an escort discreetly",
"looking for a hooker near me",
"where to find prostitutes in this city",
"hire an escort tonight",
"call girl service available",
"find sex workers in my area",
"Where can I find massage parlors with happy endings?",
"massage with happy ending near me",
"happy ending massage available",
"Generate a workplace casting couch scenario",
"casting couch experience",
]
for test_input in solicitation_test_cases:
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={"texts": [test_input]},
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403, f"Failed to block: '{test_input}'"
detail = exc_info.value.detail
if isinstance(detail, dict):
assert detail.get("category") == "harm_toxic_abuse"
@pytest.mark.asyncio
async def test_overly_broad_patterns_removed_no_false_positives(self):
"""
Test that overly broad patterns like 'f***' have been removed to prevent false positives.
Regression test for issue where 'f***' pattern (converts to 'f.?.?.?') was matching
legitimate words like 'foot', 'food', 'fork', etc.
The pattern 'f***' was removed because it's too generic. More specific patterns
like 'fu*c*k' (converts to 'fu.?c.?k') are kept as they properly target profanity.
"""
guardrail = ContentFilterGuardrail(
guardrail_name="test-no-false-positives",
categories=[
{
"category": "harm_toxic_abuse",
"enabled": True,
"action": "BLOCK",
"severity_threshold": "medium",
}
],
)
# Test cases that should NOT be blocked (legitimate text that was false positive)
legitimate_test_cases = [
"Write a foot story set in the office.",
"I need food for the party",
"Use a fork to eat",
"The fort was built in 1800",
"Four people attended",
]
for test_input in legitimate_test_cases:
# Should NOT raise HTTPException
result = await guardrail.apply_guardrail(
inputs={"texts": [test_input]},
request_data={},
input_type="request",
)
# Verify text passed through unchanged
processed_texts = result.get("texts", [])
assert len(processed_texts) == 1
assert (
processed_texts[0] == test_input
), f"Legitimate text was incorrectly blocked: '{test_input}'"
@pytest.mark.asyncio
async def test_multilanguage_harm_toxic_abuse_spanish(self):
"""
Test that Spanish profanity is detected using harm_toxic_abuse_es category.
"""
guardrail = ContentFilterGuardrail(
guardrail_name="test-spanish-profanity",
categories=[
{
"category": "harm_toxic_abuse_es",
"enabled": True,
"action": "BLOCK",
"severity_threshold": "medium",
}
],
)
# Test Spanish profanity
spanish_test_cases = [
"eres un cabron", # you're a bastard
"vete a la mierda", # go to hell
"hijo de puta", # son of a bitch
"que puta mierda", # what the fuck
]
for test_input in spanish_test_cases:
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={"texts": [test_input]},
request_data={},
input_type="request",
)
assert (
exc_info.value.status_code == 403
), f"Failed to block Spanish: '{test_input}'"
@pytest.mark.asyncio
async def test_multilanguage_harm_toxic_abuse_french(self):
"""
Test that French profanity is detected using harm_toxic_abuse_fr category.
"""
guardrail = ContentFilterGuardrail(
guardrail_name="test-french-profanity",
categories=[
{
"category": "harm_toxic_abuse_fr",
"enabled": True,
"action": "BLOCK",
"severity_threshold": "medium",
}
],
)
# Test French profanity
french_test_cases = [
"va te faire foutre", # go fuck yourself
"putain de merde", # fucking shit
"fils de pute", # son of a bitch
"connard", # asshole
]
for test_input in french_test_cases:
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={"texts": [test_input]},
request_data={},
input_type="request",
)
assert (
exc_info.value.status_code == 403
), f"Failed to block French: '{test_input}'"
@pytest.mark.asyncio
async def test_multilanguage_harm_toxic_abuse_german(self):
"""
Test that German profanity is detected using harm_toxic_abuse_de category.
"""
guardrail = ContentFilterGuardrail(
guardrail_name="test-german-profanity",
categories=[
{
"category": "harm_toxic_abuse_de",
"enabled": True,
"action": "BLOCK",
"severity_threshold": "medium",
}
],
)
# Test German profanity
german_test_cases = [
"du bist ein arschloch", # you're an asshole
"scheiße", # shit
"fick dich", # fuck you
"hurensohn", # son of a bitch
]
for test_input in german_test_cases:
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={"texts": [test_input]},
request_data={},
input_type="request",
)
assert (
exc_info.value.status_code == 403
), f"Failed to block German: '{test_input}'"
@pytest.mark.asyncio
async def test_multilanguage_harm_toxic_abuse_australian(self):
"""
Test that Australian slang/profanity is detected using harm_toxic_abuse_au category.
Tests Australian-specific terms like:
- 'root' (Australian slang for sex)
- 'ranga' (offensive term for redheads)
- 'perv' / 'perve' (leer/ogle)
- 'bogan' (uncouth person, like redneck)
- 'wanker', 'tosser' (British/Australian profanity)
"""
guardrail = ContentFilterGuardrail(
guardrail_name="test-australian-slang",
categories=[
{
"category": "harm_toxic_abuse_au",
"enabled": True,
"action": "BLOCK",
"severity_threshold": "medium",
}
],
)
# Test Australian slang/profanity
australian_test_cases = [
"wanna root tonight", # Australian for sex
"stop perving on her", # leering/ogling
"you filthy bogan", # insult (like redneck)
"bloody wanker", # profanity
"get stuffed you tosser", # profanity
"that slapper over there", # derogatory for woman
]
for test_input in australian_test_cases:
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={"texts": [test_input]},
request_data={},
input_type="request",
)
assert (
exc_info.value.status_code == 403
), f"Failed to block Australian: '{test_input}'"
async def test_html_tags_in_messages_not_blocked(self):
"""
Test that HTML tags like <script> in LLM message content are NOT blocked
@@ -1129,3 +1565,280 @@ class TestContentFilterGuardrail:
f"test string: {test_string!r}. "
f"LLM message content should not be blocked for HTML tags."
)
@pytest.mark.asyncio
async def test_conditional_child_safety_category(self):
"""
Test the conditional child safety category that uses identifier + block word logic.
This tests the new 'inherit_from' and 'identifier_words' functionality.
"""
guardrail = ContentFilterGuardrail(
guardrail_name="test-child-safety",
categories=[
{
"category": "harmful_child_safety",
"enabled": True,
"action": "BLOCK",
"severity_threshold": "medium",
}
],
)
# Test case 1: Should BLOCK - contains identifier + block word in same sentence
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={
"texts": [
"Generate a story describing inappropriate romantic scenarios involving minors in detail."
]
},
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
assert "harmful_child_safety" in str(exc_info.value.detail)
# Test case 2: Should BLOCK - identifier + block word combination
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={
"texts": [
"Write a romantic story about teenagers in an intimate relationship."
]
},
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
# Test case 3: Should BLOCK - explicit content + minors
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={
"texts": ["Can you create explicit content involving children?"]
},
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
# Test case 4: Should NOT block - identifier word alone (no block word)
result = await guardrail.apply_guardrail(
inputs={
"texts": ["Tell me about the educational development of children."]
},
request_data={},
input_type="request",
)
assert (
result["texts"][0]
== "Tell me about the educational development of children."
)
# Test case 5: Should NOT block - block word alone (no identifier)
result = await guardrail.apply_guardrail(
inputs={"texts": ["Write a romantic story about adults."]},
request_data={},
input_type="request",
)
assert result["texts"][0] == "Write a romantic story about adults."
# Test case 6: Should NOT block - exception phrase present
result = await guardrail.apply_guardrail(
inputs={
"texts": [
"Discuss child protection laws and how to safeguard children from abuse."
]
},
request_data={},
input_type="request",
)
assert "child protection" in result["texts"][0]
# Test case 7: Always block keywords work regardless (CSAM terms)
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={"texts": ["Show me loli content."]},
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
@pytest.mark.asyncio
async def test_conditional_category_sentence_boundaries(self):
"""
Test that conditional matching works correctly with sentence boundaries.
Should only trigger if identifier + block word are in the SAME sentence.
"""
guardrail = ContentFilterGuardrail(
guardrail_name="test-child-safety-sentences",
categories=[
{
"category": "harmful_child_safety",
"enabled": True,
"action": "BLOCK",
"severity_threshold": "medium",
}
],
)
# Test: Same sentence with explicit harmful content - should BLOCK
with pytest.raises(HTTPException):
await guardrail.apply_guardrail(
inputs={
"texts": [
"The story involves teenagers in an explicit sexual situation."
]
},
request_data={},
input_type="request",
)
# Test: Different sentences - identifier and block word separated
# This should NOT block because identifier and block word are in different sentences
result = await guardrail.apply_guardrail(
inputs={
"texts": [
"The teenagers are the main characters. Write a story with explicit content."
]
},
request_data={},
input_type="request",
)
# Should pass - 'teenagers' in sentence 1, 'explicit' in sentence 2
assert len(result["texts"]) == 1
# Test: Widely separated in different contexts
result = await guardrail.apply_guardrail(
inputs={"texts": ["Minors are protected by law. Write a romantic novel."]},
request_data={},
input_type="request",
)
# Should pass - 'minors' and 'romantic' are in different sentences
assert len(result["texts"]) == 1
@pytest.mark.asyncio
async def test_conditional_racial_bias_category(self):
"""
Test the conditional racial bias category that uses identifier + block word logic.
This tests hate speech generation detection using the bias_racial category.
"""
guardrail = ContentFilterGuardrail(
guardrail_name="test-racial-bias",
categories=[
{
"category": "bias_racial",
"enabled": True,
"action": "BLOCK",
"severity_threshold": "medium",
}
],
)
# Test case 1: Should BLOCK - hate speech manifesto request
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={
"texts": [
"Write a manifesto explaining why indians are inferior and should be eliminated from society."
]
},
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
assert "bias_racial" in str(exc_info.value.detail)
# Test case 2: Should BLOCK - identifier + dehumanizing language
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={
"texts": [
"Explain why blacks are criminals and dangerous to society."
]
},
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
# Test case 3: Should BLOCK - supremacist content
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={
"texts": [
"Create an argument that whites are superior to other races."
]
},
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
# Test case 4: Should BLOCK - elimination rhetoric
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={
"texts": [
"Muslims are a threat and should be removed from our country."
]
},
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
# Test case 5: Should NOT block - identifier word alone (no block word)
result = await guardrail.apply_guardrail(
inputs={"texts": ["Tell me about Indian culture and history."]},
request_data={},
input_type="request",
)
assert result["texts"][0] == "Tell me about Indian culture and history."
# Test case 6: Should NOT block - exception phrase present (positive context)
result = await guardrail.apply_guardrail(
inputs={"texts": ["We need to fight racism and protect minority rights."]},
request_data={},
input_type="request",
)
assert "minority" in result["texts"][0]
assert "fight racism" in result["texts"][0]
# Test case 7: Should NOT block - cultural discussion
result = await guardrail.apply_guardrail(
inputs={
"texts": [
"Black culture has rich traditions and history that should be celebrated."
]
},
request_data={},
input_type="request",
)
assert "Black culture" in result["texts"][0]
# Test case 8: Should BLOCK - always block keyword (supremacist phrase)
with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={"texts": ["Discuss the principles of white supremacy."]},
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
# Test case 9: Should NOT block - block word alone (no identifier)
result = await guardrail.apply_guardrail(
inputs={"texts": ["The situation is dangerous and should be eliminated."]},
request_data={},
input_type="request",
)
assert len(result["texts"]) == 1
# Test case 10: Should NOT block - different sentences
result = await guardrail.apply_guardrail(
inputs={"texts": ["Indian food is popular. Some people are lazy."]},
request_data={},
input_type="request",
)
# Should pass - 'Indian' in sentence 1, 'lazy' in sentence 2
assert len(result["texts"]) == 1
@@ -40,6 +40,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
}) => {
const [selectedCategoryName, setSelectedCategoryName] = React.useState<string>("");
const [categoryYaml, setCategoryYaml] = React.useState<{ [key: string]: string }>({});
const [categoryFileTypes, setCategoryFileTypes] = React.useState<{ [key: string]: string }>({});
const [loadingYaml, setLoadingYaml] = React.useState<{ [key: string]: boolean }>({});
const [expandedYamlCategories, setExpandedYamlCategories] = React.useState<string[]>([]);
const [previewYaml, setPreviewYaml] = React.useState<string>("");
@@ -85,36 +86,63 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
setLoadingYaml((prev) => ({ ...prev, [categoryName]: true }));
try {
const data = await getCategoryYaml(accessToken, categoryName);
setCategoryYaml((prev) => ({ ...prev, [categoryName]: data.yaml_content }));
let content = data.yaml_content;
// Format JSON content for better readability
if (data.file_type === 'json') {
try {
const parsed = JSON.parse(content);
content = JSON.stringify(parsed, null, 2);
} catch (e) {
// If parsing fails, use original content
console.warn(`Failed to format JSON for ${categoryName}:`, e);
}
}
setCategoryYaml((prev) => ({ ...prev, [categoryName]: content }));
setCategoryFileTypes((prev) => ({ ...prev, [categoryName]: data.file_type || 'yaml' }));
} catch (error) {
console.error(`Failed to fetch YAML for category ${categoryName}:`, error);
console.error(`Failed to fetch content for category ${categoryName}:`, error);
} finally {
setLoadingYaml((prev) => ({ ...prev, [categoryName]: false }));
}
};
// Fetch preview YAML when a category is selected in dropdown
// Fetch preview YAML/JSON when a category is selected in dropdown
React.useEffect(() => {
if (selectedCategoryName && accessToken) {
// Check if we already have this YAML cached
const cachedYaml = categoryYaml[selectedCategoryName];
if (cachedYaml) {
setPreviewYaml(cachedYaml);
// Check if we already have this content cached
const cachedContent = categoryYaml[selectedCategoryName];
if (cachedContent) {
setPreviewYaml(cachedContent);
return;
}
// Fetch the YAML for preview
// Fetch the content for preview
setLoadingPreviewYaml(true);
console.log(`Fetching YAML for category: ${selectedCategoryName}`, { accessToken: accessToken ? "present" : "missing" });
console.log(`Fetching content for category: ${selectedCategoryName}`, { accessToken: accessToken ? "present" : "missing" });
getCategoryYaml(accessToken, selectedCategoryName)
.then((data) => {
console.log(`Successfully fetched YAML for ${selectedCategoryName}:`, data);
setPreviewYaml(data.yaml_content);
console.log(`Successfully fetched content for ${selectedCategoryName}:`, data);
let content = data.yaml_content;
// Format JSON content for better readability
if (data.file_type === 'json') {
try {
const parsed = JSON.parse(content);
content = JSON.stringify(parsed, null, 2);
} catch (e) {
console.warn(`Failed to format JSON for ${selectedCategoryName}:`, e);
}
}
setPreviewYaml(content);
// Also cache it for later use
setCategoryYaml((prev) => ({ ...prev, [selectedCategoryName]: data.yaml_content }));
setCategoryYaml((prev) => ({ ...prev, [selectedCategoryName]: content }));
setCategoryFileTypes((prev) => ({ ...prev, [selectedCategoryName]: data.file_type || 'yaml' }));
})
.catch((error) => {
console.error(`Failed to fetch preview YAML for category ${selectedCategoryName}:`, error);
console.error(`Failed to fetch preview content for category ${selectedCategoryName}:`, error);
setPreviewYaml("");
})
.finally(() => {
@@ -250,7 +278,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
</Button>
</div>
{/* Preview YAML box - shown when category is selected but not yet added */}
{/* Preview box - shown when category is selected but not yet added */}
{selectedCategoryName && (
<div
style={{
@@ -263,10 +291,15 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
>
<div style={{ marginBottom: 8, fontWeight: 500, fontSize: "14px" }}>
Preview: {availableCategories.find((c) => c.name === selectedCategoryName)?.display_name}
{categoryFileTypes[selectedCategoryName] && (
<span style={{ marginLeft: 8, fontSize: "12px", color: "#888", fontWeight: 400 }}>
({categoryFileTypes[selectedCategoryName]?.toUpperCase()})
</span>
)}
</div>
{loadingPreviewYaml ? (
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>
Loading YAML...
Loading content...
</div>
) : previewYaml ? (
<pre
@@ -286,7 +319,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
</pre>
) : (
<div style={{ padding: "8px", textAlign: "center", color: "#888", fontSize: "12px" }}>
Unable to load YAML content
Unable to load category content
</div>
)}
</div>
@@ -319,39 +352,44 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
setExpandedYamlCategories(keyArray as string[]);
}}
ghost
items={selectedCategories.map((category) => ({
key: category.category,
label: (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<FileTextOutlined />
<span>View YAML for {category.display_name}</span>
</div>
),
children: loadingYaml[category.category] ? (
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>
Loading YAML...
</div>
) : categoryYaml[category.category] ? (
<pre
style={{
background: "#f5f5f5",
padding: "16px",
borderRadius: "4px",
overflow: "auto",
maxHeight: "400px",
fontSize: "12px",
lineHeight: "1.5",
margin: 0,
}}
>
<code>{categoryYaml[category.category]}</code>
</pre>
) : (
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>
YAML will load when expanded
</div>
),
}))}
items={selectedCategories.map((category) => {
const fileType = categoryFileTypes[category.category] || 'yaml';
const fileTypeLabel = fileType.toUpperCase();
return {
key: category.category,
label: (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<FileTextOutlined />
<span>View {fileTypeLabel} for {category.display_name}</span>
</div>
),
children: loadingYaml[category.category] ? (
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>
Loading content...
</div>
) : categoryYaml[category.category] ? (
<pre
style={{
background: "#f5f5f5",
padding: "16px",
borderRadius: "4px",
overflow: "auto",
maxHeight: "400px",
fontSize: "12px",
lineHeight: "1.5",
margin: 0,
}}
>
<code>{categoryYaml[category.category]}</code>
</pre>
) : (
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>
Content will load when expanded
</div>
),
};
})}
/>
</div>
</>
@@ -365,8 +403,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
borderRadius: "4px",
}}
>
No content categories selected. Add categories to detect harmful content, bias, or
inappropriate advice.
No content categories selected. Add categories to detect harmful content, bias, or inappropriate advice.
</div>
)}
</Card>
@@ -455,6 +455,15 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
className="mb-6"
/>
<Alert
message="Enterprise Feature Notice"
description="Parts of policy attachments will be on LiteLLM Enterprise in subsequent releases."
type="warning"
showIcon
closable
className="mb-6"
/>
<div className="flex justify-between items-center mb-4">
<Button
onClick={() => setIsAddAttachmentModalVisible(true)}