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 <yassin@berri.ai>
This commit is contained in:
Cursor Agent 2026-05-23 20:29:56 +00:00
parent ec79cec7ae
commit a8b592b634
No known key found for this signature in database
2 changed files with 23 additions and 7 deletions

View file

@ -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},
)
)

View file

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