Merge pull request #17711 from BerriAI/litellm_add_additional_drop_params_support

feat: Add nested field removal support to additional_drop_params
This commit is contained in:
Sameer Kankute 2025-12-10 15:37:39 +05:30 • committed by GitHub
commit 0d2f8ce931
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 613 additions and 3 deletions

View file

@ -117,6 +117,56 @@ response = litellm.completion(
**additional_drop_params**: List or null - Is a list of openai params you want to drop when making a call to the model.
### Nested Field Removal
Drop nested fields within complex objects using JSONPath-like notation:
<Tabs>
<TabItem value="sdk" label="SDK">
```python
import litellm
response = litellm.completion(
model="bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0",
messages=[{"role": "user", "content": "Hello"}],
tools=[{
"name": "search",
"description": "Search files",
"input_schema": {"type": "object", "properties": {"query": {"type": "string"}}},
"input_examples": [{"query": "test"}] # Will be removed
}],
additional_drop_params=["tools[*].input_examples"] # Remove from all tools
)
```
</TabItem>
<TabItem value="proxy" label="PROXY">
```yaml
model_list:
- model_name: my-bedrock-model
litellm_params:
model: bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0
additional_drop_params: ["tools[*].input_examples"] # Remove from all tools
```
</TabItem>
</Tabs>
**Supported syntax:**
- `field` - Top-level field
- `parent.child` - Nested object field
- `array[*]` - All array elements
- `array[0]` - Specific array index
- `tools[*].input_examples` - Field in all array elements
- `tools[0].metadata.field` - Specific index + nested field
**Example use cases:**
- Remove `input_examples` from tool definitions (Claude Code + AWS Bedrock)
- Drop provider-specific fields from nested structures
- Clean up nested parameters before sending to LLM
## Specify allowed openai params in a request
Tell litellm to allow specific openai params in a request. Use this if you get a `litellm.UnsupportedParamsError` and want to allow a param. LiteLLM will pass the param as is to the model.

View file

@ -1,10 +1,28 @@
"""
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-like array syntax.
Custom implementation with zero external dependencies.
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
from typing import Any, Dict, List, Optional, TypeVar, Union
T = TypeVar("T")
@ -57,3 +75,164 @@ def get_nested_value(
# Otherwise, ensure the type matches the default
return current if isinstance(current, type(default)) else default
def _parse_path_segments(path: str) -> list:
"""
Parse a JSONPath-like string into segments using regex.
Handles:
- Dot notation: "a.b.c" → ["a", "b", "c"]
- Array wildcards: "a[*].b" → ["a", "[*]", "b"]
- Array indices: "a[0].b" → ["a", "[0]", "b"]
Args:
path: JSONPath-like path string
Returns:
List of path segments
Example:
>>> _parse_path_segments("tools[*].arr[0].field")
["tools", "[*]", "arr", "[0]", "field"]
"""
import re
# Match field names OR bracket expressions
# Pattern: field_name (anything except . or [) | [anything_in_brackets]
pattern = r'[^\.\[]+|\[[^\]]*\]'
segments = re.findall(pattern, path)
return segments
def _delete_nested_value_custom(
data: Union[Dict[str, Any], List[Any]],
segments: list,
segment_index: int = 0,
) -> None:
"""
Recursively delete a field from nested data using parsed segments.
Modifies data in-place (caller must deep copy first).
Args:
data: Dictionary or list to modify
segments: Parsed path segments
segment_index: Current position in segments list
"""
if segment_index >= len(segments):
return
segment = segments[segment_index]
is_last = segment_index == len(segments) - 1
# Handle array wildcard: [*]
if segment == "[*]":
if isinstance(data, list):
for item in data:
if is_last:
# Can't delete array elements themselves, skip
pass
else:
# Only recurse if item is a dict or list (nested structure)
if isinstance(item, (dict, list)):
_delete_nested_value_custom(item, segments, segment_index + 1)
return
# Handle array index: [0], [1], [2], etc.
if segment.startswith("[") and segment.endswith("]"):
try:
index = int(segment[1:-1])
if isinstance(data, list) and 0 <= index < len(data):
if is_last:
# Can't delete array elements themselves, skip
pass
else:
# Only recurse if element is a dict or list (nested structure)
element = data[index]
if isinstance(element, (dict, list)):
_delete_nested_value_custom(element, segments, segment_index + 1)
except (ValueError, IndexError):
# Invalid index, skip
pass
return
# Handle regular field navigation
if isinstance(data, dict):
if is_last:
# Delete the field
data.pop(segment, None)
else:
# Navigate deeper
if segment in data:
next_segment = segments[segment_index + 1] if segment_index + 1 < len(segments) else None
# If next segment is array notation, current field should be list
if next_segment and (next_segment.startswith("[")):
if isinstance(data[segment], list):
_delete_nested_value_custom(data[segment], segments, segment_index + 1)
# Otherwise navigate into dict
elif isinstance(data[segment], dict):
_delete_nested_value_custom(data[segment], segments, segment_index + 1)
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.
Custom implementation - no external dependencies.
Supports:
- "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
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
result = copy.deepcopy(data)
try:
# Parse path into segments
segments = _parse_path_segments(path)
if not segments:
return result
# Delete using custom recursive implementation
_delete_nested_value_custom(result, segments, 0)
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

@ -1844,6 +1844,21 @@ class BaseLLMHTTPHandler:
},
custom_llm_provider=custom_llm_provider,
)
# Apply additional_drop_params for nested field removal
additional_drop_params = litellm_params.get("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

View file

@ -35,6 +35,7 @@ IGNORE_FUNCTIONS = [
"_fix_enum_types", # max depth set.
"_collect_argument_paths", # max depth set.
"_split_text", # max depth set.
"_delete_nested_value_custom", # max depth set (bounded by number of path segments).
]

View file

@ -0,0 +1,354 @@
"""
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]
def test_specific_array_index_removes_field_from_single_element(self):
"""Test removing a field from specific array elements using [n] syntax."""
# Test data with multiple array elements
data = {
"tools": [
{"name": "t0", "input_examples": ["ex0"], "keep": "val0"},
{"name": "t1", "input_examples": ["ex1"], "keep": "val1"},
{"name": "t2", "input_examples": ["ex2"], "keep": "val2"},
{"name": "t3", "input_examples": ["ex3"], "keep": "val3"},
{"name": "t4", "input_examples": ["ex4"], "keep": "val4"},
{"name": "t5", "input_examples": ["ex5"], "keep": "val5"},
]
}
# Test [0] - first element
result = delete_nested_value(data, "tools[0].input_examples")
assert "input_examples" not in result["tools"][0]
assert "input_examples" in result["tools"][1]
assert "input_examples" in result["tools"][2]
# Test [1] - second element
result = delete_nested_value(data, "tools[1].input_examples")
assert "input_examples" in result["tools"][0]
assert "input_examples" not in result["tools"][1]
assert "input_examples" in result["tools"][2]
# Test [2] - middle element
result = delete_nested_value(data, "tools[2].input_examples")
assert "input_examples" in result["tools"][0]
assert "input_examples" not in result["tools"][2]
assert "input_examples" in result["tools"][3]
# Test [5] - last element
result = delete_nested_value(data, "tools[5].input_examples")
assert "input_examples" in result["tools"][0]
assert "input_examples" not in result["tools"][5]
# Verify other fields preserved in all cases
assert result["tools"][0]["keep"] == "val0"
assert result["tools"][5]["keep"] == "val5"
# 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"
def test_mixed_wildcards_and_indices_with_deep_nesting(self):
"""Test combining [*] wildcards, [n] indices, and deep nesting in complex patterns."""
data = {
"tools": [
{
"name": "t0",
"configs": [
{"id": "c0", "remove_me": "val1", "keep": "yes1"},
{"id": "c1", "remove_me": "val2", "keep": "yes2"},
],
"metadata": {"drop_this": "meta1", "preserve": "preserve1"},
},
{
"name": "t1",
"configs": [
{"id": "c0", "remove_me": "val3", "keep": "yes3"},
{"id": "c1", "remove_me": "val4", "keep": "yes4"},
],
"metadata": {"drop_this": "meta2", "preserve": "preserve2"},
},
{
"name": "t2",
"configs": [
{"id": "c0", "remove_me": "val5", "keep": "yes5"},
],
"metadata": {"drop_this": "meta3", "preserve": "preserve3"},
},
]
}
# Simulate processing multiple complex paths
paths = [
"tools[*].configs[1].remove_me", # Wildcard + specific index [1] + nested
"tools[1].metadata.drop_this", # Specific index + nested
"tools[*].configs[*].id", # Double wildcard + nested
]
result = data
for path in paths:
result = delete_nested_value(result, path)
# Verify: tools[*].configs[1].remove_me removed from second config of all tools (that have one)
assert "remove_me" in result["tools"][0]["configs"][0] # First config untouched
assert (
"remove_me" not in result["tools"][0]["configs"][1]
) # Second config removed
assert "remove_me" in result["tools"][1]["configs"][0] # First config untouched
assert (
"remove_me" not in result["tools"][1]["configs"][1]
) # Second config removed
assert (
"remove_me" in result["tools"][2]["configs"][0]
) # Only has [0], unaffected
# Verify: tools[1].metadata.drop_this removed only from second tool
assert "drop_this" in result["tools"][0]["metadata"]
assert "drop_this" not in result["tools"][1]["metadata"]
assert "drop_this" in result["tools"][2]["metadata"]
# Verify: tools[*].configs[*].id removed from all configs in all tools
assert "id" not in result["tools"][0]["configs"][0]
assert "id" not in result["tools"][0]["configs"][1]
assert "id" not in result["tools"][1]["configs"][0]
assert "id" not in result["tools"][1]["configs"][1]
assert "id" not in result["tools"][2]["configs"][0]
# Verify: other fields preserved
assert result["tools"][0]["configs"][0]["keep"] == "yes1"
assert result["tools"][1]["configs"][1]["keep"] == "yes4"
assert result["tools"][0]["metadata"]["preserve"] == "preserve1"
assert result["tools"][1]["metadata"]["preserve"] == "preserve2"
assert result["tools"][2]["name"] == "t2"
# Verify original unchanged
assert "remove_me" in data["tools"][0]["configs"][1]
# Phase 1 tests - validates core functionality and complex patterns