fix(vertex-ai): preserve existing Gemini TTS models and voice mappings

This commit is contained in:
Emerson Gomes 2026-09-15 13:00:42 -05:00
parent f67e9794c3
commit c3baeed801
No known key found for this signature in database
GPG key ID: D3DF28AB5D1B5E17
5 changed files with 128 additions and 8 deletions

View file

@ -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:

View file

@ -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,

View file

@ -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,

View file

@ -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",
}

View file

@ -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")