mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(guardrails): keep the full-request scan for invoke bodies with document or search_result blocks
The Anthropic messages handler does not read the text of document sources or search_result content, so an invoke body carrying them could slip that text past text guardrails. Such bodies keep the generic pass-through scan, which sees the whole request
This commit is contained in:
parent
981b99a4ae
commit
4d16ebd414
2 changed files with 63 additions and 0 deletions
|
|
@ -14,6 +14,7 @@ from pydantic import InstanceOf, TypeAdapter, ValidationError
|
|||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.litellm_core_utils.core_helpers import as_str_mapping
|
||||
from litellm.llms.anthropic.chat.guardrail_translation.handler import AnthropicMessagesHandler
|
||||
from litellm.llms.base_llm.guardrail_translation.base_translation import (
|
||||
BaseTranslation,
|
||||
|
|
@ -61,6 +62,7 @@ if TYPE_CHECKING:
|
|||
_CONVERSE_ACTIONS: Final = frozenset({"converse", "converse-stream"})
|
||||
_INVOKE_STREAM_ACTION: Final = "invoke-with-response-stream"
|
||||
_INVOKE_ACTIONS: Final = frozenset({"invoke", _INVOKE_STREAM_ACTION})
|
||||
_BLOCK_TYPES_THE_ANTHROPIC_HANDLER_DOES_NOT_SCAN: Final = frozenset({"document", "search_result"})
|
||||
_EVENT_STREAM_CONTENT_TYPE: Final = "vnd.amazon.eventstream"
|
||||
_EVENT_STREAM_MEDIA_TYPE: Final = "application/vnd.amazon.eventstream"
|
||||
|
||||
|
|
@ -87,9 +89,31 @@ def _anthropic_invoke_body(
|
|||
invoke_body: Final = _BODY_BY_REFERENCE.validate_python(body)
|
||||
if not isinstance(invoke_body.get("anthropic_version"), str) or not isinstance(invoke_body.get("messages"), list):
|
||||
return None
|
||||
blocks: Final = _anthropic_message_blocks(_ARRAY_BY_REFERENCE.validate_python(invoke_body["messages"]))
|
||||
if any(block.get("type") in _BLOCK_TYPES_THE_ANTHROPIC_HANDLER_DOES_NOT_SCAN for block in blocks):
|
||||
return None
|
||||
return invoke_body
|
||||
|
||||
|
||||
def _anthropic_message_blocks(messages: Sequence[object]) -> tuple[Mapping[str, object], ...]:
|
||||
fields: Final = (as_str_mapping(message) for message in messages)
|
||||
return tuple(chain.from_iterable(_anthropic_content_blocks(field.get("content")) for field in fields if field))
|
||||
|
||||
|
||||
def _anthropic_content_blocks(content: object) -> tuple[Mapping[str, object], ...]:
|
||||
if not isinstance(content, list):
|
||||
return ()
|
||||
blocks: Final = tuple(
|
||||
block
|
||||
for block in (as_str_mapping(item) for item in _ARRAY_BY_REFERENCE.validate_python(content))
|
||||
if block is not None
|
||||
)
|
||||
nested: Final = (
|
||||
_anthropic_content_blocks(block.get("content")) for block in blocks if block.get("type") == "tool_result"
|
||||
)
|
||||
return (*blocks, *chain.from_iterable(nested))
|
||||
|
||||
|
||||
def _anthropic_invoke_reply(
|
||||
response: object,
|
||||
request_data: object,
|
||||
|
|
|
|||
|
|
@ -3981,6 +3981,45 @@ async def test_invoke_body_without_anthropic_version_is_scanned_whole():
|
|||
assert "structured_messages" not in guardrail.received[-1]
|
||||
|
||||
|
||||
_PLAIN_TEXT_DOCUMENT: Final = {
|
||||
"type": "document",
|
||||
"source": {"type": "text", "media_type": "text/plain", "data": "Card 4111 1111 1111 1111"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"content",
|
||||
[
|
||||
pytest.param([{"type": "text", "text": "Summarize this"}, _PLAIN_TEXT_DOCUMENT], id="document"),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
"type": "search_result",
|
||||
"source": "https://example.com/cards",
|
||||
"title": "Cards",
|
||||
"content": [{"type": "text", "text": "Card 4111 1111 1111 1111"}],
|
||||
}
|
||||
],
|
||||
id="search_result",
|
||||
),
|
||||
pytest.param(
|
||||
[{"type": "tool_result", "tool_use_id": "toolu_1", "content": [_PLAIN_TEXT_DOCUMENT]}],
|
||||
id="document_in_tool_result",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_anthropic_invoke_body_with_blocks_the_messages_handler_does_not_scan_is_scanned_whole(content: list):
|
||||
invoke = _anthropic_invoke_data()
|
||||
data = {**invoke, "data": {**invoke["data"], "messages": [{"role": "user", "content": content}]}}
|
||||
guardrail = _replying({})
|
||||
|
||||
await BedrockPassthroughGuardrailHandler().process_input_messages(data, guardrail)
|
||||
|
||||
assert json.loads(guardrail.received[-1]["texts"][0])["data"] == data["data"]
|
||||
assert "structured_messages" not in guardrail.received[-1]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_anthropic_invoke_response_masks_are_written_back():
|
||||
response = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue