From 892d7e8d709a46f66d6eb411bff2b68190d64ef5 Mon Sep 17 00:00:00 2001 From: Alexsander Hamir Date: Sat, 13 Dec 2025 12:38:07 -0800 Subject: [PATCH] [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 --- litellm/litellm_core_utils/core_helpers.py | 101 +++++++++++++++++- .../bedrock/chat/converse_transformation.py | 23 +++- .../code_coverage_tests/recursive_detector.py | 1 + 3 files changed, 121 insertions(+), 4 deletions(-) diff --git a/litellm/litellm_core_utils/core_helpers.py b/litellm/litellm_core_utils/core_helpers.py index 47034c3a5c..9378ca71f5 100644 --- a/litellm/litellm_core_utils/core_helpers.py +++ b/litellm/litellm_core_utils/core_helpers.py @@ -300,4 +300,103 @@ def safe_deep_copy(data): data["litellm_metadata"][ "litellm_parent_otel_span" ] = litellm_parent_otel_span - return new_data \ No newline at end of file + 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 + } \ No newline at end of file diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index ae0f1baf38..13dbec3952 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -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 diff --git a/tests/code_coverage_tests/recursive_detector.py b/tests/code_coverage_tests/recursive_detector.py index 1a3a3260f7..8331738bab 100644 --- a/tests/code_coverage_tests/recursive_detector.py +++ b/tests/code_coverage_tests/recursive_detector.py @@ -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. ]