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:
Sameer Kankute 2026-04-27 21:46:18 +05:30
parent 4af845d43a
commit 6fb312fb1d
No known key found for this signature in database
2 changed files with 105 additions and 13 deletions

View file

@ -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:

View file

@ -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