litellm/tests/test_litellm/proxy/guardrails/guardrail_hooks
yucheng-berri 7a81ae98e6
fix(model_armor): handle Anthropic Messages and Responses streams in post_call (#39181)
* fix(model_armor): handle Anthropic Messages and Responses streams in post_call

The post_call streaming hook buffered every chunk and fed it to
stream_chunk_builder, which only understands chat-completion deltas.
/v1/messages streams raw Anthropic SSE bytes and /v1/responses streams
typed Responses events, so both raised litellm.APIError and surfaced to
the client as a 500 on every streamed request.

Assemble each surface with its own reader, frame guardrail failures as
terminal items in that surface's wire format, and pass the stream
through unscanned when it cannot be assembled instead of raising.

* fix(model_armor): classify the stream surface and fail closed when it cannot be assembled

Decide the wire format explicitly instead of inferring it from a boolean pair, so an
opaque raw SSE stream (the Google :streamGenerateContent route) is never refused in
Anthropic framing, and a stream that cannot be assembled is blocked rather than
released unscanned unless fail_on_error is disabled.

Also scan Responses tool-call arguments, read the body only off a terminal Responses
event, and record the applied guardrail on the fail-closed path.

* test(model_armor): pin the error-only stream predicate against content-carrying streams

is_sse_error_stream decides whether a buffered stream is forwarded to the client
untouched, so a stream that still carries content must not qualify: the frames-only
join drops typed chunks, an empty stream is not a refusal, and a content event may
carry an empty error field.

* fix(model_armor): let a streamed de-identify match mask instead of blocking

A de-identify template reports MATCH_FOUND for every redaction it makes. The
streaming block check omitted allow_sanitization, so with mask_response_content
enabled that match read as a refusal and the client got a 400 where the
non-streaming sibling returned the redacted text. Pass the flag through, as the
non-streaming hook already does, and stamp the logged status from the same
decision so the spend row agrees with what the client received.

Also drop Any from the chat-completion assembler's parameter; stream_chunk_builder
takes a bare list, so list[object] carries the mutability requirement without
erasing the element type.

* fix(model_armor): fail closed when a streamed de-identify match cannot be applied

Allowing sanitization past the streaming block check is a promise to apply the
redaction Model Armor asked for. Two paths broke that promise and released the
buffered original instead: a match that comes back with no sanitized text, and a
surface with no assembled body to rewrite.

The outcome is now resolved once, before it is recorded, so the status stamped on
request metadata agrees with what the client receives rather than reporting the
success the block check alone would have implied.

* fix: scan the deltas when a Responses stream ends without a body

response.failed and response.incomplete are terminal events like
response.completed, but a turn that broke mid-generation reports an empty
output while the deltas ahead of it already spelled the answer out to the
client. Reading only the terminal body found nothing to scan there, and the
empty-content shortcut then forwarded every buffered delta past the guardrail.

Fall back to the text the delta events carry whenever a Responses stream
assembles to nothing.

* fix: read the Responses delta event types off the event enum

The hand-listed set left out response.mcp_call_arguments.delta, so a turn that
streamed only MCP tool arguments and then reported an empty body still took the
no-content shortcut and forwarded those chunks unscanned.

Deriving the set from ResponsesAPIStreamEvents keeps it complete as the enum
grows, and the str guard in the reader already covers any event whose delta is
not text.

* fix(model_armor): scan responses deltas alongside the terminal body

A /v1/responses stream spells out reasoning summaries and tool-call arguments in
delta events that its terminal body never repeats, so scanning the body alone
handed every summary delta to the client unscanned whenever the body carried text.

* fix(model_armor): scan responses delta fields apart from each other

A Responses turn spells out its reasoning summary, its visible answer and its tool-call
arguments in separate delta events. Joining every delta into one string let a finding form
across the boundary between two fields that each carry nothing to find, so a safe stream
could be blocked. Group the deltas by the field they belong to, join a field's own deltas
as they streamed, and keep the fields apart.

* fix(model_armor): scan each responses field once, not twice

Separating delta fields stopped the terminal body from matching the delta text, so a turn
with two visible fields sent Model Armor both copies. Only the delta fields the body does not
already carry are appended now.

---------

Co-authored-by: yassin <yassin@berri.ai>
2026-09-02 19:15:14 -07:00
..
azure feat(guardrails): track Azure Prompt Shield usage and cost with spend isolation (#38387) 2026-08-26 17:42:17 -07:00
content_filter test: drop the cwd-relative sys.path.insert calls from the test suite (#37802) 2026-08-22 09:25:58 -07:00
guardrails_ai style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
openai fix(guardrails): skip streaming guardrail rounds that re-scan cleared output (#39386) 2026-09-02 18:25:28 -07:00
unified_guardrails fix(guardrails): skip streaming guardrail rounds that re-scan cleared output (#39386) 2026-09-02 18:25:28 -07:00
_cisco_ai_defense_test_utils.py test: drop the cwd-relative sys.path.insert calls from the test suite (#37802) 2026-08-22 09:25:58 -07:00
code_execution_compliance_dataset.json feat(add-new-block_code_execution-guardrail): prevent agent from executing code (#22154) 2026-02-25 22:02:14 -08:00
test_aim.py fix(guardrails): walk Responses-API text taxonomy in shared content helpers (#32542) 2026-07-08 23:24:11 -07:00
test_alice.py feat(guardrails): add Alice guardrail (#38898) 2026-09-01 12:33:39 -07:00
test_bedrock_guardrails.py Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix_stream_modify_response_chunks 2026-09-01 14:49:06 -07:00
test_bedrock_invoke_guardrail_checks.py test: drop the cwd-relative sys.path.insert calls from the test suite (#37802) 2026-08-22 09:25:58 -07:00
test_block_code_execution.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_block_code_execution_compliance.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_cato_networks.py test: drop the cwd-relative sys.path.insert calls from the test suite (#37802) 2026-08-22 09:25:58 -07:00
test_cisco_ai_defense_chat.py test: drop the leftover set_verbose from eleven test files (#37845) 2026-08-22 08:23:27 -07:00
test_cisco_ai_defense_mcp.py feat(guardrails): add Cisco AI Defense integration (#28249) (#30338) 2026-06-12 23:21:23 -07:00
test_compresr.py fix(guardrails): preserve cache_control breakpoints in compresr write-back 2026-07-25 14:47:25 -07:00
test_crowdstrike_aidr.py test(guardrails): expect the deduped end-of-stream scan in crowdstrike cadence test (#39467) 2026-09-03 02:11:08 +00:00
test_deepkeep.py test(guardrails): stop five guardrail test files leaking env vars on failure (#37828) 2026-08-21 21:29:31 -07:00
test_dynamoai.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_enkryptai.py test: require a match= on broad pytest.raises, and drop duplicate parametrize cases (#37769) 2026-08-20 20:24:49 -07:00
test_generic_guardrail_api.py fix(guardrails): skip streaming guardrail rounds that re-scan cleared output (#39386) 2026-09-02 18:25:28 -07:00
test_grayswan.py fix(guardrails): forward grayswan scan id header (#32544) 2026-07-08 15:05:27 -07:00
test_headroom.py fix(headroom): stop re-compressing retrieved CCR content in client tool loops (#38591) 2026-09-02 12:07:13 -07:00
test_hiddenlayer.py fix(guardrails): exclude images from HiddenLayer v1 scans (#29210) 2026-08-31 12:50:42 -07:00
test_lakera_ai_v2.py fix(guardrails): stop Lakera monitor mode forwarding unmasked PII on Responses-API bodies (#38841) 2026-08-29 17:07:39 -07:00
test_lasso.py test: drop the cwd-relative sys.path.insert calls from the test suite (#37802) 2026-08-22 09:25:58 -07:00
test_mcp_end_user_permission.py test: drop the cwd-relative sys.path.insert calls from the test suite (#37802) 2026-08-22 09:25:58 -07:00
test_mcp_security.py Add MCP Security guardrail to block unregistered MCP servers (#21429) 2026-02-17 21:19:13 -08:00
test_microsoft_purview.py test: enforce PT012 so a pytest.raises block cannot hide dead assertions (#37748) 2026-08-20 19:36:26 -07:00
test_model_armor.py fix(model_armor): handle Anthropic Messages and Responses streams in post_call (#39181) 2026-09-02 19:15:14 -07:00
test_noma.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_noma_v2.py perf(guardrails): stop sending the conversation twice in the noma v2 payload (#36764) 2026-08-17 15:37:17 -07:00
test_onyx.py test(guardrails): stop five guardrail test files leaking env vars on failure (#37828) 2026-08-21 21:29:31 -07:00
test_ovalix.py feat: litellm oss 110626 (#30202) 2026-06-11 22:30:26 -07:00
test_pangea.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_panw_prisma_airs.py test: require a match= on broad pytest.raises, and drop duplicate parametrize cases (#37769) 2026-08-20 20:24:49 -07:00
test_presidio.py fix(guardrails): keep the presidio output masker from unmasking after an in-memory update 2026-09-02 13:32:21 -07:00
test_presidio_union_fix.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_promptguard.py Add PromptGuard guardrail integration (#24268) 2026-04-09 08:12:24 -07:00
test_qualifire.py feat(guardrails): add Lakera v2 skip-message honoring and advisory (inject_system_message) mode (#34940) 2026-08-28 14:13:49 -07:00
test_repelloai.py test(guardrails): stop five guardrail test files leaking env vars on failure (#37828) 2026-08-21 21:29:31 -07:00
test_response_rejection_guardrail_code.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_singulr.py chore(ci): merge oss branch (#33784) 2026-07-17 23:22:13 +00:00
test_straiker.py test: require a match= on broad pytest.raises, and drop duplicate parametrize cases (#37769) 2026-08-20 20:24:49 -07:00
test_structured_messages_writeback.py fix(guardrails/headroom): stop compressing the turn the model must act on (#35294) 2026-07-30 18:53:31 -07:00
test_tool_permission.py test: drop the cwd-relative sys.path.insert calls from the test suite (#37802) 2026-08-22 09:25:58 -07:00
test_tool_policy_guardrail.py test: drop the cwd-relative sys.path.insert calls from the test suite (#37802) 2026-08-22 09:25:58 -07:00
test_vigil_guard.py Litellm OSS Staging 010626 (#29422) 2026-06-01 21:42:51 -07:00
test_xecguard.py fix(xecguard): sanitize scan result before recording it for logging (#32935) 2026-07-12 02:46:02 +00:00