From 70a893ca538b7ad7d9486b72d8f23e888efae8f6 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 25 Jun 2026 19:45:44 -0700 Subject: [PATCH] feat(guardrails): support structured_messages in generic guardrail API response Guardrails that modify full message structure (e.g. prompt compression) can now return structured_messages in GUARDRAIL_INTERVENED responses. LiteLLM passes it back through GenericGuardrailAPIInputs so the compressed message array reaches the LLM provider instead of only flat text replacements. --- .../generic_guardrail_api.py | 3 +- .../guardrail_hooks/generic_guardrail_api.py | 4 +++ .../test_generic_guardrail_api.py | 28 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py b/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py index 790ee31f2e0..c8e8d2a9723 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py +++ b/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py @@ -329,8 +329,9 @@ class GenericGuardrailAPI(CustomGuardrail): tools: Any, guardrail_response: GenericGuardrailAPIResponse, ) -> GenericGuardrailAPIInputs: - # Action is NONE or no modifications needed return_inputs = GenericGuardrailAPIInputs(texts=texts) + if guardrail_response.structured_messages: + return_inputs["structured_messages"] = guardrail_response.structured_messages if guardrail_response.texts: return_inputs["texts"] = guardrail_response.texts if guardrail_response.images: 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 94f219a5fc6..13b1a3ef7df 100644 --- a/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py +++ b/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py @@ -89,6 +89,7 @@ class GenericGuardrailAPIResponse: texts: Optional[List[str]] images: Optional[List[str]] tools: Optional[List[ChatCompletionToolParam]] + structured_messages: Optional[List[AllMessageValues]] action: str blocked_reason: Optional[str] @@ -99,12 +100,14 @@ class GenericGuardrailAPIResponse: blocked_reason: Optional[str] = None, images: Optional[List[str]] = None, tools: Optional[List[ChatCompletionToolParam]] = None, + structured_messages: Optional[List[AllMessageValues]] = None, ): self.action = action self.blocked_reason = blocked_reason self.texts = texts self.images = images self.tools = tools + self.structured_messages = structured_messages @classmethod def from_dict(cls, data: dict) -> "GenericGuardrailAPIResponse": @@ -114,4 +117,5 @@ class GenericGuardrailAPIResponse: texts=data.get("texts"), images=data.get("images"), tools=data.get("tools"), + structured_messages=data.get("structured_messages"), ) 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 6ec793a1bb0..a0dc98e1d29 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 @@ -581,6 +581,34 @@ class TestGuardrailActions: assert result_texts == ["[REDACTED]"] assert result_images is None + @pytest.mark.asyncio + async def test_action_intervened_with_structured_messages( + self, generic_guardrail, mock_request_data_input + ): + compressed_messages = [ + {"role": "user", "content": "Analyze this."}, + {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "t1", "content": "compressed json"}]}, + ] + mock_response = MagicMock() + mock_response.json.return_value = { + "action": "GUARDRAIL_INTERVENED", + "structured_messages": compressed_messages, + } + mock_response.raise_for_status = MagicMock() + + with patch.object( + generic_guardrail.async_handler, "post", return_value=mock_response + ): + guardrailed_inputs = await generic_guardrail.apply_guardrail( + inputs={"texts": ["Analyze this."], "structured_messages": [ + {"role": "user", "content": "Analyze this."}, + {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "t1", "content": "very long uncompressed json " * 100}]}, + ]}, + request_data=mock_request_data_input, + input_type="request", + ) + assert guardrailed_inputs.get("structured_messages") == compressed_messages + class TestImageSupport: """Test image handling in guardrail requests"""