Merge pull request #14005 from moshemorad/bedrock_fix_structure_output

Bedrock fix structure output
This commit is contained in:
Krish Dholakia
2025-09-01 14:50:08 +03:00
committed by Mohse Morad
parent 0c9051abba
commit fd0f47d48d
3 changed files with 299 additions and 9 deletions
BIN
View File
Binary file not shown.
@@ -10,6 +10,7 @@ from typing import List, Literal, Optional, Tuple, Union, cast, overload
import httpx
import litellm
from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
from litellm.litellm_core_utils.core_helpers import map_finish_reason
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
@@ -322,7 +323,6 @@ class AmazonConverseConfig(BaseConfig):
def _create_json_tool_call_for_response_format(
self,
json_schema: Optional[dict] = None,
schema_name: str = "json_tool_call",
description: Optional[str] = None,
) -> ChatCompletionToolParam:
"""
@@ -347,7 +347,7 @@ class AmazonConverseConfig(BaseConfig):
_input_schema = json_schema
tool_param_function_chunk = ChatCompletionToolParamFunctionChunk(
name=schema_name, parameters=_input_schema
name=RESPONSE_FORMAT_TOOL_NAME, parameters=_input_schema
)
if description:
tool_param_function_chunk["description"] = description
@@ -391,14 +391,11 @@ class AmazonConverseConfig(BaseConfig):
continue
json_schema: Optional[dict] = None
schema_name: str = ""
description: Optional[str] = None
if "response_schema" in value:
json_schema = value["response_schema"]
schema_name = "json_tool_call"
elif "json_schema" in value:
json_schema = value["json_schema"]["schema"]
schema_name = value["json_schema"]["name"]
description = value["json_schema"].get("description")
if "type" in value and value["type"] == "text":
@@ -414,7 +411,6 @@ class AmazonConverseConfig(BaseConfig):
"""
_tool = self._create_json_tool_call_for_response_format(
json_schema=json_schema,
schema_name=schema_name if schema_name != "" else "json_tool_call",
description=description,
)
optional_params = self._add_tools_to_optional_params(
@@ -430,7 +426,7 @@ class AmazonConverseConfig(BaseConfig):
optional_params["tool_choice"] = ToolChoiceValuesBlock(
tool=SpecificToolChoiceBlock(
name=schema_name if schema_name != "" else "json_tool_call"
name=RESPONSE_FORMAT_TOOL_NAME
)
)
optional_params["json_mode"] = True
@@ -1119,8 +1115,7 @@ class AmazonConverseConfig(BaseConfig):
self._transform_thinking_blocks(reasoningContentBlocks)
)
chat_completion_message["content"] = content_str
if json_mode is True and tools is not None and len(tools) == 1:
# to support 'json_schema' logic on bedrock models
if json_mode is True and tools is not None and len(tools) == 1 and tools[0]["function"]["name"] == RESPONSE_FORMAT_TOOL_NAME:
json_mode_content_str: Optional[str] = tools[0]["function"].get("arguments")
if json_mode_content_str is not None:
chat_completion_message["content"] = json_mode_content_str
@@ -475,6 +475,239 @@ def test_transform_response_with_bash_tool():
assert args["command"] == "ls -la *.py"
def test_transform_response_with_structured_response_being_called():
"""Test response transformation with structured response."""
from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConfig
from litellm.types.utils import ModelResponse
# Simulate a Bedrock Converse response with a bash tool call
response_json = {
"additionalModelResponseFields": {},
"metrics": {"latencyMs": 100.0},
"output": {
"message": {
"role": "assistant",
"content": [
{
"toolUse": {
"toolUseId": "tooluse_456",
"name": "json_tool_call",
"input": {
"Current_Temperature": 62,
"Weather_Explanation": "San Francisco typically has mild, cool weather year-round due to its coastal location and marine influence. The city is known for its fog, moderate temperatures, and relatively stable climate with little seasonal variation."},
}
}
]
}
},
"stopReason": "tool_use",
"usage": {
"inputTokens": 8,
"outputTokens": 3,
"totalTokens": 11,
"cacheReadInputTokenCount": 0,
"cacheReadInputTokens": 0,
"cacheWriteInputTokenCount": 0,
"cacheWriteInputTokens": 0,
},
}
# Mock httpx.Response
class MockResponse:
def json(self):
return response_json
@property
def text(self):
return json.dumps(response_json)
config = AmazonConverseConfig()
model_response = ModelResponse()
optional_params = {
"json_mode": True,
"tools": [
{
'type': 'function',
'function': {
'name': 'get_weather',
'description': 'Get the current weather in a given location',
'parameters': {
'type': 'object',
'properties': {
'location': {
'type': 'string',
'description': 'The city and state, e.g. San Francisco, CA'
},
'unit': {
'type': 'string',
'enum': ['celsius', 'fahrenheit']
}
},
'required': ['location']
}
}
},
{
'type': 'function',
'function': {
'name': 'json_tool_call',
'parameters': {
'$schema': 'http://json-schema.org/draft-07/schema#',
'type': 'object',
'required': ['Weather_Explanation', 'Current_Temperature'],
'properties': {
'Weather_Explanation': {
'type': ['string', 'null'],
'description': '1-2 sentences explaining the weather in the location'
},
'Current_Temperature': {
'type': ['number', 'null'],
'description': 'Current temperature in the location'
}
},
'additionalProperties': False
}
}
}
]
}
# Call the transformation logic
result = config._transform_response(
model="bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0",
response=MockResponse(),
model_response=model_response,
stream=False,
logging_obj=None,
optional_params=optional_params,
api_key=None,
data=None,
messages=[],
encoding=None,
)
# Check that the tool call is present in the returned message
assert result.choices[0].message.tool_calls is None
assert result.choices[0].message.content is not None
assert result.choices[0].message.content == '{"Current_Temperature": 62, "Weather_Explanation": "San Francisco typically has mild, cool weather year-round due to its coastal location and marine influence. The city is known for its fog, moderate temperatures, and relatively stable climate with little seasonal variation."}'
def test_transform_response_with_structured_response_calling_tool():
"""Test response transformation with structured response."""
from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConfig
from litellm.types.utils import ModelResponse
# Simulate a Bedrock Converse response with a bash tool call
response_json = {
"metrics": {
"latencyMs": 1148
},
"output": {
"message":
{
"content": [
{
"text": "I\'ll check the current weather in San Francisco for you."
},
{
"toolUse": {
"input": {
"location": "San Francisco, CA",
"unit": "celsius"
},
"name": "get_weather",
"toolUseId": "tooluse_oKk__QrqSUmufMw3Q7vGaQ"
}
}
],
"role": "assistant"
}
},
"stopReason": "tool_use",
"usage": {
"cacheReadInputTokenCount": 0,
"cacheReadInputTokens": 0,
"cacheWriteInputTokenCount": 0,
"cacheWriteInputTokens": 0,
"inputTokens": 534,
"outputTokens": 69,
"totalTokens": 603
}
}
# Mock httpx.Response
class MockResponse:
def json(self):
return response_json
@property
def text(self):
return json.dumps(response_json)
config = AmazonConverseConfig()
model_response = ModelResponse()
optional_params = {
"json_mode": True,
"tools": [
{
'type': 'function',
'function': {
'name': 'get_weather',
'description': 'Get the current weather in a given location',
'parameters': {
'type': 'object',
'properties': {
'location': {
'type': 'string',
'description': 'The city and state, e.g. San Francisco, CA'
},
'unit': {
'type': 'string',
'enum': ['celsius', 'fahrenheit']
}
},
'required': ['location']
}
}
},
{
'type': 'function',
'function': {
'name': 'json_tool_call',
'parameters': {
'$schema': 'http://json-schema.org/draft-07/schema#',
'type': 'object',
'required': ['Weather_Explanation', 'Current_Temperature'],
'properties': {
'Weather_Explanation': {
'type': ['string', 'null'],
'description': '1-2 sentences explaining the weather in the location'
},
'Current_Temperature': {
'type': ['number', 'null'],
'description': 'Current temperature in the location'
}
},
'additionalProperties': False
}
}
}
]
}
# Call the transformation logic
result = config._transform_response(
model="bedrock/eu.anthropic.claude-sonnet-4-20250514-v1:0",
response=MockResponse(),
model_response=model_response,
stream=False,
logging_obj=None,
optional_params=optional_params,
api_key=None,
data=None,
messages=[],
encoding=None,
)
# Check that the tool call is present in the returned message
assert result.choices[0].message.tool_calls is not None
assert len(result.choices[0].message.tool_calls) == 1
assert result.choices[0].message.tool_calls[0].function.name == "get_weather"
assert result.choices[0].message.tool_calls[0].function.arguments == '{"location": "San Francisco, CA", "unit": "celsius"}'
@pytest.mark.asyncio
async def test_bedrock_bash_tool_acompletion():
"""Test Bedrock with bash tool for ls command using acompletion."""
@@ -938,6 +1171,68 @@ def test_transform_request_with_function_tool():
assert request_data["toolConfig"]["tools"][0]["toolSpec"]["name"] == "get_weather"
def test_map_openai_params_with_response_format():
"""Test map_openai_params with response_format."""
config = AmazonConverseConfig()
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
}
}
]
json_schema = {
"type": "json_schema",
"json_schema": {
"name": "WeatherResult",
"schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["Weather_Explanation", "Current_Temperature"],
"properties": {
"Weather_Explanation": {
"type": ["string", "null"],
"description": "1-2 sentences explaining the weather in the location",
},
"Current_Temperature": {
"type": ["number", "null"],
"description": "Current temperature in the location",
},
},
"additionalProperties": False,
},
"strict": False,
},
}
optional_params = config.map_openai_params(
non_default_params={"response_format": json_schema},
optional_params={"tools": tools},
model="eu.anthropic.claude-sonnet-4-20250514-v1:0",
drop_params=False
)
assert "tools" in optional_params
assert len(optional_params["tools"]) == 2
assert optional_params["tools"][1]["type"] == "function"
assert optional_params["tools"][1]["function"]["name"] == "json_tool_call"
@pytest.mark.asyncio
async def test_assistant_message_cache_control():
"""Test that assistant messages with cache_control generate cachePoint blocks."""