feat(guardrails): expose streaming knobs on generic_guardrail_api - #31730
Conversation
Wire streaming_end_of_stream_only and streaming_sampling_rate through optional params, initialize_guardrail, and get_config_model so the generic guardrail API participates in UnifiedLLMGuardrails streaming checks with configurable cadence and end-of-stream-only mode.
Avoids a new UP006 violation that tripped the ruff strict-rule budget gate on the PR lint job.
Non-None Pydantic defaults on GenericGuardrailAPIOptionalParams made _get_config_value treat unset nested fields as explicit values, which shadowed top-level litellm_params streaming flags whenever any other optional_params key was present. Real defaults stay in the constructor.
Validate streaming_sampling_rate >= 1 in the constructor and Pydantic optional_params (ge=1), and add /v1/responses streaming coverage through the unified post-call hook so Responses API usage is exercised alongside chat completions.
Guardrail API/UI delivers optional_params as a plain dict, so getattr was silently ignoring streaming_sampling_rate and streaming_end_of_stream_only. Handle both dict and model shapes in _get_config_value with regression tests.
Keep generic_guardrail_api streaming_end_of_stream_only / streaming_sampling_rate and get_config_model alongside upstream fail_on_error, GuardrailToolParam extra=allow, and related tests
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 2b464fd. Configure here.
Greptile SummaryThis PR adds first-class streaming configuration (
Confidence Score: 5/5Safe to merge; the change is additive and the new defaults are identical to what the unified guardrail hook already applied via The implementation is logically sound across all three layers (types, constructor, plumbing helper). Defaults are proven backward-compatible with the No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/init.py | Adds _get_config_value helper and wires streaming_end_of_stream_only / streaming_sampling_rate through initialize_guardrail; also adds a return of the created callback (new, but backward-compatible). Logic is correct and defaults match what UnifiedLLMGuardrails already applied via getattr. |
| litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py | Adds streaming_end_of_stream_only and streaming_sampling_rate constructor parameters with validation (raises ValueError for < 1), stores them as typed instance attributes, and adds get_config_model() static method. Implementation is clean and consistent with other guardrail integrations. |
| litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py | Adds streaming_end_of_stream_only: Optional[bool] and streaming_sampling_rate: Optional[int] (with ge=1) to GenericGuardrailAPIOptionalParams. Both default to None to avoid shadowing top-level litellm_params values. Correct use of Pydantic Field constraints. |
| tests/test_litellm/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py | Adds ~700 lines of new tests covering defaults, overrides, invalid rate rejection, get_config_model, initialize_guardrail plumbing with all config shapes (model/dict/mixed priority), and end-to-end unified-hook behavior for both /chat/completions and /v1/responses. Existing test changes remove unused dead variables only. |
Reviews (3): Last reviewed commit: "style(guardrails): ruff format generic_g..." | Re-trigger Greptile
| @@ -1,4 +1,4 @@ | |||
| from typing import TYPE_CHECKING | |||
| from typing import TYPE_CHECKING, Any, Optional | |||
There was a problem hiding this comment.
Avoid broad Any
_get_config_value adds broad Any annotations for untyped config shapes, which increases the static typing budget this repo tracks. The project guidance prefers object for broad parameters because it still accepts None without adding Any; this helper can keep the same runtime behavior by typing the inputs and return as object.
Rule Used: In this repo, prefer object over Optional[Any]... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Greptile SummaryThis PR exposes streaming configuration for
Confidence Score: 4/5The implementation is narrowly scoped and well covered by tests for the new streaming configuration paths. The remaining issue is a typing cleanup in the helper surface rather than a functional streaming behavior concern. litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/init.py
What T-Rex did
Reviews (1): Last reviewed commit: "style(guardrails): ruff format generic_g..." | Re-trigger Greptile |
| @@ -8,9 +8,23 @@ | |||
| from litellm.types.guardrails import Guardrail, LitellmParams | |||
|
|
|||
|
|
|||
| def _get_config_value(litellm_params: Any, optional_params: Any, attribute_name: str) -> Optional[Any]: | |||
There was a problem hiding this comment.
Use object typing
This helper introduces broad Any annotations for config values, which adds to the static type checker’s Any budget. Since these parameters can be arbitrary objects or dicts and None is still accepted by object, typing them as object preserves the current runtime behavior while keeping the proxy module aligned with the repo’s typing guidance.
Rule Used: In this repo, prefer object over Optional[Any]... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
@greptileai I think this finding is out of scope for this PR. The mutation pattern described here is in UnifiedLLMGuardrails.async_post_call_streaming_iterator_hook / process_output_streaming_response, not in generic_guardrail_api. This PR does not modify unified_guardrail.py or the responses_so_far accumulation logic. If responses_so_far is being mutated in-place and corrupting later sampled scans, that would be a pre-existing unified streaming-hook issue affecting all sampling guardrails, not a regression introduced by this PR. Also, I do not think the reported captured output comes from the new test. test_streaming_sampling_rate_override only asserts mock_post.await_count == 4, and the mock returns a fixed texts=["ABCDEF"]. It does not capture or assert mock_post.await_args_list[*].kwargs["json"]["texts"], and there is no "ABCDEFCD" value produced by the test. I'm happy to file a follow-up against UnifiedLLMGuardrails to audit whether process_output_streaming_response should receive a deep-copied snapshot of responses_so_far for incremental scans, but I don't think this should block #30924. |
|
You're right on both counts, and I appreciate the detailed walkthrough.
The finding is withdrawn. The implementation and test coverage look correct for the scope of this PR. |
|
Adopted from #30924 by @schneidermr — wires the existing streaming_sampling_rate / streaming_end_of_stream_only contract (already honored by UnifiedLLMGuardrails and exposed on grayswan / openai_moderation since #17400 and #27324) into generic_guardrail_api, opt-in with backwards-compatible defaults |
…rriAI#31730) * feat(guardrails): expose streaming knobs on generic_guardrail_api Wire streaming_end_of_stream_only and streaming_sampling_rate through optional params, initialize_guardrail, and get_config_model so the generic guardrail API participates in UnifiedLLMGuardrails streaming checks with configurable cadence and end-of-stream-only mode. * fix(guardrails): use builtin type[] in get_config_model return Avoids a new UP006 violation that tripped the ruff strict-rule budget gate on the PR lint job. * fix(guardrails): default optional streaming knobs to None Non-None Pydantic defaults on GenericGuardrailAPIOptionalParams made _get_config_value treat unset nested fields as explicit values, which shadowed top-level litellm_params streaming flags whenever any other optional_params key was present. Real defaults stay in the constructor. * fix(guardrails): address review nits on generic_guardrail_api streaming Validate streaming_sampling_rate >= 1 in the constructor and Pydantic optional_params (ge=1), and add /v1/responses streaming coverage through the unified post-call hook so Responses API usage is exercised alongside chat completions. * fix(guardrails): read nested streaming config from dict optional_params Guardrail API/UI delivers optional_params as a plain dict, so getattr was silently ignoring streaming_sampling_rate and streaming_end_of_stream_only. Handle both dict and model shapes in _get_config_value with regression tests. * fix(guardrails): clear ruff findings in generic_guardrail_api tests/types * style(guardrails): ruff format generic_guardrail_api modules --------- Co-authored-by: Marton Schneider <marton@schneider.co.nl>
Relevant issues
Linear ticket
Pre-Submission checklist
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewScreenshots / Proof of Fix
Live proxy smoke test (run the proxy with your usual
dev_config.yamland ageneric_guardrail_apiguardrail configured onpost_call), exercise both modes:Type
New Feature
Changes
Adds first-class streaming configuration to
generic_guardrail_apiso it can participate in the existingUnifiedLLMGuardrailspost-call streaming path with the same knobs other guardrails already honor viagetattr(guardrail_to_apply, "streaming_*", default)streaming_end_of_stream_only(defaultfalsewhen unset) controls whether the guardrail runs incrementally on sampled chunks (an in-flight BLOCKED stops further chunks) or once at end of stream over the assembled response (cheaper/faster, but flagged content may already have reached the client).streaming_sampling_rate(default5when unset) sets the every-Nth-chunk cadence when incremental mode is on; ignored when end-of-stream-only is truePlumbing is through
GenericGuardrailAPIOptionalParams/GenericGuardrailAPIConfigModel(UI/config surface viaget_config_model()), constructor attributes onGenericGuardrailAPI, andinitialize_guardrailwhich reads either top-levellitellm_paramsor nestedoptional_paramsso both config styles work. Optional-params fields default toNoneso unset nested values do not shadow top-level streaming flags; real defaults are applied in the constructorTests cover defaults/overrides, config model exposure, initialize_guardrail forwarding, mixed-config priority (top-level vs explicit optional_params for both model and dict shapes), safe streaming yield, mid-stream BLOCKED, sampling cadence (sampled + final aggregate), fail_open continuing the stream when the guardrail API is unreachable, non-positive
streaming_sampling_raterejection, and/v1/responsesstreaming through the unified hook (end-of-stream-only and BLOCKED)Credit
Adopted from #30924 by @schneidermr (Marton Schneider). Mirrored onto
BerriAI/litellm'slitellm_generic_guardrail_streaming_configso CircleCI and the internal lint workflow trigger; original commits and authorship preservedNote
Medium Risk
Changes post-call streaming safety behavior for generic guardrail users; end-of-stream-only mode can allow flagged content to reach clients before a block, which is an intentional tradeoff but security-relevant.
Overview
Adds streaming post-call guardrail settings to
generic_guardrail_apiso it can use the sameUnifiedLLMGuardrailsstreaming path as other integrations.streaming_end_of_stream_only(default false) chooses between sampled in-stream checks (a mid-stream BLOCKED can stop the stream) versus a single check on the assembled response at end of stream.streaming_sampling_rate(default 5) sets every-Nth-chunk cadence when incremental mode is on.Config is exposed on
GenericGuardrailAPIOptionalParams, wired throughinitialize_guardrailvia_get_config_value(optional_params dict or model wins when set; unset nestedNonedoes not override top-levellitellm_params), stored onGenericGuardrailAPI, and surfaced for UI viaget_config_model(). Constructor applies defaults and rejectsstreaming_sampling_rate< 1.Tests cover init/plumbing priority, unified-hook behavior for chat completions and
/v1/responses(cadence, BLOCKED, end-of-stream-only, fail_open on unreachable).Reviewed by Cursor Bugbot for commit 2b464fd. Bugbot is set up for automated code reviews on this repo. Configure here.