From 6cd13687d7ad43b168ea94ff86a94a13774214a0 Mon Sep 17 00:00:00 2001 From: jmxjiamingxu <1040647315@qq.com> Date: Sun, 30 Aug 2026 12:37:41 +0800 Subject: [PATCH 1/4] fix(bedrock): forward freeform tools as parameterless toolSpecs, dropping the custom field OpenAI Agents SDK freeform tools ({"type": "custom", "name": ..., "custom": {...}}) carry a top-level name and no function payload. On v1.90.1 the custom key reached Bedrock Converse and the request failed with 400 "tools.0.custom.strict: Extra inputs are not permitted" (#38799); on current main the tool is silently dropped by the unnamed-builtin guard, so the model never sees it. A named type=custom tool is now forwarded as a parameterless toolSpec with the provider-specific extras stripped; nameless built-ins (web_search, image_generation) and other named built-ins (namespace, tool_search) are still dropped. Fixes #38799 --- .../prompt_templates/factory.py | 27 +++++++++--- ...llm_core_utils_prompt_templates_factory.py | 44 ++++++++++++++++++- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 795fb36961e..9dfadbbd9b6 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -5064,14 +5064,29 @@ def _bedrock_tools_pt(tools: list, model: str | None = None) -> list[BedrockTool tool_block_list.append(tool) continue - # Responses built-in tools (web_search, image_generation, namespace, tool_search, - # custom) carry neither an OpenAI "function" nor an Anthropic "input_schema" and have - # no Bedrock toolSpec equivalent; drop them instead of emitting an empty junk toolSpec. - if isinstance(tool, dict) and "function" not in tool and "input_schema" not in tool: + # OpenAI Agents SDK freeform tools ({"type": "custom", "name": ..., + # "description": ..., "custom": {...}}) carry a top-level name and no + # function payload; forward them as parameterless toolSpecs, dropping + # provider-specific extras like `custom`, which Bedrock rejects (#38799). + # Other named-but-builtin tools (e.g. namespace) and nameless Responses + # built-ins (web_search, image_generation, ...) have no Bedrock toolSpec + # equivalent and are still dropped below. + if ( + isinstance(tool, dict) + and "function" not in tool + and "input_schema" not in tool + and tool.get("type") == "custom" + ): + freeform_name = tool.get("name") + if not (isinstance(freeform_name, str) and freeform_name.strip()): + continue + parameters = {"type": "object", "properties": {}} + raw_name = freeform_name + _tool_description = tool.get("description", None) + elif isinstance(tool, dict) and "function" not in tool and "input_schema" not in tool: continue - # OpenAI function tools, or Anthropic Messages / Claude Code ({name, input_schema, type, ...}) - if isinstance(tool, dict) and "input_schema" in tool and "function" not in tool: + elif isinstance(tool, dict) and "input_schema" in tool and "function" not in tool: parameters = copy.deepcopy(tool.get("input_schema") or {"type": "object", "properties": {}}) raw_name = tool.get("name", "") or "" _tool_description = tool.get("description", None) diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py index 72d26f31c60..5780a1bea6b 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py @@ -1030,6 +1030,42 @@ def test_bedrock_tools_pt_strict_parameter(): assert "additionalProperties" not in result[0]["toolSpec"]["inputSchema"]["json"] +def test_bedrock_tools_pt_forwards_freeform_tool_without_custom_field(): + """OpenAI Agents SDK freeform tools carry a top-level name and a provider-specific + `custom` dict instead of a function payload. Bedrock rejects the `custom` key + outright (400 "tools.0.custom.strict: Extra inputs are not permitted"), so the + tool must be forwarded as a parameterless toolSpec with the extras stripped + rather than dropped or passed through. + """ + tools = [ + { + "type": "custom", + "name": "apply_patch", + "description": "Apply a unified diff patch to the workspace.", + "custom": {"strict": None}, + } + ] + result = _bedrock_tools_pt(tools, model="us.anthropic.claude-sonnet-5") + assert len(result) == 1, "the freeform tool must not be dropped" + tool_spec = result[0]["toolSpec"] + assert tool_spec["name"] == "apply_patch" + assert tool_spec["description"] == "Apply a unified diff patch to the workspace." + assert tool_spec["inputSchema"]["json"] == {"type": "object", "properties": {}, "required": []} + assert "custom" not in tool_spec + assert "strict" not in tool_spec + + +def test_bedrock_tools_pt_still_drops_unnamed_builtin_tools(): + """Responses built-in tools (web_search, image_generation, ...) have no top-level + name and no Bedrock toolSpec equivalent; they must stay dropped. + """ + tools = [ + {"type": "web_search_preview"}, + {"type": "image_generation"}, + ] + assert _bedrock_tools_pt(tools, model="us.anthropic.claude-sonnet-5") == [] + + def test_bedrock_image_processor_content_type_fallback_url_extension(): """ Test that _post_call_image_processing falls back to URL extension @@ -1632,9 +1668,13 @@ def test_bedrock_tools_pt_does_not_handle_system_tool(): def test_bedrock_tools_pt_drops_unmappable_responses_builtin_tools(): """ Regression for LIT-3858: Responses built-in tools (image_generation, namespace, - tool_search, custom) have no Bedrock toolSpec equivalent. They must be dropped, not + tool_search) have no Bedrock toolSpec equivalent. They must be dropped, not emitted as junk ``litellm_unnamed_tool_N`` toolSpecs the model can hallucinate calls to. Mappable ``function`` and Anthropic ``input_schema`` tools must survive untouched. + + Named ``custom`` tools (freeform tools, e.g. the OpenAI Agents SDK's apply_patch) + ARE mappable — a parameterless toolSpec — and are forwarded since #38799; only + nameless built-ins stay dropped. """ from litellm.litellm_core_utils.prompt_templates.factory import _bedrock_tools_pt @@ -1657,7 +1697,7 @@ def test_bedrock_tools_pt_drops_unmappable_responses_builtin_tools(): ) names = [block["toolSpec"]["name"] for block in result if "toolSpec" in block] - assert names == ["noop"] + assert names == ["noop", "free_form"], "named built-ins like namespace must stay dropped" assert not any(name.startswith("litellm_unnamed_tool_") for name in names) From 37ee0468fef81bf800c9284ae2150b8a1f35d8fa Mon Sep 17 00:00:00 2001 From: jmxjiamingxu <1040647315@qq.com> Date: Sun, 30 Aug 2026 13:35:17 +0800 Subject: [PATCH 2/4] test(bedrock): cover nameless custom tool drop --- .../test_litellm_core_utils_prompt_templates_factory.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py index 5780a1bea6b..4ab828130f3 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py @@ -1057,11 +1057,13 @@ def test_bedrock_tools_pt_forwards_freeform_tool_without_custom_field(): def test_bedrock_tools_pt_still_drops_unnamed_builtin_tools(): """Responses built-in tools (web_search, image_generation, ...) have no top-level - name and no Bedrock toolSpec equivalent; they must stay dropped. + name and no Bedrock toolSpec equivalent; they must stay dropped. A `custom`-typed + tool without a name is equally unmappable — only a named one is forwarded. """ tools = [ {"type": "web_search_preview"}, {"type": "image_generation"}, + {"type": "custom"}, ] assert _bedrock_tools_pt(tools, model="us.anthropic.claude-sonnet-5") == [] From 1f508fe90b809d67d20cdbbe50672bad95c197ee Mon Sep 17 00:00:00 2001 From: jmxjiamingxu <1040647315@qq.com> Date: Sun, 30 Aug 2026 14:07:20 +0800 Subject: [PATCH 3/4] chore: retrigger CI after retargeting the PR base to litellm_internal_staging From 8092ed6b0aa1de54410a91dff6ddb84809396377 Mon Sep 17 00:00:00 2001 From: jmxjiamingxu <1040647315@qq.com> Date: Sun, 30 Aug 2026 14:20:16 +0800 Subject: [PATCH 4/4] style(bedrock): mark the freeform toolSpec schema dicts for the budget gate --- litellm/litellm_core_utils/prompt_templates/factory.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 9dfadbbd9b6..72a7b58e39c 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -5080,7 +5080,10 @@ def _bedrock_tools_pt(tools: list, model: str | None = None) -> list[BedrockTool freeform_name = tool.get("name") if not (isinstance(freeform_name, str) and freeform_name.strip()): continue - parameters = {"type": "object", "properties": {}} + parameters = { # mutable-ok: Bedrock toolSpec input schemas are plain JSON dicts + "type": "object", + "properties": {}, # mutable-ok: empty JSON schema object, consumed by the request builder + } raw_name = freeform_name _tool_description = tool.get("description", None) elif isinstance(tool, dict) and "function" not in tool and "input_schema" not in tool: