fix(speech): honor request cache controls on Gemini and Vertex AI TTS

Backport of #50803 to lts/1.108.x.
Cherry-picked from fdc7648f43 (litellm_speech_bridge_api_base).
This commit is contained in:
mateo-berri 2026-11-09 20:21:31 -08:00
parent fa4baa139f
commit a93452a8be
6 changed files with 67 additions and 3 deletions

View file

@ -2,6 +2,7 @@
Handler for transforming /chat/completions api requests to litellm.responses requests
"""
from collections.abc import Mapping
from typing import TYPE_CHECKING, Final
from typing_extensions import TypedDict
@ -89,6 +90,7 @@ class SpeechToCompletionBridgeHandler:
custom_llm_provider: str,
api_base: str | None,
timeout: "float | httpx.Timeout | None",
cache: Mapping[str, object] | None,
) -> "HttpxBinaryResponseContent":
received_args: Final = locals()
from litellm import completion
@ -108,7 +110,7 @@ class SpeechToCompletionBridgeHandler:
model=model,
input=input,
optional_params=optional_params,
litellm_params={**litellm_params, "api_base": api_base, "timeout": timeout},
litellm_params={**litellm_params, "api_base": api_base, "timeout": timeout, "cache": cache},
headers=headers,
litellm_logging_obj=logging_obj,
custom_llm_provider=custom_llm_provider,

View file

@ -147,6 +147,7 @@ from litellm.types.llms.openai import AllMessageValues
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import (
BaseUrlRoutingHint,
CacheControlsHint,
CustomPricingLiteLLMParams,
ModelResponseStream,
ProviderRoutingHints,
@ -4137,6 +4138,7 @@ _REQUEST_KWARGS_ADAPTER: Final = TypeAdapter(Mapping[str, object])
_MODEL_INFO_ADAPTER: Final = TypeAdapter(Mapping[str, object])
_PROVIDER_ROUTING_HINTS: Final = TypeAdapter(ProviderRoutingHints)
_BASE_URL_HINT: Final = TypeAdapter(BaseUrlRoutingHint)
_CACHE_CONTROLS_HINT: Final = TypeAdapter(CacheControlsHint)
_AWS_BEDROCK_CLIENT_CREDENTIAL_KEYS: Final = (
@ -8753,6 +8755,7 @@ def speech(
custom_llm_provider=custom_llm_provider,
api_base=api_base,
timeout=timeout,
cache=_CACHE_CONTROLS_HINT.validate_python(kwargs).get("cache"),
)
# Vertex AI Text-to-Speech (Google Cloud TTS)
@ -8806,6 +8809,7 @@ def speech(
custom_llm_provider=custom_llm_provider,
api_base=api_base,
timeout=timeout,
cache=_CACHE_CONTROLS_HINT.validate_python(kwargs).get("cache"),
)
elif custom_llm_provider == "runwayml":
from litellm.llms.runwayml.text_to_speech.transformation import (

View file

@ -4868,3 +4868,7 @@ class ProviderRoutingHints(TypedDict, total=False):
class BaseUrlRoutingHint(TypedDict, total=False):
base_url: ReadOnly[str | None]
class CacheControlsHint(TypedDict, total=False):
cache: ReadOnly[Mapping[str, object] | None]

View file

@ -1,7 +1,6 @@
"""The speech route, the audio_speech health check and deferred Vertex Live setups map OpenAI voices.
The speech bridge calls the Gemini chat path without the deployment's ``api_base`` (tracked separately),
so these rows boot their own proxy with ``GEMINI_API_BASE`` pointed at the scripted peer, and with
These rows boot their own proxy with ``GEMINI_API_BASE`` pointed at the scripted peer, and with
``LITELLM_GEMINI_LIVE_DEFER_SETUP`` so a client ``session.update`` voice builds the Vertex Live setup.
"""

View file

@ -11,6 +11,7 @@ import responses
from pydantic import ValidationError
import litellm
from litellm.caching.caching import Cache
from litellm.litellm_core_utils.exception_mapping_utils import exception_type
from litellm.llms.vertex_ai.text_to_speech.transformation import (
VertexAILyriaTextToSpeechConfig,
@ -1144,3 +1145,25 @@ async def test_aspeech_vertex_ai_gemini_tts_routes_to_configured_api_base(
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_no_cache_request_reaches_vertex(
respx_mock: respx.MockRouter, google_token_endpoint: responses.RequestsMock, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "cache", Cache(type="local"))
vertex_route: Final = respx_mock.post(
url__regex=r"https://aiplatform\.googleapis\.com/.*/models/gemini-3\.8-flash-tts:generateContent$"
).mock(return_value=httpx.Response(200, json=_GENERATE_CONTENT_RESPONSE))
speech_kwargs: Final = {
"model": "vertex_ai/gemini-3.8-flash-tts",
"input": "Say hello",
"voice": "Kore",
"vertex_credentials": _AUTHORIZED_USER,
"vertex_location": "global",
}
await litellm.aspeech(**speech_kwargs)
await litellm.aspeech(**speech_kwargs, cache={"no-cache": True})
assert vertex_route.call_count == 2

View file

@ -4838,6 +4838,38 @@ def test_speech_gemini_tts_without_a_timeout_waits_as_long_as_the_same_chat_comp
assert speech_call.request.extensions["timeout"] == chat_call.request.extensions["timeout"]
@pytest.mark.parametrize(
("first_controls", "second_controls", "expected_upstream_calls"),
[
({}, {}, 1),
({}, {"cache": {"no-cache": True}}, 2),
({"cache": {"no-store": True}}, {}, 2),
],
)
def test_speech_gemini_tts_honors_the_request_cache_controls(
respx_mock: respx.MockRouter,
monkeypatch: pytest.MonkeyPatch,
first_controls: Mapping[str, Mapping[str, bool]],
second_controls: Mapping[str, Mapping[str, bool]],
expected_upstream_calls: int,
):
monkeypatch.setattr(litellm, "cache", Cache(type="local"))
route: Final = respx_mock.post(url__regex=GEMINI_DEFAULT_GENERATE_CONTENT_URL).mock(
return_value=httpx.Response(200, json=_gemini_tts_generate_content_response())
)
speech_kwargs: Final = {
"model": f"gemini/{GEMINI_TTS_MODEL}",
"input": "hello",
"voice": "Kore",
"api_key": "fake-gemini-key",
}
litellm.speech(**speech_kwargs, **first_controls)
litellm.speech(**speech_kwargs, **second_controls)
assert route.call_count == expected_upstream_calls
FOUNDRY_HOST: Final = "https://my-project.services.ai.azure.com"