mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
Fix streaming tone detection gap and logging helper misclassification
- Add tone checker to async_post_call_streaming_iterator_hook so tone violations are detected in during_call (streaming) mode, not silently skipped - Fix _get_detection_methods: tone detections now report "regex" instead of falling through to "keyword" - Fix _build_match_details: add "tone" case with detection_method, category, and action_taken fields for correct observability logging - Add tests: streaming blocks/passes, detection method labels, match details Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8304b3ae75
commit
f7658ec06a
2 changed files with 101 additions and 1 deletions
|
|
@ -1626,6 +1626,11 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
detail["detection_method"] = "intent"
|
||||
detail["snippet"] = detection.get("intent", "")
|
||||
detail["confidence"] = detection.get("confidence")
|
||||
elif detection["type"] == "tone":
|
||||
detail["detection_method"] = "regex"
|
||||
tone_det = cast(ToneDetection, detection)
|
||||
detail["category"] = tone_det.get("category", "")
|
||||
detail["action_taken"] = "BLOCK"
|
||||
match_details.append(detail)
|
||||
return match_details
|
||||
|
||||
|
|
@ -1633,7 +1638,7 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
"""Get comma-separated detection methods used."""
|
||||
methods: set = set()
|
||||
for detection in detections:
|
||||
if detection["type"] == "pattern":
|
||||
if detection["type"] in ("pattern", "tone"):
|
||||
methods.add("regex")
|
||||
elif detection["type"] == "competitor_intent":
|
||||
methods.add("intent")
|
||||
|
|
@ -1956,6 +1961,11 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
text_to_check += " "
|
||||
|
||||
try:
|
||||
# Tone detection on streaming responses (during_call is always a response)
|
||||
if self._tone_checker and text_to_check:
|
||||
tone_result = self._tone_checker.run(text_to_check)
|
||||
if tone_result is not None:
|
||||
self._apply_tone_detection_policy(tone_result, [])
|
||||
masked_text = self._filter_single_text(text_to_check)
|
||||
if is_final and masked_text.endswith(" "):
|
||||
masked_text = masked_text[:-1]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ Covers:
|
|||
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ from fastapi import HTTPException
|
|||
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import (
|
||||
ContentFilterGuardrail,
|
||||
)
|
||||
from litellm.types.utils import ModelResponseStream
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
|
|
@ -495,3 +497,91 @@ class TestInputTypeGuard:
|
|||
{},
|
||||
"response",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# STREAMING — tone detection in during_call (streaming) mode
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_stream_chunk(content, finish_reason=None):
|
||||
"""Build a minimal ModelResponseStream chunk."""
|
||||
delta = MagicMock()
|
||||
delta.content = content
|
||||
choice = MagicMock()
|
||||
choice.delta = delta
|
||||
choice.finish_reason = finish_reason
|
||||
chunk = MagicMock(spec=ModelResponseStream)
|
||||
chunk.choices = [choice]
|
||||
return chunk
|
||||
|
||||
|
||||
async def _iter_chunks(chunks):
|
||||
for c in chunks:
|
||||
yield c
|
||||
|
||||
|
||||
class TestStreamingToneDetection:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_blocks_tone_violation(self):
|
||||
"""Tone checker should fire in streaming mode and raise on violation."""
|
||||
g = _make_guardrail()
|
||||
chunks = [
|
||||
_make_stream_chunk("That's not my problem.", finish_reason="stop"),
|
||||
]
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
async for _ in g.async_post_call_streaming_iterator_hook(
|
||||
MagicMock(), _iter_chunks(chunks), {},
|
||||
):
|
||||
pass
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_passes_clean_text(self):
|
||||
"""Clean text should stream through without raising."""
|
||||
g = _make_guardrail()
|
||||
chunks = [
|
||||
_make_stream_chunk("Thank you for reaching out!", finish_reason="stop"),
|
||||
]
|
||||
yielded = []
|
||||
async for item in g.async_post_call_streaming_iterator_hook(
|
||||
MagicMock(), _iter_chunks(chunks), {},
|
||||
):
|
||||
yielded.append(item)
|
||||
assert len(yielded) > 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LOGGING HELPERS — tone detections should be correctly labeled
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestLoggingHelpers:
|
||||
|
||||
def test_get_detection_methods_tone(self):
|
||||
"""Tone detections should report 'regex' as detection method."""
|
||||
g = _make_guardrail()
|
||||
detections = [{"type": "tone", "category": "dismissive", "matched_text": "x"}]
|
||||
result = g._get_detection_methods(detections)
|
||||
assert result == "regex"
|
||||
|
||||
def test_get_detection_methods_tone_and_keyword(self):
|
||||
"""Mixed detections should report both methods."""
|
||||
g = _make_guardrail()
|
||||
detections = [
|
||||
{"type": "tone", "category": "dismissive", "matched_text": "x"},
|
||||
{"type": "blocked_word", "keyword": "bad", "action": "BLOCK", "description": None},
|
||||
]
|
||||
result = g._get_detection_methods(detections)
|
||||
assert "regex" in result
|
||||
assert "keyword" in result
|
||||
|
||||
def test_build_match_details_tone(self):
|
||||
"""Tone detections should produce correct match_details entries."""
|
||||
g = _make_guardrail()
|
||||
detections = [{"type": "tone", "category": "impatience", "matched_text": "x"}]
|
||||
details = g._build_match_details(detections)
|
||||
assert len(details) == 1
|
||||
assert details[0]["type"] == "tone"
|
||||
assert details[0]["detection_method"] == "regex"
|
||||
assert details[0]["category"] == "impatience"
|
||||
assert details[0]["action_taken"] == "BLOCK"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue