From 2c08f8c993bc153948f77cc81240b11dfe5caaa5 Mon Sep 17 00:00:00 2001 From: yucheng Date: Tue, 3 Nov 2026 13:13:37 -0800 Subject: [PATCH] fix(guardrails): read plain string Gemini parts in the Lakera v2 guardrail --- litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py | 8 ++++++-- .../proxy/guardrails/guardrail_hooks/test_lakera_ai_v2.py | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py index 90afa69dcf7..7f46cf3a00f 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py +++ b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py @@ -254,11 +254,15 @@ def _gemini_items(value: object) -> Sequence[object]: return _OBJECT_LIST.validate_python(value) if isinstance(value, list) else () +def _gemini_part_text(part: object) -> object: + """A part's ``text`` field, or the part itself when it's a plain string, which the generateContent adapter sends as text""" + return part if isinstance(part, str) else (as_str_mapping(part) or {}).get("text") + + def _gemini_text(content: object) -> str: """Join the text parts of one Gemini ``Content`` object, skipping inline data, files and function calls.""" parts: Final = _gemini_items((as_str_mapping(content) or {}).get("parts")) - texts: Final = ((as_str_mapping(part) or {}).get("text") for part in parts) - return "\n".join(text for text in texts if isinstance(text, str) and text) + return "\n".join(text for text in map(_gemini_part_text, parts) if isinstance(text, str) and text) def _gemini_tool_part_values(part: object) -> Iterator[object]: 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 e9efda2c537..7348fc0bc7f 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 @@ -1829,6 +1829,13 @@ class TestGeminiContents: {"role": "tool", "content": expected}, ] + def test_reads_a_plain_string_part_as_text(self): + data: Final = {"contents": [{"role": "user", "parts": ["hello", {"text": "world"}]}]} + + assert _build_lakera_inspection_messages(data, call_type="pass_through_endpoint") == [ + {"role": "user", "content": "hello\nworld"} + ] + def test_reads_a_single_contents_object_with_a_single_parts_object(self): data: Final = {"contents": {"role": "user", "parts": {"text": "hello world"}}} assert _build_lakera_inspection_messages(data, call_type="pass_through_endpoint") == [