This commit is contained in:
OrangerXu 2026-10-21 03:36:45 +00:00 • committed by GitHub
commit e8a01e7a4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 37 additions and 1 deletions

View file

@ -9,6 +9,22 @@ from typing import Any, Final
from litellm._logging import verbose_proxy_logger
def _normalize_wildcard_segments(expr: str) -> tuple[str, ...]:
"""Normalize ``key[*]`` into ``key`` + ``[*]`` segments.
Idempotent: the recursion in :meth:`JsonPathExtractor.evaluate` passes
already-normalized paths (e.g. ``content.[*].text``) back through here.
Re-running ``expr.replace("[*]", ".[*]")`` on those injects empty
segments (``content..[*].text``); dropping empty segments afterwards
keeps the normalization stable for raw (``a[*].b``), adjacent
(``a[*][*].b``) and already-normalized inputs alike, so no
nested-wildcard expression is silently dropped and skips the
guardrail (#46302).
"""
segments: Final = tuple(segment for segment in expr.replace("[*]", ".[*]").split(".") if segment)
return segments
class JsonPathExtractor:
"""Extracts field values from data using JSONPath-like expressions."""
@ -55,7 +71,7 @@ class JsonPathExtractor:
if not expr or not data:
return None
parts: Final = expr.replace("[*]", ".[*]").split(".")
parts: Final = _normalize_wildcard_segments(expr)
current: Any = data
for i, part in enumerate(parts):

View file

@ -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"}