mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(bedrock/realtime): persist minted tool-call response/item ids
transform_tool_use_event already minted ids for TOOL-first turns, but transform_realtime_response discarded them so later contentEnd events still saw None. Return and store the ids in session state; cover via transform_realtime_response regression
This commit is contained in:
parent
0780c64ec0
commit
ec9887ecb2
2 changed files with 89 additions and 11 deletions
|
|
@ -1011,7 +1011,7 @@ class BedrockRealtimeConfig(BaseRealtimeConfig):
|
|||
event: dict,
|
||||
current_output_item_id: str | None,
|
||||
current_response_id: str | None,
|
||||
) -> tuple[list[OpenAIRealtimeEvents], str, str]:
|
||||
) -> tuple[list[OpenAIRealtimeEvents], str, str, str, str]:
|
||||
"""
|
||||
Transform Bedrock toolUse event to OpenAI format.
|
||||
|
||||
|
|
@ -1021,7 +1021,8 @@ class BedrockRealtimeConfig(BaseRealtimeConfig):
|
|||
current_response_id: Current response ID
|
||||
|
||||
Returns:
|
||||
Tuple of (events, tool_call_id, tool_name) for tracking
|
||||
Tuple of (events, tool_call_id, tool_name, output_item_id, response_id)
|
||||
so the caller can persist any minted IDs into session state
|
||||
"""
|
||||
verbose_logger.debug("Handling toolUse")
|
||||
tool_use = event["toolUse"]
|
||||
|
|
@ -1057,6 +1058,8 @@ class BedrockRealtimeConfig(BaseRealtimeConfig):
|
|||
[cast(OpenAIRealtimeEvents, function_call_event)],
|
||||
tool_call_id,
|
||||
tool_name,
|
||||
item_id,
|
||||
response_id,
|
||||
)
|
||||
|
||||
def transform_conversation_item_create_tool_result_event(self, json_message: dict) -> list[str]:
|
||||
|
|
@ -1223,11 +1226,14 @@ class BedrockRealtimeConfig(BaseRealtimeConfig):
|
|||
returned_messages.extend(done_events)
|
||||
|
||||
elif "toolUse" in event:
|
||||
events, tool_call_id, tool_name = self.transform_tool_use_event(
|
||||
event, current_output_item_id, current_response_id
|
||||
)
|
||||
(
|
||||
events,
|
||||
tool_call_id,
|
||||
tool_name,
|
||||
current_output_item_id,
|
||||
current_response_id,
|
||||
) = self.transform_tool_use_event(event, current_output_item_id, current_response_id)
|
||||
returned_messages.extend(events)
|
||||
# Store tool call info for potential use
|
||||
verbose_logger.debug(f"Tool use event: {tool_name} (ID: {tool_call_id})")
|
||||
|
||||
elif "promptEnd" in event or "completionEnd" in event:
|
||||
|
|
|
|||
|
|
@ -618,7 +618,7 @@ class TestBedrockRealtimeResponseTransformation:
|
|||
config = BedrockRealtimeConfig()
|
||||
|
||||
# Missing IDs still emit a function call (Nova Sonic starts tools with role=TOOL)
|
||||
events, tool_call_id, tool_name = config.transform_tool_use_event(
|
||||
events, tool_call_id, tool_name, item_id, response_id = config.transform_tool_use_event(
|
||||
{
|
||||
"toolUse": {
|
||||
"toolUseId": "tool_call_no_ids",
|
||||
|
|
@ -633,14 +633,16 @@ class TestBedrockRealtimeResponseTransformation:
|
|||
assert events[0]["type"] == "response.function_call_arguments.done"
|
||||
assert events[0]["call_id"] == "tool_call_no_ids"
|
||||
assert events[0]["name"] == "get_weather"
|
||||
assert events[0]["response_id"].startswith("resp_")
|
||||
assert events[0]["item_id"].startswith("item_")
|
||||
assert response_id.startswith("resp_")
|
||||
assert item_id.startswith("item_")
|
||||
assert events[0]["response_id"] == response_id
|
||||
assert events[0]["item_id"] == item_id
|
||||
assert json.loads(events[0]["arguments"]) == {"location": "Seattle"}
|
||||
assert tool_call_id == "tool_call_no_ids"
|
||||
assert tool_name == "get_weather"
|
||||
|
||||
# JSON string content is parsed and converted to a function call event
|
||||
events, tool_call_id, tool_name = config.transform_tool_use_event(
|
||||
events, tool_call_id, tool_name, item_id, response_id = config.transform_tool_use_event(
|
||||
{
|
||||
"toolUse": {
|
||||
"toolUseId": "tool_call_123",
|
||||
|
|
@ -655,12 +657,14 @@ class TestBedrockRealtimeResponseTransformation:
|
|||
assert events[0]["type"] == "response.function_call_arguments.done"
|
||||
assert events[0]["call_id"] == "tool_call_123"
|
||||
assert events[0]["name"] == "get_weather"
|
||||
assert response_id == "resp_123"
|
||||
assert item_id == "item_123"
|
||||
assert events[0]["response_id"] == "resp_123"
|
||||
assert events[0]["item_id"] == "item_123"
|
||||
assert json.loads(events[0]["arguments"]) == {"location": "San Francisco"}
|
||||
|
||||
# Invalid JSON content falls back to empty arguments
|
||||
events, _, _ = config.transform_tool_use_event(
|
||||
events, _, _, _, _ = config.transform_tool_use_event(
|
||||
{
|
||||
"toolUse": {
|
||||
"toolUseId": "tool_call_124",
|
||||
|
|
@ -674,6 +678,74 @@ class TestBedrockRealtimeResponseTransformation:
|
|||
assert len(events) == 1
|
||||
assert json.loads(events[0]["arguments"]) == {}
|
||||
|
||||
def test_transform_realtime_response_persists_minted_tool_ids(self):
|
||||
"""TOOL-first turns must write minted response/item ids into session state"""
|
||||
config = BedrockRealtimeConfig()
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.litellm_trace_id = "trace_123"
|
||||
|
||||
tool_use_message = {
|
||||
"event": {
|
||||
"toolUse": {
|
||||
"toolUseId": "tool_call_state",
|
||||
"toolName": "get_weather",
|
||||
"content": json.dumps({"location": "Seattle"}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = config.transform_realtime_response(
|
||||
json.dumps(tool_use_message),
|
||||
"amazon.nova-2-sonic-v1:0",
|
||||
logging_obj,
|
||||
realtime_response_transform_input={
|
||||
"session_configuration_request": json.dumps({"configured": True}),
|
||||
"current_output_item_id": None,
|
||||
"current_response_id": None,
|
||||
"current_conversation_id": "conv_123",
|
||||
"current_delta_chunks": [],
|
||||
"current_item_chunks": [],
|
||||
"current_delta_type": "text",
|
||||
},
|
||||
)
|
||||
|
||||
assert len(result["response"]) == 1
|
||||
function_call = result["response"][0]
|
||||
assert function_call["type"] == "response.function_call_arguments.done"
|
||||
assert result["current_response_id"] is not None
|
||||
assert result["current_output_item_id"] is not None
|
||||
assert result["current_response_id"].startswith("resp_")
|
||||
assert result["current_output_item_id"].startswith("item_")
|
||||
assert function_call["response_id"] == result["current_response_id"]
|
||||
assert function_call["item_id"] == result["current_output_item_id"]
|
||||
assert json.loads(function_call["arguments"]) == {"location": "Seattle"}
|
||||
|
||||
# A follow-up contentEnd must reuse the same persisted ids
|
||||
content_end_message = {
|
||||
"event": {
|
||||
"contentEnd": {
|
||||
"stopReason": "TOOL_USE",
|
||||
"type": "TOOL",
|
||||
}
|
||||
}
|
||||
}
|
||||
follow_up = config.transform_realtime_response(
|
||||
json.dumps(content_end_message),
|
||||
"amazon.nova-2-sonic-v1:0",
|
||||
logging_obj,
|
||||
realtime_response_transform_input={
|
||||
"session_configuration_request": result["session_configuration_request"],
|
||||
"current_output_item_id": result["current_output_item_id"],
|
||||
"current_response_id": result["current_response_id"],
|
||||
"current_conversation_id": result["current_conversation_id"],
|
||||
"current_delta_chunks": result["current_delta_chunks"],
|
||||
"current_item_chunks": result["current_item_chunks"],
|
||||
"current_delta_type": result["current_delta_type"],
|
||||
},
|
||||
)
|
||||
assert follow_up["current_response_id"] == result["current_response_id"]
|
||||
assert follow_up["current_output_item_id"] == result["current_output_item_id"]
|
||||
|
||||
def test_transform_content_end_text(self):
|
||||
"""Test contentEnd for text response"""
|
||||
config = BedrockRealtimeConfig()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue