From a8b592b634652926f6c7f4a1c38741cf5fafadc8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 23 May 2026 20:29:56 +0000 Subject: [PATCH] fix(gemini realtime): event_id, item copy, and dict guard for tool-call events - Emit event_id on response.output_item.added for tool calls so spec-compliant OpenAI Realtime SDK clients can index/deduplicate the event like every other server-sent event in the sequence. - Pass a shallow copy of function_call_item to response.output_item.done and conversation.item.created so downstream handlers (e.g. the beta-protocol translator) that mutate the item dict don't corrupt sibling events sharing the same reference. - Guard map_openai_event against non-dict values (e.g. Gemini's 'setupComplete: true' boolean payload) so the WebSocket session doesn't die with an AttributeError on the unguarded .get() call. Add NotRequired event_id field on OpenAIRealtimeStreamResponseOutputItemAdded to keep existing call-sites that don't set event_id compatible. Co-authored-by: Yassin Kortam --- .../llms/gemini/realtime/transformation.py | 20 +++++++++++++------ litellm/types/llms/openai.py | 10 +++++++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/litellm/llms/gemini/realtime/transformation.py b/litellm/llms/gemini/realtime/transformation.py index 2346d3b7a73..0c0baaeaf8f 100644 --- a/litellm/llms/gemini/realtime/transformation.py +++ b/litellm/llms/gemini/realtime/transformation.py @@ -1138,11 +1138,15 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): def map_openai_event( self, key: str, - value: dict, + value: Any, current_delta_type: Optional[ALL_DELTA_TYPES], ) -> Union[OpenAIRealtimeEventTypes, ResponsesAPIStreamEvents]: - model_turn_event = value.get("modelTurn") - generation_complete_event = value.get("generationComplete") + if isinstance(value, dict): + model_turn_event = value.get("modelTurn") + generation_complete_event = value.get("generationComplete") + else: + model_turn_event = None + generation_complete_event = None openai_event: Optional[ Union[OpenAIRealtimeEventTypes, ResponsesAPIStreamEvents] ] = None @@ -1399,6 +1403,7 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): returned_message.append( OpenAIRealtimeStreamResponseOutputItemAdded( type="response.output_item.added", + event_id=f"event_{uuid.uuid4()}", response_id=current_response_id, output_index=idx, item={ @@ -1431,14 +1436,17 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): ) # response.function_call_arguments.done returned_message.append(tool_call) - # response.output_item.done + # response.output_item.done — pass a fresh copy so + # downstream handlers that mutate the item dict (e.g. the + # beta-protocol translator) don't corrupt the references + # used by sibling events sharing the same function_call_item. returned_message.append( OpenAIRealtimeOutputItemDone( type="response.output_item.done", event_id=f"event_{uuid.uuid4()}", response_id=current_response_id, output_index=idx, - item=function_call_item, + item={**function_call_item}, ) ) # conversation.item.created @@ -1446,7 +1454,7 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): OpenAIRealtimeConversationItemCreated( type="conversation.item.created", event_id=f"event_{uuid.uuid4()}", - item=function_call_item, + item={**function_call_item}, ) ) diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 1ca55534441..14114b22f39 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -79,7 +79,14 @@ from pydantic import ( field_serializer, field_validator, ) -from typing_extensions import Annotated, Dict, Required, TypedDict, override +from typing_extensions import ( + Annotated, + Dict, + NotRequired, + Required, + TypedDict, + override, +) from litellm.types.llms.base import BaseLiteLLMOpenAIResponseObject from litellm.types.responses.main import ( @@ -1935,6 +1942,7 @@ class OpenAIRealtimeStreamResponseOutputItemAdded(TypedDict): response_id: str output_index: int item: OpenAIRealtimeStreamResponseOutputItem + event_id: NotRequired[str] class OpenAIRealtimeStreamResponseBaseObject(TypedDict):