fix(proxy): run post_call guardrails on /v1/messages streaming via unified guardrail translation - #35260
Merged
mateo-berri merged 2 commits intoJul 30, 2026
Conversation
…ified guardrail translation
Contributor
Greptile SummaryThis PR updates post-call streaming guardrail dispatch for Anthropic Messages streams
Confidence Score: 5/5The PR appears safe to merge No blocking failure remains; the previously reported inherited-guardrail dispatch issue is fixed because the new interface check recognizes implementations inherited from intermediate subclasses, and the regression test exercises that case
|
| Filename | Overview |
|---|---|
| litellm/proxy/utils.py | Detects non-OpenAI stream routes and reroutes applicable guardrails through unified translation while preserving masking guardrails' native hooks |
| litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py | Accepts an explicit guardrail instance and a caller-selected default for buffering until moderation |
| tests/test_litellm/proxy/test_proxy_logging_hook_detection.py | Adds focused regression tests covering Anthropic stream blocking, inherited apply-guardrail detection, explicit dispatch, and masking-path preservation |
Reviews (2): Last reviewed commit: "fix(proxy): recognize inherited apply_gu..." | Re-trigger Greptile
Contributor
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…sking guardrails on their own stream hook
Contributor
Author
tin-berri
approved these changes
Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Problem this solves:
How it solves it:
Relevant issues
Fixes #35257
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Config used for both runs: a
litellm_content_filterguardrail withmode: post_call,default_on: true, and blocked wordzebra, plusanthropic/claude-sonnet-5in the model list. All calls hit the real Anthropic and OpenAI APIsBefore, at commit 71b825a (base litellm_internal_staging, proxy on port 48731). The blocked word streams through in full and the stream completes normally
The spend log for that request records the post_call guardrail as
"guardrail_status": "success"withrisk_score0 even though the blocked keyword was deliveredAfter, at commit 23f3e10 (this branch's head, proxy on port 45951). Nothing is delivered before moderation; the client receives only the block
Clean requests still stream end to end at the same commit
Unchanged behavior checks at 23f3e10: non-streaming /v1/messages with the blocked word still returns HTTP 400, and /v1/chat/completions streaming still withholds the blocked word through the guardrail's own iterator hook (zero occurrences of the keyword in the delivered stream)
The non Anthropic provider path through /v1/messages behaves the same at 23f3e10:
{"model":"gpt-4o-mini", ...}streaming with the blocked word delivers only the block error frame, and a clean request streams the adapter's Anthropic format events end to endType
🐛 Bug Fix
Changes
ProxyLogging.async_post_call_streaming_iterator_hook(litellm/proxy/utils.py) now checks whether the request route streams a non OpenAI wire format (today/v1/messages, which streams raw Anthropic SSE bytes). On such routes, a guardrail that defines both its ownasync_post_call_streaming_iterator_hookandapply_guardrailis dispatched throughunified_guardrail, whoseAnthropicMessagesHandlertranslation already parses Anthropic SSE, instead of through its own hook, which only understands OpenAIModelResponseStreamchunks and passed raw bytes through unscanned (litellm_content_filter) or crashed assembling them (bedrock style hooks). This mirrors how_execute_guardrail_hookalready prefers the unified path for non streaming hooks, which is why non streaming /v1/messages blocking worked all alongBecause those guardrails' own iterator hooks withheld content until it was scanned, the rerouted invocation defaults
streaming_buffer_until_moderatedto true, so chunks are withheld until end of stream moderation passes. This trades time to first byte for the guarantee that blocked content never reaches the client, matching the contract the guardrail's own hook provided on /chat/completions. A guardrail can opt back into sampling based scanning by explicitly settingstreaming_buffer_until_moderated: false. Guardrails that were already dispatched through the unified path keep their existing defaultunified_guardrail.async_post_call_streaming_iterator_hooknow accepts the guardrail as an explicitguardrail_to_applyparameter instead of only reading it from a sharedrequest_datakey. With the shared key, chaining two unified routed guardrails made the last assignment win and earlier guardrails silently became passthroughTwo dispatch details landed from review feedback in 23f3e10. The reroute predicate uses
uses_apply_guardrail_interface()instead of a leaf class__dict__check, so anapply_guardrailimplementation inherited from a vendor base class is recognized and rerouted rather than left on the raw byte path unscanned. And guardrails withmask_response_contentenabled are excluded from the reroute and keep their own iterator hook: the unified streaming path cannot re-emit rewritten text on raw Anthropic SSE (its block_only mode drops rewrites and buffered replay would release the unredacted originals), while such guardrails' own hooks already handle that stream shape safely, for example PANW Prisma AIRS parses the raw bytes itself and blocks instead of masking there. Without the exclusion, a masking guardrail's rewrite would be dropped and the original unmasked content deliveredOpenAI format streaming routes are untouched: guardrails with their own iterator hooks keep running them there, preserving incremental masking
Tests are in tests/test_litellm/proxy/test_proxy_logging_hook_detection.py. The new anthropic stream test fails on the base commit by delivering the blocked word without raising, the route detection test pins the dispatch predicate including unmapped routes, the chat completions test proves the own hook path still masks (which the unified block_only path never does, so it fails if dispatch over reroutes), and the explicit parameter test fails on the base signature. The review fix tests fail on the previous head: the inherited
apply_guardrailtest delivers the blocked word when the predicate only checks the leaf class, and the masking guardrail test observes the own hook being bypassed whenmask_response_contentis not excludedFinal Attestation