diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index c4fd9078dda..cac98836d9d 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -193,12 +193,24 @@ def _normalize_gemini_speech_config_item(value: object) -> object: def normalize_gemini_speech_config( value: Mapping[str, object], ) -> dict[str, object]: # mutable-ok: provider request serialization requires a concrete dict - normalized_value: Final = _normalize_gemini_speech_config_item(value) - if isinstance(normalized_value, dict): - return cast( # cast-ok: the runtime dict check establishes the normalizer's concrete result shape - dict[str, object], normalized_value - ) - return {} # mutable-ok: provider request serialization requires a concrete empty dict + nested_config: Final = value.get("speechConfig", value.get("speech_config")) + config: Final = nested_config if isinstance(nested_config, Mapping) else value + normalized_value: Final = _normalize_gemini_speech_config_item(config) + if not isinstance(normalized_value, dict): + return {} # mutable-ok: provider request serialization requires a concrete empty dict + normalized: Final = cast( # cast-ok: runtime dict check establishes the normalizer's concrete result shape + dict[str, object], normalized_value + ) + voice_name: Final = next( + (normalized[key] for key in ("name", "voiceName", "voice") if isinstance(normalized.get(key), str)), + None, + ) + if voice_name is None or "voiceConfig" in normalized or "multiSpeakerVoiceConfig" in normalized: + return normalized + return { # mutable-ok: provider request serialization requires a concrete dict + **{key: item for key, item in normalized.items() if key not in ("name", "voiceName", "voice", "modelName")}, + "voiceConfig": {"prebuiltVoiceConfig": {"voiceName": voice_name}}, + } class VertexAIBaseConfig: diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index b444261b485..bc4e172556f 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -65812,10 +65812,46 @@ "output_cost_per_token": 2e-06, "source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing" }, + "vertex_ai/gemini-2.5-flash-tts": { + "input_cost_per_token": 5e-07, + "litellm_provider": "vertex_ai-language-models", + "mode": "audio_speech", + "output_cost_per_audio_token": 9.999999999999999e-06, + "output_cost_per_token": 9.999999999999999e-06, + "source": "https://cloud.google.com/text-to-speech/pricing", + "supported_endpoints": [ + "/v1/audio/speech" + ], + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "audio" + ], + "supports_audio_output": true + }, + "vertex_ai/gemini-2.5-pro-tts": { + "input_cost_per_token": 1e-06, + "litellm_provider": "vertex_ai-language-models", + "mode": "audio_speech", + "output_cost_per_audio_token": 1.9999999999999998e-05, + "output_cost_per_token": 1.9999999999999998e-05, + "source": "https://cloud.google.com/text-to-speech/pricing", + "supported_endpoints": [ + "/v1/audio/speech" + ], + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "audio" + ], + "supports_audio_output": true + }, "vertex_ai/gemini-2.5-flash-preview-tts": { "input_cost_per_token": 5e-07, "input_cost_per_token_batches": 2.5e-07, - "litellm_provider": "vertex_ai", + "litellm_provider": "vertex_ai-language-models", "mode": "audio_speech", "output_cost_per_audio_token": 1e-05, "output_cost_per_token": 1e-05, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index b444261b485..bc4e172556f 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -65812,10 +65812,46 @@ "output_cost_per_token": 2e-06, "source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing" }, + "vertex_ai/gemini-2.5-flash-tts": { + "input_cost_per_token": 5e-07, + "litellm_provider": "vertex_ai-language-models", + "mode": "audio_speech", + "output_cost_per_audio_token": 9.999999999999999e-06, + "output_cost_per_token": 9.999999999999999e-06, + "source": "https://cloud.google.com/text-to-speech/pricing", + "supported_endpoints": [ + "/v1/audio/speech" + ], + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "audio" + ], + "supports_audio_output": true + }, + "vertex_ai/gemini-2.5-pro-tts": { + "input_cost_per_token": 1e-06, + "litellm_provider": "vertex_ai-language-models", + "mode": "audio_speech", + "output_cost_per_audio_token": 1.9999999999999998e-05, + "output_cost_per_token": 1.9999999999999998e-05, + "source": "https://cloud.google.com/text-to-speech/pricing", + "supported_endpoints": [ + "/v1/audio/speech" + ], + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "audio" + ], + "supports_audio_output": true + }, "vertex_ai/gemini-2.5-flash-preview-tts": { "input_cost_per_token": 5e-07, "input_cost_per_token_batches": 2.5e-07, - "litellm_provider": "vertex_ai", + "litellm_provider": "vertex_ai-language-models", "mode": "audio_speech", "output_cost_per_audio_token": 1e-05, "output_cost_per_token": 1e-05, diff --git a/tests/test_litellm/llms/gemini/test_gemini_tts.py b/tests/test_litellm/llms/gemini/test_gemini_tts.py index b22372b2f8c..ae6f7a3abd8 100644 --- a/tests/test_litellm/llms/gemini/test_gemini_tts.py +++ b/tests/test_litellm/llms/gemini/test_gemini_tts.py @@ -527,3 +527,28 @@ class TestGeminiTTSSpeechConfigInRequestBody: if __name__ == "__main__": pytest.main([__file__]) + + +@pytest.mark.parametrize("voice_key", ["name", "voiceName", "voice_name", "voice"]) +@pytest.mark.parametrize("wrapper", [None, "speechConfig", "speech_config"]) +def test_speech_bridge_preserves_single_speaker_voice_mapping(voice_key: str, wrapper: str | None): + handler = SpeechToCompletionBridgeTransformationHandler() + config = {voice_key: "Umbriel", "language_code": "en-US"} + voice = {wrapper: config} if wrapper else config + result = handler.transform_request( + model=f"vertex_ai/{GEMINI_3_1_FLASH_TTS_MODEL}", + input="Hello", + voice=voice, + optional_params={}, + litellm_params={}, + headers={}, + litellm_logging_obj=MagicMock(), + custom_llm_provider="vertex_ai", + ) + assert result["audio"] == { + "speech_config": { + "voiceConfig": {"prebuiltVoiceConfig": {"voiceName": "Umbriel"}}, + "languageCode": "en-US", + }, + "format": "pcm16", + } diff --git a/tests/test_litellm/test_gemini_3_1_flash_tts_metadata.py b/tests/test_litellm/test_gemini_3_1_flash_tts_metadata.py index 9a95bb750b4..676b116790f 100644 --- a/tests/test_litellm/test_gemini_3_1_flash_tts_metadata.py +++ b/tests/test_litellm/test_gemini_3_1_flash_tts_metadata.py @@ -72,3 +72,14 @@ def test_gemini_tts_detection_uses_model_metadata(monkeypatch): _invalidate_model_cost_lowercase_map() assert is_gemini_tts_model(model) + + +@pytest.mark.parametrize("model", ["gemini-2.5-flash-preview-tts", "gemini-2.5-flash-tts", "gemini-2.5-pro-tts"]) +@pytest.mark.parametrize("prefixed", [False, True]) +def test_existing_vertex_tts_models_keep_gemini_dispatch(model: str, prefixed: bool, monkeypatch): + model_cost = _load_model_cost_map(Path(__file__).parents[2] / "model_prices_and_context_window.json") + monkeypatch.setattr(litellm, "model_cost", model_cost) + litellm.get_model_info.cache_clear() + _invalidate_model_cost_lowercase_map() + requested_model = f"vertex_ai/{model}" if prefixed else model + assert is_gemini_tts_model(requested_model, custom_llm_provider="vertex_ai")