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 <yassin@berri.ai>
This commit is contained in:
Cursor Agent 2026-05-22 16:45:13 +00:00
parent 86ace3ef1a
commit 24af92663d
No known key found for this signature in database
2 changed files with 17 additions and 6 deletions

View file

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

View file

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