From 612c8cdf83eeec96006c617642f2277abfb83621 Mon Sep 17 00:00:00 2001 From: Chenglun Hu Date: Tue, 9 Jun 2026 15:30:25 +0800 Subject: [PATCH 1/2] fix(content_filter): wire keyword_redaction_tag and pattern_redaction_format through initialize_guardrail Fixes #30008. ContentFilterGuardrail accepts keyword_redaction_tag and pattern_redaction_format in its constructor (litellm_content_filter/content_filter.py:165), the type schema has them (litellm_content_filter.py:155,159), but initialize_guardrail constructed the filter without passing them through. Configuring them in the YAML did nothing because the bootstrap dropped them on the floor. Adds the two passthrough lines. Two regression tests: one for the propagation path on this PR, one for default-fallback so the existing behavior is pinned. --- .../litellm_content_filter/__init__.py | 8 +- .../test_initialize_guardrail.py | 81 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_initialize_guardrail.py diff --git a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/__init__.py index 8f1cee2672a..af0bd3313fb 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/__init__.py +++ b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/__init__.py @@ -47,7 +47,13 @@ def initialize_guardrail( competitor_intent_config=getattr(litellm_params, "competitor_intent_config", None), end_session_after_n_fails=getattr(litellm_params, "end_session_after_n_fails", None), on_violation=getattr(litellm_params, "on_violation", None), - realtime_violation_message=getattr(litellm_params, "realtime_violation_message", None), + realtime_violation_message=getattr( + litellm_params, "realtime_violation_message", None + ), + keyword_redaction_tag=getattr(litellm_params, "keyword_redaction_tag", None), + pattern_redaction_format=getattr( + litellm_params, "pattern_redaction_format", None + ), ) litellm.logging_callback_manager.add_litellm_callback(content_filter_guardrail) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_initialize_guardrail.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_initialize_guardrail.py new file mode 100644 index 00000000000..604787e2cec --- /dev/null +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_initialize_guardrail.py @@ -0,0 +1,81 @@ +"""Regression for #30008 — keyword_redaction_tag / pattern_redaction_format must be +propagated from LitellmParams through initialize_guardrail to ContentFilterGuardrail.""" + +import os +import sys +from types import SimpleNamespace +from unittest.mock import patch + +import pytest + +sys.path.insert(0, os.path.abspath("../../")) + +import litellm +from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter import ( + initialize_guardrail, +) +from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import ( + ContentFilterGuardrail, +) + + +def _make_litellm_params(**overrides): + """Build a permissive stand-in for LitellmParams — fields are read via getattr.""" + defaults = dict( + patterns=None, + blocked_words=None, + blocked_words_file=None, + mode="pre_call", + default_on=False, + categories=None, + severity_threshold="medium", + image_model=None, + competitor_intent_config=None, + end_session_after_n_fails=None, + on_violation=None, + realtime_violation_message=None, + keyword_redaction_tag=None, + pattern_redaction_format=None, + ) + defaults.update(overrides) + return SimpleNamespace(**defaults) + + +@pytest.fixture(autouse=True) +def _quiet_callback_register(): + """initialize_guardrail registers a callback on success — keep test side-effect-free.""" + with patch.object( + litellm.logging_callback_manager, "add_litellm_callback", lambda _: None + ): + yield + + +def test_initialize_guardrail_propagates_custom_redaction_tags(): + """Custom keyword_redaction_tag + pattern_redaction_format must reach the filter.""" + params = _make_litellm_params( + keyword_redaction_tag="***REDACTED***", + pattern_redaction_format="***{pattern_name}***", + ) + guardrail = {"guardrail_name": "test_filter"} + + filter_obj = initialize_guardrail(litellm_params=params, guardrail=guardrail) + + assert isinstance(filter_obj, ContentFilterGuardrail) + assert filter_obj.keyword_redaction_tag == "***REDACTED***" + assert filter_obj.pattern_redaction_format == "***{pattern_name}***" + + +def test_initialize_guardrail_defaults_when_redaction_tags_missing(): + """Omitting the redaction tags falls back to the class defaults (current behavior).""" + params = _make_litellm_params() + guardrail = {"guardrail_name": "test_filter_defaults"} + + filter_obj = initialize_guardrail(litellm_params=params, guardrail=guardrail) + + assert ( + filter_obj.keyword_redaction_tag == ContentFilterGuardrail.KEYWORD_REDACTION_STR + ) + assert ( + filter_obj.pattern_redaction_format + == ContentFilterGuardrail.PATTERN_REDACTION_FORMAT + ) From ad4fdcf07180ee8d8df5449ff3331f86dbb9ac0c Mon Sep 17 00:00:00 2001 From: Chenglun Hu Date: Thu, 16 Jul 2026 03:28:53 +0800 Subject: [PATCH 2/2] chore: ruff format --- .../litellm_content_filter/__init__.py | 8 ++------ .../content_filter/test_initialize_guardrail.py | 13 +++---------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/__init__.py index af0bd3313fb..68edd68881e 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/__init__.py +++ b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/__init__.py @@ -47,13 +47,9 @@ def initialize_guardrail( competitor_intent_config=getattr(litellm_params, "competitor_intent_config", None), end_session_after_n_fails=getattr(litellm_params, "end_session_after_n_fails", None), on_violation=getattr(litellm_params, "on_violation", None), - realtime_violation_message=getattr( - litellm_params, "realtime_violation_message", None - ), + realtime_violation_message=getattr(litellm_params, "realtime_violation_message", None), keyword_redaction_tag=getattr(litellm_params, "keyword_redaction_tag", None), - pattern_redaction_format=getattr( - litellm_params, "pattern_redaction_format", None - ), + pattern_redaction_format=getattr(litellm_params, "pattern_redaction_format", None), ) litellm.logging_callback_manager.add_litellm_callback(content_filter_guardrail) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_initialize_guardrail.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_initialize_guardrail.py index 604787e2cec..fe5295b2be3 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_initialize_guardrail.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_initialize_guardrail.py @@ -44,9 +44,7 @@ def _make_litellm_params(**overrides): @pytest.fixture(autouse=True) def _quiet_callback_register(): """initialize_guardrail registers a callback on success — keep test side-effect-free.""" - with patch.object( - litellm.logging_callback_manager, "add_litellm_callback", lambda _: None - ): + with patch.object(litellm.logging_callback_manager, "add_litellm_callback", lambda _: None): yield @@ -72,10 +70,5 @@ def test_initialize_guardrail_defaults_when_redaction_tags_missing(): filter_obj = initialize_guardrail(litellm_params=params, guardrail=guardrail) - assert ( - filter_obj.keyword_redaction_tag == ContentFilterGuardrail.KEYWORD_REDACTION_STR - ) - assert ( - filter_obj.pattern_redaction_format - == ContentFilterGuardrail.PATTERN_REDACTION_FORMAT - ) + assert filter_obj.keyword_redaction_tag == ContentFilterGuardrail.KEYWORD_REDACTION_STR + assert filter_obj.pattern_redaction_format == ContentFilterGuardrail.PATTERN_REDACTION_FORMAT