mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
fix(guardrails): read Gemini function calls and code parts in the Lakera v2 guardrail
A model turn's function call arguments and code execution parts reach the provider as history, so Lakera now reads them as tool messages too. Images and files stay unread.
This commit is contained in:
parent
b0145da548
commit
b7d08bf5f8
3 changed files with 57 additions and 18 deletions
|
|
@ -3,7 +3,7 @@ import json
|
|||
import os
|
||||
from collections.abc import Mapping, Sequence
|
||||
from datetime import datetime
|
||||
from itertools import chain
|
||||
from itertools import chain, product
|
||||
from string import Formatter
|
||||
from types import MappingProxyType
|
||||
from typing import Final, Literal
|
||||
|
|
@ -232,6 +232,20 @@ def _unmaskable_reason(
|
|||
return None
|
||||
|
||||
|
||||
_GEMINI_TOOL_PART_FIELDS: Final = MappingProxyType(
|
||||
{
|
||||
"functionCall": "args",
|
||||
"function_call": "args",
|
||||
"functionResponse": "response",
|
||||
"function_response": "response",
|
||||
"executableCode": "code",
|
||||
"executable_code": "code",
|
||||
"codeExecutionResult": "output",
|
||||
"code_execution_result": "output",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _gemini_items(value: object) -> Sequence[object]:
|
||||
"""A repeated Gemini field as a sequence: a JSON array, or the single object Google also accepts in its place."""
|
||||
single: Final = as_str_mapping(value)
|
||||
|
|
@ -247,13 +261,19 @@ def _gemini_text(content: object) -> str:
|
|||
return "\n".join(text for text in texts if isinstance(text, str) and text)
|
||||
|
||||
|
||||
def _gemini_tool_results(content: object) -> str:
|
||||
"""The results one Gemini ``Content`` hands back to the model in its function response parts, as JSON."""
|
||||
def _gemini_tool_text(content: object) -> str:
|
||||
"""What one Gemini ``Content`` carries in its function call, function response and code execution parts, with
|
||||
structured values as JSON."""
|
||||
parts: Final = _gemini_items((as_str_mapping(content) or {}).get("parts"))
|
||||
part_fields: Final = (as_str_mapping(part) or {} for part in parts)
|
||||
responses: Final = (fields.get("functionResponse") or fields.get("function_response") for fields in part_fields)
|
||||
results: Final = ((as_str_mapping(response) or {}).get("response") for response in responses)
|
||||
return "\n".join(json.dumps(result, default=str, ensure_ascii=False) for result in results if result)
|
||||
values: Final = (
|
||||
(as_str_mapping((as_str_mapping(part) or {}).get(part_name)) or {}).get(field)
|
||||
for part, (part_name, field) in product(parts, _GEMINI_TOOL_PART_FIELDS.items())
|
||||
)
|
||||
return "\n".join(
|
||||
value if isinstance(value, str) else json.dumps(value, default=str, ensure_ascii=False)
|
||||
for value in values
|
||||
if value
|
||||
)
|
||||
|
||||
|
||||
def _has_gemini_contents(data: object, call_type: str | None) -> bool:
|
||||
|
|
@ -274,15 +294,15 @@ def _has_gemini_contents(data: object, call_type: str | None) -> bool:
|
|||
def _gemini_inspection_messages(data: Mapping[str, object], call_type: str | None) -> Sequence[Mapping[str, str]]:
|
||||
"""Chat-style messages for a Gemini ``generateContent`` body on a pass-through or Google ``generateContent``
|
||||
call: ``systemInstruction`` first as a system message, then each ``contents`` turn with its text parts joined and
|
||||
``model`` read as ``assistant``, followed by a tool message with the function results it carries. Turns with no
|
||||
text are dropped."""
|
||||
``model`` read as ``assistant``, followed by a tool message with the tool parts it carries. Turns with no text
|
||||
are dropped."""
|
||||
if not _has_gemini_contents(data, call_type):
|
||||
return []
|
||||
system_text: Final = _gemini_text(data.get("systemInstruction") or data.get("system_instruction"))
|
||||
turns: Final = chain.from_iterable(
|
||||
(
|
||||
{"role": _GEMINI_ROLES.get(str(content.get("role") or "user"), "user"), "content": _gemini_text(content)},
|
||||
{"role": "tool", "content": _gemini_tool_results(content)},
|
||||
{"role": "tool", "content": _gemini_tool_text(content)},
|
||||
)
|
||||
for content in map(as_str_mapping, _gemini_items(data.get("contents")))
|
||||
if content is not None
|
||||
|
|
|
|||
|
|
@ -177,13 +177,17 @@ def test_card_in_gemini_body_is_blocked_before_the_provider(rig: GeminiLakeraRig
|
|||
assert rig.upstream.drain() == ()
|
||||
|
||||
|
||||
def test_card_in_a_gemini_function_response_is_blocked_before_the_provider(rig: GeminiLakeraRig) -> None:
|
||||
body: Final = {
|
||||
"contents": [
|
||||
{"role": "model", "parts": [{"functionCall": {"name": "lookup", "args": {}}}]},
|
||||
{"role": "user", "parts": [{"functionResponse": {"name": "lookup", "response": {"result": PII_PROMPT}}}]},
|
||||
]
|
||||
}
|
||||
@pytest.mark.parametrize(
|
||||
"part",
|
||||
[
|
||||
{"functionCall": {"name": "lookup", "args": {"note": PII_PROMPT}}},
|
||||
{"functionResponse": {"name": "lookup", "response": {"result": PII_PROMPT}}},
|
||||
],
|
||||
)
|
||||
def test_card_in_a_gemini_tool_part_is_blocked_before_the_provider(
|
||||
rig: GeminiLakeraRig, part: dict[str, JsonValue]
|
||||
) -> None:
|
||||
body: Final = {"contents": [{"role": "user", "parts": [part]}]}
|
||||
|
||||
response: Final = _send(rig, "block", body)
|
||||
|
||||
|
|
|
|||
|
|
@ -1799,7 +1799,6 @@ class TestGeminiContents:
|
|||
"role": "user",
|
||||
"parts": [{"text": "hi"}, {field: {"name": "lookup", "response": {"card": "4111"}}}],
|
||||
},
|
||||
{"role": "model", "parts": [{"functionCall": {"name": "lookup", "args": {"card": "5555"}}}]},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -1808,6 +1807,22 @@ class TestGeminiContents:
|
|||
{"role": "tool", "content": '{"card": "4111"}'},
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("part", "expected"),
|
||||
[
|
||||
({"functionCall": {"name": "lookup", "args": {"card": "4111"}}}, '{"card": "4111"}'),
|
||||
({"function_call": {"name": "lookup", "args": {"card": "4111"}}}, '{"card": "4111"}'),
|
||||
({"executableCode": {"language": "PYTHON", "code": "print(4111)"}}, "print(4111)"),
|
||||
({"codeExecutionResult": {"outcome": "OUTCOME_OK", "output": "4111"}}, "4111"),
|
||||
],
|
||||
)
|
||||
def test_gemini_tool_part_is_read_as_a_tool_turn(self, part: dict[str, object], expected: str):
|
||||
data: Final = {"contents": [{"role": "model", "parts": [part]}]}
|
||||
|
||||
assert _build_lakera_inspection_messages(data, call_type="pass_through_endpoint") == [
|
||||
{"role": "tool", "content": expected},
|
||||
]
|
||||
|
||||
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") == [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue