mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-20 00:11:50 +00:00
fix(guardrails): read Prompt Security modified rows with the slot count's own predicate
A chat row whose content carried an empty text part counted two slots in the chat completions handler while Prompt Security read one text out of the modified row, so the structured rewrite was dropped and the request got the named rejection. One shared helper now lists a row's slot texts and both the slot count and the modified-row reader use it.
This commit is contained in:
parent
16c326537f
commit
e01d97ea08
3 changed files with 34 additions and 18 deletions
|
|
@ -373,13 +373,17 @@ def _content_part_text(part: object) -> str | None:
|
|||
return text if isinstance(text, str) else None
|
||||
|
||||
|
||||
def message_text_slot_count(message: AllMessageValues) -> int:
|
||||
def message_slot_texts(message: Mapping[str, object]) -> tuple[str, ...]:
|
||||
content: Final = message.get("content")
|
||||
if isinstance(content, str):
|
||||
return 1
|
||||
return (content,)
|
||||
if isinstance(content, list):
|
||||
return sum(1 for part in content if _content_part_text(part) is not None)
|
||||
return 0
|
||||
return tuple(text for part in content if (text := _content_part_text(part)) is not None)
|
||||
return ()
|
||||
|
||||
|
||||
def message_text_slot_count(message: AllMessageValues) -> int:
|
||||
return len(message_slot_texts(message))
|
||||
|
||||
|
||||
def _part_with_text(part: object, text: str) -> object:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from litellm.integrations.custom_guardrail import (
|
|||
CustomGuardrail,
|
||||
log_guardrail_information,
|
||||
)
|
||||
from litellm.llms.base_llm.guardrail_translation.utils import message_with_slot_texts
|
||||
from litellm.llms.base_llm.guardrail_translation.utils import message_slot_texts, message_with_slot_texts
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
get_async_httpx_client,
|
||||
httpxSpecialProvider,
|
||||
|
|
@ -399,19 +399,7 @@ class PromptSecurityGuardrail(CustomGuardrail):
|
|||
return inputs
|
||||
|
||||
def _extract_texts_from_messages(self, messages: Sequence[Mapping[str, object]]) -> list[str]:
|
||||
"""Extract text content from messages."""
|
||||
texts: Final = []
|
||||
for message in messages:
|
||||
content = message.get("content")
|
||||
if isinstance(content, str):
|
||||
texts.append(content)
|
||||
elif isinstance(content, list):
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text = item.get("text")
|
||||
if text:
|
||||
texts.append(text)
|
||||
return texts
|
||||
return [text for message in messages for text in message_slot_texts(message)]
|
||||
|
||||
async def _process_standalone_images(self, images: list[str], user_api_key_alias: str | None) -> None:
|
||||
"""Process standalone images from inputs (data URLs)."""
|
||||
|
|
|
|||
|
|
@ -269,6 +269,30 @@ async def test_modify_with_unexpected_message_count_keeps_texts_only(monkeypatch
|
|||
assert result["texts"] == ["Look up [REDACTED]"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modify_keeps_empty_text_parts_as_slots(monkeypatch: pytest.MonkeyPatch):
|
||||
"""The chat handler counts an empty text part as a slot, so a modify verdict
|
||||
that echoes the empty part still lines up with the row and its texts."""
|
||||
monkeypatch.setenv("PROMPT_SECURITY_API_KEY", "test-key")
|
||||
monkeypatch.setenv("PROMPT_SECURITY_API_BASE", "https://test.prompt.security")
|
||||
guardrail = PromptSecurityGuardrail(guardrail_name="test-guard", event_hook="pre_call", default_on=True)
|
||||
messages: list[AllMessageValues] = [
|
||||
{"role": "user", "content": [{"type": "text", "text": "Look up 123-45-6789"}, {"type": "text", "text": ""}]}
|
||||
]
|
||||
inputs = {"texts": ["Look up 123-45-6789", ""], "structured_messages": messages}
|
||||
modified_messages = [
|
||||
{"role": "user", "content": [{"type": "text", "text": "Look up [REDACTED]"}, {"type": "text", "text": ""}]}
|
||||
]
|
||||
|
||||
with patch.object(guardrail.async_handler, "post", return_value=_modify_response(modified_messages)):
|
||||
result = await guardrail.apply_guardrail(
|
||||
inputs=inputs, request_data={"messages": messages}, input_type="request"
|
||||
)
|
||||
|
||||
assert result["structured_messages"] == modified_messages
|
||||
assert result["texts"] == ["Look up [REDACTED]", ""]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_guardrail_allow_request(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Test that apply_guardrail allows safe prompts"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue