mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(bedrock): filter out custom field from tools to prevent 400 errors (#22861)
Claude Code v2.1.69+ sends `custom: {defer_loading: true}` on tool
definitions. Anthropic's API accepts this field, but Bedrock rejects it
with "Extra inputs are not permitted", causing ~90% of requests to fail.
Strip the `custom` field from each tool in the request body before
sending to Bedrock, in both the Messages API and Chat API invoke paths.
Fixes #22847
Co-authored-by: Ishaan Jaff <ishaanjaffer0324@gmail.com>
This commit is contained in:
parent
a8cf646850
commit
cbbb79a92f
4 changed files with 135 additions and 1 deletions
|
|
@ -6,7 +6,10 @@ from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
|||
from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation import (
|
||||
AmazonInvokeConfig,
|
||||
)
|
||||
from litellm.llms.bedrock.common_utils import get_anthropic_beta_from_headers
|
||||
from litellm.llms.bedrock.common_utils import (
|
||||
get_anthropic_beta_from_headers,
|
||||
remove_custom_field_from_tools,
|
||||
)
|
||||
from litellm.types.llms.anthropic import ANTHROPIC_TOOL_SEARCH_BETA_HEADER
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
from litellm.types.utils import ModelResponse
|
||||
|
|
@ -108,6 +111,12 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
|||
if "anthropic_version" not in _anthropic_request:
|
||||
_anthropic_request["anthropic_version"] = self.anthropic_version
|
||||
|
||||
# Remove `custom` field from tools (Bedrock doesn't support it)
|
||||
# Claude Code sends `custom: {defer_loading: true}` on tool definitions,
|
||||
# which causes Bedrock to reject the request with "Extra inputs are not permitted"
|
||||
# Ref: https://github.com/BerriAI/litellm/issues/22847
|
||||
remove_custom_field_from_tools(_anthropic_request)
|
||||
|
||||
tools = optional_params.get("tools")
|
||||
tool_search_used = self.is_tool_search_used(tools)
|
||||
programmatic_tool_calling_used = self.is_programmatic_tool_calling_used(tools)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,27 @@ def get_cached_model_info():
|
|||
return _get_model_info
|
||||
|
||||
|
||||
def remove_custom_field_from_tools(request_body: dict) -> None:
|
||||
"""
|
||||
Remove ``custom`` field from each tool in the request body.
|
||||
|
||||
Claude Code (v2.1.69+) sends ``custom: {defer_loading: true}`` on tool
|
||||
definitions, which Anthropic's API accepts but Bedrock rejects with
|
||||
``"Extra inputs are not permitted"``.
|
||||
|
||||
Args:
|
||||
request_body: The request dictionary to modify in-place.
|
||||
|
||||
Ref: https://github.com/BerriAI/litellm/issues/22847
|
||||
"""
|
||||
tools = request_body.get("tools")
|
||||
if not tools or not isinstance(tools, list):
|
||||
return
|
||||
for tool in tools:
|
||||
if isinstance(tool, dict):
|
||||
tool.pop("custom", None)
|
||||
|
||||
|
||||
class AmazonBedrockGlobalConfig:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation
|
|||
from litellm.llms.bedrock.common_utils import (
|
||||
get_anthropic_beta_from_headers,
|
||||
is_claude_4_5_on_bedrock,
|
||||
remove_custom_field_from_tools,
|
||||
)
|
||||
from litellm.types.llms.anthropic import ANTHROPIC_TOOL_SEARCH_BETA_HEADER
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
|
@ -402,6 +403,12 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
anthropic_messages_request=anthropic_messages_request,
|
||||
)
|
||||
|
||||
# 5a. Remove `custom` field from tools (Bedrock doesn't support it)
|
||||
# Claude Code sends `custom: {defer_loading: true}` on tool definitions,
|
||||
# which causes Bedrock to reject the request with "Extra inputs are not permitted"
|
||||
# Ref: https://github.com/BerriAI/litellm/issues/22847
|
||||
remove_custom_field_from_tools(anthropic_messages_request)
|
||||
|
||||
# 6. AUTO-INJECT beta headers based on features used
|
||||
anthropic_model_info = AnthropicModelInfo()
|
||||
tools = anthropic_messages_optional_request_params.get("tools")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import pytest
|
|||
sys.path.insert(0, os.path.abspath("../../../../../.."))
|
||||
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.llms.bedrock.common_utils import remove_custom_field_from_tools
|
||||
from litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_transformation import (
|
||||
AmazonAnthropicClaudeMessagesConfig,
|
||||
AmazonAnthropicClaudeMessagesStreamDecoder,
|
||||
|
|
@ -178,3 +179,99 @@ def test_remove_ttl_from_cache_control():
|
|||
request5 = {}
|
||||
cfg._remove_ttl_from_cache_control(request5)
|
||||
assert request5 == {}
|
||||
|
||||
|
||||
def test_remove_custom_field_from_tools():
|
||||
"""
|
||||
Ensure the `custom` field is stripped from every tool definition.
|
||||
|
||||
Claude Code v2.1.69+ sends `custom: {defer_loading: true}` on tool
|
||||
objects. Bedrock does not accept this extra field and returns
|
||||
"Extra inputs are not permitted".
|
||||
|
||||
Ref: https://github.com/BerriAI/litellm/issues/22847
|
||||
"""
|
||||
|
||||
# Case 1: tool with `custom` field should have it removed
|
||||
request = {
|
||||
"tools": [
|
||||
{
|
||||
"name": "Read",
|
||||
"description": "Read a file",
|
||||
"input_schema": {"type": "object", "properties": {}},
|
||||
"custom": {"defer_loading": True},
|
||||
},
|
||||
{
|
||||
"name": "Write",
|
||||
"description": "Write a file",
|
||||
"input_schema": {"type": "object", "properties": {}},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
remove_custom_field_from_tools(request)
|
||||
|
||||
for tool in request["tools"]:
|
||||
assert "custom" not in tool, f"Tool {tool['name']} still has 'custom' field"
|
||||
# Other fields should be preserved
|
||||
assert request["tools"][0]["name"] == "Read"
|
||||
assert request["tools"][1]["name"] == "Write"
|
||||
|
||||
# Case 2: request without tools key (should not raise error)
|
||||
request2 = {"messages": [{"role": "user", "content": "hi"}]}
|
||||
remove_custom_field_from_tools(request2)
|
||||
assert "tools" not in request2
|
||||
|
||||
# Case 3: empty tools list (should not raise error)
|
||||
request3 = {"tools": []}
|
||||
remove_custom_field_from_tools(request3)
|
||||
assert request3["tools"] == []
|
||||
|
||||
# Case 4: tools with None value (should not raise error)
|
||||
request4 = {"tools": None}
|
||||
remove_custom_field_from_tools(request4)
|
||||
assert request4["tools"] is None
|
||||
|
||||
def test_remove_scope_from_cache_control():
|
||||
"""Ensure scope field is removed from cache_control for Bedrock (not supported)."""
|
||||
|
||||
cfg = AmazonAnthropicClaudeMessagesConfig()
|
||||
|
||||
# Test case 1: System with cache_control containing scope
|
||||
request = {
|
||||
"system": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "You are an AI assistant.",
|
||||
"cache_control": {
|
||||
"type": "ephemeral",
|
||||
"scope": "global",
|
||||
},
|
||||
}
|
||||
],
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Hello",
|
||||
"cache_control": {
|
||||
"type": "ephemeral",
|
||||
"scope": "global",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
cfg._remove_ttl_from_cache_control(request)
|
||||
|
||||
# Verify scope is removed from system
|
||||
assert "scope" not in request["system"][0]["cache_control"]
|
||||
assert request["system"][0]["cache_control"]["type"] == "ephemeral"
|
||||
|
||||
# Verify scope is removed from messages
|
||||
assert "scope" not in request["messages"][0]["content"][0]["cache_control"]
|
||||
assert request["messages"][0]["content"][0]["cache_control"]["type"] == "ephemeral"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue