From 6ef3c0e05605815e6b9989e9b6e96cd6cd7611c2 Mon Sep 17 00:00:00 2001 From: yucheng Date: Mon, 2 Nov 2026 18:05:07 -0800 Subject: [PATCH] fix(guardrails): read only the admin opt-out settings for a global Lakera guardrail on a Gemini body The opt-out check reran should_run_guardrail on metadata alone, so a streaming-only or tag-scoped global guardrail looked opted out and skipped the Gemini body. It now reads the key's and team's opt-out fields. --- .../guardrail_hooks/lakera_ai_v2.py | 22 +++++------ .../guardrail_hooks/test_lakera_ai_v2.py | 37 +++++++++++++++++++ 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py index d818c67d524..5ca64b3c0bd 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py +++ b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py @@ -298,18 +298,16 @@ def _opted_out_of_global_guardrail(guardrail: "LakeraAIGuardrail", user_api_key_ """True if the key's or team's admin metadata opts out of this ``default_on`` guardrail. A pass-through body has no admin metadata for ``should_run_guardrail`` to read, so it is read from the key here.""" admin_fields: Final = user_api_key_dict.model_dump(include={"metadata", "team_metadata"}) - return ( - guardrail.default_on is True - and guardrail.should_run_guardrail( # pyright: ignore[reportUnknownMemberType] # takes an untyped data dict - data={ - "metadata": { - "user_api_key_metadata": admin_fields.get("metadata"), - "user_api_key_team_metadata": admin_fields.get("team_metadata"), - } - }, - event_type=GuardrailEventHooks.pre_call, - ) - is not True + admin_data: Final = { + "metadata": { + "user_api_key_metadata": admin_fields.get("metadata"), + "user_api_key_team_metadata": admin_fields.get("team_metadata"), + } + } + return guardrail.default_on is True and ( + guardrail.get_disable_global_guardrail(admin_data) # pyright: ignore[reportUnknownMemberType] # takes an untyped data dict + is True + or guardrail.guardrail_name in guardrail.get_opted_out_global_guardrails_from_metadata(admin_data) # pyright: ignore[reportUnknownMemberType] # takes an untyped data dict ) diff --git a/tests/unit/proxy/guardrails/guardrail_hooks/test_lakera_ai_v2.py b/tests/unit/proxy/guardrails/guardrail_hooks/test_lakera_ai_v2.py index 7a587545807..e9efda2c537 100644 --- a/tests/unit/proxy/guardrails/guardrail_hooks/test_lakera_ai_v2.py +++ b/tests/unit/proxy/guardrails/guardrail_hooks/test_lakera_ai_v2.py @@ -1940,6 +1940,43 @@ class TestGeminiContents: call_type="pass_through_endpoint", ) + @pytest.mark.asyncio + @pytest.mark.parametrize("call_type", ["pass_through_endpoint", "agenerate_content_stream"]) + async def test_streaming_only_global_guardrail_reads_a_gemini_stream(self, call_type: str): + guardrail: Final = LakeraAIGuardrail( + api_key="test_key", guardrail_name="lakera-global", default_on=True, stream_scope="streaming" + ) + with ( + patch.object(guardrail, "call_v2_guard", new=AsyncMock(return_value=(GEMINI_FLAGGED, {}))), + pytest.raises(HTTPException), + ): + await guardrail.async_pre_call_hook( + user_api_key_dict=UserAPIKeyAuth(api_key="test_key"), + cache=DualCache(), + data={**_gemini_request(), "stream": True}, + call_type=call_type, + ) + + @pytest.mark.asyncio + async def test_tag_scoped_global_guardrail_reads_a_gemini_body_with_its_tag(self): + guardrail: Final = LakeraAIGuardrail( + api_key="test_key", + guardrail_name="lakera-global", + default_on=True, + event_hook=Mode(tags={"prod": "pre_call"}, default="during_call"), + ) + with ( + patch.object(guardrail, "call_v2_guard", new=AsyncMock(return_value=(GEMINI_FLAGGED, {}))), + patch("litellm.proxy.proxy_server.premium_user", True), + pytest.raises(HTTPException, match="Violated"), + ): + await guardrail.async_pre_call_hook( + user_api_key_dict=UserAPIKeyAuth(api_key="test_key"), + cache=DualCache(), + data={**_gemini_request(), "metadata": {"tags": ["prod"]}}, + call_type="pass_through_endpoint", + ) + @pytest.mark.parametrize("chat_field", ["messages", "input"]) def test_a_pass_through_chat_body_with_stray_contents_is_read_as_chat(self, chat_field: str): data: Final = {chat_field: [USER_MSG], "contents": [{"parts": [{"text": "stray"}]}]}