fix(speech): forward the caching flag and drop malformed cache controls on Gemini and Vertex AI TTS

Backport of #50803 to lts/1.108.x.
Cherry-picked from 526a8889fd (litellm_speech_bridge_api_base).
This commit is contained in:
mateo-berri 2026-11-09 20:21:42 -08:00
parent a93452a8be
commit 6e7600bbff
4 changed files with 35 additions and 8 deletions

View file

@ -2,7 +2,6 @@
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
@ -12,6 +11,7 @@ if TYPE_CHECKING:
from litellm import LiteLLMLoggingObj
from litellm.types.llms.openai import HttpxBinaryResponseContent
from litellm.types.utils import CacheControlsHint
class SpeechToCompletionBridgeHandlerInputKwargs(TypedDict):
@ -90,7 +90,7 @@ class SpeechToCompletionBridgeHandler:
custom_llm_provider: str,
api_base: str | None,
timeout: "float | httpx.Timeout | None",
cache: Mapping[str, object] | None,
cache_controls: "CacheControlsHint",
) -> "HttpxBinaryResponseContent":
received_args: Final = locals()
from litellm import completion
@ -110,7 +110,7 @@ class SpeechToCompletionBridgeHandler:
model=model,
input=input,
optional_params=optional_params,
litellm_params={**litellm_params, "api_base": api_base, "timeout": timeout, "cache": cache},
litellm_params={**litellm_params, "api_base": api_base, "timeout": timeout, **cache_controls},
headers=headers,
litellm_logging_obj=logging_obj,
custom_llm_provider=custom_llm_provider,

View file

@ -8755,7 +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"),
cache_controls=_CACHE_CONTROLS_HINT.validate_python(kwargs),
)
# Vertex AI Text-to-Speech (Google Cloud TTS)
@ -8809,7 +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"),
cache_controls=_CACHE_CONTROLS_HINT.validate_python(kwargs),
)
elif custom_llm_provider == "runwayml":
from litellm.llms.runwayml.text_to_speech.transformation import (

View file

@ -40,8 +40,10 @@ from pydantic import (
Field,
FieldSerializationInfo,
JsonValue,
OnErrorOmit,
PrivateAttr,
SkipValidation,
StrictBool,
field_serializer,
field_validator,
)
@ -4871,4 +4873,5 @@ class BaseUrlRoutingHint(TypedDict, total=False):
class CacheControlsHint(TypedDict, total=False):
cache: ReadOnly[Mapping[str, object] | None]
cache: ReadOnly[OnErrorOmit[Mapping[str, object] | None]]
caching: ReadOnly[OnErrorOmit[StrictBool | None]]

View file

@ -4844,13 +4844,14 @@ def test_speech_gemini_tts_without_a_timeout_waits_as_long_as_the_same_chat_comp
({}, {}, 1),
({}, {"cache": {"no-cache": True}}, 2),
({"cache": {"no-store": True}}, {}, 2),
({}, {"caching": False}, 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]],
first_controls: Mapping[str, object],
second_controls: Mapping[str, object],
expected_upstream_calls: int,
):
monkeypatch.setattr(litellm, "cache", Cache(type="local"))
@ -4870,6 +4871,29 @@ def test_speech_gemini_tts_honors_the_request_cache_controls(
assert route.call_count == expected_upstream_calls
@pytest.mark.parametrize("malformed_controls", [{"caching": "yes"}, {"caching": 1}, {"cache": {1: True}}])
def test_speech_gemini_tts_without_a_cache_ignores_malformed_cache_controls(
respx_mock: respx.MockRouter,
monkeypatch: pytest.MonkeyPatch,
malformed_controls: Mapping[str, object],
):
monkeypatch.setattr(litellm, "cache", None)
route: Final = respx_mock.post(url__regex=GEMINI_DEFAULT_GENERATE_CONTENT_URL).mock(
return_value=httpx.Response(200, json=_gemini_tts_generate_content_response())
)
response: Final = litellm.speech(
model=f"gemini/{GEMINI_TTS_MODEL}",
input="hello",
voice="Kore",
api_key="fake-gemini-key",
**malformed_controls,
)
assert route.call_count == 1
assert response.content.startswith(b"RIFF")
FOUNDRY_HOST: Final = "https://my-project.services.ai.azure.com"