mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-28 06:23:07 +00:00
fix(guardrails): extract nested-wildcard request_fields so the check runs
JsonPathExtractor.evaluate normalized an expression by replacing [*]
with .[*]. and splitting on dots, then recursed into wildcard items
passing the ALREADY-normalized remaining path. Re-running the replace on
that path injected an empty segment (content..[*].text), so every
expression with two array wildcards - e.g. messages[*].content[*].text,
the natural request_fields for chat-shaped pass-through payloads -
evaluated to nothing. extract_fields returned an empty string and the
pass-through handler skipped the guardrail entirely ('No text to check'),
sending the request upstream unchecked while still recording the
guardrail as applied.
The normalization now splits each dot segment by peeling trailing
wildcards one at a time, which is idempotent for the paths recursion
feeds back through, and adjacent wildcards keep working.
Fixes #46302
This commit is contained in:
parent
c581c85559
commit
9a99e317ec
2 changed files with 40 additions and 1 deletions
|
|
@ -55,7 +55,26 @@ class JsonPathExtractor:
|
|||
if not expr or not data:
|
||||
return None
|
||||
|
||||
parts: Final = expr.replace("[*]", ".[*]").split(".")
|
||||
# Normalize "key[*]" into "key" + "[*]" segments. Idempotent: the
|
||||
# recursion below passes already-normalized paths (e.g.
|
||||
# "content.[*].text") back through here, and re-running
|
||||
# expr.replace("[*]", ".[*]") on those would inject empty segments
|
||||
# ("content..[*].text") and silently drop every nested-wildcard
|
||||
# expression, skipping the guardrail (#46302).
|
||||
segments: list[str] = []
|
||||
for segment in expr.split("."):
|
||||
if segment == "[*]":
|
||||
segments.append("[*]")
|
||||
continue
|
||||
# Peel trailing wildcards one at a time so adjacent wildcards
|
||||
# ("a[*][*]") and already-normalized paths both split cleanly.
|
||||
while segment.endswith("[*]"):
|
||||
segments.append(segment[:-3])
|
||||
segments.append("[*]")
|
||||
segment = ""
|
||||
if segment:
|
||||
segments.append(segment)
|
||||
parts: Final = segments
|
||||
current: Any = data
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
|
|
|
|||
|
|
@ -155,6 +155,26 @@ class TestJsonPathExtractorEvaluate:
|
|||
result = JsonPathExtractor.evaluate(data, "messages[*].content")
|
||||
assert result == ["You are helpful", "Hello"]
|
||||
|
||||
def test_nested_array_wildcards(self):
|
||||
"""Should extract values through two array wildcards.
|
||||
|
||||
Regression test for #46302: the recursive wildcard branch passes the
|
||||
already-normalized remaining path (e.g. "content.[*].text") back
|
||||
through evaluate, where the old replace-based normalization injected
|
||||
an empty segment ("content..[*].text") and silently dropped every
|
||||
nested-wildcard expression - so a pass-through guardrail whose only
|
||||
request_fields entry is messages[*].content[*].text never saw any
|
||||
text and skipped the check entirely.
|
||||
"""
|
||||
data = {
|
||||
"messages": [
|
||||
{"role": "user", "content": [{"type": "text", "text": "item1"}]},
|
||||
{"role": "assistant", "content": [{"type": "text", "text": "item2"}]},
|
||||
]
|
||||
}
|
||||
assert JsonPathExtractor.evaluate(data, "messages[*].content[*].text") == ["item1", "item2"]
|
||||
assert JsonPathExtractor.extract_fields(data, ["messages[*].content[*].text"]) == "item1" + chr(10) + "item2"
|
||||
|
||||
def test_missing_key_returns_none(self):
|
||||
"""Should return None for missing key."""
|
||||
data = {"query": "test"}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue