mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
fix(bedrock): sanitize tool_use IDs in pass-through messages
Bedrock requires tool_use IDs to match ^[a-zA-Z0-9_-]+$ but the Anthropic native API allows broader characters. When clients like Claude Code send requests through the /v1/messages pass-through endpoint, IDs containing dots, plus signs, or other special characters cause Bedrock to return 400 Bad Request errors. Added _sanitize_tool_use_ids() to AmazonAnthropicClaudeMessagesConfig that replaces invalid characters with underscores in both tool_use.id and tool_result.tool_use_id fields during Bedrock-specific request transformation. Fixes #21114 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d7c419bfee
commit
28c2873dae
2 changed files with 186 additions and 0 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import re
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
|
|
@ -372,6 +373,49 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
schema_text = {"type": "text", "text": json.dumps(schema)}
|
||||
content.append(schema_text)
|
||||
|
||||
|
||||
_VALID_TOOL_ID_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")
|
||||
_INVALID_TOOL_ID_CHARS = re.compile(r"[^a-zA-Z0-9_-]")
|
||||
|
||||
def _sanitize_tool_use_ids(
|
||||
self, anthropic_messages_request: Dict
|
||||
) -> None:
|
||||
"""
|
||||
Sanitize tool_use IDs to match Bedrock's required pattern.
|
||||
|
||||
Bedrock requires tool_use IDs to match ``^[a-zA-Z0-9_-]+$`` but the
|
||||
Anthropic native API allows broader characters. Clients like Claude Code
|
||||
send requests through the pass-through endpoint with IDs that Bedrock
|
||||
rejects with 400 Bad Request.
|
||||
|
||||
Replaces any invalid characters with underscores in both ``tool_use.id``
|
||||
and ``tool_result.tool_use_id`` fields.
|
||||
|
||||
Fixes: https://github.com/BerriAI/litellm/issues/21114
|
||||
"""
|
||||
messages = anthropic_messages_request.get("messages")
|
||||
if not isinstance(messages, list):
|
||||
return
|
||||
|
||||
for message in messages:
|
||||
if not isinstance(message, dict):
|
||||
continue
|
||||
content = message.get("content")
|
||||
if not isinstance(content, list):
|
||||
continue
|
||||
for block in content:
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
block_type = block.get("type")
|
||||
if block_type == "tool_use" and "id" in block:
|
||||
tool_id = block["id"]
|
||||
if isinstance(tool_id, str) and not self._VALID_TOOL_ID_PATTERN.match(tool_id):
|
||||
block["id"] = self._INVALID_TOOL_ID_CHARS.sub("_", tool_id)
|
||||
elif block_type == "tool_result" and "tool_use_id" in block:
|
||||
tool_use_id = block["tool_use_id"]
|
||||
if isinstance(tool_use_id, str) and not self._VALID_TOOL_ID_PATTERN.match(tool_use_id):
|
||||
block["tool_use_id"] = self._INVALID_TOOL_ID_CHARS.sub("_", tool_use_id)
|
||||
|
||||
def transform_anthropic_messages_request(
|
||||
self,
|
||||
model: str,
|
||||
|
|
@ -429,6 +473,12 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
# Ref: https://github.com/BerriAI/litellm/issues/22847
|
||||
remove_custom_field_from_tools(anthropic_messages_request)
|
||||
|
||||
# 7. Sanitize tool_use IDs (Bedrock requires ^[a-zA-Z0-9_-]+$)
|
||||
# The Anthropic native API allows broader characters in tool_use IDs,
|
||||
# but Bedrock rejects them with 400 Bad Request.
|
||||
# Fixes: https://github.com/BerriAI/litellm/issues/21114
|
||||
self._sanitize_tool_use_ids(anthropic_messages_request)
|
||||
|
||||
# 6. AUTO-INJECT beta headers based on features used
|
||||
anthropic_model_info = AnthropicModelInfo()
|
||||
tools = anthropic_messages_optional_request_params.get("tools")
|
||||
|
|
|
|||
|
|
@ -342,3 +342,139 @@ def test_bedrock_messages_strips_output_config_with_output_format():
|
|||
|
||||
assert "output_config" not in result
|
||||
assert "output_format" not in result
|
||||
|
||||
|
||||
class TestSanitizeToolUseIds:
|
||||
"""Tests for _sanitize_tool_use_ids in AmazonAnthropicClaudeMessagesConfig.
|
||||
|
||||
Bedrock requires tool_use IDs to match ^[a-zA-Z0-9_-]+$ but the Anthropic
|
||||
native API allows broader characters.
|
||||
Fixes: https://github.com/BerriAI/litellm/issues/21114
|
||||
"""
|
||||
|
||||
def test_sanitize_tool_use_id_with_invalid_chars(self):
|
||||
"""tool_use.id with invalid characters should be sanitized."""
|
||||
cfg = AmazonAnthropicClaudeMessagesConfig()
|
||||
request = {
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_abc.123+xyz/foo",
|
||||
"name": "test_tool",
|
||||
"input": {},
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
cfg._sanitize_tool_use_ids(request)
|
||||
assert request["messages"][0]["content"][0]["id"] == "toolu_abc_123_xyz_foo"
|
||||
|
||||
def test_sanitize_tool_result_id_with_invalid_chars(self):
|
||||
"""tool_result.tool_use_id with invalid characters should be sanitized."""
|
||||
cfg = AmazonAnthropicClaudeMessagesConfig()
|
||||
request = {
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_abc.123+xyz/foo",
|
||||
"content": "result",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
cfg._sanitize_tool_use_ids(request)
|
||||
assert (
|
||||
request["messages"][0]["content"][0]["tool_use_id"]
|
||||
== "toolu_abc_123_xyz_foo"
|
||||
)
|
||||
|
||||
def test_valid_ids_unchanged(self):
|
||||
"""IDs that already match the Bedrock pattern should not be modified."""
|
||||
cfg = AmazonAnthropicClaudeMessagesConfig()
|
||||
request = {
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_valid-id_123",
|
||||
"name": "test_tool",
|
||||
"input": {},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_valid-id_123",
|
||||
"content": "result",
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
cfg._sanitize_tool_use_ids(request)
|
||||
assert request["messages"][0]["content"][0]["id"] == "toolu_valid-id_123"
|
||||
assert (
|
||||
request["messages"][1]["content"][0]["tool_use_id"] == "toolu_valid-id_123"
|
||||
)
|
||||
|
||||
def test_no_messages_key(self):
|
||||
"""Should handle request without messages key gracefully."""
|
||||
cfg = AmazonAnthropicClaudeMessagesConfig()
|
||||
request = {"max_tokens": 1024}
|
||||
cfg._sanitize_tool_use_ids(request) # Should not raise
|
||||
|
||||
def test_string_content_ignored(self):
|
||||
"""Messages with string content (not list) should be skipped."""
|
||||
cfg = AmazonAnthropicClaudeMessagesConfig()
|
||||
request = {
|
||||
"messages": [{"role": "user", "content": "hello"}]
|
||||
}
|
||||
cfg._sanitize_tool_use_ids(request) # Should not raise
|
||||
|
||||
def test_consistent_sanitization_across_pairs(self):
|
||||
"""tool_use.id and matching tool_result.tool_use_id should sanitize identically."""
|
||||
cfg = AmazonAnthropicClaudeMessagesConfig()
|
||||
original_id = "toolu_01A.B+C/D:E"
|
||||
request = {
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": original_id,
|
||||
"name": "test",
|
||||
"input": {},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": original_id,
|
||||
"content": "result",
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
cfg._sanitize_tool_use_ids(request)
|
||||
sanitized_tool_use = request["messages"][0]["content"][0]["id"]
|
||||
sanitized_tool_result = request["messages"][1]["content"][0]["tool_use_id"]
|
||||
assert sanitized_tool_use == sanitized_tool_result
|
||||
assert sanitized_tool_use == "toolu_01A_B_C_D_E"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue