mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
fix(bedrock): sanitize client tool_call ids to Bedrock toolUseId constraints
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
0c98afa780
commit
d5c7e279d7
2 changed files with 97 additions and 3 deletions
|
|
@ -1515,6 +1515,23 @@ def _sanitize_anthropic_tool_use_id(tool_use_id: str) -> str:
|
|||
return sanitized
|
||||
|
||||
|
||||
_BEDROCK_TOOL_USE_ID_MAX_LEN: Final = 64
|
||||
_BEDROCK_TOOL_USE_ID_HASH_LEN: Final = 8
|
||||
|
||||
|
||||
def _sanitize_bedrock_tool_use_id(tool_use_id: str) -> str:
|
||||
"""
|
||||
Bedrock Converse requires toolUseId to match [a-zA-Z0-9_.:-]+ and be at most 64 chars.
|
||||
Over-long ids are truncated and suffixed with a short hash of the original so two ids
|
||||
that only differ past the cut still map to distinct values.
|
||||
"""
|
||||
sanitized: Final = re.sub(r"[^a-zA-Z0-9_.:-]", "_", tool_use_id) or "tool_use_id"
|
||||
if len(sanitized) <= _BEDROCK_TOOL_USE_ID_MAX_LEN:
|
||||
return sanitized
|
||||
digest: Final = hashlib.sha256(tool_use_id.encode()).hexdigest()[:_BEDROCK_TOOL_USE_ID_HASH_LEN]
|
||||
return f"{sanitized[: _BEDROCK_TOOL_USE_ID_MAX_LEN - _BEDROCK_TOOL_USE_ID_HASH_LEN - 1]}_{digest}"
|
||||
|
||||
|
||||
_ANTHROPIC_DOCUMENT_BASE64_MEDIA_TYPES: Final = {"application/pdf", "text/plain"}
|
||||
|
||||
|
||||
|
|
@ -3661,7 +3678,9 @@ def _convert_to_bedrock_tool_call_invoke(
|
|||
if parsed_objects:
|
||||
# First object keeps the original tool id.
|
||||
for obj_idx, obj in enumerate(parsed_objects):
|
||||
block_id = tool_id if obj_idx == 0 else f"{tool_id}_{obj_idx}"
|
||||
block_id = _sanitize_bedrock_tool_use_id(
|
||||
tool_id if obj_idx == 0 else f"{tool_id}_{obj_idx}"
|
||||
)
|
||||
bedrock_tool = BedrockToolUseBlock(input=obj, name=name, toolUseId=block_id)
|
||||
_parts_list.append(BedrockContentBlock(toolUse=bedrock_tool))
|
||||
# cache_control applies to the whole original
|
||||
|
|
@ -3678,7 +3697,9 @@ def _convert_to_bedrock_tool_call_invoke(
|
|||
# Fallback: no objects extracted — use empty dict.
|
||||
arguments_dict = {}
|
||||
|
||||
bedrock_tool = BedrockToolUseBlock(input=arguments_dict, name=name, toolUseId=tool_id)
|
||||
bedrock_tool = BedrockToolUseBlock(
|
||||
input=arguments_dict, name=name, toolUseId=_sanitize_bedrock_tool_use_id(tool_id)
|
||||
)
|
||||
bedrock_content_block = BedrockContentBlock(toolUse=bedrock_tool)
|
||||
_parts_list.append(bedrock_content_block)
|
||||
|
||||
|
|
@ -3849,7 +3870,7 @@ def _convert_to_bedrock_tool_call_result(
|
|||
tool_result_content_blocks, used_search_results = _build_bedrock_tool_result_content_blocks(message)
|
||||
|
||||
message.get("name", "")
|
||||
id: Final = str(message.get("tool_call_id", str(uuid.uuid4())))
|
||||
id: Final = _sanitize_bedrock_tool_use_id(str(message.get("tool_call_id", str(uuid.uuid4()))))
|
||||
|
||||
tool_result: Final = BedrockToolResultBlock(content=tool_result_content_blocks, toolUseId=id)
|
||||
if used_search_results:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import base64
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Final
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -2208,6 +2209,78 @@ def test_bedrock_tool_call_invoke_empty_arguments():
|
|||
assert result[0]["toolUse"]["input"] == {}
|
||||
|
||||
|
||||
_BEDROCK_TOOL_USE_ID_RE = re.compile(r"^[a-zA-Z0-9_.:-]{1,64}$")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tool_call_id",
|
||||
[
|
||||
"call_" + "x" * 100,
|
||||
"call|with|pipes",
|
||||
"call_" + "y" * 60 + "|end",
|
||||
"call:ok.dots-and_under",
|
||||
],
|
||||
)
|
||||
def test_bedrock_tool_use_id_is_sanitized_consistently_for_invoke_and_result(tool_call_id):
|
||||
"""
|
||||
Regression test for https://github.com/BerriAI/litellm/issues/34239: client-minted
|
||||
tool_call ids longer than 64 chars or with chars outside [a-zA-Z0-9_.:-] made Bedrock
|
||||
return a 400. The invoke and result paths must produce the same valid toolUseId so the
|
||||
toolUse/toolResult pair still correlates.
|
||||
"""
|
||||
invoke = _convert_to_bedrock_tool_call_invoke(
|
||||
[
|
||||
{
|
||||
"id": tool_call_id,
|
||||
"type": "function",
|
||||
"function": {"name": "get_weather", "arguments": '{"location": "Boston"}'},
|
||||
}
|
||||
]
|
||||
)
|
||||
result = _convert_to_bedrock_tool_call_result(
|
||||
{"tool_call_id": tool_call_id, "role": "tool", "name": "get_weather", "content": "sunny"}
|
||||
)
|
||||
tool_use_id = invoke[0]["toolUse"]["toolUseId"]
|
||||
assert _BEDROCK_TOOL_USE_ID_RE.match(tool_use_id)
|
||||
assert result["toolResult"]["toolUseId"] == tool_use_id
|
||||
|
||||
|
||||
def test_bedrock_tool_use_id_valid_ids_pass_through_unchanged():
|
||||
result = _convert_to_bedrock_tool_call_result(
|
||||
{"tool_call_id": "tooluse_Ab.c:1-2_3", "role": "tool", "name": "f", "content": "ok"}
|
||||
)
|
||||
assert result["toolResult"]["toolUseId"] == "tooluse_Ab.c:1-2_3"
|
||||
|
||||
|
||||
def test_bedrock_tool_use_id_truncation_keeps_distinct_ids_distinct():
|
||||
prefix = "call_" + "z" * 70
|
||||
ids = {
|
||||
_convert_to_bedrock_tool_call_result(
|
||||
{"tool_call_id": f"{prefix}{suffix}", "role": "tool", "name": "f", "content": "ok"}
|
||||
)["toolResult"]["toolUseId"]
|
||||
for suffix in ("a", "b")
|
||||
}
|
||||
assert len(ids) == 2
|
||||
assert all(len(i) == 64 for i in ids)
|
||||
|
||||
|
||||
def test_bedrock_tool_call_invoke_concatenated_json_long_id_stays_within_limit():
|
||||
long_id = "call_" + "q" * 62
|
||||
result = _convert_to_bedrock_tool_call_invoke(
|
||||
[
|
||||
{
|
||||
"id": long_id,
|
||||
"type": "function",
|
||||
"function": {"name": "run", "arguments": '{"cmd":"a"}{"cmd":"b"}'},
|
||||
}
|
||||
]
|
||||
)
|
||||
ids = [block["toolUse"]["toolUseId"] for block in result]
|
||||
assert len(ids) == 2
|
||||
assert len(set(ids)) == 2
|
||||
assert all(_BEDROCK_TOOL_USE_ID_RE.match(i) for i in ids)
|
||||
|
||||
|
||||
def test_bedrock_tool_call_invoke_concatenated_json():
|
||||
"""
|
||||
Tool call whose arguments contain multiple concatenated JSON objects
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue