fix(realtime): emit response.created preamble before tool-call events

- Emit response.created, output_item.added, and conversation.item.created for function calls
- Ensures OpenAI Realtime API spec compliance
- Add test for preamble emission

Made-with: Cursor
This commit is contained in:
Sameer Kankute 2026-04-27 19:37:05 +05:30
parent 6274bc0d7b
commit 77571e811d
No known key found for this signature in database
2 changed files with 101 additions and 1 deletions

View file

@ -1077,12 +1077,65 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
returned_message.append(transformed_message)
elif openai_event == ResponsesAPIStreamEvents.FUNCTION_CALL_ARGUMENTS_DONE:
# Handle toolCall from Gemini
# Emit response.created preamble if this is the first event in the response
if current_response_id is None:
current_response_id = f"resp_{uuid.uuid4()}"
current_output_item_id = f"item_{uuid.uuid4()}"
current_conversation_id = current_conversation_id or f"conv_{uuid.uuid4()}"
# Emit response.created
returned_message.append({
"type": "response.created",
"event_id": f"event_{uuid.uuid4()}",
"response": {
"object": "realtime.response",
"id": current_response_id,
"status": "in_progress",
"output": [],
"conversation_id": current_conversation_id,
},
})
tool_call_events = self.transform_tool_call_events(
value,
response_id=current_response_id,
output_item_id=current_output_item_id,
)
returned_message.extend(tool_call_events)
# Emit output_item.added and conversation.item.created for each function call
for idx, tool_call in enumerate(tool_call_events):
item_id = tool_call["item_id"]
# response.output_item.added
returned_message.append({
"type": "response.output_item.added",
"event_id": f"event_{uuid.uuid4()}",
"response_id": current_response_id,
"output_index": idx,
"item": {
"id": item_id,
"object": "realtime.item",
"type": "function_call",
"status": "in_progress",
"call_id": tool_call["call_id"],
"name": tool_call["name"],
"arguments": "",
},
})
# response.function_call_arguments.done
returned_message.append(tool_call)
# conversation.item.created
returned_message.append({
"type": "conversation.item.created",
"event_id": f"event_{uuid.uuid4()}",
"item": {
"id": item_id,
"object": "realtime.item",
"type": "function_call",
"status": "completed",
"call_id": tool_call["call_id"],
"name": tool_call["name"],
"arguments": tool_call["arguments"],
},
})
elif openai_event == OpenAIRealtimeEventTypes.RESPONSE_DONE:
transformed_response_done_event = self.transform_response_done_event(
message=BidiGenerateContentServerMessage(**json_message), # type: ignore

View file

@ -618,3 +618,50 @@ def test_gemini_session_update_includes_input_audio_transcription_default():
assert "setup" in setup
assert "inputAudioTranscription" in setup["setup"]
assert setup["setup"]["inputAudioTranscription"] == {}
def test_gemini_tool_call_emits_response_created_preamble():
"""Verify response.created is emitted before tool call events when response_id is None."""
config = GeminiRealtimeConfig()
logging_obj = MagicMock()
logging_obj.litellm_trace_id = "trace_123"
gemini_tool_call = {
"toolCall": {
"functionCalls": [
{
"id": "call_123",
"name": "get_weather",
"args": {"location": "San Francisco", "unit": "fahrenheit"}
}
]
}
}
# Transform with current_response_id=None to trigger preamble emission
result = 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,
},
)
responses = result["response"]
# Should have: response.created, output_item.added, function_call_arguments.done, conversation.item.created
assert len(responses) >= 4
assert responses[0]["type"] == "response.created"
assert "response" in responses[0]
assert responses[0]["response"]["status"] == "in_progress"
assert responses[1]["type"] == "response.output_item.added"
assert responses[1]["item"]["type"] == "function_call"
assert responses[2]["type"] == "response.function_call_arguments.done"
assert responses[3]["type"] == "conversation.item.created"
assert responses[3]["item"]["type"] == "function_call"