mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-23 05:34:41 +00:00
Merge 0f7e214168 into 8b1859c50d
This commit is contained in:
commit
8120c6dc48
2 changed files with 143 additions and 0 deletions
|
|
@ -1,3 +1,4 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import Final, cast
|
||||
|
||||
import litellm
|
||||
|
|
@ -34,6 +35,8 @@ def init_guardrails_v2(
|
|||
source="config",
|
||||
)
|
||||
except (ValueError, TypeError) as init_error:
|
||||
entry = cast("Mapping[str, object]", guardrail) # cast-ok: config entries are str-keyed mappings
|
||||
_raise_if_default_on(entry, init_error)
|
||||
verbose_proxy_logger.error(
|
||||
"Skipping guardrail '%s': invalid configuration, proxy is starting WITHOUT this guardrail: %s",
|
||||
guardrail.get("guardrail_name"),
|
||||
|
|
@ -49,6 +52,39 @@ def init_guardrails_v2(
|
|||
_populate_router_guardrail_list(guardrail_list=guardrail_list)
|
||||
|
||||
|
||||
_TRUE_WORDS: Final = frozenset({"true", "1", "yes", "on", "t", "y"})
|
||||
|
||||
|
||||
def _raise_if_default_on(guardrail: Mapping[str, object], init_error: Exception) -> None:
|
||||
if _is_default_on(guardrail):
|
||||
raise ValueError(
|
||||
f"Guardrail '{guardrail.get('guardrail_name')}' is default_on and could not be "
|
||||
f"initialized, so the proxy will not start without it: {init_error}"
|
||||
) from init_error
|
||||
|
||||
|
||||
def _config_value(entry: object, key: str) -> object:
|
||||
if isinstance(entry, Mapping):
|
||||
return cast("Mapping[str, object]", entry).get(key) # cast-ok: config mappings are str-keyed
|
||||
return getattr(entry, key, None)
|
||||
|
||||
|
||||
def _is_default_on(guardrail: Mapping[str, object]) -> bool:
|
||||
"""Whether a config entry asks to run on every request (`litellm_params.default_on`).
|
||||
|
||||
Read from the raw config, before `LitellmParams` has parsed it, so the same values
|
||||
pydantic would accept as true (`true`, `"true"`, `1`, `"yes"`) count as true here.
|
||||
"""
|
||||
value: Final = _config_value(guardrail.get("litellm_params"), "default_on")
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, int):
|
||||
return value == 1
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in _TRUE_WORDS
|
||||
return False
|
||||
|
||||
|
||||
def _populate_router_guardrail_list(guardrail_list: list[Guardrail]) -> None:
|
||||
"""
|
||||
Populate the router's guardrail_list from initialized guardrails.
|
||||
|
|
|
|||
|
|
@ -371,6 +371,113 @@ def test_init_guardrails_v2_skips_invalid_guardrail_instead_of_crashing_boot():
|
|||
assert "healthy_presidio" in guardrail_names
|
||||
|
||||
|
||||
def test_init_guardrails_v2_stops_boot_when_a_default_on_guardrail_cannot_be_initialized():
|
||||
"""
|
||||
BerriAI/litellm#45028: a `default_on` guardrail whose type this release does not know
|
||||
(a config written for a newer LiteLLM, for example) used to be skipped with one error
|
||||
line, and the proxy started without it. For a guardrail that is meant to run on every
|
||||
request that is fail-open: every request reaches the provider carrying whatever the
|
||||
guardrail was configured to withhold. Initialization of a default_on guardrail is fatal.
|
||||
"""
|
||||
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
|
||||
|
||||
IN_MEMORY_GUARDRAIL_HANDLER.IN_MEMORY_GUARDRAILS.clear()
|
||||
IN_MEMORY_GUARDRAIL_HANDLER.guardrail_id_to_custom_guardrail.clear()
|
||||
|
||||
all_guardrails = [
|
||||
{
|
||||
"guardrail_name": "pii-redaction",
|
||||
"litellm_params": {
|
||||
"guardrail": "a_guardrail_type_this_release_does_not_know",
|
||||
"mode": "pre_call",
|
||||
"default_on": True,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
with pytest.raises(ValueError, match="'pii-redaction' is default_on and could not be initialized"):
|
||||
init_guardrails_v2(all_guardrails=all_guardrails)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("default_on", [True, "true", "True", 1, "1", "yes"])
|
||||
def test_default_on_is_read_the_way_litellm_params_would_parse_it(default_on):
|
||||
"""The config value reaches this check raw, before LitellmParams parses it. Every spelling
|
||||
pydantic accepts as true must stop the boot too; `default_on: "true"` from a YAML string
|
||||
is as required as `default_on: true`."""
|
||||
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
|
||||
|
||||
IN_MEMORY_GUARDRAIL_HANDLER.IN_MEMORY_GUARDRAILS.clear()
|
||||
IN_MEMORY_GUARDRAIL_HANDLER.guardrail_id_to_custom_guardrail.clear()
|
||||
|
||||
all_guardrails = [
|
||||
{
|
||||
"guardrail_name": "pii-redaction",
|
||||
"litellm_params": {
|
||||
"guardrail": "a_guardrail_type_this_release_does_not_know",
|
||||
"mode": "pre_call",
|
||||
"default_on": default_on,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
with pytest.raises(ValueError, match="is default_on and could not be initialized"):
|
||||
init_guardrails_v2(all_guardrails=all_guardrails)
|
||||
|
||||
|
||||
def test_default_on_is_read_from_a_parsed_litellm_params_object_too():
|
||||
"""A config entry can arrive with `litellm_params` already parsed into a model, not a dict;
|
||||
the attribute is read the same way. `None` and an unknown spelling count as not default_on."""
|
||||
from litellm.proxy.guardrails.init_guardrails import _is_default_on
|
||||
from litellm.types.guardrails import LitellmParams
|
||||
|
||||
parsed = LitellmParams(guardrail="presidio", mode="pre_call", default_on=True)
|
||||
assert _is_default_on({"litellm_params": parsed}) is True
|
||||
assert _is_default_on({"litellm_params": LitellmParams(guardrail="presidio", mode="pre_call")}) is False
|
||||
assert _is_default_on({"litellm_params": {"default_on": None}}) is False
|
||||
assert _is_default_on({"litellm_params": {"default_on": "sometimes"}}) is False
|
||||
assert _is_default_on({}) is False
|
||||
|
||||
|
||||
def test_init_guardrails_v2_still_skips_an_optional_guardrail_it_cannot_initialize():
|
||||
"""
|
||||
The skip (#34940) stays for guardrails that are not default_on: nothing runs them
|
||||
unless a request names them, so starting without one withholds nothing by default.
|
||||
The healthy guardrail beside it is still registered.
|
||||
"""
|
||||
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
|
||||
|
||||
IN_MEMORY_GUARDRAIL_HANDLER.IN_MEMORY_GUARDRAILS.clear()
|
||||
IN_MEMORY_GUARDRAIL_HANDLER.guardrail_id_to_custom_guardrail.clear()
|
||||
|
||||
all_guardrails = [
|
||||
{
|
||||
"guardrail_name": "optional-unknown",
|
||||
"litellm_params": {
|
||||
"guardrail": "a_guardrail_type_this_release_does_not_know",
|
||||
"mode": "pre_call",
|
||||
"default_on": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"guardrail_name": "healthy_presidio",
|
||||
"litellm_params": {
|
||||
"guardrail": SupportedGuardrailIntegrations.PRESIDIO.value,
|
||||
"mode": "pre_call",
|
||||
"presidio_analyzer_api_base": "https://fakelink.com/v1/presidio/analyze",
|
||||
"presidio_anonymizer_api_base": "https://fakelink.com/v1/presidio/anonymize",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
init_guardrails_v2(all_guardrails=all_guardrails)
|
||||
|
||||
guardrail_names = {
|
||||
guardrail["guardrail_name"] for guardrail in IN_MEMORY_GUARDRAIL_HANDLER.IN_MEMORY_GUARDRAILS.values()
|
||||
}
|
||||
assert "optional-unknown" not in guardrail_names
|
||||
assert "healthy_presidio" in guardrail_names
|
||||
|
||||
|
||||
def test_init_guardrails_v2_keeps_guardrail_whose_env_values_are_blank():
|
||||
"""
|
||||
Regression: a config.yaml guardrail with `default_on: os.environ/GUARDRAIL_DEFAULT_ON` and
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue