diff --git a/litellm/endpoints/speech/speech_to_completion_bridge/handler.py b/litellm/endpoints/speech/speech_to_completion_bridge/handler.py index 8b6e6a84045..cfda9049584 100644 --- a/litellm/endpoints/speech/speech_to_completion_bridge/handler.py +++ b/litellm/endpoints/speech/speech_to_completion_bridge/handler.py @@ -2,6 +2,7 @@ Handler for transforming /chat/completions api requests to litellm.responses requests """ +import json from collections.abc import Mapping from typing import TYPE_CHECKING, Final @@ -15,6 +16,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 @@ -115,7 +126,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, }, diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 4126c5c30df..d91c37f5fc0 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -4956,33 +4956,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", + ) FOUNDRY_HOST: Final = "https://my-project.services.ai.azure.com"