mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(speech): send the deployment's extra_headers and chat's caching rule through the TTS bridge
Gemini and Vertex AI speech now forward the deployment's extra_headers to the inner chat call, pass caching as sent so chat's own rule decides, and keep the router's deployment timeout so speech waits as long as chat
This commit is contained in:
parent
d447b89bed
commit
33584bcc0d
5 changed files with 57 additions and 5 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
Handler for transforming /chat/completions api requests to litellm.responses requests
|
Handler for transforming /chat/completions api requests to litellm.responses requests
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
from typing import TYPE_CHECKING, Final
|
from typing import TYPE_CHECKING, Final
|
||||||
|
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
@ -91,6 +92,7 @@ class SpeechToCompletionBridgeHandler:
|
||||||
logging_obj: "LiteLLMLoggingObj",
|
logging_obj: "LiteLLMLoggingObj",
|
||||||
custom_llm_provider: str,
|
custom_llm_provider: str,
|
||||||
api_base: str | None,
|
api_base: str | None,
|
||||||
|
extra_headers: Mapping[str, str] | None,
|
||||||
timeout: "float | httpx.Timeout | None",
|
timeout: "float | httpx.Timeout | None",
|
||||||
cache_controls: "CacheControlsHint",
|
cache_controls: "CacheControlsHint",
|
||||||
) -> "HttpxBinaryResponseContent":
|
) -> "HttpxBinaryResponseContent":
|
||||||
|
|
@ -112,7 +114,13 @@ class SpeechToCompletionBridgeHandler:
|
||||||
model=model,
|
model=model,
|
||||||
input=input,
|
input=input,
|
||||||
optional_params=optional_params,
|
optional_params=optional_params,
|
||||||
litellm_params={**litellm_params, "api_base": api_base, "timeout": timeout, **cache_controls},
|
litellm_params={
|
||||||
|
**litellm_params,
|
||||||
|
"api_base": api_base,
|
||||||
|
"extra_headers": extra_headers,
|
||||||
|
"timeout": timeout,
|
||||||
|
**cache_controls,
|
||||||
|
},
|
||||||
headers=headers,
|
headers=headers,
|
||||||
litellm_logging_obj=logging_obj,
|
litellm_logging_obj=logging_obj,
|
||||||
custom_llm_provider=custom_llm_provider,
|
custom_llm_provider=custom_llm_provider,
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@ from litellm.types.utils import (
|
||||||
AgenticLoopParams,
|
AgenticLoopParams,
|
||||||
BaseUrlRoutingHint,
|
BaseUrlRoutingHint,
|
||||||
CacheControlsHint,
|
CacheControlsHint,
|
||||||
|
DeploymentHeadersHint,
|
||||||
ModelResponseStream,
|
ModelResponseStream,
|
||||||
ProviderRoutingHints,
|
ProviderRoutingHints,
|
||||||
RawRequestTypedDict,
|
RawRequestTypedDict,
|
||||||
|
|
@ -4362,6 +4363,7 @@ _REQUEST_KWARGS_ADAPTER: Final = TypeAdapter(Mapping[str, object])
|
||||||
_PROVIDER_ROUTING_HINTS: Final = TypeAdapter(ProviderRoutingHints)
|
_PROVIDER_ROUTING_HINTS: Final = TypeAdapter(ProviderRoutingHints)
|
||||||
_BASE_URL_HINT: Final = TypeAdapter(BaseUrlRoutingHint)
|
_BASE_URL_HINT: Final = TypeAdapter(BaseUrlRoutingHint)
|
||||||
_CACHE_CONTROLS_HINT: Final = TypeAdapter(CacheControlsHint)
|
_CACHE_CONTROLS_HINT: Final = TypeAdapter(CacheControlsHint)
|
||||||
|
_DEPLOYMENT_HEADERS_HINT: Final = TypeAdapter(DeploymentHeadersHint)
|
||||||
_OPAQUE_VALUE_ADAPTER: Final = TypeAdapter(object)
|
_OPAQUE_VALUE_ADAPTER: Final = TypeAdapter(object)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -9237,6 +9239,7 @@ def speech(
|
||||||
logging_obj=logging_obj,
|
logging_obj=logging_obj,
|
||||||
custom_llm_provider=custom_llm_provider,
|
custom_llm_provider=custom_llm_provider,
|
||||||
api_base=api_base,
|
api_base=api_base,
|
||||||
|
extra_headers=_DEPLOYMENT_HEADERS_HINT.validate_python(kwargs).get("extra_headers"),
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
cache_controls=_CACHE_CONTROLS_HINT.validate_python(kwargs),
|
cache_controls=_CACHE_CONTROLS_HINT.validate_python(kwargs),
|
||||||
)
|
)
|
||||||
|
|
@ -9291,6 +9294,7 @@ def speech(
|
||||||
logging_obj=logging_obj,
|
logging_obj=logging_obj,
|
||||||
custom_llm_provider=custom_llm_provider,
|
custom_llm_provider=custom_llm_provider,
|
||||||
api_base=api_base,
|
api_base=api_base,
|
||||||
|
extra_headers=_DEPLOYMENT_HEADERS_HINT.validate_python(kwargs).get("extra_headers"),
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
cache_controls=_CACHE_CONTROLS_HINT.validate_python(kwargs),
|
cache_controls=_CACHE_CONTROLS_HINT.validate_python(kwargs),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ from pydantic import (
|
||||||
OnErrorOmit,
|
OnErrorOmit,
|
||||||
PrivateAttr,
|
PrivateAttr,
|
||||||
SkipValidation,
|
SkipValidation,
|
||||||
StrictBool,
|
|
||||||
TypeAdapter,
|
TypeAdapter,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
field_serializer,
|
field_serializer,
|
||||||
|
|
@ -5402,4 +5401,8 @@ class BaseUrlRoutingHint(TypedDict, total=False):
|
||||||
|
|
||||||
class CacheControlsHint(TypedDict, total=False):
|
class CacheControlsHint(TypedDict, total=False):
|
||||||
cache: ReadOnly[OnErrorOmit[Mapping[str, object] | None]]
|
cache: ReadOnly[OnErrorOmit[Mapping[str, object] | None]]
|
||||||
caching: ReadOnly[OnErrorOmit[StrictBool | None]]
|
caching: ReadOnly[OnErrorOmit[JsonValue]]
|
||||||
|
|
||||||
|
|
||||||
|
class DeploymentHeadersHint(TypedDict, total=False):
|
||||||
|
extra_headers: ReadOnly[OnErrorOmit[Mapping[str, str] | None]]
|
||||||
|
|
|
||||||
|
|
@ -1774,10 +1774,12 @@ async def test_aspeech_vertex_ai_gemini_tts_routes_to_configured_api_base(
|
||||||
vertex_credentials=_AUTHORIZED_USER,
|
vertex_credentials=_AUTHORIZED_USER,
|
||||||
vertex_location="global",
|
vertex_location="global",
|
||||||
api_base="https://vertex.gateway.internal",
|
api_base="https://vertex.gateway.internal",
|
||||||
|
extra_headers={"x-gateway-key": "gateway-secret"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert gateway_route.call_count == 1
|
assert gateway_route.call_count == 1
|
||||||
assert gateway_route.calls.last.request.headers["authorization"] == "Bearer minted-google-token"
|
assert gateway_route.calls.last.request.headers["authorization"] == "Bearer minted-google-token"
|
||||||
|
assert gateway_route.calls.last.request.headers["x-gateway-key"] == "gateway-secret"
|
||||||
assert _PCM_AUDIO in response.content
|
assert _PCM_AUDIO in response.content
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6221,6 +6221,7 @@ async def test_speech_gemini_tts_routes_to_configured_api_base(
|
||||||
"input": "hello from litellm",
|
"input": "hello from litellm",
|
||||||
"voice": "Kore",
|
"voice": "Kore",
|
||||||
"api_key": "fake-gemini-key",
|
"api_key": "fake-gemini-key",
|
||||||
|
"extra_headers": {"x-gateway-key": "gateway-secret"},
|
||||||
endpoint_param: GEMINI_GATEWAY_BASE,
|
endpoint_param: GEMINI_GATEWAY_BASE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6228,6 +6229,7 @@ async def test_speech_gemini_tts_routes_to_configured_api_base(
|
||||||
|
|
||||||
assert gateway_route.call_count == 1
|
assert gateway_route.call_count == 1
|
||||||
assert gateway_route.calls.last.request.headers["x-goog-api-key"] == "fake-gemini-key"
|
assert gateway_route.calls.last.request.headers["x-goog-api-key"] == "fake-gemini-key"
|
||||||
|
assert gateway_route.calls.last.request.headers["x-gateway-key"] == "gateway-secret"
|
||||||
assert b"pcm-audio-bytes" in response.content
|
assert b"pcm-audio-bytes" in response.content
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -6261,6 +6263,34 @@ 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"]
|
assert speech_call.request.extensions["timeout"] == chat_call.request.extensions["timeout"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_speech_gemini_tts_behind_the_router_waits_as_long_as_the_same_chat_completion(
|
||||||
|
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
|
||||||
|
):
|
||||||
|
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||||
|
route: Final = respx_mock.post(url__regex=GEMINI_DEFAULT_GENERATE_CONTENT_URL).mock(
|
||||||
|
return_value=httpx.Response(200, json=_gemini_tts_generate_content_response())
|
||||||
|
)
|
||||||
|
router: Final = litellm.Router(
|
||||||
|
model_list=[
|
||||||
|
{
|
||||||
|
"model_name": "gemini-tts",
|
||||||
|
"litellm_params": {"model": f"gemini/{GEMINI_TTS_MODEL}", "api_key": "fake-gemini-key"},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
await router.aspeech(model="gemini-tts", input="hello", voice="Kore")
|
||||||
|
await router.acompletion(
|
||||||
|
model="gemini-tts",
|
||||||
|
messages=[{"role": "user", "content": "hello"}],
|
||||||
|
modalities=["audio"],
|
||||||
|
audio={"voice": "Kore", "format": "pcm16"},
|
||||||
|
)
|
||||||
|
|
||||||
|
speech_call, chat_call = route.calls
|
||||||
|
assert speech_call.request.extensions["timeout"] == chat_call.request.extensions["timeout"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("first_controls", "second_controls", "expected_upstream_calls"),
|
("first_controls", "second_controls", "expected_upstream_calls"),
|
||||||
[
|
[
|
||||||
|
|
@ -6268,6 +6298,8 @@ def test_speech_gemini_tts_without_a_timeout_waits_as_long_as_the_same_chat_comp
|
||||||
({}, {"cache": {"no-cache": True}}, 2),
|
({}, {"cache": {"no-cache": True}}, 2),
|
||||||
({"cache": {"no-store": True}}, {}, 2),
|
({"cache": {"no-store": True}}, {}, 2),
|
||||||
({}, {"caching": False}, 2),
|
({}, {"caching": False}, 2),
|
||||||
|
({}, {"caching": "yes"}, 2),
|
||||||
|
({}, {"caching": 0}, 2),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_speech_gemini_tts_honors_the_request_cache_controls(
|
def test_speech_gemini_tts_honors_the_request_cache_controls(
|
||||||
|
|
@ -6294,8 +6326,11 @@ def test_speech_gemini_tts_honors_the_request_cache_controls(
|
||||||
assert route.call_count == expected_upstream_calls
|
assert route.call_count == expected_upstream_calls
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("malformed_controls", [{"caching": "yes"}, {"caching": 1}, {"cache": {1: True}}])
|
@pytest.mark.parametrize(
|
||||||
def test_speech_gemini_tts_without_a_cache_ignores_malformed_cache_controls(
|
"malformed_controls",
|
||||||
|
[{"caching": "yes"}, {"caching": 1}, {"cache": {1: True}}, {"extra_headers": {"x-gateway-key": 5}}],
|
||||||
|
)
|
||||||
|
def test_speech_gemini_tts_without_a_cache_ignores_malformed_controls(
|
||||||
respx_mock: respx.MockRouter,
|
respx_mock: respx.MockRouter,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
malformed_controls: Mapping[str, object],
|
malformed_controls: Mapping[str, object],
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue