[Fix] CI/CD - Fix Bedrock tool calling test failures with non-serializable objects and internal parameters (#17930)

* fix(bedrock): filter non-serializable objects from request params

- Enhanced filter_exceptions_from_params() to filter callable objects (functions) and Logging objects
- Applied filtering in Bedrock's _prepare_request_params() before deepcopy
- Applied filtering to additional_request_params before JSON serialization
- Prevents TypeError during deepcopy (APIConnectionError objects) and JSON serialization (functions, Logging objects)
- Fixes test_bedrock_tool_calling test failures

Root cause: MCP-related functions (handle_chat_completion_with_mcp, completion_callable) and litellm_logging_obj were incorrectly added to optional_params via add_provider_specific_params_to_optional_params(), which then ended up in additional_request_params. These objects should be in litellm_params, not optional_params.

* fix(bedrock): filter internal MCP parameters from API requests

Filter out LiteLLM internal/MCP-related parameters (skip_mcp_handler,
mcp_handler_context, _skip_mcp_handler) from additional_request_params
before sending to Bedrock API to prevent 'extraneous key' errors.

- Added filter_internal_params() helper function in core_helpers.py
- Applied filtering in Bedrock's _prepare_request_params() method
- Fixes test_bedrock_completion.py::test_bedrock_tool_calling

* fix: mypy type error

* fix: add filter_exceptions_from_params to recursive function ignore list

- Add filter_exceptions_from_params to IGNORE_FUNCTIONS in recursive_detector.py
- Function is safe: has max_depth parameter (default 20) to prevent infinite recursion
This commit is contained in:
Alexsander Hamir
2025-12-13 12:38:07 -08:00
committed by GitHub
parent 5a3cf8b171
commit 892d7e8d70
3 changed files with 121 additions and 4 deletions
+100 -1
View File
@@ -300,4 +300,103 @@ def safe_deep_copy(data):
data["litellm_metadata"][
"litellm_parent_otel_span"
] = litellm_parent_otel_span
return new_data
return new_data
def filter_exceptions_from_params(data: Any, max_depth: int = 20) -> Any:
"""
Recursively filter out Exception objects and callable objects from dicts/lists.
This is a defensive utility to prevent deepcopy failures when exception objects
are accidentally stored in parameter dictionaries (e.g., optional_params).
Also filters callable objects (functions) to prevent JSON serialization errors.
Exceptions and callables should not be stored in params - this function removes them.
Args:
data: The data structure to filter (dict, list, or any other type)
max_depth: Maximum recursion depth to prevent infinite loops
Returns:
Filtered data structure with Exception and callable objects removed, or None if the
entire input was an Exception or callable
"""
if max_depth <= 0:
return data
# Skip exception objects
if isinstance(data, Exception):
return None
# Skip callable objects (functions, methods, lambdas) but not classes (type objects)
if callable(data) and not isinstance(data, type):
return None
# Skip known non-serializable object types (Logging, etc.)
obj_type_name = type(data).__name__
if obj_type_name in ["Logging", "LiteLLMLoggingObj"]:
return None
if isinstance(data, dict):
result: dict[str, Any] = {}
for k, v in data.items():
# Skip exception and callable values
if isinstance(v, Exception) or (callable(v) and not isinstance(v, type)):
continue
try:
filtered = filter_exceptions_from_params(v, max_depth - 1)
if filtered is not None:
result[k] = filtered
except Exception:
# Skip values that cause errors during filtering
continue
return result
elif isinstance(data, list):
result_list: list[Any] = []
for item in data:
# Skip exception and callable items
if isinstance(item, Exception) or (callable(item) and not isinstance(item, type)):
continue
try:
filtered = filter_exceptions_from_params(item, max_depth - 1)
if filtered is not None:
result_list.append(filtered)
except Exception:
# Skip items that cause errors during filtering
continue
return result_list
else:
return data
def filter_internal_params(data: dict, additional_internal_params: Optional[set] = None) -> dict:
"""
Filter out LiteLLM internal parameters that shouldn't be sent to provider APIs.
This removes internal/MCP-related parameters that are used by LiteLLM internally
but should not be included in API requests to providers.
Args:
data: Dictionary of parameters to filter
additional_internal_params: Optional set of additional internal parameter names to filter
Returns:
Filtered dictionary with internal parameters removed
"""
if not isinstance(data, dict):
return data
# Known internal parameters that should never be sent to provider APIs
internal_params = {
"skip_mcp_handler",
"mcp_handler_context",
"_skip_mcp_handler",
}
# Add any additional internal params if provided
if additional_internal_params:
internal_params.update(additional_internal_params)
# Filter out internal parameters
return {
k: v
for k, v in data.items()
if k not in internal_params
}
@@ -12,7 +12,12 @@ import httpx
import litellm
from litellm._logging import verbose_logger
from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
from litellm.litellm_core_utils.core_helpers import map_finish_reason
from litellm.litellm_core_utils.core_helpers import (
filter_exceptions_from_params,
filter_internal_params,
map_finish_reason,
safe_deep_copy,
)
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.litellm_core_utils.prompt_templates.common_utils import (
_parse_content_for_reasoning,
@@ -879,7 +884,10 @@ class AmazonConverseConfig(BaseConfig):
self, optional_params: dict, model: str
) -> Tuple[dict, dict, dict]:
"""Prepare and separate request parameters."""
inference_params = copy.deepcopy(optional_params)
# Filter out exception objects before deepcopy to prevent deepcopy failures
# Exceptions should not be stored in optional_params (this is a defensive fix)
cleaned_params = filter_exceptions_from_params(optional_params)
inference_params = safe_deep_copy(cleaned_params)
supported_converse_params = list(
AmazonConverseConfig.__annotations__.keys()
) + ["top_k"]
@@ -904,11 +912,20 @@ class AmazonConverseConfig(BaseConfig):
inference_params = {
k: v for k, v in inference_params.items() if k in total_supported_params
}
# Only set the topK value in for models that support it
additional_request_params.update(
self._handle_top_k_value(model, inference_params)
)
# Filter out internal/MCP-related parameters that shouldn't be sent to the API
# These are LiteLLM internal parameters, not API parameters
additional_request_params = filter_internal_params(additional_request_params)
# Filter out non-serializable objects (exceptions, callables, logging objects, etc.)
# from additional_request_params to prevent JSON serialization errors
# This filters: Exception objects, callable objects (functions), Logging objects, etc.
additional_request_params = filter_exceptions_from_params(additional_request_params)
return inference_params, additional_request_params, request_metadata
@@ -36,6 +36,7 @@ IGNORE_FUNCTIONS = [
"_collect_argument_paths", # max depth set.
"_split_text", # max depth set.
"_delete_nested_value_custom", # max depth set (bounded by number of path segments).
"filter_exceptions_from_params", # max depth set (default 20) to prevent infinite recursion.
]