From 7e1fa89b07948b1544ec84603da058a773f1d082 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:07:31 +0000 Subject: [PATCH] fix(bedrock_mantle): unwrap Codex's explicit functions tool namespace Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../responses/transformation.py | 43 ++++++++- ...bedrock_mantle_responses_transformation.py | 92 +++++++++++++++++++ 2 files changed, 132 insertions(+), 3 deletions(-) diff --git a/litellm/llms/bedrock_mantle/responses/transformation.py b/litellm/llms/bedrock_mantle/responses/transformation.py index 92da5835b2d..fa05ec4580f 100644 --- a/litellm/llms/bedrock_mantle/responses/transformation.py +++ b/litellm/llms/bedrock_mantle/responses/transformation.py @@ -15,7 +15,7 @@ role / access key / profile / web identity), signed via the shared BaseAWSLLM._sign_request after the request body is finalized. """ -from typing import Any, Final +from typing import Any, Final, cast import litellm from litellm._logging import verbose_logger @@ -48,6 +48,8 @@ _BEDROCK_MANTLE_SUPPORTED_RESPONSE_TOOL_TYPES = frozenset({"function", "mcp", "c _BEDROCK_MANTLE_SUPPORTED_SERVICE_TIERS: Final = frozenset({"auto", "default"}) +_BEDROCK_MANTLE_RESERVED_TOOL_NAMESPACE: Final = "functions" + _CODEX_ADDITIONAL_TOOLS_INPUT_ITEM_TYPE: Final = "additional_tools" @@ -100,6 +102,41 @@ class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPI def supports_native_websocket(self) -> bool: return False + @staticmethod + def _reserved_namespace_nested_tools(tool: object) -> "tuple[object, ...] | None": + if not isinstance(tool, dict): + return None + entry: Final = cast("dict[str, object]", tool) + if entry.get("type") != "namespace" or entry.get("name") != _BEDROCK_MANTLE_RESERVED_TOOL_NAMESPACE: + return None + nested: Final = entry.get("tools") + return tuple(cast("list[object]", nested)) if isinstance(nested, list) else () + + @classmethod + def _flatten_reserved_namespace_tools(cls, tools: "list[object]") -> "list[object]": + """Codex CLI >= 0.147.0 groups its plain function/custom tools into an + explicit {"type": "namespace", "name": "functions", "tools": [...]} entry. + Mantle serves top-level function tools under that same namespace already, + so it rejects the request with "Invalid Value: 'tools.namespace'. + User-defined namespace 'functions' collides with an existing tool + namespace." Unwrapping the entry back to top-level tools is identity + preserving on both ends: Mantle addresses those tools by bare name, and + Codex normalizes a missing namespace to "functions" when it routes the + resulting tool calls. + """ + unwrapped: Final = tuple((tool, cls._reserved_namespace_nested_tools(tool)) for tool in tools) + if all(nested is None for _, nested in unwrapped): + return tools + verbose_logger.debug( + "Bedrock Mantle Responses API: unwrapping the reserved %r tool namespace into top-level tools.", + _BEDROCK_MANTLE_RESERVED_TOOL_NAMESPACE, + ) + return [tool for original, nested in unwrapped for tool in ((original,) if nested is None else nested)] + + @classmethod + def _normalize_tools(cls, tools: "list[object]") -> "list[object]": + return cls._filter_unsupported_tools(cls._flatten_reserved_namespace_tools(tools)) + @staticmethod def _filter_unsupported_tools(tools: list[Any]) -> list[Any]: """Keep only tool types Mantle's Responses API accepts.""" @@ -208,7 +245,7 @@ class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPI len(hoisted_tools), len(additional_tools_items), ) - return remaining_input, cls._filter_unsupported_tools(hoisted_tools) + return remaining_input, cls._normalize_tools(hoisted_tools) def map_openai_params( self, @@ -230,7 +267,7 @@ class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPI return params tools_list: Final = tools if isinstance(tools, list) else [tools] - filtered: Final = self._filter_unsupported_tools(tools_list) + filtered: Final = self._normalize_tools(tools_list) if filtered: params["tools"] = filtered else: diff --git a/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py b/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py index bea979aec64..a094745573b 100644 --- a/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py +++ b/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py @@ -632,6 +632,98 @@ class TestBedrockMantleCodexAdditionalTools: assert "additional_tools" in str(mock_debug.call_args) +class TestBedrockMantleCodexFunctionsNamespace: + """Codex CLI 0.147.0 canonicalizes its default function/custom tools under an + explicit "functions" namespace (openai/codex#37022). Mantle already serves + top-level function tools under that namespace, so it 400s the request with + "Invalid Value: 'tools.namespace'. User-defined namespace 'functions' + collides with an existing tool namespace." (issue #36182), and the config has + to unwrap it back to top-level tools.""" + + _USER_MESSAGE = { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "Say hi in one word."}], + } + + def _functions_namespace(self): + return { + "type": "namespace", + "name": "functions", + "tools": [_codex_exec_tool(), _codex_wait_tool()], + } + + def _transform(self, input, params=None): + cfg = BedrockMantleResponsesAPIConfig() + return cfg.transform_responses_api_request( + model="openai.gpt-5.6-luna", + input=input, + response_api_optional_request_params=params if params is not None else {}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + def test_top_level_functions_namespace_is_unwrapped(self): + params = BedrockMantleResponsesAPIConfig().map_openai_params( + response_api_optional_params={"tools": [self._functions_namespace()]}, + model="openai.gpt-5.6-luna", + drop_params=False, + ) + assert params["tools"] == [_codex_exec_tool(), _codex_wait_tool()] + + def test_other_namespaces_are_preserved(self): + collaboration = {"type": "namespace", "name": "collaboration", "tools": [{"type": "function", "name": "spawn"}]} + params = BedrockMantleResponsesAPIConfig().map_openai_params( + response_api_optional_params={"tools": [self._functions_namespace(), collaboration]}, + model="openai.gpt-5.6-luna", + drop_params=False, + ) + assert params["tools"] == [_codex_exec_tool(), _codex_wait_tool(), collaboration] + + def test_unsupported_tool_types_inside_namespace_are_dropped(self): + params = BedrockMantleResponsesAPIConfig().map_openai_params( + response_api_optional_params={ + "tools": [ + { + "type": "namespace", + "name": "functions", + "tools": [{"type": "web_search"}, _codex_wait_tool()], + } + ] + }, + model="openai.gpt-5.6-luna", + drop_params=False, + ) + assert params["tools"] == [_codex_wait_tool()] + + def test_empty_functions_namespace_removes_tools(self): + params = BedrockMantleResponsesAPIConfig().map_openai_params( + response_api_optional_params={"tools": [{"type": "namespace", "name": "functions", "tools": []}]}, + model="openai.gpt-5.6-luna", + drop_params=False, + ) + assert "tools" not in params + + def test_functions_namespace_hoisted_out_of_additional_tools_is_unwrapped(self): + body = self._transform( + input=[ + {"type": "additional_tools", "role": "developer", "tools": [self._functions_namespace()]}, + self._USER_MESSAGE, + ] + ) + assert body["input"] == [self._USER_MESSAGE] + assert body["tools"] == [_codex_exec_tool(), _codex_wait_tool()] + + def test_request_without_functions_namespace_is_untouched(self): + tools = [_codex_wait_tool(), {"type": "namespace", "name": "collaboration", "tools": []}] + params = BedrockMantleResponsesAPIConfig().map_openai_params( + response_api_optional_params={"tools": list(tools)}, + model="openai.gpt-5.6-luna", + drop_params=False, + ) + assert params["tools"] == tools + + class TestBedrockMantleResponsesRegistry: def test_registry_returns_config_for_gpt_5_5(self, local_cost_map): # gpt-5.x advertises /v1/responses in supported_endpoints (capability)