mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-25 08:19:03 +00:00
Litellm dev 07 05 2025 p3 (#12349)
* refactor(aim.py): refactor to support adding aim guardrails on UI * fix(base.py): add ui_friendly_name to config model * feat(ui/): support loading new guardrails from backend api call removes need to onboard each guardrail to ui * fix: don't show optional params if not set and don't show ui_friendly_name (internal param0 * fix(ui/add_guardrail_form.tsx): ensure dynamic provider value is used * fix(ui/): just one-time update the provider map dictionary * fix(ui/): show masked api base / api key on guardrail update * refactor(aporia_ai/): refactor to show on UI * feat(aporia_ai/): add aporia ai guardrail to UI * refactor(guardrails_ai/): refactor to add via UI * refactor(lasso.py): refactor to enable adding lasso guardrails via UI * feat(pangea.py): add pangea guardrail on UI * feat(panw): add panw prisma airs through UI * test: update tests * fix: fix ruff linting error * test: update tests * fix: add missing docs * fix: fix guardrail init * fix: suppress linting errors * fix(proxy_server.py): fix linting error
This commit is contained in:
@@ -484,6 +484,7 @@ router_settings:
|
||||
| GOOGLE_CLIENT_ID | Client ID for Google OAuth
|
||||
| GOOGLE_CLIENT_SECRET | Client secret for Google OAuth
|
||||
| GOOGLE_KMS_RESOURCE_NAME | Name of the resource in Google KMS
|
||||
| GUARDRAILS_AI_API_BASE | Base URL for Guardrails AI API
|
||||
| HEALTH_CHECK_TIMEOUT_SECONDS | Timeout in seconds for health checks. Default is 60
|
||||
| HF_API_BASE | Base URL for Hugging Face API
|
||||
| HCP_VAULT_ADDR | Address for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault)
|
||||
@@ -522,6 +523,10 @@ router_settings:
|
||||
| LANGSMITH_PROJECT | Project name for Langsmith integration
|
||||
| LANGSMITH_SAMPLING_RATE | Sampling rate for Langsmith logging
|
||||
| LANGTRACE_API_KEY | API key for Langtrace service
|
||||
| LASSO_API_BASE | Base URL for Lasso API
|
||||
| LASSO_API_KEY | API key for Lasso service
|
||||
| LASSO_USER_ID | User ID for Lasso service
|
||||
| LASSO_CONVERSATION_ID | Conversation ID for Lasso service
|
||||
| LENGTH_OF_LITELLM_GENERATED_KEY | Length of keys generated by LiteLLM. Default is 16
|
||||
| LITERAL_API_KEY | API key for Literal integration
|
||||
| LITERAL_API_URL | API URL for Literal service
|
||||
@@ -538,6 +543,7 @@ router_settings:
|
||||
| LITELLM_LICENSE | License key for LiteLLM usage
|
||||
| LITELLM_LOCAL_MODEL_COST_MAP | Local configuration for model cost mapping in LiteLLM
|
||||
| LITELLM_LOG | Enable detailed logging for LiteLLM
|
||||
| LITELLM_MASTER_KEY | Master key for proxy authentication
|
||||
| LITELLM_MODE | Operating mode for LiteLLM (e.g., production, development)
|
||||
| LITELLM_RATE_LIMIT_WINDOW_SIZE | Rate limit window size for LiteLLM. Default is 60
|
||||
| LITELLM_SALT_KEY | Salt key for encryption in LiteLLM
|
||||
@@ -595,6 +601,8 @@ router_settings:
|
||||
| OTEL_SERVICE_NAME | Service name identifier for OpenTelemetry
|
||||
| OTEL_TRACER_NAME | Tracer name for OpenTelemetry tracing
|
||||
| PAGERDUTY_API_KEY | API key for PagerDuty Alerting
|
||||
| PANW_PRISMA_AIRS_API_KEY | API key for PANW Prisma AIRS service
|
||||
| PANW_PRISMA_AIRS_API_BASE | Base URL for PANW Prisma AIRS service
|
||||
| PHOENIX_API_KEY | API key for Arize Phoenix
|
||||
| PHOENIX_COLLECTOR_ENDPOINT | API endpoint for Arize Phoenix
|
||||
| PHOENIX_COLLECTOR_HTTP_ENDPOINT | API http endpoint for Arize Phoenix
|
||||
@@ -612,7 +620,6 @@ router_settings:
|
||||
| PROXY_BUDGET_RESCHEDULER_MAX_TIME | Maximum time in seconds to wait before checking database for budget resets. Default is 605
|
||||
| PROXY_BUDGET_RESCHEDULER_MIN_TIME | Minimum time in seconds to wait before checking database for budget resets. Default is 597
|
||||
| PROXY_LOGOUT_URL | URL for logging out of the proxy service
|
||||
| LITELLM_MASTER_KEY | Master key for proxy authentication
|
||||
| QDRANT_API_BASE | Base URL for Qdrant API
|
||||
| QDRANT_API_KEY | API key for Qdrant service
|
||||
| QDRANT_SCALAR_QUANTILE | Scalar quantile for Qdrant operations. Default is 0.99
|
||||
|
||||
@@ -123,7 +123,7 @@ def initialize_callbacks_on_proxy( # noqa: PLR0915
|
||||
lakera_moderations_object = lakeraAI_Moderation(**init_params)
|
||||
imported_list.append(lakera_moderations_object)
|
||||
elif isinstance(callback, str) and callback == "aporia_prompt_injection":
|
||||
from litellm.proxy.guardrails.guardrail_hooks.aporia_ai import (
|
||||
from litellm.proxy.guardrails.guardrail_hooks.aporia_ai.aporia_ai import (
|
||||
AporiaGuardrail,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
CRUD ENDPOINTS FOR GUARDRAILS
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Type, Union, cast
|
||||
import inspect
|
||||
from typing import Any, Dict, List, Optional, Type, TypeVar, Union, cast
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
@@ -755,115 +756,135 @@ def _get_list_element_options(field_annotation: Any) -> Optional[List[str]]:
|
||||
return None
|
||||
|
||||
|
||||
def _extract_fields_recursive(
|
||||
model: Type[BaseModel],
|
||||
depth: int = 0,
|
||||
) -> Dict[str, Any]:
|
||||
|
||||
# Check if we've exceeded the maximum recursion depth
|
||||
if depth > DEFAULT_MAX_RECURSE_DEPTH:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Max depth of {DEFAULT_MAX_RECURSE_DEPTH} exceeded while processing model fields. Please check the model structure for excessive nesting.",
|
||||
)
|
||||
|
||||
fields = {}
|
||||
|
||||
for field_name, field in model.model_fields.items():
|
||||
# Skip optional_params if it's not meaningfully overridden
|
||||
if field_name == "optional_params":
|
||||
field_annotation = field.annotation
|
||||
if field_annotation is None:
|
||||
continue
|
||||
# Check if the annotation is still a generic TypeVar (not specialized)
|
||||
if isinstance(field_annotation, TypeVar) or (
|
||||
hasattr(field_annotation, "__origin__")
|
||||
and field_annotation.__origin__ is TypeVar
|
||||
):
|
||||
# Skip this field as it's not meaningfully overridden
|
||||
continue
|
||||
# Also skip if it's a generic type that wasn't specialized
|
||||
if hasattr(field_annotation, "__name__") and field_annotation.__name__ in (
|
||||
"T",
|
||||
"TypeVar",
|
||||
):
|
||||
continue
|
||||
|
||||
# Get field metadata
|
||||
description = field.description or field_name
|
||||
|
||||
# Check if this field is required
|
||||
required = field.is_required()
|
||||
|
||||
# Check if the field annotation is a BaseModel subclass
|
||||
field_annotation = field.annotation
|
||||
|
||||
# Handle Optional types and get the actual type
|
||||
if field_annotation is None:
|
||||
continue
|
||||
|
||||
if (
|
||||
hasattr(field_annotation, "__origin__")
|
||||
and field_annotation.__origin__ is Union
|
||||
and hasattr(field_annotation, "__args__")
|
||||
):
|
||||
# For Optional[BaseModel], get the non-None type
|
||||
args = field_annotation.__args__
|
||||
non_none_args = [arg for arg in args if arg is not type(None)]
|
||||
if non_none_args:
|
||||
field_annotation = non_none_args[0]
|
||||
|
||||
# Check if this is a BaseModel subclass
|
||||
is_basemodel_subclass = (
|
||||
inspect.isclass(field_annotation)
|
||||
and issubclass(field_annotation, BaseModel)
|
||||
and field_annotation is not BaseModel
|
||||
)
|
||||
|
||||
if is_basemodel_subclass:
|
||||
# Recursively get fields from the nested model
|
||||
nested_fields = _extract_fields_recursive(
|
||||
cast(Type[BaseModel], field_annotation), depth + 1
|
||||
)
|
||||
fields[field_name] = {
|
||||
"description": description,
|
||||
"required": required,
|
||||
"type": "nested",
|
||||
"fields": nested_fields,
|
||||
}
|
||||
else:
|
||||
# Determine the field type from annotation
|
||||
field_type = _get_field_type_from_annotation(field_annotation)
|
||||
|
||||
# Check for custom UI type override
|
||||
field_json_schema_extra = getattr(field, "json_schema_extra", {})
|
||||
if field_json_schema_extra and "ui_type" in field_json_schema_extra:
|
||||
field_type = field_json_schema_extra["ui_type"].value
|
||||
elif field_json_schema_extra and "type" in field_json_schema_extra:
|
||||
field_type = field_json_schema_extra["type"]
|
||||
|
||||
# Add the field to the dictionary
|
||||
field_dict = {
|
||||
"description": description,
|
||||
"required": required,
|
||||
"type": field_type,
|
||||
}
|
||||
|
||||
# Extract options from type annotations
|
||||
if field_type == "dict":
|
||||
# For Dict[Literal[...], T] types, extract key options
|
||||
dict_key_options = _get_dict_key_options(field_annotation)
|
||||
if dict_key_options:
|
||||
field_dict["dict_key_options"] = dict_key_options
|
||||
|
||||
# Extract value type for the dict values
|
||||
dict_value_type = _get_dict_value_type(field_annotation)
|
||||
field_dict["dict_value_type"] = dict_value_type
|
||||
|
||||
elif field_type == "array":
|
||||
# For List[Literal[...]] types, extract element options
|
||||
list_element_options = _get_list_element_options(field_annotation)
|
||||
if list_element_options:
|
||||
field_dict["options"] = list_element_options
|
||||
field_dict["type"] = "multiselect"
|
||||
|
||||
# Add options if they exist in json_schema_extra (this takes precedence)
|
||||
if field_json_schema_extra and "options" in field_json_schema_extra:
|
||||
field_dict["options"] = field_json_schema_extra["options"]
|
||||
|
||||
# Add default value if it exists
|
||||
if field.default is not None and field.default is not ...:
|
||||
field_dict["default_value"] = field.default
|
||||
|
||||
fields[field_name] = field_dict
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
def _get_fields_from_model(model_class: Type[BaseModel]) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the fields from a Pydantic model as a nested dictionary structure
|
||||
"""
|
||||
import inspect
|
||||
|
||||
def _extract_fields_recursive(
|
||||
model: Type[BaseModel],
|
||||
depth: int = 0,
|
||||
) -> Dict[str, Any]:
|
||||
# Check if we've exceeded the maximum recursion depth
|
||||
if depth > DEFAULT_MAX_RECURSE_DEPTH:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Max depth of {DEFAULT_MAX_RECURSE_DEPTH} exceeded while processing model fields. Please check the model structure for excessive nesting.",
|
||||
)
|
||||
|
||||
fields = {}
|
||||
|
||||
for field_name, field in model.model_fields.items():
|
||||
# Get field metadata
|
||||
description = field.description or field_name
|
||||
|
||||
# Check if this field is required
|
||||
required = field.is_required()
|
||||
|
||||
# Check if the field annotation is a BaseModel subclass
|
||||
field_annotation = field.annotation
|
||||
|
||||
# Handle Optional types and get the actual type
|
||||
if field_annotation is None:
|
||||
continue
|
||||
|
||||
if (
|
||||
hasattr(field_annotation, "__origin__")
|
||||
and field_annotation.__origin__ is Union
|
||||
and hasattr(field_annotation, "__args__")
|
||||
):
|
||||
# For Optional[BaseModel], get the non-None type
|
||||
args = field_annotation.__args__
|
||||
non_none_args = [arg for arg in args if arg is not type(None)]
|
||||
if non_none_args:
|
||||
field_annotation = non_none_args[0]
|
||||
|
||||
# Check if this is a BaseModel subclass
|
||||
is_basemodel_subclass = (
|
||||
inspect.isclass(field_annotation)
|
||||
and issubclass(field_annotation, BaseModel)
|
||||
and field_annotation is not BaseModel
|
||||
)
|
||||
|
||||
if is_basemodel_subclass:
|
||||
# Recursively get fields from the nested model
|
||||
nested_fields = _extract_fields_recursive(
|
||||
cast(Type[BaseModel], field_annotation), depth + 1
|
||||
)
|
||||
fields[field_name] = {
|
||||
"description": description,
|
||||
"required": required,
|
||||
"type": "nested",
|
||||
"fields": nested_fields,
|
||||
}
|
||||
else:
|
||||
# Determine the field type from annotation
|
||||
field_type = _get_field_type_from_annotation(field_annotation)
|
||||
|
||||
# Check for custom UI type override
|
||||
field_json_schema_extra = getattr(field, "json_schema_extra", {})
|
||||
if field_json_schema_extra and "ui_type" in field_json_schema_extra:
|
||||
field_type = field_json_schema_extra["ui_type"].value
|
||||
elif field_json_schema_extra and "type" in field_json_schema_extra:
|
||||
field_type = field_json_schema_extra["type"]
|
||||
|
||||
# Add the field to the dictionary
|
||||
field_dict = {
|
||||
"description": description,
|
||||
"required": required,
|
||||
"type": field_type,
|
||||
}
|
||||
|
||||
# Extract options from type annotations
|
||||
if field_type == "dict":
|
||||
# For Dict[Literal[...], T] types, extract key options
|
||||
dict_key_options = _get_dict_key_options(field_annotation)
|
||||
if dict_key_options:
|
||||
field_dict["dict_key_options"] = dict_key_options
|
||||
|
||||
# Extract value type for the dict values
|
||||
dict_value_type = _get_dict_value_type(field_annotation)
|
||||
field_dict["dict_value_type"] = dict_value_type
|
||||
|
||||
elif field_type == "array":
|
||||
# For List[Literal[...]] types, extract element options
|
||||
list_element_options = _get_list_element_options(field_annotation)
|
||||
if list_element_options:
|
||||
field_dict["options"] = list_element_options
|
||||
field_dict["type"] = "multiselect"
|
||||
|
||||
# Add options if they exist in json_schema_extra (this takes precedence)
|
||||
if field_json_schema_extra and "options" in field_json_schema_extra:
|
||||
field_dict["options"] = field_json_schema_extra["options"]
|
||||
|
||||
# Add default value if it exists
|
||||
if field.default is not None and field.default is not ...:
|
||||
field_dict["default_value"] = field.default
|
||||
|
||||
fields[field_name] = field_dict
|
||||
|
||||
return fields
|
||||
|
||||
return _extract_fields_recursive(model_class, depth=0)
|
||||
|
||||
@@ -944,6 +965,8 @@ async def get_provider_specific_params():
|
||||
|
||||
if guardrail_config_model:
|
||||
fields = _get_fields_from_model(guardrail_config_model)
|
||||
ui_friendly_name = guardrail_config_model.ui_friendly_name()
|
||||
fields["ui_friendly_name"] = ui_friendly_name
|
||||
provider_params[guardrail_name] = fields
|
||||
|
||||
return provider_params
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from litellm.types.guardrails import SupportedGuardrailIntegrations
|
||||
|
||||
from .aim import AimGuardrail
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.guardrails import Guardrail, LitellmParams
|
||||
|
||||
|
||||
def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"):
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.guardrail_hooks.aim import AimGuardrail
|
||||
|
||||
_aim_callback = AimGuardrail(
|
||||
api_base=litellm_params.api_base,
|
||||
api_key=litellm_params.api_key,
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
event_hook=litellm_params.mode,
|
||||
default_on=litellm_params.default_on,
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_aim_callback)
|
||||
|
||||
return _aim_callback
|
||||
|
||||
|
||||
guardrail_initializer_registry = {
|
||||
SupportedGuardrailIntegrations.AIM.value: initialize_guardrail,
|
||||
}
|
||||
|
||||
|
||||
guardrail_class_registry = {
|
||||
SupportedGuardrailIntegrations.AIM.value: AimGuardrail,
|
||||
}
|
||||
+14
-2
@@ -7,7 +7,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from typing import Any, AsyncGenerator, Literal, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, AsyncGenerator, Literal, Optional, Type, Union
|
||||
|
||||
from fastapi import HTTPException
|
||||
from pydantic import BaseModel
|
||||
@@ -22,7 +22,6 @@ from litellm.llms.custom_httpx.http_handler import (
|
||||
httpxSpecialProvider,
|
||||
)
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.proxy_server import StreamingCallbackError
|
||||
from litellm.types.utils import (
|
||||
Choices,
|
||||
EmbeddingResponse,
|
||||
@@ -31,6 +30,9 @@ from litellm.types.utils import (
|
||||
ModelResponseStream,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel
|
||||
|
||||
|
||||
class AimGuardrailMissingSecrets(Exception):
|
||||
pass
|
||||
@@ -313,6 +315,8 @@ class AimGuardrail(CustomGuardrail):
|
||||
if result.get("done"):
|
||||
return
|
||||
if blocking_message := result.get("blocking_message"):
|
||||
from litellm.proxy.proxy_server import StreamingCallbackError
|
||||
|
||||
raise StreamingCallbackError(blocking_message)
|
||||
verbose_proxy_logger.error(
|
||||
f"Unknown message received from AIM: {result}"
|
||||
@@ -334,3 +338,11 @@ class AimGuardrail(CustomGuardrail):
|
||||
|
||||
def _set_dlp_entities(self, entities: list[dict]) -> None:
|
||||
self.dlp_entities = entities[: self._max_dlp_entities]
|
||||
|
||||
@staticmethod
|
||||
def get_config_model() -> Optional[Type["GuardrailConfigModel"]]:
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.aim import (
|
||||
AimGuardrailConfigModel,
|
||||
)
|
||||
|
||||
return AimGuardrailConfigModel
|
||||
@@ -0,0 +1,33 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from litellm.types.guardrails import SupportedGuardrailIntegrations
|
||||
|
||||
from .aporia_ai import AporiaGuardrail
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.guardrails import Guardrail, LitellmParams
|
||||
|
||||
|
||||
def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"):
|
||||
import litellm
|
||||
|
||||
_aporia_callback = AporiaGuardrail(
|
||||
api_base=litellm_params.api_base,
|
||||
api_key=litellm_params.api_key,
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
event_hook=litellm_params.mode,
|
||||
default_on=litellm_params.default_on,
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_aporia_callback)
|
||||
|
||||
return _aporia_callback
|
||||
|
||||
|
||||
guardrail_initializer_registry = {
|
||||
SupportedGuardrailIntegrations.APORIA.value: initialize_guardrail,
|
||||
}
|
||||
|
||||
|
||||
guardrail_class_registry = {
|
||||
SupportedGuardrailIntegrations.APORIA.value: AporiaGuardrail,
|
||||
}
|
||||
+16
-2
@@ -13,7 +13,7 @@ sys.path.insert(
|
||||
) # Adds the parent directory to the system path
|
||||
import json
|
||||
import sys
|
||||
from typing import Any, List, Literal, Optional
|
||||
from typing import TYPE_CHECKING, Any, List, Literal, Optional, Type
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
@@ -31,13 +31,15 @@ from litellm.llms.custom_httpx.http_handler import (
|
||||
httpxSpecialProvider,
|
||||
)
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.guardrails.guardrail_helpers import should_proceed_based_on_metadata
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
|
||||
litellm.set_verbose = True
|
||||
|
||||
GUARDRAIL_NAME = "aporia"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel
|
||||
|
||||
|
||||
class AporiaGuardrail(CustomGuardrail):
|
||||
def __init__(
|
||||
@@ -194,12 +196,16 @@ class AporiaGuardrail(CustomGuardrail):
|
||||
from litellm.proxy.common_utils.callback_utils import (
|
||||
add_guardrail_to_applied_guardrails_header,
|
||||
)
|
||||
from litellm.proxy.guardrails.guardrail_helpers import (
|
||||
should_proceed_based_on_metadata,
|
||||
)
|
||||
|
||||
event_type: GuardrailEventHooks = GuardrailEventHooks.during_call
|
||||
if self.should_run_guardrail(data=data, event_type=event_type) is not True:
|
||||
return
|
||||
|
||||
# old implementation - backwards compatibility
|
||||
|
||||
if (
|
||||
await should_proceed_based_on_metadata(
|
||||
data=data,
|
||||
@@ -226,3 +232,11 @@ class AporiaGuardrail(CustomGuardrail):
|
||||
"Aporia AI: not running guardrail. No messages in data"
|
||||
)
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_config_model() -> Optional[Type["GuardrailConfigModel"]]:
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.aporia_ai import (
|
||||
AporiaGuardrailConfigModel,
|
||||
)
|
||||
|
||||
return AporiaGuardrailConfigModel
|
||||
@@ -0,0 +1,39 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from litellm.types.guardrails import SupportedGuardrailIntegrations
|
||||
|
||||
from .guardrails_ai import GuardrailsAI
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.guardrails import Guardrail, LitellmParams
|
||||
|
||||
|
||||
def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"):
|
||||
import litellm
|
||||
|
||||
if litellm_params.guard_name is None:
|
||||
raise Exception(
|
||||
"GuardrailsAIException - Please pass the Guardrails AI guard name via 'litellm_params::guard_name'"
|
||||
)
|
||||
|
||||
_guardrails_ai_callback = GuardrailsAI(
|
||||
api_base=litellm_params.api_base,
|
||||
api_key=litellm_params.api_key,
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
event_hook=litellm_params.mode,
|
||||
default_on=litellm_params.default_on,
|
||||
guard_name=litellm_params.guard_name,
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_guardrails_ai_callback)
|
||||
|
||||
return _guardrails_ai_callback
|
||||
|
||||
|
||||
guardrail_initializer_registry = {
|
||||
SupportedGuardrailIntegrations.GUARDRAILS_AI.value: initialize_guardrail,
|
||||
}
|
||||
|
||||
|
||||
guardrail_class_registry = {
|
||||
SupportedGuardrailIntegrations.GUARDRAILS_AI.value: GuardrailsAI,
|
||||
}
|
||||
+20
-5
@@ -6,7 +6,8 @@
|
||||
# Thank you for using Litellm! - Krrish & Ishaan
|
||||
|
||||
import json
|
||||
from typing import Optional, TypedDict
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional, Type, TypedDict
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
@@ -20,11 +21,11 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
get_content_from_model_response,
|
||||
)
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.common_utils.callback_utils import (
|
||||
add_guardrail_to_applied_guardrails_header,
|
||||
)
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel
|
||||
|
||||
|
||||
class GuardrailsAIResponse(TypedDict):
|
||||
callId: str
|
||||
@@ -45,7 +46,9 @@ class GuardrailsAI(CustomGuardrail):
|
||||
"GuardrailsAIException - Please pass the Guardrails AI guard name via 'litellm_params::guard_name'"
|
||||
)
|
||||
# store kwargs as optional_params
|
||||
self.guardrails_ai_api_base = api_base or "http://0.0.0.0:8000"
|
||||
self.guardrails_ai_api_base = (
|
||||
api_base or os.getenv("GUARDRAILS_AI_API_BASE") or "http://0.0.0.0:8000"
|
||||
)
|
||||
self.guardrails_ai_guard_name = guard_name
|
||||
self.optional_params = kwargs
|
||||
supported_event_hooks = [GuardrailEventHooks.post_call]
|
||||
@@ -94,6 +97,10 @@ class GuardrailsAI(CustomGuardrail):
|
||||
|
||||
It can be used to reject a response
|
||||
"""
|
||||
from litellm.proxy.common_utils.callback_utils import (
|
||||
add_guardrail_to_applied_guardrails_header,
|
||||
)
|
||||
|
||||
event_type: GuardrailEventHooks = GuardrailEventHooks.post_call
|
||||
if self.should_run_guardrail(data=data, event_type=event_type) is not True:
|
||||
return
|
||||
@@ -112,3 +119,11 @@ class GuardrailsAI(CustomGuardrail):
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def get_config_model() -> Optional[Type["GuardrailConfigModel"]]:
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.guardrails_ai import (
|
||||
GuardrailsAIGuardrailConfigModel,
|
||||
)
|
||||
|
||||
return GuardrailsAIGuardrailConfigModel
|
||||
@@ -0,0 +1,35 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from litellm.types.guardrails import SupportedGuardrailIntegrations
|
||||
|
||||
from .lasso import LassoGuardrail
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.guardrails import Guardrail, LitellmParams
|
||||
|
||||
|
||||
def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"):
|
||||
import litellm
|
||||
|
||||
_lasso_callback = LassoGuardrail(
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
api_key=litellm_params.api_key,
|
||||
api_base=litellm_params.api_base,
|
||||
user_id=litellm_params.lasso_user_id,
|
||||
conversation_id=litellm_params.lasso_conversation_id,
|
||||
event_hook=litellm_params.mode,
|
||||
default_on=litellm_params.default_on,
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_lasso_callback)
|
||||
|
||||
return _lasso_callback
|
||||
|
||||
|
||||
guardrail_initializer_registry = {
|
||||
SupportedGuardrailIntegrations.LASSO.value: initialize_guardrail,
|
||||
}
|
||||
|
||||
|
||||
guardrail_class_registry = {
|
||||
SupportedGuardrailIntegrations.LASSO.value: LassoGuardrail,
|
||||
}
|
||||
+18
-4
@@ -6,7 +6,7 @@
|
||||
# +-------------------------------------------------------------+
|
||||
|
||||
import os
|
||||
from typing import Any, Dict, List, Literal, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Type, Union
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
@@ -22,6 +22,9 @@ from litellm.llms.custom_httpx.http_handler import (
|
||||
)
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel
|
||||
|
||||
|
||||
class LassoGuardrailMissingSecrets(Exception):
|
||||
pass
|
||||
@@ -37,6 +40,7 @@ class LassoGuardrail(CustomGuardrail):
|
||||
def __init__(
|
||||
self,
|
||||
lasso_api_key: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
api_base: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
conversation_id: Optional[str] = None,
|
||||
@@ -45,7 +49,7 @@ class LassoGuardrail(CustomGuardrail):
|
||||
self.async_handler = get_async_httpx_client(
|
||||
llm_provider=httpxSpecialProvider.GuardrailCallback
|
||||
)
|
||||
self.lasso_api_key = lasso_api_key or os.environ.get("LASSO_API_KEY")
|
||||
self.lasso_api_key = lasso_api_key or api_key or os.environ.get("LASSO_API_KEY")
|
||||
self.user_id = user_id or os.environ.get("LASSO_USER_ID")
|
||||
self.conversation_id = conversation_id or os.environ.get(
|
||||
"LASSO_CONVERSATION_ID"
|
||||
@@ -58,7 +62,9 @@ class LassoGuardrail(CustomGuardrail):
|
||||
)
|
||||
raise LassoGuardrailMissingSecrets(msg)
|
||||
|
||||
self.api_base = api_base or "https://server.lasso.security/gateway/v2/classify"
|
||||
self.api_base = (
|
||||
api_base or os.getenv("LASSO_API_BASE") or "https://server.lasso.security"
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@log_guardrail_information
|
||||
@@ -169,7 +175,7 @@ class LassoGuardrail(CustomGuardrail):
|
||||
"""Call the Lasso API and return the response."""
|
||||
verbose_proxy_logger.debug(f"Sending request to Lasso API: {payload}")
|
||||
response = await self.async_handler.post(
|
||||
url=self.api_base,
|
||||
url=f"{self.api_base}/gateway/v2/classify",
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=10.0,
|
||||
@@ -203,3 +209,11 @@ class LassoGuardrail(CustomGuardrail):
|
||||
if is_violated:
|
||||
violated_deputies.append(deputy)
|
||||
return violated_deputies
|
||||
|
||||
@staticmethod
|
||||
def get_config_model() -> Optional[Type["GuardrailConfigModel"]]:
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.lasso import (
|
||||
LassoGuardrailConfigModel,
|
||||
)
|
||||
|
||||
return LassoGuardrailConfigModel
|
||||
@@ -0,0 +1,38 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from litellm.types.guardrails import SupportedGuardrailIntegrations
|
||||
|
||||
from .pangea import PangeaHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.guardrails import Guardrail, LitellmParams
|
||||
|
||||
|
||||
def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"):
|
||||
import litellm
|
||||
|
||||
guardrail_name = guardrail.get("guardrail_name")
|
||||
if not guardrail_name:
|
||||
raise ValueError("Pangea guardrail name is required")
|
||||
|
||||
_pangea_callback = PangeaHandler(
|
||||
guardrail_name=guardrail_name,
|
||||
pangea_input_recipe=litellm_params.pangea_input_recipe,
|
||||
pangea_output_recipe=litellm_params.pangea_output_recipe,
|
||||
api_base=litellm_params.api_base,
|
||||
api_key=litellm_params.api_key,
|
||||
default_on=litellm_params.default_on,
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_pangea_callback)
|
||||
|
||||
return _pangea_callback
|
||||
|
||||
|
||||
guardrail_initializer_registry = {
|
||||
SupportedGuardrailIntegrations.PANGEA.value: initialize_guardrail,
|
||||
}
|
||||
|
||||
|
||||
guardrail_class_registry = {
|
||||
SupportedGuardrailIntegrations.PANGEA.value: PangeaHandler,
|
||||
}
|
||||
+14
-4
@@ -1,6 +1,6 @@
|
||||
# litellm/proxy/guardrails/guardrail_hooks/pangea.py
|
||||
import os
|
||||
from typing import Any, Optional, Protocol
|
||||
from typing import TYPE_CHECKING, Any, Optional, Protocol, Type
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
@@ -10,7 +10,6 @@ from litellm.integrations.custom_guardrail import (
|
||||
CustomGuardrail,
|
||||
log_guardrail_information,
|
||||
)
|
||||
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
get_async_httpx_client,
|
||||
httpxSpecialProvider,
|
||||
@@ -22,6 +21,9 @@ from litellm.proxy.common_utils.callback_utils import (
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
from litellm.types.utils import LLMResponseTypes, ModelResponse, TextCompletionResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel
|
||||
|
||||
|
||||
class PangeaGuardrailMissingSecrets(Exception):
|
||||
"""Custom exception for missing Pangea secrets."""
|
||||
@@ -30,10 +32,10 @@ class PangeaGuardrailMissingSecrets(Exception):
|
||||
|
||||
|
||||
class _Transformer(Protocol):
|
||||
def get_messages(self) -> list[dict]:
|
||||
def get_messages(self) -> list[dict]: # noqa: E704
|
||||
...
|
||||
|
||||
def update_original_body(self, prompt_messages: list[dict]) -> Any:
|
||||
def update_original_body(self, prompt_messages: list[dict]) -> Any: # noqa: E704
|
||||
...
|
||||
|
||||
|
||||
@@ -397,3 +399,11 @@ class PangeaHandler(CustomGuardrail):
|
||||
"exceptions": str(e),
|
||||
},
|
||||
) from e
|
||||
|
||||
@staticmethod
|
||||
def get_config_model() -> Optional[Type["GuardrailConfigModel"]]:
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.pangea import (
|
||||
PangeaGuardrailConfigModel,
|
||||
)
|
||||
|
||||
return PangeaGuardrailConfigModel
|
||||
@@ -0,0 +1,42 @@
|
||||
from typing import TYPE_CHECKING, Optional, cast
|
||||
|
||||
from litellm.types.guardrails import SupportedGuardrailIntegrations
|
||||
|
||||
from .panw_prisma_airs import PanwPrismaAirsHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.guardrails import Guardrail, LitellmParams
|
||||
|
||||
|
||||
def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"):
|
||||
import litellm
|
||||
|
||||
guardrail_name = guardrail.get("guardrail_name")
|
||||
profile_name = cast(Optional[str], getattr(litellm_params, "profile_name", None))
|
||||
if not litellm_params.api_key:
|
||||
raise ValueError("PANW Prisma AIRS: api_key is required")
|
||||
if not profile_name:
|
||||
raise ValueError("PANW Prisma AIRS: profile_name is required")
|
||||
if not guardrail_name:
|
||||
raise ValueError("PANW Prisma AIRS: guardrail_name is required")
|
||||
|
||||
_panw_callback = PanwPrismaAirsHandler(
|
||||
**{
|
||||
**litellm_params.model_dump(),
|
||||
"guardrail_name": guardrail_name,
|
||||
"default_on": litellm_params.default_on or False,
|
||||
}
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_panw_callback)
|
||||
|
||||
return _panw_callback
|
||||
|
||||
|
||||
guardrail_initializer_registry = {
|
||||
SupportedGuardrailIntegrations.PANW_PRISMA_AIRS.value: initialize_guardrail,
|
||||
}
|
||||
|
||||
|
||||
guardrail_class_registry = {
|
||||
SupportedGuardrailIntegrations.PANW_PRISMA_AIRS.value: PanwPrismaAirsHandler,
|
||||
}
|
||||
+31
-9
@@ -4,8 +4,9 @@ PANW Prisma AIRS Built-in Guardrail for LiteLLM
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Literal, Optional
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Type, cast
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
@@ -21,6 +22,9 @@ from litellm.llms.custom_httpx.http_handler import (
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.types.utils import ModelResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel
|
||||
|
||||
|
||||
class PanwPrismaAirsHandler(CustomGuardrail):
|
||||
"""
|
||||
@@ -52,8 +56,12 @@ class PanwPrismaAirsHandler(CustomGuardrail):
|
||||
super().__init__(guardrail_name=guardrail_name, default_on=default_on, **kwargs)
|
||||
|
||||
# Store configuration
|
||||
self.api_key = api_key
|
||||
self.api_base = api_base
|
||||
self.api_key = api_key or os.getenv("PANW_PRISMA_AIRS_API_KEY")
|
||||
self.api_base = (
|
||||
api_base
|
||||
or os.getenv("PANW_PRISMA_AIRS_API_BASE")
|
||||
or "https://service.api.aisecurity.paloaltonetworks.com"
|
||||
)
|
||||
self.profile_name = profile_name
|
||||
|
||||
verbose_proxy_logger.info(
|
||||
@@ -98,14 +106,15 @@ class PanwPrismaAirsHandler(CustomGuardrail):
|
||||
def _extract_response_text(self, response: ModelResponse) -> str:
|
||||
"""Extract text from LLM response."""
|
||||
try:
|
||||
from litellm.types.utils import Choices
|
||||
|
||||
if (
|
||||
hasattr(response, "choices")
|
||||
and response.choices
|
||||
and len(response.choices) > 0
|
||||
and hasattr(response.choices[0], "message")
|
||||
and hasattr(response.choices[0].message, "content")
|
||||
):
|
||||
return response.choices[0].message.content or ""
|
||||
return cast(Choices, response.choices[0]).message.content or ""
|
||||
except (AttributeError, IndexError):
|
||||
verbose_proxy_logger.error(
|
||||
"PANW Prisma AIRS: Error extracting response text"
|
||||
@@ -132,9 +141,9 @@ class PanwPrismaAirsHandler(CustomGuardrail):
|
||||
"tr_id": transaction_id,
|
||||
"ai_profile": {"profile_name": self.profile_name},
|
||||
"metadata": {
|
||||
"app_user": metadata.get("user", "litellm_user")
|
||||
if metadata
|
||||
else "litellm_user",
|
||||
"app_user": (
|
||||
metadata.get("user", "litellm_user") if metadata else "litellm_user"
|
||||
),
|
||||
"ai_model": metadata.get("model", "unknown") if metadata else "unknown",
|
||||
"source": "litellm_builtin_guardrail",
|
||||
},
|
||||
@@ -157,7 +166,10 @@ class PanwPrismaAirsHandler(CustomGuardrail):
|
||||
)
|
||||
|
||||
response = await async_client.post(
|
||||
self.api_base, headers=headers, json=payload, timeout=10.0
|
||||
f"{self.api_base}/v1/scan/sync/request",
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=10.0,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -231,6 +243,8 @@ class PanwPrismaAirsHandler(CustomGuardrail):
|
||||
"image_generation",
|
||||
"moderation",
|
||||
"audio_transcription",
|
||||
"pass_through_endpoint",
|
||||
"rerank",
|
||||
],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
@@ -328,3 +342,11 @@ class PanwPrismaAirsHandler(CustomGuardrail):
|
||||
raise HTTPException(status_code=400, detail=error_detail)
|
||||
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def get_config_model() -> Optional[Type["GuardrailConfigModel"]]:
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.panw_prisma_airs import (
|
||||
PanwPrismaAirsGuardrailConfigModel,
|
||||
)
|
||||
|
||||
return PanwPrismaAirsGuardrailConfigModel
|
||||
@@ -4,23 +4,6 @@ from litellm.proxy._types import CommonProxyErrors
|
||||
from litellm.types.guardrails import *
|
||||
|
||||
|
||||
def initialize_aporia(
|
||||
litellm_params: LitellmParams,
|
||||
guardrail: Guardrail,
|
||||
):
|
||||
from litellm.proxy.guardrails.guardrail_hooks.aporia_ai import AporiaGuardrail
|
||||
|
||||
_aporia_callback = AporiaGuardrail(
|
||||
api_base=litellm_params.api_base,
|
||||
api_key=litellm_params.api_key,
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
event_hook=litellm_params.mode,
|
||||
default_on=litellm_params.default_on,
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_aporia_callback)
|
||||
return _aporia_callback
|
||||
|
||||
|
||||
def initialize_bedrock(litellm_params: LitellmParams, guardrail: Guardrail):
|
||||
from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import (
|
||||
BedrockGuardrail,
|
||||
@@ -83,21 +66,6 @@ def initialize_lakera_v2(litellm_params: LitellmParams, guardrail: Guardrail):
|
||||
return _lakera_v2_callback
|
||||
|
||||
|
||||
def initialize_aim(litellm_params: LitellmParams, guardrail: Guardrail):
|
||||
from litellm.proxy.guardrails.guardrail_hooks.aim import AimGuardrail
|
||||
|
||||
_aim_callback = AimGuardrail(
|
||||
api_base=litellm_params.api_base,
|
||||
api_key=litellm_params.api_key,
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
event_hook=litellm_params.mode,
|
||||
default_on=litellm_params.default_on,
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_aim_callback)
|
||||
|
||||
return _aim_callback
|
||||
|
||||
|
||||
def initialize_presidio(litellm_params: LitellmParams, guardrail: Guardrail):
|
||||
from litellm.proxy.guardrails.guardrail_hooks.presidio import (
|
||||
_OPTIONAL_PresidioPIIMasking,
|
||||
@@ -152,81 +120,3 @@ def initialize_hide_secrets(litellm_params: LitellmParams, guardrail: Guardrail)
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_secret_detection_object)
|
||||
return _secret_detection_object
|
||||
|
||||
|
||||
def initialize_guardrails_ai(litellm_params, guardrail):
|
||||
from litellm.proxy.guardrails.guardrail_hooks.guardrails_ai import GuardrailsAI
|
||||
|
||||
_guard_name = litellm_params.guard_name
|
||||
if not _guard_name:
|
||||
raise Exception(
|
||||
"GuardrailsAIException - Please pass the Guardrails AI guard name via 'litellm_params::guard_name'"
|
||||
)
|
||||
|
||||
_guardrails_ai_callback = GuardrailsAI(
|
||||
api_base=litellm_params.api_base,
|
||||
guard_name=_guard_name,
|
||||
guardrail_name=SupportedGuardrailIntegrations.GURDRAILS_AI.value,
|
||||
default_on=litellm_params.default_on,
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_guardrails_ai_callback)
|
||||
|
||||
return _guardrails_ai_callback
|
||||
|
||||
|
||||
def initialize_pangea(litellm_params, guardrail):
|
||||
from litellm.proxy.guardrails.guardrail_hooks.pangea import PangeaHandler
|
||||
|
||||
_pangea_callback = PangeaHandler(
|
||||
guardrail_name=guardrail["guardrail_name"],
|
||||
pangea_input_recipe=litellm_params.pangea_input_recipe,
|
||||
pangea_output_recipe=litellm_params.pangea_output_recipe,
|
||||
api_base=litellm_params.api_base,
|
||||
api_key=litellm_params.api_key,
|
||||
default_on=litellm_params.default_on,
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_pangea_callback)
|
||||
|
||||
return _pangea_callback
|
||||
|
||||
|
||||
def initialize_lasso(
|
||||
litellm_params: LitellmParams,
|
||||
guardrail: Guardrail,
|
||||
):
|
||||
from litellm.proxy.guardrails.guardrail_hooks.lasso import LassoGuardrail
|
||||
|
||||
_lasso_callback = LassoGuardrail(
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
lasso_api_key=litellm_params.api_key,
|
||||
api_base=litellm_params.api_base,
|
||||
user_id=litellm_params.lasso_user_id,
|
||||
conversation_id=litellm_params.lasso_conversation_id,
|
||||
event_hook=litellm_params.mode,
|
||||
default_on=litellm_params.default_on,
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_lasso_callback)
|
||||
|
||||
return _lasso_callback
|
||||
|
||||
|
||||
def initialize_panw_prisma_airs(litellm_params, guardrail):
|
||||
from litellm.proxy.guardrails.guardrail_hooks.panw_prisma_airs import (
|
||||
PanwPrismaAirsHandler,
|
||||
)
|
||||
|
||||
if not litellm_params.api_key:
|
||||
raise ValueError("PANW Prisma AIRS: api_key is required")
|
||||
if not litellm_params.profile_name:
|
||||
raise ValueError("PANW Prisma AIRS: profile_name is required")
|
||||
|
||||
_panw_callback = PanwPrismaAirsHandler(
|
||||
guardrail_name=guardrail.get("guardrail_name", "panw_prisma_airs"), # Use .get() with default
|
||||
api_key=litellm_params.api_key,
|
||||
api_base=litellm_params.api_base or "https://service.api.aisecurity.paloaltonetworks.com/v1/scan/sync/request",
|
||||
profile_name=litellm_params.profile_name,
|
||||
default_on=litellm_params.default_on,
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_panw_callback)
|
||||
|
||||
return _panw_callback
|
||||
|
||||
@@ -21,31 +21,19 @@ from litellm.types.guardrails import (
|
||||
)
|
||||
|
||||
from .guardrail_initializers import (
|
||||
initialize_aim,
|
||||
initialize_aporia,
|
||||
initialize_bedrock,
|
||||
initialize_guardrails_ai,
|
||||
initialize_hide_secrets,
|
||||
initialize_lakera,
|
||||
initialize_lakera_v2,
|
||||
initialize_lasso,
|
||||
initialize_pangea,
|
||||
initialize_panw_prisma_airs,
|
||||
initialize_presidio,
|
||||
)
|
||||
|
||||
guardrail_initializer_registry = {
|
||||
SupportedGuardrailIntegrations.APORIA.value: initialize_aporia,
|
||||
SupportedGuardrailIntegrations.BEDROCK.value: initialize_bedrock,
|
||||
SupportedGuardrailIntegrations.LAKERA.value: initialize_lakera,
|
||||
SupportedGuardrailIntegrations.LAKERA_V2.value: initialize_lakera_v2,
|
||||
SupportedGuardrailIntegrations.AIM.value: initialize_aim,
|
||||
SupportedGuardrailIntegrations.PRESIDIO.value: initialize_presidio,
|
||||
SupportedGuardrailIntegrations.HIDE_SECRETS.value: initialize_hide_secrets,
|
||||
SupportedGuardrailIntegrations.GURDRAILS_AI.value: initialize_guardrails_ai,
|
||||
SupportedGuardrailIntegrations.PANGEA.value: initialize_pangea,
|
||||
SupportedGuardrailIntegrations.LASSO.value: initialize_lasso,
|
||||
SupportedGuardrailIntegrations.PANW_PRISMA_AIRS.value: initialize_panw_prisma_airs,
|
||||
}
|
||||
|
||||
guardrail_class_registry: Dict[str, Type[CustomGuardrail]] = {}
|
||||
@@ -111,7 +99,7 @@ def get_guardrail_initializer_from_hooks():
|
||||
)
|
||||
|
||||
except ImportError as e:
|
||||
verbose_proxy_logger.debug(f"Could not import {module_path}: {e}")
|
||||
verbose_proxy_logger.error(f"Could not import {module_path}: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.error(f"Error processing {module_path}: {e}")
|
||||
@@ -183,7 +171,7 @@ def get_guardrail_class_from_hooks():
|
||||
verbose_proxy_logger.debug(f"Could not import {module_path}: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.error(f"Error processing {module_path}: {e}")
|
||||
verbose_proxy_logger.exception(f"Error processing {module_path}: {e}")
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
@@ -323,7 +311,7 @@ class GuardrailRegistry:
|
||||
|
||||
guardrails: List[Guardrail] = []
|
||||
for guardrail in guardrails_from_db:
|
||||
guardrails.append(Guardrail(**(dict(guardrail))))
|
||||
guardrails.append(Guardrail(**(dict(guardrail)))) # type: ignore
|
||||
|
||||
return guardrails
|
||||
except Exception as e:
|
||||
@@ -343,7 +331,7 @@ class GuardrailRegistry:
|
||||
if not guardrail:
|
||||
return None
|
||||
|
||||
return Guardrail(**(dict(guardrail)))
|
||||
return Guardrail(**(dict(guardrail))) # type: ignore
|
||||
except Exception as e:
|
||||
raise Exception(f"Error getting guardrail from DB: {str(e)}")
|
||||
|
||||
@@ -361,7 +349,7 @@ class GuardrailRegistry:
|
||||
if not guardrail:
|
||||
return None
|
||||
|
||||
return Guardrail(**(dict(guardrail)))
|
||||
return Guardrail(**(dict(guardrail))) # type: ignore
|
||||
except Exception as e:
|
||||
raise Exception(f"Error getting guardrail from DB: {str(e)}")
|
||||
|
||||
@@ -384,7 +372,7 @@ class InMemoryGuardrailHandler:
|
||||
|
||||
def initialize_guardrail(
|
||||
self,
|
||||
guardrail: Dict,
|
||||
guardrail: Guardrail,
|
||||
config_file_path: Optional[str] = None,
|
||||
) -> Optional[Guardrail]:
|
||||
"""
|
||||
@@ -404,7 +392,10 @@ class InMemoryGuardrailHandler:
|
||||
litellm_params_data = guardrail["litellm_params"]
|
||||
verbose_proxy_logger.debug("litellm_params= %s", litellm_params_data)
|
||||
|
||||
litellm_params = LitellmParams(**litellm_params_data)
|
||||
if isinstance(litellm_params_data, dict):
|
||||
litellm_params = LitellmParams(**litellm_params_data)
|
||||
else:
|
||||
litellm_params = litellm_params_data
|
||||
|
||||
if (
|
||||
"category_thresholds" in litellm_params_data
|
||||
@@ -433,7 +424,7 @@ class InMemoryGuardrailHandler:
|
||||
custom_guardrail_callback = initializer(litellm_params, guardrail)
|
||||
elif isinstance(guardrail_type, str) and "." in guardrail_type:
|
||||
custom_guardrail_callback = self.initialize_custom_guardrail(
|
||||
guardrail=guardrail,
|
||||
guardrail=cast(dict, guardrail),
|
||||
guardrail_type=guardrail_type,
|
||||
litellm_params=litellm_params,
|
||||
config_file_path=config_file_path,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, cast
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
@@ -83,7 +83,7 @@ def init_guardrails_v2(
|
||||
|
||||
for guardrail in all_guardrails:
|
||||
initialized_guardrail = IN_MEMORY_GUARDRAIL_HANDLER.initialize_guardrail(
|
||||
guardrail=guardrail,
|
||||
guardrail=cast(Guardrail, guardrail),
|
||||
config_file_path=config_file_path,
|
||||
)
|
||||
if initialized_guardrail:
|
||||
|
||||
@@ -2828,7 +2828,7 @@ class ProxyConfig:
|
||||
)
|
||||
for guardrail in guardrails_in_db:
|
||||
IN_MEMORY_GUARDRAIL_HANDLER.initialize_guardrail(
|
||||
guardrail=dict(guardrail),
|
||||
guardrail=cast(Guardrail, guardrail),
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(
|
||||
@@ -2978,6 +2978,7 @@ async def initialize( # noqa: PLR0915
|
||||
):
|
||||
global user_model, user_api_base, user_debug, user_detailed_debug, user_user_max_tokens, user_request_timeout, user_temperature, user_telemetry, user_headers, experimental, llm_model_list, llm_router, general_settings, master_key, user_custom_auth, prisma_client
|
||||
from litellm.proxy.common_utils.banner import show_banner
|
||||
|
||||
show_banner()
|
||||
if os.getenv("LITELLM_DONT_SHOW_FEEDBACK_BOX", "").lower() != "true":
|
||||
generate_feedback_box()
|
||||
|
||||
@@ -22,7 +22,7 @@ guardrails:
|
||||
class SupportedGuardrailIntegrations(Enum):
|
||||
APORIA = "aporia"
|
||||
BEDROCK = "bedrock"
|
||||
GURDRAILS_AI = "guardrails_ai"
|
||||
GUARDRAILS_AI = "guardrails_ai"
|
||||
LAKERA = "lakera"
|
||||
LAKERA_V2 = "lakera_v2"
|
||||
PRESIDIO = "presidio"
|
||||
@@ -409,11 +409,23 @@ class LitellmParams(
|
||||
kwargs["default_on"] = False
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __contains__(self, key):
|
||||
# Define custom behavior for the 'in' operator
|
||||
return hasattr(self, key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
# Custom .get() method to access attributes with a default value if the attribute doesn't exist
|
||||
return getattr(self, key, default)
|
||||
|
||||
def __getitem__(self, key):
|
||||
# Allow dictionary-style access to attributes
|
||||
return getattr(self, key)
|
||||
|
||||
|
||||
class Guardrail(TypedDict, total=False):
|
||||
guardrail_id: Optional[str]
|
||||
guardrail_name: str
|
||||
litellm_params: LitellmParams
|
||||
guardrail_name: Required[str]
|
||||
litellm_params: Required[LitellmParams]
|
||||
guardrail_info: Optional[Dict]
|
||||
created_at: Optional[datetime]
|
||||
updated_at: Optional[datetime]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from .base import GuardrailConfigModel
|
||||
|
||||
|
||||
class AimGuardrailConfigModel(GuardrailConfigModel):
|
||||
api_key: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The API key for the Aim guardrail. If not provided, the `AIM_API_KEY` environment variable is checked.",
|
||||
)
|
||||
api_base: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The API base for the Aim guardrail. Default is https://api.aim.security. Also checks if the `AIM_API_BASE` environment variable is set.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "AIM Guardrail"
|
||||
@@ -0,0 +1,20 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from .base import GuardrailConfigModel
|
||||
|
||||
|
||||
class AporiaGuardrailConfigModel(GuardrailConfigModel):
|
||||
api_key: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The API key for the Aporia guardrail. If not provided, the `APORIA_API_KEY` environment variable is checked.",
|
||||
)
|
||||
api_base: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The API base for the Aporia guardrail. If not provided, the `APORIA_API_BASE` environment variable is checked.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "Aporia AI"
|
||||
@@ -29,4 +29,6 @@ class AzurePromptShieldGuardrailConfigModel(
|
||||
AzureContentSafetyConfigModel,
|
||||
GuardrailConfigModel,
|
||||
):
|
||||
pass
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "Azure Content Safety Prompt Shield"
|
||||
|
||||
@@ -80,3 +80,7 @@ class AzureContentSafetyTextModerationConfigModel(
|
||||
optional_params: AzureTextModerationOptionalParams = Field(
|
||||
description="Optional parameters for the Azure Content Safety Text Moderation guardrail",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "Azure Content Safety Text Moderation"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -5,9 +6,15 @@ from pydantic import BaseModel, Field
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class GuardrailConfigModel(BaseModel, Generic[T]):
|
||||
class GuardrailConfigModel(BaseModel, Generic[T], ABC):
|
||||
"""Base model for guardrail configuration"""
|
||||
|
||||
optional_params: T = Field(
|
||||
description="Optional parameters for the guardrail",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def ui_friendly_name() -> str:
|
||||
"""UI-friendly name for the guardrail"""
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from .base import GuardrailConfigModel
|
||||
|
||||
|
||||
class GuardrailsAIGuardrailConfigModel(GuardrailConfigModel):
|
||||
api_base: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The API base for the Guardrails AI guardrail. Defaults to http://0.0.0.0:8000, the `GUARDRAILS_AI_API_BASE` environment variable is checked.",
|
||||
)
|
||||
|
||||
guard_name: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The name of the Guardrails AI guardrail. Required for the Guardrails AI guardrail.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "Guardrails AI"
|
||||
@@ -0,0 +1,33 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .base import GuardrailConfigModel
|
||||
|
||||
|
||||
class LassoGuardrailConfigModelOptionalParams(BaseModel):
|
||||
user_id: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The user ID for the Lasso guardrail. If not provided, the `LASSO_USER_ID` environment variable is checked.",
|
||||
)
|
||||
conversation_id: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The conversation ID for the Lasso guardrail. If not provided, the `LASSO_CONVERSATION_ID` environment variable is checked.",
|
||||
)
|
||||
|
||||
|
||||
class LassoGuardrailConfigModel(
|
||||
GuardrailConfigModel[LassoGuardrailConfigModelOptionalParams]
|
||||
):
|
||||
api_key: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The API key for the Lasso guardrail. If not provided, the `LASSO_API_KEY` environment variable is checked.",
|
||||
)
|
||||
api_base: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The API base for the Lasso guardrail. Default is https://server.lasso.security. Also checks if the `LASSO_API_BASE` environment variable is set.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "Lasso Guardrail"
|
||||
@@ -0,0 +1,33 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .base import GuardrailConfigModel
|
||||
|
||||
|
||||
class PangeaGuardrailConfigModelOptionalParams(BaseModel):
|
||||
pangea_input_recipe: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The Pangea input recipe for the Pangea guardrail. Used for pre-call hook.",
|
||||
)
|
||||
pangea_output_recipe: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The Pangea output recipe for the Pangea guardrail. Used for post-call hook.",
|
||||
)
|
||||
|
||||
|
||||
class PangeaGuardrailConfigModel(
|
||||
GuardrailConfigModel[PangeaGuardrailConfigModelOptionalParams]
|
||||
):
|
||||
api_key: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The Pangea API key. Reads from PANGEA_API_KEY env var if None.",
|
||||
)
|
||||
api_base: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The Pangea API base URL. Defaults to https://ai-guard.aws.us.pangea.cloud. Also checks if the PANGEA_API_BASE env var is set.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "Pangea Guardrail"
|
||||
@@ -0,0 +1,25 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from .base import GuardrailConfigModel
|
||||
|
||||
|
||||
class PanwPrismaAirsGuardrailConfigModel(GuardrailConfigModel):
|
||||
api_key: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The API key for the PANW Prisma AIRS guardrail. If not provided, the `PANW_PRISMA_AIRS_API_KEY` environment variable is checked.",
|
||||
)
|
||||
api_base: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The API base for the PANW Prisma AIRS guardrail. Defaults to https://service.api.aisecurity.paloaltonetworks.com. If not provided, the `PANW_PRISMA_AIRS_API_BASE` environment variable is checked.",
|
||||
)
|
||||
|
||||
profile_name: str = Field(
|
||||
default="default",
|
||||
description="PANW Prisma AIRS security profile name. Required.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "PANW Prisma AIRS"
|
||||
@@ -8,9 +8,15 @@ import pytest
|
||||
|
||||
from litellm import DualCache
|
||||
from litellm.proxy.proxy_server import UserAPIKeyAuth
|
||||
from litellm.proxy.guardrails.guardrail_hooks.lasso import LassoGuardrailMissingSecrets, LassoGuardrail, LassoGuardrailAPIError
|
||||
from litellm.proxy.guardrails.guardrail_hooks.lasso.lasso import (
|
||||
LassoGuardrailMissingSecrets,
|
||||
LassoGuardrail,
|
||||
LassoGuardrailAPIError,
|
||||
)
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../..")) # Adds the parent directory to the system path
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.init_guardrails import init_guardrails_v2
|
||||
|
||||
@@ -35,7 +41,7 @@ def test_lasso_guard_config():
|
||||
],
|
||||
config_file_path="",
|
||||
)
|
||||
|
||||
|
||||
# Clean up
|
||||
del os.environ["LASSO_API_KEY"]
|
||||
|
||||
@@ -43,12 +49,14 @@ def test_lasso_guard_config():
|
||||
def test_lasso_guard_config_no_api_key():
|
||||
litellm.set_verbose = True
|
||||
litellm.guardrail_name_config_map = {}
|
||||
|
||||
|
||||
# Ensure LASSO_API_KEY is not in environment
|
||||
if "LASSO_API_KEY" in os.environ:
|
||||
del os.environ["LASSO_API_KEY"]
|
||||
|
||||
with pytest.raises(LassoGuardrailMissingSecrets, match="Couldn't get Lasso api key"):
|
||||
|
||||
with pytest.raises(
|
||||
LassoGuardrailMissingSecrets, match="Couldn't get Lasso api key"
|
||||
):
|
||||
init_guardrails_v2(
|
||||
all_guardrails=[
|
||||
{
|
||||
@@ -69,7 +77,7 @@ async def test_callback():
|
||||
# Set environment variable for testing
|
||||
os.environ["LASSO_API_KEY"] = "test-key"
|
||||
os.environ["LASSO_USER_ID"] = "test-user"
|
||||
os.environ["LASSO_CONVERSATION_ID"] = "test-conversation"
|
||||
os.environ["LASSO_CONVERSATION_ID"] = "test-conversation"
|
||||
init_guardrails_v2(
|
||||
all_guardrails=[
|
||||
{
|
||||
@@ -82,7 +90,9 @@ async def test_callback():
|
||||
}
|
||||
],
|
||||
)
|
||||
lasso_guardrails = litellm.logging_callback_manager.get_custom_loggers_for_type(LassoGuardrail)
|
||||
lasso_guardrails = litellm.logging_callback_manager.get_custom_loggers_for_type(
|
||||
LassoGuardrail
|
||||
)
|
||||
print("found lasso guardrails", lasso_guardrails)
|
||||
lasso_guardrail = lasso_guardrails[0]
|
||||
|
||||
@@ -98,73 +108,83 @@ async def test_callback():
|
||||
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
|
||||
return_value=Response(
|
||||
json={
|
||||
"deputies": {
|
||||
"jailbreak": True,
|
||||
"custom-policies": False,
|
||||
"sexual": False,
|
||||
"hate": False,
|
||||
"illegality": False,
|
||||
"violence": False,
|
||||
"pattern-detection": False
|
||||
},
|
||||
"deputies_predictions": {
|
||||
"jailbreak": 0.923,
|
||||
"custom-policies": 0.234,
|
||||
"sexual": 0.145,
|
||||
"hate": 0.156,
|
||||
"illegality": 0.167,
|
||||
"violence": 0.178,
|
||||
"pattern-detection": 0.189
|
||||
},
|
||||
"violations_detected": True
|
||||
},
|
||||
status_code=200,
|
||||
request=Request(method="POST", url="https://server.lasso.security/gateway/v1/chat"),
|
||||
),
|
||||
):
|
||||
await lasso_guardrail.async_pre_call_hook(
|
||||
data=data, cache=DualCache(), user_api_key_dict=UserAPIKeyAuth(), call_type="completion"
|
||||
)
|
||||
|
||||
# Check for the correct error message
|
||||
assert "Violated Lasso guardrail policy" in str(excinfo.value.detail)
|
||||
assert "jailbreak" in str(excinfo.value.detail)
|
||||
|
||||
# Test no violation
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
|
||||
return_value=Response(
|
||||
json={
|
||||
"deputies": {
|
||||
"jailbreak": False,
|
||||
"jailbreak": True,
|
||||
"custom-policies": False,
|
||||
"sexual": False,
|
||||
"hate": False,
|
||||
"illegality": False,
|
||||
"violence": False,
|
||||
"pattern-detection": False
|
||||
"pattern-detection": False,
|
||||
},
|
||||
"deputies_predictions": {
|
||||
"jailbreak": 0.123,
|
||||
"jailbreak": 0.923,
|
||||
"custom-policies": 0.234,
|
||||
"sexual": 0.145,
|
||||
"hate": 0.156,
|
||||
"illegality": 0.167,
|
||||
"violence": 0.178,
|
||||
"pattern-detection": 0.189
|
||||
"pattern-detection": 0.189,
|
||||
},
|
||||
"violations_detected": False
|
||||
"violations_detected": True,
|
||||
},
|
||||
status_code=200,
|
||||
request=Request(
|
||||
method="POST", url="https://server.lasso.security/gateway/v1/chat"
|
||||
),
|
||||
),
|
||||
):
|
||||
await lasso_guardrail.async_pre_call_hook(
|
||||
data=data,
|
||||
cache=DualCache(),
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
call_type="completion",
|
||||
)
|
||||
|
||||
# Check for the correct error message
|
||||
assert "Violated Lasso guardrail policy" in str(excinfo.value.detail)
|
||||
assert "jailbreak" in str(excinfo.value.detail)
|
||||
|
||||
# Test no violation
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
|
||||
return_value=Response(
|
||||
json={
|
||||
"deputies": {
|
||||
"jailbreak": False,
|
||||
"custom-policies": False,
|
||||
"sexual": False,
|
||||
"hate": False,
|
||||
"illegality": False,
|
||||
"violence": False,
|
||||
"pattern-detection": False,
|
||||
},
|
||||
"deputies_predictions": {
|
||||
"jailbreak": 0.123,
|
||||
"custom-policies": 0.234,
|
||||
"sexual": 0.145,
|
||||
"hate": 0.156,
|
||||
"illegality": 0.167,
|
||||
"violence": 0.178,
|
||||
"pattern-detection": 0.189,
|
||||
},
|
||||
"violations_detected": False,
|
||||
},
|
||||
status_code=200,
|
||||
request=Request(method="POST", url="https://server.lasso.security/gateway/v1/chat"),
|
||||
request=Request(
|
||||
method="POST", url="https://server.lasso.security/gateway/v1/chat"
|
||||
),
|
||||
),
|
||||
):
|
||||
result = await lasso_guardrail.async_pre_call_hook(
|
||||
data=data, cache=DualCache(), user_api_key_dict=UserAPIKeyAuth(), call_type="completion"
|
||||
data=data,
|
||||
cache=DualCache(),
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
call_type="completion",
|
||||
)
|
||||
|
||||
|
||||
assert result == data # Should return the original data unchanged
|
||||
|
||||
|
||||
# Clean up
|
||||
del os.environ["LASSO_API_KEY"]
|
||||
del os.environ["LASSO_USER_ID"]
|
||||
@@ -175,21 +195,22 @@ async def test_callback():
|
||||
async def test_empty_messages():
|
||||
"""Test handling of empty messages"""
|
||||
os.environ["LASSO_API_KEY"] = "test-key"
|
||||
|
||||
|
||||
lasso_guardrail = LassoGuardrail(
|
||||
guardrail_name="test-guard",
|
||||
event_hook="pre_call",
|
||||
default_on=True
|
||||
guardrail_name="test-guard", event_hook="pre_call", default_on=True
|
||||
)
|
||||
|
||||
|
||||
data = {"messages": []}
|
||||
|
||||
|
||||
result = await lasso_guardrail.async_pre_call_hook(
|
||||
data=data, cache=DualCache(), user_api_key_dict=UserAPIKeyAuth(), call_type="completion"
|
||||
data=data,
|
||||
cache=DualCache(),
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
call_type="completion",
|
||||
)
|
||||
|
||||
|
||||
assert result == data
|
||||
|
||||
|
||||
# Clean up
|
||||
del os.environ["LASSO_API_KEY"]
|
||||
|
||||
@@ -198,48 +219,52 @@ async def test_empty_messages():
|
||||
async def test_api_error_handling():
|
||||
"""Test handling of API errors"""
|
||||
os.environ["LASSO_API_KEY"] = "test-key"
|
||||
|
||||
|
||||
lasso_guardrail = LassoGuardrail(
|
||||
guardrail_name="test-guard",
|
||||
event_hook="pre_call",
|
||||
default_on=True
|
||||
guardrail_name="test-guard", event_hook="pre_call", default_on=True
|
||||
)
|
||||
|
||||
|
||||
data = {
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello, how are you?"},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# Test handling of connection error
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
|
||||
side_effect=Exception("Connection error")
|
||||
side_effect=Exception("Connection error"),
|
||||
):
|
||||
# Expect the guardrail to raise a LassoGuardrailAPIError
|
||||
with pytest.raises(LassoGuardrailAPIError) as excinfo:
|
||||
await lasso_guardrail.async_pre_call_hook(
|
||||
data=data, cache=DualCache(), user_api_key_dict=UserAPIKeyAuth(), call_type="completion"
|
||||
data=data,
|
||||
cache=DualCache(),
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
call_type="completion",
|
||||
)
|
||||
|
||||
|
||||
# Verify the error message
|
||||
assert "Failed to verify request safety with Lasso API" in str(excinfo.value)
|
||||
assert "Connection error" in str(excinfo.value)
|
||||
|
||||
|
||||
# Test with a different error message
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
|
||||
side_effect=Exception("API timeout")
|
||||
side_effect=Exception("API timeout"),
|
||||
):
|
||||
# Expect the guardrail to raise a LassoGuardrailAPIError
|
||||
with pytest.raises(LassoGuardrailAPIError) as excinfo:
|
||||
await lasso_guardrail.async_pre_call_hook(
|
||||
data=data, cache=DualCache(), user_api_key_dict=UserAPIKeyAuth(), call_type="completion"
|
||||
data=data,
|
||||
cache=DualCache(),
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
call_type="completion",
|
||||
)
|
||||
|
||||
|
||||
# Verify the error message for the second test
|
||||
assert "Failed to verify request safety with Lasso API" in str(excinfo.value)
|
||||
assert "API timeout" in str(excinfo.value)
|
||||
|
||||
|
||||
# Clean up
|
||||
del os.environ["LASSO_API_KEY"]
|
||||
|
||||
@@ -10,7 +10,7 @@ from fastapi.exceptions import HTTPException
|
||||
from httpx import Request, Response
|
||||
|
||||
from litellm import DualCache
|
||||
from litellm.proxy.guardrails.guardrail_hooks.aim import (
|
||||
from litellm.proxy.guardrails.guardrail_hooks.aim.aim import (
|
||||
AimGuardrail,
|
||||
AimGuardrailMissingSecrets,
|
||||
)
|
||||
@@ -298,7 +298,7 @@ async def test_post_call_stream__all_chunks_are_valid(monkeypatch, length: int):
|
||||
yield websocket_mock
|
||||
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.guardrails.guardrail_hooks.aim.connect", connect_mock
|
||||
"litellm.proxy.guardrails.guardrail_hooks.aim.aim.connect", connect_mock
|
||||
)
|
||||
|
||||
results = []
|
||||
@@ -316,6 +316,8 @@ async def test_post_call_stream__all_chunks_are_valid(monkeypatch, length: int):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_call_stream__blocked_chunks(monkeypatch):
|
||||
from litellm.proxy.proxy_server import StreamingCallbackError
|
||||
|
||||
init_guardrails_v2(
|
||||
all_guardrails=[
|
||||
{
|
||||
@@ -357,17 +359,27 @@ async def test_post_call_stream__blocked_chunks(monkeypatch):
|
||||
yield websocket_mock
|
||||
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.guardrails.guardrail_hooks.aim.connect", connect_mock
|
||||
"litellm.proxy.guardrails.guardrail_hooks.aim.aim.connect", connect_mock
|
||||
)
|
||||
|
||||
results = []
|
||||
with pytest.raises(StreamingCallbackError, match="Jailbreak detected"):
|
||||
# For async generators, we need to manually iterate and catch the exception
|
||||
exception_caught = False
|
||||
try:
|
||||
async for result in aim_guardrail.async_post_call_streaming_iterator_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
response=llm_response(),
|
||||
request_data=data,
|
||||
):
|
||||
results.append(result)
|
||||
except StreamingCallbackError:
|
||||
exception_caught = True
|
||||
except Exception as e:
|
||||
print("INSIDE EXCEPTION")
|
||||
raise e
|
||||
|
||||
# Assert that the exception was caught
|
||||
assert exception_caught, "StreamingCallbackError should have been raised"
|
||||
|
||||
# Chunks that were received before the blocking message should be returned as usual.
|
||||
assert len(results) == 1
|
||||
|
||||
@@ -4,7 +4,7 @@ import httpx
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from litellm.proxy.guardrails.guardrail_hooks.pangea import (
|
||||
from litellm.proxy.guardrails.guardrail_hooks.pangea.pangea import (
|
||||
PangeaGuardrailMissingSecrets,
|
||||
PangeaHandler,
|
||||
)
|
||||
|
||||
@@ -3,25 +3,24 @@ Test suite for PANW AIRS Guardrail Integration
|
||||
|
||||
This test file follows LiteLLM's testing patterns and covers:
|
||||
- Guardrail initialization
|
||||
- Prompt scanning (blocking and allowing)
|
||||
- Prompt scanning (blocking and allowing)
|
||||
- Response scanning
|
||||
- Error handling
|
||||
- Configuration validation
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
from fastapi import HTTPException
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.guardrails.guardrail_hooks.panw_prisma_airs import (
|
||||
PanwPrismaAirsHandler,
|
||||
initialize_guardrail,
|
||||
)
|
||||
from litellm.proxy.guardrails.guardrail_initializers import (
|
||||
initialize_panw_prisma_airs,
|
||||
)
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.types.utils import ModelResponse, Choices, Message
|
||||
from litellm.types.utils import Choices, Message, ModelResponse
|
||||
|
||||
|
||||
class TestPanwAirsInitialization:
|
||||
@@ -42,18 +41,22 @@ class TestPanwAirsInitialization:
|
||||
assert handler.api_base == "https://test.panw.com/api"
|
||||
assert handler.profile_name == "test_profile"
|
||||
|
||||
def test_initialize_panw_prisma_airs_function(self):
|
||||
"""Test the initialize_panw_prisma_airs function."""
|
||||
litellm_params = SimpleNamespace(
|
||||
def test_initialize_guardrail_function(self):
|
||||
"""Test the initialize_guardrail function."""
|
||||
from litellm.types.guardrails import LitellmParams
|
||||
|
||||
litellm_params = LitellmParams(
|
||||
guardrail="panw_prisma_airs",
|
||||
mode="pre_call",
|
||||
api_key="test_key",
|
||||
profile_name="test_profile",
|
||||
profile_name="test_profile",
|
||||
api_base="https://test.panw.com/api",
|
||||
default_on=True,
|
||||
)
|
||||
guardrail_config = {"guardrail_name": "test_guardrail"}
|
||||
|
||||
with patch("litellm.logging_callback_manager.add_litellm_callback"):
|
||||
handler = initialize_panw_prisma_airs(litellm_params, guardrail_config)
|
||||
handler = initialize_guardrail(litellm_params, guardrail_config)
|
||||
|
||||
assert isinstance(handler, PanwPrismaAirsHandler)
|
||||
assert handler.guardrail_name == "test_guardrail"
|
||||
@@ -64,12 +67,12 @@ class TestPanwAirsInitialization:
|
||||
profile_name="test_profile",
|
||||
api_base=None,
|
||||
default_on=True,
|
||||
api_key=None # Missing API key
|
||||
api_key=None, # Missing API key
|
||||
)
|
||||
guardrail_config = {"guardrail_name": "test_guardrail"}
|
||||
|
||||
with pytest.raises(ValueError, match="api_key is required"):
|
||||
initialize_panw_prisma_airs(litellm_params, guardrail_config)
|
||||
initialize_guardrail(litellm_params, guardrail_config)
|
||||
|
||||
def test_missing_profile_name_raises_error(self):
|
||||
"""Test that missing profile name raises ValueError."""
|
||||
@@ -77,12 +80,12 @@ class TestPanwAirsInitialization:
|
||||
api_key="test_key",
|
||||
api_base=None,
|
||||
default_on=True,
|
||||
profile_name=None # Missing profile name
|
||||
profile_name=None, # Missing profile name
|
||||
)
|
||||
guardrail_config = {"guardrail_name": "test_guardrail"}
|
||||
|
||||
with pytest.raises(ValueError, match="profile_name is required"):
|
||||
initialize_panw_prisma_airs(litellm_params, guardrail_config)
|
||||
initialize_guardrail(litellm_params, guardrail_config)
|
||||
|
||||
|
||||
class TestPanwAirsPromptScanning:
|
||||
@@ -331,7 +334,7 @@ class TestPanwAirsAPIIntegration:
|
||||
mock_response.raise_for_status.return_value = None
|
||||
|
||||
with patch(
|
||||
"litellm.proxy.guardrails.guardrail_hooks.panw_prisma_airs.get_async_httpx_client"
|
||||
"litellm.proxy.guardrails.guardrail_hooks.panw_prisma_airs.panw_prisma_airs.get_async_httpx_client"
|
||||
) as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.post = AsyncMock(return_value=mock_response)
|
||||
@@ -351,7 +354,7 @@ class TestPanwAirsAPIIntegration:
|
||||
"""Test API error handling (fail closed)."""
|
||||
# Mock the HTTP client to raise an exception
|
||||
with patch(
|
||||
"litellm.proxy.guardrails.guardrail_hooks.panw_prisma_airs.get_async_httpx_client"
|
||||
"litellm.proxy.guardrails.guardrail_hooks.panw_prisma_airs.panw_prisma_airs.get_async_httpx_client"
|
||||
) as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.post = AsyncMock(side_effect=Exception("API Error"))
|
||||
@@ -374,7 +377,7 @@ class TestPanwAirsAPIIntegration:
|
||||
mock_response.raise_for_status.return_value = None
|
||||
|
||||
with patch(
|
||||
"litellm.proxy.guardrails.guardrail_hooks.panw_prisma_airs.get_async_httpx_client"
|
||||
"litellm.proxy.guardrails.guardrail_hooks.panw_prisma_airs.panw_prisma_airs.get_async_httpx_client"
|
||||
) as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.post = AsyncMock(return_value=mock_response)
|
||||
@@ -403,7 +406,11 @@ class TestPanwAirsConfiguration:
|
||||
|
||||
def test_default_api_base(self):
|
||||
"""Test that default API base is set correctly."""
|
||||
litellm_params = SimpleNamespace(
|
||||
from litellm.types.guardrails import LitellmParams
|
||||
|
||||
litellm_params = LitellmParams(
|
||||
guardrail="panw_prisma_airs",
|
||||
mode="pre_call",
|
||||
api_key="test_key",
|
||||
profile_name="test_profile",
|
||||
api_base=None, # No api_base provided
|
||||
@@ -412,17 +419,18 @@ class TestPanwAirsConfiguration:
|
||||
guardrail_config = {"guardrail_name": "test"}
|
||||
|
||||
with patch("litellm.logging_callback_manager.add_litellm_callback"):
|
||||
handler = initialize_panw_prisma_airs(litellm_params, guardrail_config)
|
||||
handler = initialize_guardrail(litellm_params, guardrail_config)
|
||||
|
||||
assert (
|
||||
handler.api_base
|
||||
== "https://service.api.aisecurity.paloaltonetworks.com/v1/scan/sync/request"
|
||||
)
|
||||
assert handler.api_base == "https://service.api.aisecurity.paloaltonetworks.com"
|
||||
|
||||
def test_custom_api_base(self):
|
||||
"""Test custom API base configuration."""
|
||||
from litellm.types.guardrails import LitellmParams
|
||||
|
||||
custom_base = "https://custom.panw.com/api/v2/scan"
|
||||
litellm_params = SimpleNamespace(
|
||||
litellm_params = LitellmParams(
|
||||
guardrail="panw_prisma_airs",
|
||||
mode="pre_call",
|
||||
api_key="test_key",
|
||||
profile_name="test_profile",
|
||||
api_base=custom_base,
|
||||
@@ -431,24 +439,30 @@ class TestPanwAirsConfiguration:
|
||||
guardrail_config = {"guardrail_name": "test"}
|
||||
|
||||
with patch("litellm.logging_callback_manager.add_litellm_callback"):
|
||||
handler = initialize_panw_prisma_airs(litellm_params, guardrail_config)
|
||||
handler = initialize_guardrail(litellm_params, guardrail_config)
|
||||
|
||||
assert handler.api_base == custom_base
|
||||
|
||||
def test_default_guardrail_name(self):
|
||||
"""Test default guardrail name."""
|
||||
litellm_params = SimpleNamespace(
|
||||
from litellm.types.guardrails import LitellmParams
|
||||
|
||||
litellm_params = LitellmParams(
|
||||
guardrail="panw_prisma_airs",
|
||||
mode="pre_call",
|
||||
api_key="test_key",
|
||||
profile_name="test_profile",
|
||||
api_base=None,
|
||||
default_on=True,
|
||||
)
|
||||
guardrail_config = {} # No guardrail_name
|
||||
guardrail_config = {
|
||||
"guardrail_name": "test_guardrail",
|
||||
} # No guardrail_name
|
||||
|
||||
with patch("litellm.logging_callback_manager.add_litellm_callback"):
|
||||
handler = initialize_panw_prisma_airs(litellm_params, guardrail_config)
|
||||
handler = initialize_guardrail(litellm_params, guardrail_config)
|
||||
|
||||
assert handler.guardrail_name == "panw_prisma_airs"
|
||||
assert handler.guardrail_name == "test_guardrail"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -178,9 +178,11 @@ def test_get_provider_specific_params():
|
||||
AzureContentSafetyTextModerationGuardrail,
|
||||
)
|
||||
|
||||
fields = _get_fields_from_model(
|
||||
AzureContentSafetyTextModerationGuardrail.get_config_model()
|
||||
)
|
||||
config_model = AzureContentSafetyTextModerationGuardrail.get_config_model()
|
||||
if config_model is None:
|
||||
pytest.skip("Azure config model not available")
|
||||
|
||||
fields = _get_fields_from_model(config_model)
|
||||
print("FIELDS", fields)
|
||||
|
||||
# Test that we get the expected nested structure
|
||||
@@ -231,3 +233,70 @@ def test_get_provider_specific_params():
|
||||
assert (
|
||||
nested_fields["outputType"]["type"] == "select"
|
||||
) # Literal type should be select
|
||||
|
||||
|
||||
def test_optional_params_not_returned_when_not_overridden():
|
||||
"""Test that optional_params is not returned when the config model doesn't override it"""
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from litellm.proxy.guardrails.guardrail_endpoints import _get_fields_from_model
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel
|
||||
|
||||
class TestGuardrailConfig(GuardrailConfigModel):
|
||||
api_key: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Test API key",
|
||||
)
|
||||
api_base: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Test API base",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "Test Guardrail"
|
||||
|
||||
# Get fields from the model
|
||||
fields = _get_fields_from_model(TestGuardrailConfig)
|
||||
print("FIELDS", fields)
|
||||
assert "optional_params" not in fields
|
||||
|
||||
|
||||
def test_optional_params_returned_when_properly_overridden():
|
||||
"""Test that optional_params IS returned when the config model properly overrides it"""
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from litellm.proxy.guardrails.guardrail_endpoints import _get_fields_from_model
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel
|
||||
|
||||
# Create specific optional params model
|
||||
class SpecificOptionalParams(BaseModel):
|
||||
threshold: Optional[float] = Field(
|
||||
default=0.5, description="Detection threshold"
|
||||
)
|
||||
categories: Optional[List[str]] = Field(
|
||||
default=None, description="Categories to check"
|
||||
)
|
||||
|
||||
# Create a config model that DOES override optional_params with a specific type
|
||||
class TestGuardrailConfigWithOptionalParams(
|
||||
GuardrailConfigModel[SpecificOptionalParams]
|
||||
):
|
||||
api_key: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Test API key",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "Test Guardrail With Optional Params"
|
||||
|
||||
# Get fields from the model
|
||||
fields = _get_fields_from_model(TestGuardrailConfigWithOptionalParams)
|
||||
|
||||
print("FIELDS", fields)
|
||||
assert "optional_params" in fields
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from litellm.proxy.guardrails.guardrail_registry import (
|
||||
get_guardrail_initializer_from_hooks,
|
||||
)
|
||||
|
||||
|
||||
def test_get_guardrail_initializer_from_hooks():
|
||||
initializers = get_guardrail_initializer_from_hooks()
|
||||
print(f"initializers: {initializers}")
|
||||
assert "aim" in initializers
|
||||
|
||||
|
||||
def test_guardrail_class_registry():
|
||||
from litellm.proxy.guardrails.guardrail_registry import guardrail_class_registry
|
||||
|
||||
print(f"guardrail_class_registry: {guardrail_class_registry}")
|
||||
assert "aim" in guardrail_class_registry
|
||||
assert "aporia" in guardrail_class_registry
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Card, Form, Typography, Select, Input, Switch, Tooltip, Modal, message, Divider, Space, Tag, Image, Steps } from 'antd';
|
||||
import { Button, TextInput } from '@tremor/react';
|
||||
import type { FormInstance } from 'antd';
|
||||
import { GuardrailProviders, guardrail_provider_map, shouldRenderPIIConfigSettings, guardrailLogoMap } from './guardrail_info_helpers';
|
||||
import { GuardrailProviders, guardrail_provider_map, shouldRenderPIIConfigSettings, guardrailLogoMap, populateGuardrailProviders, populateGuardrailProviderMap, getGuardrailProviders } from './guardrail_info_helpers';
|
||||
import { createGuardrailCall, getGuardrailUISettings, getGuardrailProviderSpecificParams } from '../networking';
|
||||
import PiiConfiguration from './pii_configuration';
|
||||
import GuardrailProviderFields from './guardrail_provider_fields';
|
||||
@@ -95,6 +95,10 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({
|
||||
|
||||
setGuardrailSettings(uiSettings);
|
||||
setProviderParams(providerParamsResp);
|
||||
|
||||
// Populate dynamic providers from API response
|
||||
populateGuardrailProviders(providerParamsResp);
|
||||
populateGuardrailProviderMap(providerParamsResp);
|
||||
} catch (error) {
|
||||
console.error('Error fetching guardrail data:', error);
|
||||
message.error('Failed to load guardrail configuration');
|
||||
@@ -288,6 +292,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({
|
||||
// Use pre-fetched provider params to copy recognised params
|
||||
if (providerParams && selectedProvider) {
|
||||
const providerKey = guardrail_provider_map[selectedProvider]?.toLowerCase();
|
||||
console.log("providerKey: ", providerKey);
|
||||
const providerSpecificParams = providerParams[providerKey] || {};
|
||||
|
||||
const allowedParams = new Set<string>();
|
||||
@@ -368,7 +373,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({
|
||||
dropdownRender={menu => menu}
|
||||
showSearch={true}
|
||||
>
|
||||
{Object.entries(GuardrailProviders).map(([key, value]) => (
|
||||
{Object.entries(getGuardrailProviders()).map(([key, value]) => (
|
||||
<Option
|
||||
key={key}
|
||||
value={key}
|
||||
@@ -510,6 +515,8 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({
|
||||
const renderOptionalParams = () => {
|
||||
if (!selectedProvider || !providerParams) return null;
|
||||
|
||||
console.log("guardrail_provider_map: ", guardrail_provider_map);
|
||||
console.log("selectedProvider: ", selectedProvider);
|
||||
const providerKey = guardrail_provider_map[selectedProvider]?.toLowerCase();
|
||||
const providerFields = providerParams && providerParams[providerKey];
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Form, Typography, Select, Input, Switch, Modal, message, Divider } from 'antd';
|
||||
import { Button, TextInput } from '@tremor/react';
|
||||
import { GuardrailProviders, guardrail_provider_map, guardrailLogoMap } from './guardrail_info_helpers';
|
||||
import { GuardrailProviders, guardrail_provider_map, guardrailLogoMap, getGuardrailProviders } from './guardrail_info_helpers';
|
||||
import { getGuardrailUISettings } from '../networking';
|
||||
import PiiConfiguration from './pii_configuration';
|
||||
|
||||
@@ -375,7 +375,7 @@ const EditGuardrailForm: React.FC<EditGuardrailFormProps> = ({
|
||||
disabled={true} // Disable changing provider in edit mode
|
||||
optionLabelProp="label"
|
||||
>
|
||||
{Object.entries(GuardrailProviders).map(([key, value]) => (
|
||||
{Object.entries(getGuardrailProviders()).map(([key, value]) => (
|
||||
<Option
|
||||
key={key}
|
||||
value={key}
|
||||
|
||||
@@ -484,6 +484,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({
|
||||
) || null}
|
||||
accessToken={accessToken}
|
||||
providerParams={guardrailProviderSpecificParams}
|
||||
value={guardrailData.litellm_params}
|
||||
/>
|
||||
|
||||
{/* Optional parameters */}
|
||||
|
||||
@@ -1,19 +1,65 @@
|
||||
// Legacy enum - keeping for backward compatibility
|
||||
export enum GuardrailProviders {
|
||||
PresidioPII = "Presidio PII",
|
||||
Bedrock = "Bedrock Guardrail",
|
||||
Lakera = "Lakera",
|
||||
AzureContentSafetyPromptShield = "Azure Content Safety Prompt Shield",
|
||||
AzureContentSafetyTextModeration = "Azure Content Safety Text Moderation"
|
||||
}
|
||||
|
||||
// Dynamic guardrail providers object - populated from API response
|
||||
export let DynamicGuardrailProviders: Record<string, string> = {};
|
||||
|
||||
// Function to populate dynamic providers from API response
|
||||
export const populateGuardrailProviders = (providerParamsResponse: Record<string, any>) => {
|
||||
const providers: Record<string, string> = {};
|
||||
|
||||
// Legacy hardcoded providers for backward compatibility
|
||||
providers.PresidioPII = "Presidio PII";
|
||||
providers.Bedrock = "Bedrock Guardrail";
|
||||
providers.Lakera = "Lakera";
|
||||
|
||||
// Add dynamic providers from API response
|
||||
Object.entries(providerParamsResponse).forEach(([key, value]) => {
|
||||
if (value && typeof value === 'object' && 'ui_friendly_name' in value) {
|
||||
// Create a key from the provider name (camelCase)
|
||||
const providerKey = key.split('_').map((word, index) =>
|
||||
index === 0 ? word.charAt(0).toUpperCase() + word.slice(1) :
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join('');
|
||||
|
||||
providers[providerKey] = value.ui_friendly_name;
|
||||
}
|
||||
});
|
||||
|
||||
DynamicGuardrailProviders = providers;
|
||||
return providers;
|
||||
};
|
||||
|
||||
// Function to get current guardrail providers (dynamic or fallback to legacy)
|
||||
export const getGuardrailProviders = () => {
|
||||
return Object.keys(DynamicGuardrailProviders).length > 0 ? DynamicGuardrailProviders : GuardrailProviders;
|
||||
};
|
||||
|
||||
export const guardrail_provider_map: Record<string, string> = {
|
||||
PresidioPII: "presidio",
|
||||
Bedrock: "bedrock",
|
||||
Lakera: "lakera_v2",
|
||||
AzureContentSafetyPromptShield: "azure/prompt_shield",
|
||||
AzureContentSafetyTextModeration: "azure/text_moderations"
|
||||
};
|
||||
|
||||
// Function to populate provider map from API response - updates the original map
|
||||
export const populateGuardrailProviderMap = (providerParamsResponse: Record<string, any>) => {
|
||||
// Add dynamic providers from API response directly to the main map
|
||||
Object.entries(providerParamsResponse).forEach(([key, value]) => {
|
||||
if (value && typeof value === 'object' && 'ui_friendly_name' in value) {
|
||||
// Create a key from the provider name (camelCase)
|
||||
const providerKey = key.split('_').map((word, index) =>
|
||||
index === 0 ? word.charAt(0).toUpperCase() + word.slice(1) :
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join('');
|
||||
|
||||
guardrail_provider_map[providerKey] = key; // Add directly to the main map
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Decides if we should render the PII config settings for a given provider
|
||||
// For now we only support PII config settings for Presidio PII
|
||||
@@ -21,9 +67,10 @@ export const shouldRenderPIIConfigSettings = (provider: string | null) => {
|
||||
if (!provider) {
|
||||
return false;
|
||||
}
|
||||
// cast provider to GuardrailProviders enum
|
||||
const providerEnum = GuardrailProviders[provider as keyof typeof GuardrailProviders];
|
||||
return providerEnum === GuardrailProviders.PresidioPII;
|
||||
// Check both dynamic and legacy providers
|
||||
const currentProviders = getGuardrailProviders();
|
||||
const providerEnum = currentProviders[provider as keyof typeof currentProviders];
|
||||
return providerEnum === "Presidio PII";
|
||||
};
|
||||
|
||||
// Decides if we should render the Azure Text Moderation config settings for a given provider
|
||||
@@ -31,19 +78,20 @@ export const shouldRenderAzureTextModerationConfigSettings = (provider: string |
|
||||
if (!provider) {
|
||||
return false;
|
||||
}
|
||||
// cast provider to GuardrailProviders enum
|
||||
const providerEnum = GuardrailProviders[provider as keyof typeof GuardrailProviders];
|
||||
return providerEnum === GuardrailProviders.AzureContentSafetyTextModeration;
|
||||
// Check both dynamic and legacy providers
|
||||
const currentProviders = getGuardrailProviders();
|
||||
const providerEnum = currentProviders[provider as keyof typeof currentProviders];
|
||||
return providerEnum === "Azure Content Safety Text Moderation";
|
||||
};
|
||||
|
||||
const asset_logos_folder = '../ui/assets/logos/';
|
||||
|
||||
export const guardrailLogoMap: Record<string, string> = {
|
||||
[GuardrailProviders.PresidioPII]: `${asset_logos_folder}presidio.png`,
|
||||
[GuardrailProviders.Bedrock]: `${asset_logos_folder}bedrock.svg`,
|
||||
[GuardrailProviders.Lakera]: `${asset_logos_folder}lakeraai.jpeg`,
|
||||
[GuardrailProviders.AzureContentSafetyPromptShield]: `${asset_logos_folder}presidio.png`,
|
||||
[GuardrailProviders.AzureContentSafetyTextModeration]: `${asset_logos_folder}presidio.png`,
|
||||
"Presidio PII": `${asset_logos_folder}presidio.png`,
|
||||
"Bedrock Guardrail": `${asset_logos_folder}bedrock.svg`,
|
||||
"Lakera": `${asset_logos_folder}lakeraai.jpeg`,
|
||||
"Azure Content Safety Prompt Shield": `${asset_logos_folder}presidio.png`,
|
||||
"Azure Content Safety Text Moderation": `${asset_logos_folder}presidio.png`,
|
||||
};
|
||||
|
||||
export const getGuardrailLogoAndName = (guardrailValue: string): { logo: string, displayName: string } => {
|
||||
@@ -60,9 +108,10 @@ export const getGuardrailLogoAndName = (guardrailValue: string): { logo: string,
|
||||
return { logo: "", displayName: guardrailValue };
|
||||
}
|
||||
|
||||
// Get the display name from GuardrailProviders enum and logo from map
|
||||
const displayName = GuardrailProviders[enumKey as keyof typeof GuardrailProviders];
|
||||
// Get the display name from current GuardrailProviders and logo from map
|
||||
const currentProviders = getGuardrailProviders();
|
||||
const displayName = currentProviders[enumKey as keyof typeof currentProviders];
|
||||
const logo = guardrailLogoMap[displayName as keyof typeof guardrailLogoMap];
|
||||
|
||||
return { logo, displayName };
|
||||
return { logo: logo || "", displayName: displayName || guardrailValue };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Form, Select, Spin } from "antd";
|
||||
import { TextInput } from "@tremor/react";
|
||||
import { GuardrailProviders, guardrail_provider_map } from './guardrail_info_helpers';
|
||||
import { GuardrailProviders, guardrail_provider_map, populateGuardrailProviders, populateGuardrailProviderMap } from './guardrail_info_helpers';
|
||||
import { getGuardrailProviderSpecificParams } from "../networking";
|
||||
import NumericalInput from "../shared/numerical_input";
|
||||
|
||||
@@ -9,6 +9,7 @@ interface GuardrailProviderFieldsProps {
|
||||
selectedProvider: string | null;
|
||||
accessToken?: string | null;
|
||||
providerParams?: ProviderParamsResponse | null;
|
||||
value?: Record<string, any> | null;
|
||||
}
|
||||
|
||||
interface ProviderParam {
|
||||
@@ -30,7 +31,8 @@ interface ProviderParamsResponse {
|
||||
const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
||||
selectedProvider,
|
||||
accessToken,
|
||||
providerParams: providerParamsProp = null
|
||||
providerParams: providerParamsProp = null,
|
||||
value = null
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [providerParams, setProviderParams] = useState<ProviderParamsResponse | null>(providerParamsProp);
|
||||
@@ -54,6 +56,10 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
||||
const data = await getGuardrailProviderSpecificParams(accessToken);
|
||||
console.log("Provider params API response:", data);
|
||||
setProviderParams(data);
|
||||
|
||||
// Populate dynamic providers from API response
|
||||
populateGuardrailProviders(data);
|
||||
populateGuardrailProviderMap(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching provider params:", error);
|
||||
setError("Failed to load provider parameters");
|
||||
@@ -96,10 +102,17 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
||||
return <div>No configuration fields available for this provider.</div>;
|
||||
}
|
||||
|
||||
console.log("Value:", value);
|
||||
// Convert object to array of entries and render fields
|
||||
const renderFields = (fields: { [key: string]: ProviderParam }, parentKey = "") => {
|
||||
const renderFields = (fields: { [key: string]: ProviderParam }, parentKey = "", parentValue?: any) => {
|
||||
return Object.entries(fields).map(([fieldKey, field]) => {
|
||||
const fullFieldKey = parentKey ? `${parentKey}.${fieldKey}` : fieldKey;
|
||||
const fieldValue = parentValue ? parentValue[fieldKey] : value?.[fieldKey];
|
||||
console.log("Field value:", fieldValue);
|
||||
// Skip ui_friendly_name - it's metadata for the UI dropdown, not a user configuration field
|
||||
if (fieldKey === "ui_friendly_name") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Skip optional_params - they are handled in a separate step
|
||||
if (fieldKey === "optional_params" && field.type === "nested" && field.fields) {
|
||||
@@ -112,7 +125,7 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
||||
<div key={fullFieldKey}>
|
||||
<div className="mb-2 font-medium">{fieldKey}</div>
|
||||
<div className="ml-4 border-l-2 border-gray-200 pl-4">
|
||||
{renderFields(field.fields, fullFieldKey)}
|
||||
{renderFields(field.fields, fullFieldKey, fieldValue)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -129,7 +142,7 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
||||
{field.type === "select" && field.options ? (
|
||||
<Select
|
||||
placeholder={field.description}
|
||||
defaultValue={field.default_value}
|
||||
defaultValue={fieldValue || field.default_value}
|
||||
>
|
||||
{field.options.map((option) => (
|
||||
<Select.Option key={option} value={option}>
|
||||
@@ -141,7 +154,7 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={field.description}
|
||||
defaultValue={field.default_value}
|
||||
defaultValue={fieldValue || field.default_value}
|
||||
>
|
||||
{field.options.map((option) => (
|
||||
<Select.Option key={option} value={option}>
|
||||
@@ -152,7 +165,7 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
||||
) : field.type === "bool" || field.type === "boolean" ? (
|
||||
<Select
|
||||
placeholder={field.description}
|
||||
defaultValue={field.default_value}
|
||||
defaultValue={fieldValue !== undefined ? String(fieldValue) : field.default_value}
|
||||
>
|
||||
<Select.Option value="true">True</Select.Option>
|
||||
<Select.Option value="false">False</Select.Option>
|
||||
@@ -162,16 +175,19 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
||||
step={1}
|
||||
width={400}
|
||||
placeholder={field.description}
|
||||
defaultValue={fieldValue !== undefined ? Number(fieldValue) : undefined}
|
||||
/>
|
||||
) : fieldKey.includes("password") || fieldKey.includes("secret") || fieldKey.includes("key") ? (
|
||||
<TextInput
|
||||
placeholder={field.description}
|
||||
type="password"
|
||||
defaultValue={fieldValue || ""}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
placeholder={field.description}
|
||||
type="text"
|
||||
defaultValue={fieldValue || ""}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
+5
-1
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Form, Select, Spin } from 'antd';
|
||||
import { TextInput } from '@tremor/react';
|
||||
import { GuardrailProviders, guardrail_provider_map } from './guardrail_info_helpers';
|
||||
import { GuardrailProviders, guardrail_provider_map, populateGuardrailProviders, populateGuardrailProviderMap } from './guardrail_info_helpers';
|
||||
import { getGuardrailProviderSpecificParams } from '../networking';
|
||||
import NumericalInput from '../shared/numerical_input';
|
||||
|
||||
@@ -52,6 +52,10 @@ const GuardrailProviderSpecificFields: React.FC<GuardrailProviderSpecificFieldsP
|
||||
try {
|
||||
const data = await getGuardrailProviderSpecificParams(accessToken);
|
||||
setProviderParams(data);
|
||||
|
||||
// Populate dynamic providers from API response
|
||||
populateGuardrailProviders(data);
|
||||
populateGuardrailProviderMap(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching provider params:", error);
|
||||
setError("Failed to load provider parameters");
|
||||
|
||||
Reference in New Issue
Block a user