mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(bedrock): keep tool_reference results as text on converse (#44590)
* test(e2e): cover tool_reference tool results on bedrock converse Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(bedrock): keep tool_reference results as text on converse Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * style(bedrock): format tool reference parser Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): cover bedrock converse tool_result of only tool_reference blocks Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(e2e): tag converse tool_reference case with e2e metadata Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(bedrock): avoid unchecked cast in converse tool_reference mapping Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(anthropic): keep tool_reference results as text on responses paths Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(anthropic): tighten tool_reference mapping on responses paths Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(completion-extras): expect tool_reference names in responses input Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * revert(bedrock): drop responses converter changes, keep converse tool_reference fix only Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: kerry <kerry@berri.ai>
This commit is contained in:
parent
627356d3a0
commit
5003a3a11b
5 changed files with 279 additions and 1 deletions
|
|
@ -3897,6 +3897,11 @@ def _parse_bedrock_tool_result_content_list(
|
|||
_append_bedrock_tool_result_image_url_block(tool_result_content_blocks, content)
|
||||
elif content["type"] == "file":
|
||||
_append_bedrock_tool_result_file_block(tool_result_content_blocks, content)
|
||||
elif content["type"] == "tool_reference":
|
||||
content_map = cast(dict[str, object], content) # cast-ok: untyped anthropic json
|
||||
tool_name = content_map.get("tool_name")
|
||||
if isinstance(tool_name, str):
|
||||
tool_result_content_blocks.append(BedrockToolResultContentBlock(text=tool_name))
|
||||
return tool_result_content_blocks
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from typing import Final
|
||||
|
||||
import pytest
|
||||
from anthropic.types import RawContentBlockDeltaEvent, RawMessageDeltaEvent, TextBlock, TextDelta
|
||||
from anthropic.types import RawContentBlockDeltaEvent, RawMessageDeltaEvent, TextBlock, TextDelta, ToolParam
|
||||
from e2e_config import unique_marker
|
||||
from e2e_metadata import Capability, Domain, Mode, Provider, Route, Subject, meta
|
||||
from lifecycle import ResourceManager
|
||||
|
|
@ -14,9 +14,38 @@ from structured_output import SENTIMENT_OUTPUT_FORMAT, SENTIMENT_PROMPT, assert_
|
|||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
GPT_6_1_SOL_BACKEND: Final = "bedrock/global.openai.gpt-6.1-sol"
|
||||
CONVERSE_CLAUDE_BACKEND: Final = "bedrock/converse/us.anthropic.claude-haiku-4-5-20251001-v1:0"
|
||||
NOVA_BACKEND: Final = "bedrock/us.amazon.nova-2-lite-v1:0"
|
||||
|
||||
TOOL_SEARCH: Final[ToolParam] = {
|
||||
"name": "ToolSearch",
|
||||
"description": "Find available tools by query.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"q": {"type": "string"}},
|
||||
"required": ["q"],
|
||||
},
|
||||
}
|
||||
WEB_FETCH: Final[ToolParam] = {
|
||||
"name": "WebFetch",
|
||||
"description": "Fetch a web page.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"url": {"type": "string"}, "prompt": {"type": "string"}},
|
||||
"required": ["url", "prompt"],
|
||||
},
|
||||
}
|
||||
WEB_SEARCH: Final[ToolParam] = {
|
||||
"name": "WebSearch",
|
||||
"description": "Search the web.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string"}},
|
||||
"required": ["query"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _register(proxy: ProxyClient, resources: ResourceManager, backend: str) -> str:
|
||||
model = f"e2e-messages-bedrock-{unique_marker()}"
|
||||
|
|
@ -34,6 +63,53 @@ def _register(proxy: ProxyClient, resources: ResourceManager, backend: str) -> s
|
|||
|
||||
|
||||
class TestBedrockMessages:
|
||||
@meta(
|
||||
Subject(
|
||||
domain=Domain.LLM_TRANSLATION,
|
||||
route=Route.MESSAGES,
|
||||
providers=(Provider.BEDROCK,),
|
||||
models=(GPT_6_1_SOL_BACKEND,),
|
||||
capabilities=(Capability.TOOL_SEARCH,),
|
||||
mode=Mode.NONSTREAM,
|
||||
)
|
||||
)
|
||||
def test_converse_tool_result_of_only_tool_references_is_accepted(
|
||||
self, proxy: ProxyClient, resources: ResourceManager, sdk: SdkClients
|
||||
) -> None:
|
||||
model = _register(proxy, resources, GPT_6_1_SOL_BACKEND)
|
||||
client = sdk.anthropic(resources.key()).with_options(timeout=180)
|
||||
|
||||
message = client.messages.create(
|
||||
model=model,
|
||||
max_tokens=64,
|
||||
tools=[TOOL_SEARCH, WEB_FETCH, WEB_SEARCH],
|
||||
messages=[
|
||||
{"role": "user", "content": "find web tools"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{"type": "tool_use", "id": "toolu_1", "name": "ToolSearch", "input": {"q": "web"}}],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_1",
|
||||
"content": [
|
||||
{"type": "tool_reference", "tool_name": "WebFetch"},
|
||||
{"type": "tool_reference", "tool_name": "WebSearch"},
|
||||
],
|
||||
},
|
||||
{"type": "text", "text": "reply ok"},
|
||||
],
|
||||
},
|
||||
], # pyright: ignore[reportArgumentType] # Anthropic SDK types omit tool_reference blocks
|
||||
extra_body=NO_PROXY_CACHE,
|
||||
)
|
||||
|
||||
assert message.stop_reason is not None, f"Bedrock Converse returned no stop_reason: {message!r}"
|
||||
assert message.content, f"Bedrock Converse returned no content blocks: {message!r}"
|
||||
|
||||
@meta(
|
||||
Subject(
|
||||
domain=Domain.LLM_TRANSLATION,
|
||||
|
|
|
|||
|
|
@ -116,6 +116,12 @@ model_list:
|
|||
api_base: http://127.0.0.1:8191
|
||||
api_key: synthetic-bedrock-key
|
||||
aws_region_name: us-east-1
|
||||
- model_name: bedrock/global.openai.gpt-6.1-sol
|
||||
litellm_params:
|
||||
model: bedrock/global.openai.gpt-6.1-sol
|
||||
api_base: http://127.0.0.1:8191
|
||||
api_key: synthetic-bedrock-key
|
||||
aws_region_name: us-east-1
|
||||
- model_name: openai/gpt-5.4
|
||||
litellm_params:
|
||||
model: openai/gpt-5.4
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
from typing import Final
|
||||
from unittest.mock import ANY
|
||||
|
||||
import pytest
|
||||
from integration._support.client import Gateway
|
||||
from integration._support.provider import SharedProvider
|
||||
from integration.translation.case import TranslationTestCase
|
||||
from integration.translation.runner import assert_translation
|
||||
|
||||
GPT_6_1_SOL_TOOL_SEARCH_TEST_CASE: Final = TranslationTestCase(
|
||||
scenario="tool_search",
|
||||
litellm_endpoint="/v1/messages",
|
||||
litellm_request={
|
||||
"model": "bedrock/global.openai.gpt-6.1-sol",
|
||||
"max_tokens": 1024,
|
||||
"thinking": {"type": "adaptive"},
|
||||
"output_config": {"effort": "low"},
|
||||
"tools": [
|
||||
{
|
||||
"name": "ToolSearch",
|
||||
"description": "Load deferred tools by name.",
|
||||
"input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]},
|
||||
},
|
||||
{
|
||||
"name": "WebSearch",
|
||||
"description": "Search the web.",
|
||||
"input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]},
|
||||
},
|
||||
],
|
||||
"messages": [
|
||||
{"role": "user", "content": "Load the web search tool."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "call_tool_search_1",
|
||||
"name": "ToolSearch",
|
||||
"input": {"query": "select:WebSearch"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "call_tool_search_1",
|
||||
"content": [{"type": "tool_reference", "tool_name": "WebSearch"}],
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
"cache": {"no-cache": True},
|
||||
},
|
||||
expected_provider_endpoint="/model/global.openai.gpt-6.1-sol/converse",
|
||||
expected_provider_headers={"authorization": "Bearer synthetic-bedrock-key", "content-type": "application/json"},
|
||||
expected_provider_request={
|
||||
"additionalModelRequestFields": {"reasoning": {"effort": "low"}},
|
||||
"inferenceConfig": {"maxTokens": 1024},
|
||||
"messages": [
|
||||
{"role": "user", "content": [{"text": "Load the web search tool."}]},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"toolUse": {
|
||||
"input": {"query": "select:WebSearch"},
|
||||
"name": "ToolSearch",
|
||||
"toolUseId": "call_tool_search_1",
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"toolResult": {
|
||||
"content": [{"text": "WebSearch"}],
|
||||
"toolUseId": "call_tool_search_1",
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
"toolConfig": {
|
||||
"tools": [
|
||||
{
|
||||
"toolSpec": {
|
||||
"description": "Load deferred tools by name.",
|
||||
"inputSchema": {
|
||||
"json": {
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string"}},
|
||||
"required": ["query"],
|
||||
}
|
||||
},
|
||||
"name": "ToolSearch",
|
||||
}
|
||||
},
|
||||
{
|
||||
"toolSpec": {
|
||||
"description": "Search the web.",
|
||||
"inputSchema": {
|
||||
"json": {
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string"}},
|
||||
"required": ["query"],
|
||||
}
|
||||
},
|
||||
"name": "WebSearch",
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
mock_provider_response={
|
||||
"metrics": {"latencyMs": 1252},
|
||||
"output": {
|
||||
"message": {
|
||||
"content": [{"text": "The web search tool is loaded and ready to use."}],
|
||||
"role": "assistant",
|
||||
}
|
||||
},
|
||||
"stopReason": "end_turn",
|
||||
"usage": {
|
||||
"cacheReadInputTokenCount": 0,
|
||||
"cacheReadInputTokens": 0,
|
||||
"cacheWriteInputTokenCount": 0,
|
||||
"cacheWriteInputTokens": 0,
|
||||
"inputTokens": 100,
|
||||
"outputTokens": 15,
|
||||
"serverToolUsage": {},
|
||||
"totalTokens": 115,
|
||||
},
|
||||
},
|
||||
expected_litellm_response={
|
||||
"id": ANY,
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "bedrock/global.openai.gpt-6.1-sol",
|
||||
"stop_sequence": None,
|
||||
"usage": {"input_tokens": 100, "output_tokens": 15},
|
||||
"content": [{"type": "text", "text": "The web search tool is loaded and ready to use."}],
|
||||
"stop_reason": "end_turn",
|
||||
"stop_details": None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", [GPT_6_1_SOL_TOOL_SEARCH_TEST_CASE], ids=lambda case: case.id)
|
||||
def test_messages_tool_search_bedrock_converse(
|
||||
case: TranslationTestCase, gateway: Gateway, provider: SharedProvider
|
||||
) -> None:
|
||||
assert_translation(case, gateway, provider)
|
||||
|
|
@ -3264,6 +3264,41 @@ def test_convert_to_anthropic_tool_result_openai_file_pdf_becomes_document():
|
|||
assert tool_result["content"][0]["document"]["source"]["bytes"] == pdf_b64
|
||||
|
||||
|
||||
def test_convert_to_bedrock_tool_call_result_maps_tool_references_to_text() -> None:
|
||||
tool_reference_message: Final[ChatCompletionToolMessage] = {
|
||||
"role": "tool",
|
||||
"tool_call_id": "toolu_1",
|
||||
"content": [
|
||||
{"type": "tool_reference", "tool_name": "WebFetch"},
|
||||
{"type": "tool_reference", "tool_name": "WebSearch"},
|
||||
],
|
||||
}
|
||||
mixed_message: Final[ChatCompletionToolMessage] = {
|
||||
"role": "tool",
|
||||
"tool_call_id": "toolu_2",
|
||||
"content": [
|
||||
{"type": "text", "text": "Loaded tools:"},
|
||||
{"type": "tool_reference", "tool_name": "WebSearch"},
|
||||
],
|
||||
}
|
||||
|
||||
tool_reference_result: Final = _convert_to_bedrock_tool_call_result(tool_reference_message)
|
||||
mixed_result: Final = _convert_to_bedrock_tool_call_result(mixed_message)
|
||||
|
||||
assert tool_reference_result == {
|
||||
"toolResult": {
|
||||
"toolUseId": "toolu_1",
|
||||
"content": [{"text": "WebFetch"}, {"text": "WebSearch"}],
|
||||
}
|
||||
}
|
||||
assert mixed_result == {
|
||||
"toolResult": {
|
||||
"toolUseId": "toolu_2",
|
||||
"content": [{"text": "Loaded tools:"}, {"text": "WebSearch"}],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_bedrock_converse_messages_pt_document_various_formats():
|
||||
"""Test that various document media types produce the correct format value."""
|
||||
test_cases = [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue