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.
This commit is contained in:
Krrish Dholakia 2026-06-25 19:45:44 -07:00
parent 97008bad29
commit 70a893ca53
3 changed files with 34 additions and 1 deletions

View file

@ -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:

View file

@ -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"),
)

View file

@ -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"""