diff --git a/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/chunked_evaluation.py b/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/chunked_evaluation.py index d69f292edfb..444aaff89d8 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/chunked_evaluation.py +++ b/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/chunked_evaluation.py @@ -38,26 +38,6 @@ class SegmentVerdict: masked_chunks: list[tuple[str, str]] | None = None -@dataclass(frozen=True) -class WindowConfig: - """Tuning for the chunk-seam overlap. - - ``overlap`` sizes the read-only prefix each chunk carries from the previous - owned region (see ``_overlap_chunks``). There are no cross-segment windows: - on the request side message parts are concatenated into one joined document - before scanning (so their junctions are interior chunk seams); on the - response side each segment is an independent choice or tool-call arg that the - model never concatenates. - """ - - overlap: int = CHUNK_OVERLAP_CHARS - - -# Shared default so the ``evaluate_segments`` signature has a plain-name default -# (no call in the argument default); safe to share since ``WindowConfig`` is frozen. -_DEFAULT_WINDOW_CONFIG = WindowConfig() - - def _split_text(text: str, max_chars: int) -> list[str]: """Split ``text`` into <= ``max_chars`` chunks with ``"".join(chunks) == text``. @@ -154,7 +134,7 @@ async def evaluate_segments( evaluate: Callable[[str], Awaitable[Any]], max_chars: int = MAX_PROMPT_CHARS, max_concurrency: int = DEFAULT_MAX_CONCURRENCY, - windows: WindowConfig = _DEFAULT_WINDOW_CONFIG, + overlap: int = CHUNK_OVERLAP_CHARS, ) -> list[SegmentVerdict]: """Evaluate every segment (chunked) in parallel; return one verdict per segment. @@ -169,7 +149,9 @@ async def evaluate_segments( The request side passes a single joined document here (one segment) so the common case is one call; the response side passes one segment per choice / - tool-call arg. There is no cross-segment window (see ``WindowConfig``). + tool-call arg. There is no cross-segment overlap: message parts are already + joined into one document on the request side, and response choices are + independent. """ semaphore = asyncio.Semaphore(max_concurrency) @@ -178,7 +160,7 @@ async def evaluate_segments( return await evaluate(text) # Keep each chunk's scan input (prefix + owned) within the prompt limit. - ov = min(windows.overlap, max_chars // 2) + ov = min(overlap, max_chars // 2) seg_chunks = [_overlap_chunks(s, max_chars, ov) for s in segments] index: list[tuple[int, int]] = [] diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/test_chunked_evaluation.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/test_chunked_evaluation.py index 951d7190084..8f978330515 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/test_chunked_evaluation.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/test_chunked_evaluation.py @@ -8,7 +8,6 @@ import pytest from litellm.proxy.guardrails.guardrail_hooks.alice_wonderfence.chunked_evaluation import ( MAX_PROMPT_CHARS, SegmentVerdict, - WindowConfig, _split_text, evaluate_segments, ) @@ -99,7 +98,7 @@ async def test_mask_rejoins_masked_owned_regions_into_full_segment(): chunks = _split_text(segment, 20) assert len(chunks) > 1 - verdicts = await evaluate_segments([segment], evaluate, max_chars=20, windows=WindowConfig(overlap=0)) + verdicts = await evaluate_segments([segment], evaluate, max_chars=20, overlap=0) assert verdicts[0].action == "MASK" assert verdicts[0].masked_text == segment.replace("SECRET", "[X]") assert verdicts[0].masked_chunks is not None @@ -115,7 +114,7 @@ async def test_unmasked_chunks_keep_original_text_on_rejoin(): async def evaluate(text): return _result("MASK", action_text=text.replace("w0", "[X]")) if "w0 " in text else _result("") - verdicts = await evaluate_segments([segment], evaluate, max_chars=20, windows=WindowConfig(overlap=0)) + verdicts = await evaluate_segments([segment], evaluate, max_chars=20, overlap=0) assert verdicts[0].action == "MASK" assert verdicts[0].masked_text == segment.replace("w0", "[X]", 1) @@ -191,7 +190,7 @@ async def test_block_phrase_split_across_chunk_seam_is_detected(): async def evaluate(text): return _result("BLOCK" if "BLOCK ME" in text else "") - verdicts = await evaluate_segments([segment], evaluate, max_chars=12, windows=WindowConfig(overlap=6)) + verdicts = await evaluate_segments([segment], evaluate, max_chars=12, overlap=6) assert verdicts[0].action == "BLOCK" @@ -204,7 +203,7 @@ async def test_no_overlap_lets_seam_phrase_evade(): async def evaluate(text): return _result("BLOCK" if "BLOCK ME" in text else "") - verdicts = await evaluate_segments([segment], evaluate, max_chars=12, windows=WindowConfig(overlap=0)) + verdicts = await evaluate_segments([segment], evaluate, max_chars=12, overlap=0) assert verdicts[0].action == "" @@ -235,7 +234,7 @@ async def test_mask_straddling_a_seam_fails_closed_as_block(): # it; the redaction lands in the prefix bytes -> fail closed. return _result("MASK", action_text=text.replace("SECRET HERE", "[X]")) if "SECRET HERE" in text else _result("") - verdicts = await evaluate_segments([segment], evaluate, max_chars=12, windows=WindowConfig(overlap=6)) + verdicts = await evaluate_segments([segment], evaluate, max_chars=12, overlap=6) assert verdicts[0].action == "BLOCK"