diff --git a/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py b/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py index 7f167b8b660..b3f6dae40eb 100644 --- a/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py +++ b/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py @@ -40,21 +40,25 @@ class GenericGuardrailAPIOptionalParams(BaseModel): ) streaming_end_of_stream_only: Optional[bool] = Field( - default=False, + default=None, description=( - "If False (default), the guardrail runs on sampled chunks during the stream " - "at the cadence set by streaming_sampling_rate, and an in-flight BLOCKED " - "stops further chunks from streaming. If True, the guardrail runs once at " - "end of stream over the assembled response; lower cost and latency, but " - "flagged content has already streamed to the client before the terminal block." + "If False (default when unset), the guardrail runs on sampled chunks during " + "the stream at the cadence set by streaming_sampling_rate, and an in-flight " + "BLOCKED stops further chunks from streaming. If True, the guardrail runs " + "once at end of stream over the assembled response; lower cost and latency, " + "but flagged content has already streamed to the client before the terminal " + "block. Defaults are applied in GenericGuardrailAPI.__init__ when None so " + "unset optional_params does not shadow top-level litellm_params." ), ) streaming_sampling_rate: Optional[int] = Field( - default=5, + default=None, description=( "When streaming_end_of_stream_only is False, the guardrail runs every Nth " - "streamed chunk. Ignored when streaming_end_of_stream_only is True." + "streamed chunk. Ignored when streaming_end_of_stream_only is True. " + "Defaults to 5 in GenericGuardrailAPI.__init__ when None so unset " + "optional_params does not shadow top-level litellm_params." ), ) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py index d3263d71697..ad5f1f3e30d 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py @@ -1124,6 +1124,73 @@ class TestGenericGuardrailAPIStreamingConfig: assert guardrail.streaming_end_of_stream_only is False assert guardrail.streaming_sampling_rate == 3 + def test_initialize_guardrail_optional_params_defaults_do_not_shadow_top_level( + self, + ): + """Top-level streaming knobs win when optional_params only carries siblings.""" + from litellm.proxy.guardrails.guardrail_hooks.generic_guardrail_api import ( + initialize_guardrail, + ) + from litellm.types.guardrails import LitellmParams + from litellm.types.proxy.guardrails.guardrail_hooks.generic_guardrail_api import ( + GenericGuardrailAPIOptionalParams, + ) + + litellm_params = LitellmParams( + guardrail="generic_guardrail_api", + mode="post_call", + api_base="https://api.test.guardrail.com", + default_on=False, + ) + litellm_params.streaming_end_of_stream_only = True # type: ignore[attr-defined] + litellm_params.streaming_sampling_rate = 2 # type: ignore[attr-defined] + # Sibling optional_params only; streaming fields stay at Pydantic default None. + litellm_params.optional_params = GenericGuardrailAPIOptionalParams( # type: ignore[attr-defined] + additional_provider_specific_params={"tenant": "acme"}, + ) + + guardrail_config = {"guardrail_name": "test-generic-streaming-mixed"} + + with patch( + "litellm.logging_callback_manager.add_litellm_callback" + ): + guardrail = initialize_guardrail(litellm_params, guardrail_config) + + assert guardrail.streaming_end_of_stream_only is True + assert guardrail.streaming_sampling_rate == 2 + + def test_initialize_guardrail_explicit_optional_params_streaming_wins(self): + from litellm.proxy.guardrails.guardrail_hooks.generic_guardrail_api import ( + initialize_guardrail, + ) + from litellm.types.guardrails import LitellmParams + from litellm.types.proxy.guardrails.guardrail_hooks.generic_guardrail_api import ( + GenericGuardrailAPIOptionalParams, + ) + + litellm_params = LitellmParams( + guardrail="generic_guardrail_api", + mode="post_call", + api_base="https://api.test.guardrail.com", + default_on=False, + ) + litellm_params.streaming_end_of_stream_only = False # type: ignore[attr-defined] + litellm_params.streaming_sampling_rate = 9 # type: ignore[attr-defined] + litellm_params.optional_params = GenericGuardrailAPIOptionalParams( # type: ignore[attr-defined] + streaming_end_of_stream_only=True, + streaming_sampling_rate=1, + ) + + guardrail_config = {"guardrail_name": "test-generic-streaming-nested-wins"} + + with patch( + "litellm.logging_callback_manager.add_litellm_callback" + ): + guardrail = initialize_guardrail(litellm_params, guardrail_config) + + assert guardrail.streaming_end_of_stream_only is True + assert guardrail.streaming_sampling_rate == 1 + class TestGenericGuardrailAPIStreamingViaUnified: """Streaming output checks routed through UnifiedLLMGuardrails."""