diff --git a/litellm/litellm_core_utils/realtime_streaming.py b/litellm/litellm_core_utils/realtime_streaming.py index ae2fae8bd9a..a44e6cff1f7 100644 --- a/litellm/litellm_core_utils/realtime_streaming.py +++ b/litellm/litellm_core_utils/realtime_streaming.py @@ -525,7 +525,10 @@ class RealTimeStreaming: else [transformed_response] ) for event in events: - if isinstance(event, dict) and event.get("type") == "session.created": + is_session_created_event = ( + isinstance(event, dict) and event.get("type") == "session.created" + ) + if is_session_created_event: if self._session_created_sent_to_client: # A synthetic session.created (with placeholder defaults) was # already forwarded to the client when we connected. The @@ -535,16 +538,14 @@ class RealTimeStreaming: # `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() 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) - and event.get("type") == "session.created" - and self._has_audio_transcription_guardrails() - ): + ## For audio/VAD guardrail path: forward the (possibly re-typed) + ## session.created first, then inject the guardrail turn-detection + ## update. Handling first and duplicate session.created via the same + ## path keeps the event-then-guardrail ordering consistent. + if is_session_created_event and self._has_audio_transcription_guardrails(): self.store_message(event_str) await self.websocket.send_text(event_str) await self._maybe_send_guardrail_turn_detection_update() diff --git a/litellm/llms/vertex_ai/realtime/transformation.py b/litellm/llms/vertex_ai/realtime/transformation.py index 555b04ab883..3ef829b24f6 100644 --- a/litellm/llms/vertex_ai/realtime/transformation.py +++ b/litellm/llms/vertex_ai/realtime/transformation.py @@ -155,6 +155,21 @@ class VertexAIRealtimeConfig(GeminiRealtimeConfig): def _build_vertex_ai_setup_config(self, model: str, session_params: dict) -> dict: """Build Vertex AI setup configuration with proper model path and defaults.""" + # Normalize ``turn_detection`` to the top level so map_openai_params + # picks it up whether the client used the flat beta shape or the + # GA nested shape (session.audio.input.turn_detection). Without + # this, guardrail-injected ``create_response: False`` would be + # silently dropped for GA clients because map_openai_params only + # looks at top-level keys. + extracted_turn_detection = self._extract_turn_detection(session_params) + if ( + extracted_turn_detection is not None + and "turn_detection" not in session_params + ): + session_params = { + **session_params, + "turn_detection": extracted_turn_detection, + } setup_config = self.map_openai_params( optional_params={}, non_default_params=session_params )