From 2286091a9a08e231e87c6e8abe7274364d7a14ad Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:38:38 -0700 Subject: [PATCH] fix(guardrails): skip None fields in in-memory guardrail updates so constructor defaults survive --- litellm/integrations/custom_guardrail.py | 6 +++++- .../guardrail_hooks/tool_permission.py | 8 ++++---- .../integrations/test_custom_guardrail.py | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index 8754d116537..1562f3d092e 100644 --- a/litellm/integrations/custom_guardrail.py +++ b/litellm/integrations/custom_guardrail.py @@ -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 diff --git a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py index 88e24db207a..d6bea312031 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py +++ b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py @@ -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: diff --git a/tests/test_litellm/integrations/test_custom_guardrail.py b/tests/test_litellm/integrations/test_custom_guardrail.py index c84fcd5ac11..3983f698ef0 100644 --- a/tests/test_litellm/integrations/test_custom_guardrail.py +++ b/tests/test_litellm/integrations/test_custom_guardrail.py @@ -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()