From 24af92663d0efc6a524e1f758dc8d1aab2bd2786 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 22 May 2026 16:45:13 +0000 Subject: [PATCH] fix(realtime): correct deferred-setup session.created modalities and reset IDs after response.done - Convert provider's real session.created to session.updated when a synthetic one was already forwarded so clients receive the authoritative modalities derived from their session.update instead of the synthetic placeholder. - Reset current_response_id / current_output_item_id after Gemini RESPONSE_DONE so a toolCall arriving in a later frame starts a fresh response instead of reusing the completed response's ID and emitting a duplicate response.done. Co-authored-by: Yassin Kortam --- .../litellm_core_utils/realtime_streaming.py | 17 +++++++++++------ litellm/llms/gemini/realtime/transformation.py | 6 ++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/litellm/litellm_core_utils/realtime_streaming.py b/litellm/litellm_core_utils/realtime_streaming.py index 2f2a9ca334a..ae2fae8bd9a 100644 --- a/litellm/litellm_core_utils/realtime_streaming.py +++ b/litellm/litellm_core_utils/realtime_streaming.py @@ -525,15 +525,20 @@ class RealTimeStreaming: else [transformed_response] ) for event in events: - event_str = json.dumps(event) if isinstance(event, dict) and event.get("type") == "session.created": if self._session_created_sent_to_client: + # A synthetic session.created (with placeholder defaults) was + # already forwarded to the client when we connected. The + # provider's real session.created (e.g. emitted from Gemini + # `setupComplete`) carries the authoritative modalities/model + # from the client's session.update. Re-emit it as + # `session.updated` so the client learns the corrected + # configuration without seeing two `session.created` events. + event = {**event, "type": "session.updated"} await self._maybe_send_guardrail_turn_detection_update() - verbose_logger.debug( - "Skipping duplicate session.created from provider stream" - ) - continue - self._session_created_sent_to_client = True + else: + self._session_created_sent_to_client = True + event_str = json.dumps(event) ## For audio/VAD guardrail path: forward session.created first, then inject. if ( isinstance(event, dict) diff --git a/litellm/llms/gemini/realtime/transformation.py b/litellm/llms/gemini/realtime/transformation.py index a987f18a0c7..eb9d4ef0cbd 100644 --- a/litellm/llms/gemini/realtime/transformation.py +++ b/litellm/llms/gemini/realtime/transformation.py @@ -1276,6 +1276,12 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): output_items=None, ) returned_message.append(transformed_response_done_event) + # Reset IDs so a subsequent turn (e.g. a `toolCall` arriving in + # a later WebSocket frame after `turnComplete`) starts a fresh + # response with its own `response.created` preamble instead of + # reusing the just-completed response ID. + current_output_item_id = None + current_response_id = None elif ( openai_event == OpenAIRealtimeEventTypes.RESPONSE_TEXT_DELTA or openai_event == OpenAIRealtimeEventTypes.RESPONSE_TEXT_DONE