diff --git a/litellm/llms/gemini/realtime/transformation.py b/litellm/llms/gemini/realtime/transformation.py index 12b7f877579..eef6c503dbd 100644 --- a/litellm/llms/gemini/realtime/transformation.py +++ b/litellm/llms/gemini/realtime/transformation.py @@ -368,10 +368,29 @@ class GeminiRealtimeConfig(BaseRealtimeConfig): if isinstance(original_realtime_input_config, dict) and isinstance( new_realtime_input_config, dict ): - follow_up_setup["realtimeInputConfig"] = { + merged_realtime_input_config = { **original_realtime_input_config, **new_realtime_input_config, } + # Deep-merge ``automaticActivityDetection`` so a partial VAD + # update (e.g. the guardrail-injected ``disabled: True`` from + # ``create_response: False``) does not silently drop unrelated + # knobs like ``silenceDurationMs`` / ``prefixPaddingMs`` from + # the original setup. + original_automatic_activity_detection = original_realtime_input_config.get( + "automaticActivityDetection" + ) + new_automatic_activity_detection = new_realtime_input_config.get( + "automaticActivityDetection" + ) + if isinstance(original_automatic_activity_detection, dict) and isinstance( + new_automatic_activity_detection, dict + ): + merged_realtime_input_config["automaticActivityDetection"] = { + **original_automatic_activity_detection, + **new_automatic_activity_detection, + } + follow_up_setup["realtimeInputConfig"] = merged_realtime_input_config verbose_logger.debug( "Gemini Realtime: Forwarding session.update as follow-up setup" ) 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 9db244834c8..c5f10075406 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 @@ -1047,3 +1047,43 @@ def test_gemini_follow_up_session_update_preserves_response_modalities_on_partia assert follow_up["generationConfig"]["responseModalities"] == ["AUDIO"] assert follow_up["generationConfig"]["maxOutputTokens"] == 2048 assert follow_up["generationConfig"]["temperature"] == 0.7 + + +def test_gemini_subsequent_session_update_preserves_automatic_activity_detection_subfields(): + """A follow-up turn_detection update that only sets ``create_response`` + (mapped to ``disabled``) must not drop ``silenceDurationMs`` / + ``prefixPaddingMs`` from the original ``automaticActivityDetection`` + block.""" + config = GeminiRealtimeConfig() + + original_setup = { + "setup": { + "model": "models/gemini-2.5-flash-native-audio", + "generationConfig": {"responseModalities": ["AUDIO"]}, + "realtimeInputConfig": { + "automaticActivityDetection": { + "disabled": False, + "silenceDurationMs": 500, + "prefixPaddingMs": 100, + } + }, + } + } + + session_update = { + "type": "session.update", + "session": {"turn_detection": {"create_response": False}}, + } + + messages = config.transform_realtime_request( + json.dumps(session_update), + "gemini-2.5-flash-native-audio", + session_configuration_request=json.dumps(original_setup), + ) + + automatic_activity_detection = json.loads(messages[0])["setup"][ + "realtimeInputConfig" + ]["automaticActivityDetection"] + assert automatic_activity_detection["disabled"] is True + assert automatic_activity_detection["silenceDurationMs"] == 500 + assert automatic_activity_detection["prefixPaddingMs"] == 100