From 2cd0639fd66465650aee25b11148c5b4dad1f581 Mon Sep 17 00:00:00 2001 From: Emerson Gomes Date: Tue, 22 Sep 2026 19:16:11 -0500 Subject: [PATCH] fix(vertex-ai): preserve nested speech locale and voice --- .../text_to_speech/transformation.py | 12 +++++++++-- .../text_to_speech/test_transformation.py | 21 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/litellm/llms/vertex_ai/text_to_speech/transformation.py b/litellm/llms/vertex_ai/text_to_speech/transformation.py index 48598411670..2b799145422 100644 --- a/litellm/llms/vertex_ai/text_to_speech/transformation.py +++ b/litellm/llms/vertex_ai/text_to_speech/transformation.py @@ -245,10 +245,13 @@ class VertexAITextToSpeechConfig(BaseTextToSpeechConfig, VertexBase): self, voice: dict, # mutable-ok: provider voice payload arrives as a concrete dictionary ) -> str | None: - voice_name: Final = self._get_str_value(voice, "name", "voiceName", "voice_name") + voice_name: Final = self._get_str_value(voice, "name", "voiceName", "voice_name", "voice") if voice_name is not None: return voice_name speech_config: Final = self._get_dict_value(voice, "speechConfig", "speech_config") or voice + nested_voice_name: Final = self._get_str_value(speech_config, "name", "voiceName", "voice_name", "voice") + if nested_voice_name is not None: + return nested_voice_name voice_config: Final = self._get_dict_value(speech_config, "voiceConfig", "voice_config") if voice_config is None: return None @@ -273,7 +276,12 @@ class VertexAITextToSpeechConfig(BaseTextToSpeechConfig, VertexBase): "name": voice, } - language_code: Final = self._get_str_value(voice, "languageCode", "language_code") or self.DEFAULT_LANGUAGE_CODE + speech_config: Final = self._get_dict_value(voice, "speechConfig", "speech_config") or voice + language_code: Final = ( + self._get_str_value(voice, "languageCode", "language_code") + or self._get_str_value(speech_config, "languageCode", "language_code") + or self.DEFAULT_LANGUAGE_CODE + ) model_name: Final = model speaker_configs: Final = self._extract_gemini_tts_speaker_configs(voice) if speaker_configs: diff --git a/tests/unit/llms/vertex_ai/text_to_speech/test_transformation.py b/tests/unit/llms/vertex_ai/text_to_speech/test_transformation.py index e85c81807bb..9c31a8a67d2 100644 --- a/tests/unit/llms/vertex_ai/text_to_speech/test_transformation.py +++ b/tests/unit/llms/vertex_ai/text_to_speech/test_transformation.py @@ -190,6 +190,23 @@ class TestVertexAITextToSpeechConfig: assert optional_params["audioEncoding"] == "LINEAR16" assert optional_params["vertex_voice_dict"] == expected_voice + def test_gemini_tts_nested_language_and_voice_alias_reach_cloud_tts(self): + config: Final = VertexAITextToSpeechConfig() + voice: Final = {"speech_config": {"language_code": "fr-FR"}, "voice": "Kore"} + + voice_name, optional_params = config.map_openai_params( + model="gemini-3.1-flash-tts-preview", + optional_params={"response_format": "mp3"}, + voice=voice, + ) + + assert voice_name is None + assert optional_params["vertex_voice_dict"] == { + "languageCode": "fr-FR", + "modelName": "gemini-3.1-flash-tts-preview", + "name": "Kore", + } + @pytest.mark.parametrize( "voice", [ @@ -481,9 +498,7 @@ class TestVertexAILyriaTextToSpeechConfig: ) def test_get_complete_url_encodes_injected_predict_path_segments(self, monkeypatch: pytest.MonkeyPatch) -> None: - injected: Final = ( - "victim-project/locations/us-central1/publishers/google/models/other-model:predict?ignored=" - ) + injected: Final = "victim-project/locations/us-central1/publishers/google/models/other-model:predict?ignored=" encoded: Final = ( "victim-project%2Flocations%2Fus-central1%2Fpublishers%2Fgoogle" "%2Fmodels%2Fother-model%3Apredict%3Fignored%3D"