mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
fix(realtime): include function name in toolResponse (P1)
- Store call_id → name mapping when receiving toolCall from Gemini - Look up and include name in functionResponses when sending tool results - Required by Gemini Live API spec for proper tool call routing - Add test to verify name field is included in round-trip Made-with: Cursor
This commit is contained in:
parent
4af845d43a
commit
6fb312fb1d
2 changed files with 105 additions and 13 deletions
|
|
@ -68,6 +68,11 @@ MAP_GEMINI_FIELD_TO_OPENAI_EVENT: Dict[str, Union[OpenAIRealtimeEventTypes, Resp
|
|||
|
||||
|
||||
class GeminiRealtimeConfig(BaseRealtimeConfig):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Store call_id → function_name mapping for tool call round-trip
|
||||
self._tool_call_id_to_name: Dict[str, str] = {}
|
||||
|
||||
def validate_environment(
|
||||
self, headers: dict, model: str, api_key: Optional[str] = None
|
||||
) -> dict:
|
||||
|
|
@ -283,15 +288,25 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
|||
except json.JSONDecodeError:
|
||||
output_dict = {"result": output}
|
||||
|
||||
# Look up the function name from stored mapping
|
||||
function_name = self._tool_call_id_to_name.get(call_id)
|
||||
if not function_name:
|
||||
verbose_logger.warning(
|
||||
f"Gemini Realtime: Function name not found for call_id={call_id}. "
|
||||
"This may cause Gemini to reject the response."
|
||||
)
|
||||
|
||||
# Build Gemini toolResponse format
|
||||
function_response = {
|
||||
"id": call_id,
|
||||
"response": output_dict,
|
||||
}
|
||||
if function_name:
|
||||
function_response["name"] = function_name
|
||||
|
||||
tool_response_message = {
|
||||
"toolResponse": {
|
||||
"functionResponses": [
|
||||
{
|
||||
"id": call_id,
|
||||
"response": output_dict,
|
||||
}
|
||||
]
|
||||
"functionResponses": [function_response]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -679,6 +694,7 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
|||
Transform Gemini toolCall message to OpenAI function call events.
|
||||
|
||||
Converts Gemini's functionCalls format to OpenAI's response.function_call_arguments.done events.
|
||||
Also stores call_id → name mapping for later use in function_call_output responses.
|
||||
"""
|
||||
function_calls = tool_call_message.get("functionCalls", [])
|
||||
resolved_response_id = response_id or f"resp_{uuid.uuid4()}"
|
||||
|
|
@ -687,19 +703,28 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
|||
verbose_logger.debug(
|
||||
f"Gemini Realtime: Transforming {len(function_calls)} tool call(s) to OpenAI format"
|
||||
)
|
||||
return [
|
||||
{
|
||||
|
||||
events = []
|
||||
for idx, fc in enumerate(function_calls):
|
||||
call_id = fc.get("id", "")
|
||||
name = fc.get("name", "")
|
||||
|
||||
# Store call_id → name mapping for round-trip
|
||||
if call_id and name:
|
||||
self._tool_call_id_to_name[call_id] = name
|
||||
|
||||
events.append({
|
||||
"type": "response.function_call_arguments.done",
|
||||
"event_id": f"event_{uuid.uuid4()}",
|
||||
"response_id": resolved_response_id,
|
||||
"item_id": f"{resolved_output_item_id}_tool_{idx}",
|
||||
"output_index": idx,
|
||||
"call_id": fc.get("id", ""),
|
||||
"name": fc.get("name", ""),
|
||||
"call_id": call_id,
|
||||
"name": name,
|
||||
"arguments": json.dumps(fc.get("args", {})),
|
||||
}
|
||||
for idx, fc in enumerate(function_calls)
|
||||
]
|
||||
})
|
||||
|
||||
return events
|
||||
|
||||
@staticmethod
|
||||
def get_nested_value(obj: dict, path: str) -> Any:
|
||||
|
|
|
|||
|
|
@ -674,3 +674,70 @@ def test_gemini_tool_call_emits_response_created_preamble():
|
|||
assert responses[5]["response"]["status"] == "completed"
|
||||
assert len(responses[5]["response"]["output"]) == 1
|
||||
assert responses[5]["response"]["output"][0]["type"] == "function_call"
|
||||
|
||||
|
||||
def test_gemini_function_call_output_includes_name():
|
||||
"""Verify function_call_output includes name field from stored mapping."""
|
||||
config = GeminiRealtimeConfig()
|
||||
|
||||
# First, receive a toolCall from Gemini (this stores the call_id → name mapping)
|
||||
gemini_tool_call = {
|
||||
"toolCall": {
|
||||
"functionCalls": [
|
||||
{
|
||||
"id": "call_123",
|
||||
"name": "get_weather",
|
||||
"args": {"location": "San Francisco"}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.litellm_trace_id = "trace_123"
|
||||
|
||||
config.transform_realtime_response(
|
||||
json.dumps(gemini_tool_call),
|
||||
"gemini-2.5-flash",
|
||||
logging_obj,
|
||||
realtime_response_transform_input={
|
||||
"session_configuration_request": None,
|
||||
"current_output_item_id": None,
|
||||
"current_response_id": None,
|
||||
"current_conversation_id": None,
|
||||
"current_delta_chunks": [],
|
||||
"current_item_chunks": [],
|
||||
"current_delta_type": None,
|
||||
},
|
||||
)
|
||||
|
||||
# Verify mapping was stored
|
||||
assert "call_123" in config._tool_call_id_to_name
|
||||
assert config._tool_call_id_to_name["call_123"] == "get_weather"
|
||||
|
||||
# Now send a function_call_output back (this should include the name)
|
||||
function_output = {
|
||||
"type": "conversation.item.create",
|
||||
"item": {
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_123",
|
||||
"output": json.dumps({"result": "72 degrees"})
|
||||
}
|
||||
}
|
||||
|
||||
result = config.transform_realtime_request(
|
||||
json.dumps(function_output),
|
||||
"gemini-2.5-flash",
|
||||
session_configuration_request="{}",
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
tool_response = json.loads(result[0])
|
||||
assert "toolResponse" in tool_response
|
||||
assert "functionResponses" in tool_response["toolResponse"]
|
||||
assert len(tool_response["toolResponse"]["functionResponses"]) == 1
|
||||
|
||||
function_response = tool_response["toolResponse"]["functionResponses"][0]
|
||||
assert function_response["id"] == "call_123"
|
||||
assert function_response["name"] == "get_weather" # ✅ Name is included
|
||||
assert "response" in function_response
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue