mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(bedrock): strip custom tool fields for Converse API to fix Haiku 4.5 rejection
Claude Code sends custom: {eager_input_streaming: true} on tool definitions.
Bedrock Converse accepts this for Claude 4 models (Sonnet 4.6, Opus 4.6) but
rejects it for Haiku 4.5 with 'Extra inputs are not permitted'.
- Add strip_custom_from_tools_list() in common_utils for list-based stripping
- Refactor remove_custom_field_from_tools to use the new helper
- Call strip_custom_from_tools_list in _process_tools_and_beta before
passing tools to _bedrock_tools_pt
- Add unit tests for strip_custom_from_tools_list and _process_tools_and_beta
Fixes #23825
Related: #16679
Made-with: Cursor
This commit is contained in:
parent
245a3d2b26
commit
9cf8cf6f39
4 changed files with 149 additions and 1 deletions
|
|
@ -73,6 +73,7 @@ from ..common_utils import (
|
|||
get_anthropic_beta_from_headers,
|
||||
get_bedrock_tool_name,
|
||||
is_claude_4_5_on_bedrock,
|
||||
strip_custom_from_tools_list,
|
||||
)
|
||||
|
||||
# Computer use tool prefixes supported by Bedrock
|
||||
|
|
@ -1299,6 +1300,12 @@ class AmazonConverseConfig(BaseConfig):
|
|||
continue
|
||||
filtered_tools.append(tool)
|
||||
|
||||
# Strip custom field from tools before sending to Bedrock Converse.
|
||||
# Claude Code sends custom: {eager_input_streaming: true} etc. which Anthropic
|
||||
# accepts but Bedrock rejects for some models (e.g. Haiku 4.5) with
|
||||
# "Extra inputs are not permitted". Ref: https://github.com/BerriAI/litellm/issues/23825
|
||||
strip_custom_from_tools_list(filtered_tools)
|
||||
|
||||
# Only separate tools if computer use tools are actually present
|
||||
if filtered_tools and self.is_computer_use_tool_used(filtered_tools, model):
|
||||
# Separate computer use tools from regular function tools
|
||||
|
|
|
|||
|
|
@ -63,11 +63,36 @@ def remove_custom_field_from_tools(request_body: dict) -> None:
|
|||
Ref: https://github.com/BerriAI/litellm/issues/22847
|
||||
"""
|
||||
tools = request_body.get("tools")
|
||||
if not tools or not isinstance(tools, list):
|
||||
return
|
||||
strip_custom_from_tools_list(tools)
|
||||
|
||||
|
||||
def strip_custom_from_tools_list(tools: list) -> None:
|
||||
"""
|
||||
Strip ``custom`` field from each tool in the list (modifies in-place).
|
||||
|
||||
Claude Code sends Anthropic-specific fields like ``custom: {eager_input_streaming: true}``
|
||||
or ``custom: {input_examples: [...]}`` on tool definitions. Anthropic's API accepts these
|
||||
but Bedrock Converse rejects them with "Extra inputs are not permitted" for some models
|
||||
(e.g. Haiku 4.5).
|
||||
|
||||
Handles both top-level tool ``custom`` and nested ``function.custom`` (OpenAI format).
|
||||
|
||||
Args:
|
||||
tools: List of tool dicts to modify in-place.
|
||||
|
||||
Ref: https://github.com/BerriAI/litellm/issues/23825
|
||||
Ref: https://github.com/BerriAI/litellm/issues/16679
|
||||
"""
|
||||
if not tools or not isinstance(tools, list):
|
||||
return
|
||||
for tool in tools:
|
||||
if isinstance(tool, dict):
|
||||
tool.pop("custom", None)
|
||||
func = tool.get("function")
|
||||
if isinstance(func, dict):
|
||||
func.pop("custom", None)
|
||||
|
||||
|
||||
class AmazonBedrockGlobalConfig:
|
||||
|
|
|
|||
|
|
@ -319,6 +319,45 @@ def test_get_supported_openai_params_bedrock_converse():
|
|||
print(f"✅ Passed for model: {model}")
|
||||
|
||||
|
||||
def test_process_tools_and_beta_strips_custom_field():
|
||||
"""
|
||||
Test that _process_tools_and_beta strips custom field from tools before sending to Bedrock.
|
||||
|
||||
Claude Code sends custom: {eager_input_streaming: true} on tool definitions.
|
||||
Bedrock Converse rejects this for Haiku 4.5 with "Extra inputs are not permitted".
|
||||
Ref: https://github.com/BerriAI/litellm/issues/23825
|
||||
"""
|
||||
config = AmazonConverseConfig()
|
||||
tools_with_custom = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get the weather",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"location": {"type": "string"}},
|
||||
"required": ["location"],
|
||||
},
|
||||
},
|
||||
"custom": {"eager_input_streaming": True},
|
||||
},
|
||||
]
|
||||
additional_params = {}
|
||||
bedrock_tools, _ = config._process_tools_and_beta(
|
||||
original_tools=tools_with_custom,
|
||||
model="us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
headers=None,
|
||||
additional_request_params=additional_params,
|
||||
)
|
||||
# Should produce valid Bedrock tools (toolSpec format) without custom
|
||||
assert len(bedrock_tools) == 1
|
||||
assert "toolSpec" in bedrock_tools[0]
|
||||
assert bedrock_tools[0]["toolSpec"]["name"] == "get_weather"
|
||||
# Original tools list should have been modified in-place (custom stripped)
|
||||
assert "custom" not in tools_with_custom[0]
|
||||
|
||||
|
||||
def test_transform_request_helper_includes_anthropic_beta_and_tools():
|
||||
"""Test _transform_request_helper includes anthropic_beta for computer tools."""
|
||||
config = AmazonConverseConfig()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,84 @@ sys.path.insert(
|
|||
) # Adds the parent directory to the system path
|
||||
|
||||
|
||||
from litellm.llms.bedrock.common_utils import BedrockModelInfo
|
||||
from litellm.llms.bedrock.common_utils import (
|
||||
BedrockModelInfo,
|
||||
remove_custom_field_from_tools,
|
||||
strip_custom_from_tools_list,
|
||||
)
|
||||
|
||||
|
||||
def test_strip_custom_from_tools_list():
|
||||
"""
|
||||
Ensure strip_custom_from_tools_list removes custom field from tools.
|
||||
|
||||
Claude Code sends custom: {eager_input_streaming: true} or custom: {input_examples: [...]}
|
||||
on tool definitions. Bedrock Converse rejects these for some models (e.g. Haiku 4.5)
|
||||
with "Extra inputs are not permitted".
|
||||
|
||||
Ref: https://github.com/BerriAI/litellm/issues/23825
|
||||
Ref: https://github.com/BerriAI/litellm/issues/16679
|
||||
"""
|
||||
# Case 1: OpenAI format - custom at tool level
|
||||
tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get weather",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
"custom": {"eager_input_streaming": True},
|
||||
},
|
||||
]
|
||||
strip_custom_from_tools_list(tools)
|
||||
assert "custom" not in tools[0]
|
||||
assert tools[0]["function"]["name"] == "get_weather"
|
||||
|
||||
# Case 2: OpenAI format - custom nested in function
|
||||
tools2 = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search",
|
||||
"parameters": {},
|
||||
"custom": {"input_examples": [{"query": "test"}]},
|
||||
},
|
||||
},
|
||||
]
|
||||
strip_custom_from_tools_list(tools2)
|
||||
assert "custom" not in tools2[0]["function"]
|
||||
|
||||
# Case 3: Anthropic format - custom at top level
|
||||
tools3 = [
|
||||
{
|
||||
"name": "some_tool",
|
||||
"input_schema": {"type": "object"},
|
||||
"custom": {"eager_input_streaming": True},
|
||||
},
|
||||
]
|
||||
strip_custom_from_tools_list(tools3)
|
||||
assert "custom" not in tools3[0]
|
||||
assert tools3[0]["name"] == "some_tool"
|
||||
|
||||
# Case 4: empty list (no-op)
|
||||
tools4 = []
|
||||
strip_custom_from_tools_list(tools4)
|
||||
assert tools4 == []
|
||||
|
||||
# Case 5: None (no-op)
|
||||
strip_custom_from_tools_list(None) # type: ignore
|
||||
|
||||
|
||||
def test_remove_custom_field_from_tools_uses_strip_custom_from_tools_list():
|
||||
"""Ensure remove_custom_field_from_tools delegates to strip_custom_from_tools_list."""
|
||||
request = {
|
||||
"tools": [
|
||||
{"name": "tool1", "input_schema": {}, "custom": {"eager_input_streaming": True}},
|
||||
]
|
||||
}
|
||||
remove_custom_field_from_tools(request)
|
||||
assert "custom" not in request["tools"][0]
|
||||
|
||||
|
||||
def test_deepseek_cris():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue