fix(speech): send a number or boolean deployment header as its config text through the TTS bridge

A header value has to be text, and the merge base never sent deployment headers, so failing like chat turned a TTS deployment with a number in extra_headers from 200 into 500. Null, list and map values still fail like chat
This commit is contained in:
mateo-berri 2026-11-10 02:45:57 -08:00
parent 9dafd85341
commit 2519b429ea
2 changed files with 29 additions and 23 deletions

View file

@ -2,6 +2,7 @@
Handler for transforming /chat/completions api requests to litellm.responses requests
"""
import json
from collections.abc import Coroutine, Mapping
from typing import TYPE_CHECKING, Final
@ -17,6 +18,16 @@ if TYPE_CHECKING:
from litellm.types.utils import CacheControlsHint
def _header_text(value: object) -> object:
return json.dumps(value) if isinstance(value, bool | int | float) else value
def _headers_as_text(extra_headers: Mapping[str, object] | None) -> Mapping[str, object] | None:
if extra_headers is None:
return None
return {name: _header_text(value) for name, value in extra_headers.items()}
class SpeechToCompletionBridgeHandlerInputKwargs(TypedDict):
model: str
input: str
@ -117,7 +128,7 @@ class SpeechToCompletionBridgeHandler:
litellm_params={
**litellm_params,
"api_base": api_base,
"extra_headers": extra_headers,
"extra_headers": _headers_as_text(extra_headers),
"timeout": timeout,
**cache_controls,
},

View file

@ -6379,33 +6379,28 @@ def test_speech_gemini_tts_keeps_cached_audio_only_for_the_request_ttl(
assert route.call_count == 2
def test_speech_gemini_tts_fails_like_chat_on_a_non_string_deployment_header(respx_mock: respx.MockRouter):
def test_speech_gemini_tts_sends_number_and_boolean_deployment_headers_as_their_config_text(
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())
)
deployment_headers: Final = {"x-gateway-key": "gateway-secret", "x-org-id": 12345}
with pytest.raises(litellm.APIConnectionError) as chat_error:
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",
extra_headers=deployment_headers,
)
with pytest.raises(litellm.APIConnectionError) as speech_error:
litellm.speech(
model=f"gemini/{GEMINI_TTS_MODEL}",
input="hello",
voice="Kore",
api_key="fake-gemini-key",
extra_headers=deployment_headers,
)
litellm.speech(
model=f"gemini/{GEMINI_TTS_MODEL}",
input="hello",
voice="Kore",
api_key="fake-gemini-key",
extra_headers={"x-gateway-key": "gateway-secret", "x-org-id": 12345, "x-ratio": 1.5, "x-audit": True},
)
assert "Header value must be str or bytes" in str(chat_error.value)
assert "Header value must be str or bytes" in str(speech_error.value)
assert route.call_count == 0
sent: Final = route.calls.last.request.headers
assert (sent["x-gateway-key"], sent["x-org-id"], sent["x-ratio"], sent["x-audit"]) == (
"gateway-secret",
"12345",
"1.5",
"true",
)
async def test_aspeech_gemini_tts_waits_for_a_stalled_upstream_without_holding_a_worker_thread(