fix(speech): use the deployment's api_base and timeout for Gemini and Vertex AI TTS

Gemini and Vertex AI Gemini TTS go through the speech-to-completion bridge,
which built its completion call from params that never carried api_base or
timeout. A deployment with a gateway api_base sent /v1/audio/speech straight
to Google, and a caller's timeout fell back to the 600 second default

Both bridge call sites now pass the deployment's api_base and the caller's
timeout to the handler, and None keeps today's behavior
This commit is contained in:
mateo-berri 2026-10-29 00:02:44 -07:00
parent cbf4e40d38
commit 86cd49d476
4 changed files with 94 additions and 1 deletions

View file

@ -9,6 +9,8 @@ from typing_extensions import TypedDict
from litellm._internal_context import internal_sub_call
if TYPE_CHECKING:
import httpx
from litellm import LiteLLMLoggingObj
from litellm.types.llms.openai import HttpxBinaryResponseContent
@ -87,6 +89,8 @@ class SpeechToCompletionBridgeHandler:
headers: dict,
logging_obj: "LiteLLMLoggingObj",
custom_llm_provider: str,
api_base: str | None,
timeout: "float | httpx.Timeout | None",
) -> "HttpxBinaryResponseContent":
received_args: Final = locals()
from litellm import completion
@ -106,7 +110,7 @@ class SpeechToCompletionBridgeHandler:
model=model,
input=input,
optional_params=optional_params,
litellm_params=litellm_params,
litellm_params={**litellm_params, "api_base": api_base, "timeout": timeout},
headers=headers,
litellm_logging_obj=logging_obj,
custom_llm_provider=custom_llm_provider,

View file

@ -8937,6 +8937,8 @@ def speech(
headers=headers or {},
logging_obj=logging_obj,
custom_llm_provider=custom_llm_provider,
api_base=api_base,
timeout=timeout,
)
# Vertex AI Text-to-Speech (Google Cloud TTS)
@ -8988,6 +8990,8 @@ def speech(
headers=headers or {},
logging_obj=logging_obj,
custom_llm_provider=custom_llm_provider,
api_base=api_base,
timeout=timeout,
)
elif custom_llm_provider == "runwayml":
from litellm.llms.runwayml.text_to_speech.transformation import (

View file

@ -1702,6 +1702,30 @@ async def test_aspeech_vertex_ai_gemini_tts_formats_the_bridge_serves_stay_on_ge
assert _PCM_AUDIO in response.content
@pytest.mark.parametrize("custom_llm_provider", ("vertex_ai", "vertex_ai_beta"))
@pytest.mark.asyncio
async def test_aspeech_vertex_ai_gemini_tts_routes_to_configured_api_base(
respx_mock: respx.MockRouter, google_token_endpoint: responses.RequestsMock, custom_llm_provider: str
) -> None:
gateway_route: Final = respx_mock.post(
"https://vertex.gateway.internal/v1/projects/test-project/locations/global"
"/publishers/google/models/gemini-3.8-flash-tts:generateContent"
).mock(return_value=httpx.Response(200, json=_GENERATE_CONTENT_RESPONSE))
response: Final = await litellm.aspeech(
model=f"{custom_llm_provider}/gemini-3.8-flash-tts",
input="Say hello",
voice="Kore",
vertex_credentials=_AUTHORIZED_USER,
vertex_location="global",
api_base="https://vertex.gateway.internal",
)
assert gateway_route.call_count == 1
assert gateway_route.calls.last.request.headers["authorization"] == "Bearer minted-google-token"
assert _PCM_AUDIO in response.content
@pytest.mark.asyncio
async def test_aspeech_vertex_ai_gemini_tts_mp3_reaches_cloud_tts_for_any_gemini_tts_model(
respx_mock: respx.MockRouter, google_token_endpoint: responses.RequestsMock

View file

@ -5454,6 +5454,67 @@ def test_speech_mistral_routes_to_configured_api_base(respx_mock: respx.MockRout
assert response.content == audio_bytes
GEMINI_TTS_MODEL: Final = "gemini-3.8-flash-tts"
GEMINI_GATEWAY_BASE: Final = "https://gemini.gateway.internal/v1beta"
GEMINI_DEFAULT_GENERATE_CONTENT_URL: Final = (
rf"https://generativelanguage\.googleapis\.com/[^/]+/models/{GEMINI_TTS_MODEL}:generateContent"
)
@pytest.mark.parametrize("endpoint_param", ["api_base", "base_url"])
@pytest.mark.parametrize("sync_mode", [True, False])
async def test_speech_gemini_tts_routes_to_configured_api_base(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch, endpoint_param: str, sync_mode: bool
):
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
gateway_route: Final = respx_mock.post(f"{GEMINI_GATEWAY_BASE}/models/{GEMINI_TTS_MODEL}:generateContent").mock(
return_value=httpx.Response(200, json=_gemini_tts_generate_content_response())
)
speech_kwargs: Final = {
"model": f"gemini/{GEMINI_TTS_MODEL}",
"input": "hello from litellm",
"voice": "Kore",
"api_key": "fake-gemini-key",
endpoint_param: GEMINI_GATEWAY_BASE,
}
response: Final = litellm.speech(**speech_kwargs) if sync_mode else await litellm.aspeech(**speech_kwargs)
assert gateway_route.call_count == 1
assert gateway_route.calls.last.request.headers["x-goog-api-key"] == "fake-gemini-key"
assert b"pcm-audio-bytes" in response.content
def test_speech_gemini_tts_applies_the_caller_timeout(respx_mock: respx.MockRouter):
route: Final = respx_mock.post(url__regex=GEMINI_DEFAULT_GENERATE_CONTENT_URL).mock(
return_value=httpx.Response(200, json=_gemini_tts_generate_content_response())
)
litellm.speech(
model=f"gemini/{GEMINI_TTS_MODEL}", input="hello", voice="Kore", api_key="fake-gemini-key", timeout=7.5
)
assert route.calls.last.request.extensions["timeout"] == httpx.Timeout(7.5).as_dict()
def test_speech_gemini_tts_without_a_timeout_waits_as_long_as_the_same_chat_completion(respx_mock: respx.MockRouter):
route: Final = respx_mock.post(url__regex=GEMINI_DEFAULT_GENERATE_CONTENT_URL).mock(
return_value=httpx.Response(200, json=_gemini_tts_generate_content_response())
)
litellm.speech(model=f"gemini/{GEMINI_TTS_MODEL}", input="hello", voice="Kore", api_key="fake-gemini-key")
litellm.completion(
model=f"gemini/{GEMINI_TTS_MODEL}",
messages=[{"role": "user", "content": "hello"}],
modalities=["audio"],
audio={"voice": "Kore", "format": "pcm16"},
api_key="fake-gemini-key",
)
speech_call, chat_call = route.calls
assert speech_call.request.extensions["timeout"] == chat_call.request.extensions["timeout"]
FOUNDRY_HOST: Final = "https://my-project.services.ai.azure.com"