diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index d0b2d5e3da7..7149e16fdfe 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -5167,7 +5167,6 @@ class BaseLLMHTTPHandler: if synthetic_session is not None: await websocket.send_text(json.dumps(synthetic_session)) realtime_streaming._session_created_sent_to_client = True - await realtime_streaming._maybe_send_guardrail_turn_detection_update() verbose_logger.debug( "Sent synthetic session.created to client to unblock connection" ) diff --git a/litellm/llms/gemini/realtime/transformation.py b/litellm/llms/gemini/realtime/transformation.py index 36e11b011e8..94090950ced 100644 --- a/litellm/llms/gemini/realtime/transformation.py +++ b/litellm/llms/gemini/realtime/transformation.py @@ -239,6 +239,7 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): "generationConfig", {} ) generation_config.setdefault("responseModalities", ["AUDIO"]) + client_session_configuration_request.setdefault("inputAudioTranscription", {}) client_session_configuration_request["model"] = f"models/{model}" gemini_setup_msg = json.dumps({"setup": client_session_configuration_request}) verbose_logger.debug( diff --git a/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py b/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py index a1fcc8385d1..e4b85cd35d4 100644 --- a/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py +++ b/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py @@ -1080,3 +1080,40 @@ async def test_duplicate_session_created_still_triggers_guardrail_turn_detection sent_update = json.loads(streaming._send_to_backend.await_args_list[0].args[0]) assert sent_update["type"] == "session.update" assert sent_update["session"]["turn_detection"]["create_response"] is False + + +@pytest.mark.asyncio +async def test_guardrail_update_respects_idempotency_flag(): + """Verify guardrail turn-detection update uses idempotency flag correctly.""" + client_ws = AsyncMock() + backend_ws = MagicMock() + backend_ws.send = AsyncMock() + + logging_obj = MagicMock() + logging_obj.litellm_trace_id = "trace_1" + logging_obj.async_success_handler = AsyncMock() + logging_obj.success_handler = MagicMock() + + provider_config = MagicMock() + provider_config.transform_realtime_request = MagicMock( + side_effect=lambda msg, model, session_config: [msg] + ) + + streaming = RealTimeStreaming( + websocket=client_ws, + backend_ws=backend_ws, + logging_obj=logging_obj, + provider_config=provider_config, + model="gemini-2.5-flash", + ) + streaming._has_audio_transcription_guardrails = MagicMock(return_value=True) # type: ignore[method-assign] + + # First call should send the update + assert streaming._guardrail_turn_detection_update_sent is False + await streaming._maybe_send_guardrail_turn_detection_update() + assert streaming._guardrail_turn_detection_update_sent is True + assert backend_ws.send.await_count == 1 + + # Second call should be a no-op (idempotent) + await streaming._maybe_send_guardrail_turn_detection_update() + assert backend_ws.send.await_count == 1 # Still 1, not 2 diff --git a/tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py b/tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py index c3e43e4a2c5..201899b8924 100644 --- a/tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py +++ b/tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py @@ -584,3 +584,37 @@ def test_gemini_realtime_multi_tool_calls_have_unique_item_ids(): assert responses[0]["item_id"] != responses[1]["item_id"] assert responses[0]["output_index"] == 0 assert responses[1]["output_index"] == 1 + + +def test_gemini_session_update_includes_input_audio_transcription_default(): + """Verify _handle_session_update includes inputAudioTranscription default.""" + config = GeminiRealtimeConfig() + session_update = { + "type": "session.update", + "session": { + "modalities": ["text", "audio"], + "tools": [ + { + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": { + "type": "object", + "properties": {"location": {"type": "string"}}, + }, + } + ], + }, + } + + result = config.transform_realtime_request( + json.dumps(session_update), + "gemini-2.5-flash", + session_configuration_request=None, + ) + + assert len(result) == 1 + setup = json.loads(result[0]) + assert "setup" in setup + assert "inputAudioTranscription" in setup["setup"] + assert setup["setup"]["inputAudioTranscription"] == {}