feat: Add nested field removal support to additional_drop_params using JSONPath

This commit is contained in:
mzagar 2025-11-17 15:29:01 +01:00 committed by Sameer Kankute
parent 5b926ae2c6
commit da2aa2ba8d
6 changed files with 2345 additions and 2857 deletions

View File

@ -1,7 +1,25 @@
"""
This file contains the logic for dot notation indexing.
Path-based navigation utilities for nested dictionaries.
Used by JWT Auth to get the user role from the token.
This module provides utilities for reading and deleting values in nested
dictionaries using dot notation and JSONPath array syntax.
Uses jsonpath-ng library for standard JSONPath parsing and navigation.
Supported syntax:
- "field" - top-level field
- "parent.child" - nested field
- "array[*]" - all array elements (wildcard)
- "array[0]" - specific array element (index)
- "array[*].field" - field in all array elements
Examples:
>>> data = {"tools": [{"name": "t1", "input_examples": ["ex"]}]}
>>> delete_nested_value(data, "tools[*].input_examples")
{"tools": [{"name": "t1"}]}
Used by JWT Auth to get the user role from the token, and by
additional_drop_params to remove nested fields from optional parameters.
"""
from typing import Any, Dict, Optional, TypeVar
@ -57,3 +75,78 @@ def get_nested_value(
# Otherwise, ensure the type matches the default
return current if isinstance(current, type(default)) else default
def delete_nested_value(
data: Dict[str, Any],
path: str,
depth: int = 0,
max_depth: int = 20,
) -> Dict[str, Any]:
"""
Delete a field from nested data using JSONPath notation.
Uses jsonpath-ng library for standard JSONPath parsing.
Supports:
- "field" - top-level field
- "parent.child" - nested field
- "array[*]" - all array elements
- "array[0]" - specific array element
- "array[*].field" - field in all array elements
Args:
data: Dictionary to modify (creates deep copy)
path: JSONPath-like path string
depth: Current recursion depth (kept for API compatibility)
max_depth: Maximum recursion depth (kept for API compatibility)
Returns:
New dictionary with field removed at path
Example:
>>> data = {"tools": [{"name": "t1", "input_examples": ["ex"]}]}
>>> delete_nested_value(data, "tools[*].input_examples")
{"tools": [{"name": "t1"}]}
"""
import copy
from jsonpath_ng import parse
result = copy.deepcopy(data)
# Add $ prefix required by jsonpath-ng
if not path.startswith("$"):
path = f"$.{path}"
try:
expr = parse(path)
matches = expr.find(result)
# Process matches in reverse to handle array deletions correctly
for match in reversed(matches):
parent = match.context.value if match.context else result
if isinstance(parent, list):
if hasattr(match.path, "index"):
idx = match.path.index
if 0 <= idx < len(parent):
parent.pop(idx)
elif isinstance(parent, dict):
if hasattr(match.path, "fields") and match.path.fields:
field = match.path.fields[0]
parent.pop(field, None)
except Exception:
# Invalid path or parsing error - silently skip
pass
return result
def is_nested_path(path: str) -> bool:
"""
Check if path requires nested handling.
Returns True if path contains '.' or '[' (array notation).
"""
return "." in path or "[" in path

View File

@ -1843,6 +1843,21 @@ class BaseLLMHTTPHandler:
},
custom_llm_provider=custom_llm_provider,
)
# Apply additional_drop_params for nested field removal
additional_drop_params = litellm_params.additional_drop_params
if additional_drop_params:
from litellm.litellm_core_utils.dot_notation_indexing import (
delete_nested_value,
is_nested_path,
)
nested_paths = [p for p in additional_drop_params if is_nested_path(p)]
for path in nested_paths:
anthropic_messages_optional_request_params = delete_nested_value(
anthropic_messages_optional_request_params, path
)
# Prepare request body
request_body = anthropic_messages_provider_config.transform_anthropic_messages_request(
model=model,

View File

@ -138,6 +138,10 @@ from litellm.litellm_core_utils.redact_messages import (
LiteLLMLoggingObject,
redact_message_input_output_from_logging,
)
from litellm.litellm_core_utils.dot_notation_indexing import (
delete_nested_value,
is_nested_path,
)
from litellm.litellm_core_utils.rules import Rules
from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper
from litellm.litellm_core_utils.token_counter import get_modified_max_tokens
@ -4148,6 +4152,13 @@ def get_optional_params( # noqa: PLR0915
non_default_params=non_default_params,
allowed_openai_params=allowed_openai_params,
)
# Apply nested drops from additional_drop_params
if additional_drop_params:
nested_paths = [p for p in additional_drop_params if is_nested_path(p)]
for path in nested_paths:
optional_params = delete_nested_value(optional_params, path)
return optional_params

4844
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -68,6 +68,7 @@ semantic-router = {version = ">=0.1.12", optional = true, python = ">=3.9,<3.14"
mlflow = {version = ">3.1.4", optional = true, python = ">=3.10"}
soundfile = {version = "^0.12.1", optional = true}
grpcio = ">=1.62.3,<1.68.0" # Constrain to < 1.68.0 to avoid resource exhausted bug (https://github.com/grpc/grpc/issues/38290). Minimum 1.62.3 required by grpcio-status.
jsonpath-ng = "^1.7.0"
[tool.poetry.extras]
proxy = [

View File

@ -0,0 +1,234 @@
"""
Test nested path support in additional_drop_params.
This tests the new JSONPath-like syntax for removing nested fields.
"""
import os
import sys
# Add parent directory to path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
from litellm.litellm_core_utils.dot_notation_indexing import (
delete_nested_value,
is_nested_path,
)
class TestIsNestedPath:
"""Test path detection."""
def test_top_level_path(self):
"""Top-level paths should return False."""
assert is_nested_path("temperature") is False
assert is_nested_path("response_format") is False
def test_nested_path_with_dot(self):
"""Paths with dots are nested."""
assert is_nested_path("parent.child") is True
def test_nested_path_with_array(self):
"""Paths with array notation are nested."""
assert is_nested_path("tools[*].input_examples") is True
assert is_nested_path("tools[0].field") is True
class TestDeleteNestedValue:
"""Test the core deletion logic."""
def test_array_wildcard_removes_field_from_all_elements(self):
"""Test removing a field from all array elements."""
data = {
"tools": [
{"name": "tool1", "input_examples": ["ex1"]},
{"name": "tool2", "input_examples": ["ex2"]},
],
"temperature": 0.7,
}
result = delete_nested_value(data, "tools[*].input_examples")
# Verify structure preserved
assert len(result["tools"]) == 2
assert result["tools"][0]["name"] == "tool1"
assert result["tools"][1]["name"] == "tool2"
assert result["temperature"] == 0.7
# Verify input_examples removed
assert "input_examples" not in result["tools"][0]
assert "input_examples" not in result["tools"][1]
# Verify original unchanged (deep copy)
assert "input_examples" in data["tools"][0]
class TestComplexNestedPatterns:
"""Test complex nested patterns with multiple wildcards and deep nesting."""
def test_multiple_jsonpath_patterns_in_list(self):
"""Test processing multiple JSONPath patterns sequentially."""
data = {
"tools": [
{
"name": "tool1",
"input_examples": ["ex1"],
"some_arr": [
{
"some_struct": {
"remove_this_field": "val1",
"keep_this": "val2",
}
},
{
"some_struct": {
"remove_this_field": "val3",
"keep_this": "val4",
}
},
],
},
{
"name": "tool2",
"input_examples": ["ex2"],
"some_arr": [
{
"some_struct": {
"remove_this_field": "val5",
"keep_this": "val6",
}
}
],
},
],
"temperature": 0.7,
}
# Simulate multiple paths being processed (as in utils.py:4134-4137)
paths = [
"tools[*].input_examples",
"tools[*].some_arr[*].some_struct.remove_this_field",
]
result = data
for path in paths:
result = delete_nested_value(result, path)
# Verify input_examples removed from all tools
assert "input_examples" not in result["tools"][0]
assert "input_examples" not in result["tools"][1]
# Verify deeply nested field removed from all array elements
assert (
"remove_this_field"
not in result["tools"][0]["some_arr"][0]["some_struct"]
)
assert (
"remove_this_field"
not in result["tools"][0]["some_arr"][1]["some_struct"]
)
assert (
"remove_this_field"
not in result["tools"][1]["some_arr"][0]["some_struct"]
)
# Verify other fields preserved
assert result["tools"][0]["some_arr"][0]["some_struct"]["keep_this"] == "val2"
assert result["tools"][1]["some_arr"][0]["some_struct"]["keep_this"] == "val6"
assert result["temperature"] == 0.7
def test_remove_entire_nested_array_field(self):
"""Test removing entire array fields (not just array elements)."""
data = {
"tools": [
{"name": "t1", "some_arr": [1, 2, 3], "other_field": "keep"},
{"name": "t2", "some_arr": [4, 5, 6], "other_field": "keep"},
]
}
result = delete_nested_value(data, "tools[*].some_arr")
# Verify entire array field removed (not individual elements)
assert "some_arr" not in result["tools"][0]
assert "some_arr" not in result["tools"][1]
# Verify other fields preserved
assert result["tools"][0]["name"] == "t1"
assert result["tools"][0]["other_field"] == "keep"
assert result["tools"][1]["name"] == "t2"
assert result["tools"][1]["other_field"] == "keep"
def test_triple_nested_wildcards(self):
"""Test extreme nesting: tools[*].arr1[*].arr2[*].field."""
data = {
"tools": [
{
"name": "t1",
"arr1": [
{
"arr2": [
{"field": "remove1", "keep": "yes1"},
{"field": "remove2", "keep": "yes2"},
]
},
{
"arr2": [
{"field": "remove3", "keep": "yes3"},
]
},
],
}
]
}
result = delete_nested_value(data, "tools[*].arr1[*].arr2[*].field")
# Verify deeply nested field removed from all levels
assert "field" not in result["tools"][0]["arr1"][0]["arr2"][0]
assert "field" not in result["tools"][0]["arr1"][0]["arr2"][1]
assert "field" not in result["tools"][0]["arr1"][1]["arr2"][0]
# Verify keep field preserved at all levels
assert result["tools"][0]["arr1"][0]["arr2"][0]["keep"] == "yes1"
assert result["tools"][0]["arr1"][0]["arr2"][1]["keep"] == "yes2"
assert result["tools"][0]["arr1"][1]["arr2"][0]["keep"] == "yes3"
def test_combination_of_simple_and_complex_paths(self):
"""Test mixing simple nested paths with complex multi-wildcard paths."""
data = {
"tools": [
{
"name": "t1",
"simple_nested": {"remove": "val1", "keep": "val2"},
"complex": [{"nested": {"remove": "val3", "keep": "val4"}}],
}
],
"top_level_remove": "should_go",
"top_level_keep": "should_stay",
}
# Process multiple different types of paths
paths = [
"tools[*].simple_nested.remove",
"tools[*].complex[*].nested.remove",
]
result = data
for path in paths:
result = delete_nested_value(result, path)
# Verify simple nested removal
assert "remove" not in result["tools"][0]["simple_nested"]
assert result["tools"][0]["simple_nested"]["keep"] == "val2"
# Verify complex nested removal
assert "remove" not in result["tools"][0]["complex"][0]["nested"]
assert result["tools"][0]["complex"][0]["nested"]["keep"] == "val4"
# Verify top-level fields unchanged
assert result["top_level_remove"] == "should_go"
assert result["top_level_keep"] == "should_stay"
# Phase 1 tests - validates core functionality and complex patterns