fix(guardrails): skip None fields in in-memory guardrail updates so constructor defaults survive

This commit is contained in:
mateo-berri 2026-09-01 18:38:38 -07:00
parent 8c72342ad5
commit 2286091a9a
3 changed files with 27 additions and 5 deletions

View file

@ -1298,6 +1298,9 @@ class CustomGuardrail(CustomLogger):
resync ``event_hook`` when the update carries a new ``mode``. The new
mode is validated against ``supported_event_hooks`` before any state
is mutated, so a rejected update leaves the guardrail untouched.
``None`` values are skipped because both sources serialize every unset
LitellmParams field as ``None``; applying them would clobber
constructor-derived state (e.g. dict defaults) with ``None``.
"""
updated_params: Final[Mapping[str, object]] = (
litellm_params if isinstance(litellm_params, Mapping) else vars(litellm_params)
@ -1307,7 +1310,8 @@ class CustomGuardrail(CustomLogger):
if new_event_hook is not None and self.supported_event_hooks:
self._validate_or_warn_event_hook(new_event_hook, self.supported_event_hooks)
for key, value in updated_params.items():
setattr(self, key, value)
if value is not None:
setattr(self, key, value)
if new_event_hook is not None:
self.event_hook = new_event_hook

View file

@ -176,10 +176,10 @@ class ToolPermissionGuardrail(CustomGuardrail):
super().update_in_memory_litellm_params(litellm_params)
# The generic update above sets ``self.rules`` from the incoming value
# (None on a partial update that omits rules), but never rebuilds the
# compiled maps. Rebuild them when rules are provided; otherwise restore
# the previous ruleset so a partial update doesn't silently wipe it. An
# explicit empty list still clears the rules.
# (skipping None) but never rebuilds the compiled maps. Rebuild them
# when a rules list is provided; otherwise restore the previous ruleset
# so a non-list value can't silently wipe it. An explicit empty list
# still clears the rules.
rules: Final = params.get("rules")
if isinstance(rules, list):
try:

View file

@ -2281,6 +2281,24 @@ class TestUpdateInMemoryLitellmParams:
assert getattr(guardrail, "api_base", None) == "https://guardrail.example.com"
assert guardrail.should_run_guardrail(data={}, event_type=GuardrailEventHooks.post_call) is True
def test_none_values_do_not_clobber_constructor_state(self):
guardrail = self._guardrail()
guardrail.additional_provider_specific_params = {"team": "security"}
guardrail.api_base = "https://guardrail.example.com"
guardrail.update_in_memory_litellm_params(
{
"mode": "post_call",
"api_base": None,
"additional_provider_specific_params": None,
"extra_headers": None,
}
)
assert guardrail.additional_provider_specific_params == {"team": "security"}
assert guardrail.api_base == "https://guardrail.example.com"
assert guardrail.event_hook is GuardrailEventHooks.post_call
def test_strict_mode_rejects_unsupported_mode_without_mutating(self, monkeypatch):
monkeypatch.delenv("LITELLM_STRICT_GUARDRAIL_MODES", raising=False)
guardrail = self._guardrail()