Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion litellm/decisions/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
}
)


def supported_decisions_providers() -> tuple[str, ...]:
return tuple(DECISIONS_ENDPOINTS)

DecisionsQuestions: TypeAlias = (
Mapping[str, DecisionQuestion | Mapping[str, object]] | Sequence[OpenAIDecisionQuestion | Mapping[str, object]]
)
Expand All @@ -77,7 +81,7 @@ class _PreparedDecisionsRequest:
def _resolve_provider_model(model: str, custom_llm_provider: str | None) -> tuple[str, str]:
provider: Final = model.partition("/")[0] if custom_llm_provider is None else custom_llm_provider
if provider not in DECISIONS_ENDPOINTS:
supported: Final = ", ".join(DECISIONS_ENDPOINTS)
supported: Final = ", ".join(supported_decisions_providers())
raise litellm.BadRequestError(
message=f"Unknown Decisions provider '{provider}'. Supported providers: {supported}",
model=model,
Expand Down
112 changes: 110 additions & 2 deletions litellm/proxy/_lazy_openapi_snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -10214,6 +10214,32 @@
"description": "Will mask response content if guardrail makes any changes",
"title": "Mask Response Content"
},
"max_concurrent_decision_calls": {
"anyOf": [
{
"exclusiveMinimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"description": "Maximum decisions calls in flight at once on the guardrail in each proxy worker. Implemented by guardrail='decision_model'.",
"title": "Max Concurrent Decision Calls"
},
"max_input_chars": {
"anyOf": [
{
"exclusiveMinimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"description": "Character budget for each text sent to the guardrail's model. Implemented by guardrail='decision_model'.",
"title": "Max Input Chars"
},
"model": {
"anyOf": [
{
Expand Down Expand Up @@ -10485,7 +10511,7 @@
},
"unreachable_fallback": {
"default": "fail_closed",
"description": "Behavior when a guardrail endpoint is unreachable due to network errors. Implemented by guardrail='generic_guardrail_api', 'agent_365', 'akto', 'vigil_guard', 'repelloai', 'headroom', 'compresr', and 'typesafe'. 'fail_closed' raises an error (default). 'fail_open' logs a critical error and allows the request to proceed.",
"description": "Behavior when a guardrail endpoint is unreachable due to network errors. Implemented by guardrail='generic_guardrail_api', 'agent_365', 'akto', 'vigil_guard', 'repelloai', 'headroom', 'compresr', and 'typesafe'. 'fail_closed' raises an error (default). 'fail_open' logs a critical error and allows the request to proceed. Also implemented by guardrail='decision_model'.",
"enum": [
"fail_closed",
"fail_open"
Expand Down Expand Up @@ -11027,6 +11053,43 @@
"title": "CreateGuardrailRequest",
"type": "object"
},
"DecisionModelCheck": {
"description": "One predicate the decision model scores the request or response against.",
"properties": {
"action": {
"default": "block",
"enum": [
"block",
"log"
],
"title": "Action",
"type": "string"
},
"instructions": {
"minLength": 1,
"title": "Instructions",
"type": "string"
},
"name": {
"minLength": 1,
"title": "Name",
"type": "string"
},
"threshold": {
"default": 0.7,
"maximum": 1.0,
"minimum": 0.0,
"title": "Threshold",
"type": "number"
}
},
"required": [
"name",
"instructions"
],
"title": "DecisionModelCheck",
"type": "object"
},
"GUARDRAIL_DEFINITION_LOCATION": {
"enum": [
"db",
Expand Down Expand Up @@ -11947,11 +12010,18 @@
{
"$ref": "#/components/schemas/BedrockChecksConfigModel"
},
{
"items": {
"$ref": "#/components/schemas/DecisionModelCheck"
},
"type": "array"
},
{
"type": "null"
}
],
"description": "Inline safeguards for the resource-less InvokeGuardrailChecks API (contentFilter / promptAttack / sensitiveInformation). When set, the guardrail calls InvokeGuardrailChecks instead of ApplyGuardrail and no guardrailIdentifier is required. Mutually exclusive with guardrailIdentifier."
"description": "Inline Bedrock InvokeGuardrailChecks config for guardrail='bedrock', or the predicate checks a decision_model guardrail scores for guardrail='decision_model'",
"title": "Checks"
},
"chunk_budget_chars": {
"default": 25000,
Expand Down Expand Up @@ -12067,6 +12137,18 @@
"description": "Python-like code containing the apply_guardrail function for custom guardrail logic",
"title": "Custom Code"
},
"decision_model": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "For guardrail='decision_model': the Decisions-API model that scores each check",
"title": "Decision Model"
},
"deepkeep_firewall_id": {
"anyOf": [
{
Expand Down Expand Up @@ -12477,6 +12559,32 @@
"description": "Will mask response content if guardrail makes any changes",
"title": "Mask Response Content"
},
"max_concurrent_decision_calls": {
"anyOf": [
{
"exclusiveMinimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"description": "Maximum decisions calls in flight at once on the guardrail in each proxy worker. Implemented by guardrail='decision_model'.",
"title": "Max Concurrent Decision Calls"
},
"max_input_chars": {
"anyOf": [
{
"exclusiveMinimum": 0.0,
"type": "integer"
},
{
"type": "null"
}
],
"description": "Character budget for each text sent to the guardrail's model. Implemented by guardrail='decision_model'.",
"title": "Max Input Chars"
},
"metadata": {
"anyOf": [
{
Expand Down
12 changes: 9 additions & 3 deletions litellm/proxy/guardrails/guardrail_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ async def list_guardrails_v2(
number_of_asterisks=4,
)
masked_litellm_params = (
BaseLitellmParams(**masked_litellm_params_dict) if masked_litellm_params_dict else None
BaseLitellmParams.model_validate(masked_litellm_params_dict) if masked_litellm_params_dict else None
)
guardrail_configs.append(
GuardrailInfoResponse(
Expand Down Expand Up @@ -300,7 +300,9 @@ async def list_guardrails_v2(
number_of_asterisks=4,
)
masked_in_memory_litellm_params_typed = (
BaseLitellmParams(**masked_in_memory_litellm_params) if masked_in_memory_litellm_params else None
BaseLitellmParams.model_validate(masked_in_memory_litellm_params)
if masked_in_memory_litellm_params
else None
)
guardrail_configs.append(
GuardrailInfoResponse(
Expand Down Expand Up @@ -1360,7 +1362,9 @@ async def get_guardrail_info(guardrail_id: str):
unmasked_length=4,
number_of_asterisks=4,
)
masked_litellm_params = BaseLitellmParams(**masked_litellm_params_dict) if masked_litellm_params_dict else None
masked_litellm_params = (
BaseLitellmParams.model_validate(masked_litellm_params_dict) if masked_litellm_params_dict else None
)

return GuardrailInfoResponse(
guardrail_id=result.get("guardrail_id"),
Expand Down Expand Up @@ -1392,6 +1396,7 @@ async def get_guardrail_ui_settings():
- PII entity categories for UI organization
- Content filter settings (patterns and categories)
"""
from litellm.decisions.main import supported_decisions_providers
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.patterns import (
PATTERN_CATEGORIES,
get_available_content_categories,
Expand Down Expand Up @@ -1423,6 +1428,7 @@ async def get_guardrail_ui_settings():
supported_modes=[mode.value for mode in GuardrailEventHooks],
supported_modes_by_provider=supported_modes_by_provider,
pii_entity_categories=category_maps,
decision_model_providers=supported_decisions_providers(),
content_filter_settings={
"prebuilt_patterns": get_pattern_metadata(),
"pattern_categories": list(PATTERN_CATEGORIES.keys()),
Expand Down
109 changes: 109 additions & 0 deletions litellm/proxy/guardrails/guardrail_hooks/decision_model/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from __future__ import annotations

from collections.abc import Sequence
from typing import TYPE_CHECKING, Final

from pydantic import TypeAdapter, ValidationError

from litellm.types.guardrails import (
GuardrailEventHooks,
Mode,
SupportedGuardrailIntegrations,
)
from litellm.types.proxy.guardrails.guardrail_hooks.decision_model import (
DecisionModelCheck,
)

from .decision_model import DecisionModelGuardrail

if TYPE_CHECKING:
from litellm.types.guardrails import Guardrail, LitellmParams

_CHECKS_ADAPTER: Final = TypeAdapter(list[DecisionModelCheck])


def _coerce_event_hook(
mode: str | Sequence[str] | Mode,
) -> GuardrailEventHooks | tuple[GuardrailEventHooks, ...] | Mode:
if isinstance(mode, Mode):
return mode
if isinstance(mode, str):
return GuardrailEventHooks(mode)
return tuple(GuardrailEventHooks(item) for item in mode)


def _parse_checks(raw_checks: object) -> tuple[DecisionModelCheck, ...]:
try:
checks: Final = _CHECKS_ADAPTER.validate_python(raw_checks)
except ValidationError as error:
raise ValueError(f"decision_model guardrail has invalid checks: {error}") from error
return tuple(checks)


def _streaming_end_of_stream_only(litellm_params: LitellmParams) -> bool:
value: Final[object] = getattr(litellm_params, "streaming_end_of_stream_only", None)
if value is None:
return False
if not isinstance(value, bool):
raise TypeError(f"decision_model guardrail streaming_end_of_stream_only must be a boolean, got {value!r}")
return value


def _streaming_sampling_rate(litellm_params: LitellmParams) -> int:
value: Final[object] = getattr(litellm_params, "streaming_sampling_rate", None)
if value is None:
return 5
if isinstance(value, bool) or not isinstance(value, int):
raise TypeError(f"decision_model guardrail streaming_sampling_rate must be an integer, got {value!r}")
if value < 1:
raise ValueError(f"decision_model guardrail streaming_sampling_rate must be at least 1, got {value}")
return value


def initialize_guardrail(litellm_params: LitellmParams, guardrail: Guardrail) -> DecisionModelGuardrail:
import litellm

decision_model: Final = litellm_params.decision_model
if not decision_model:
raise ValueError("decision_model guardrail requires decision_model in litellm_params")

checks: Final = _parse_checks(litellm_params.checks)

_callback: Final = DecisionModelGuardrail(
guardrail_name=guardrail["guardrail_name"],
decision_model=decision_model,
checks=checks,
event_hook=_coerce_event_hook(litellm_params.mode),
default_on=litellm_params.default_on or False,
unreachable_fallback=(
litellm_params.unreachable_fallback
if "unreachable_fallback" in litellm_params.model_fields_set
else "fail_closed"
),
max_input_chars=litellm_params.max_input_chars or 24000,
timeout=litellm_params.timeout,
max_concurrent_decision_calls=litellm_params.max_concurrent_decision_calls or 8,
streaming_end_of_stream_only=_streaming_end_of_stream_only(litellm_params),
streaming_sampling_rate=_streaming_sampling_rate(litellm_params),
)
litellm.logging_callback_manager.add_litellm_callback( # pyright: ignore[reportUnknownMemberType] # callback manager is untyped
_callback
)
return _callback


guardrail_initializer_registry: Final = {
SupportedGuardrailIntegrations.DECISION_MODEL.value: initialize_guardrail,
}

guardrail_class_registry: Final = {
SupportedGuardrailIntegrations.DECISION_MODEL.value: DecisionModelGuardrail,
}


__all__ = [
"DecisionModelGuardrail",
"guardrail_class_registry",
"guardrail_initializer_registry",
"initialize_guardrail",
]
Loading
Loading