This commit is contained in:
xu jiaming 2026-09-23 14:51:07 +00:00 • committed by GitHub
commit 85284436b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 68 additions and 8 deletions

View file

@ -5107,14 +5107,32 @@ 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 = { # 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:
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)

View file

@ -1067,6 +1067,44 @@ 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. 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") == []
def test_bedrock_image_processor_content_type_fallback_url_extension():
"""
Test that _post_call_image_processing falls back to URL extension
@ -1668,9 +1706,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
@ -1693,7 +1735,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)