diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index d90f7bd4514..df5365017ac 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -25,7 +25,10 @@ from litellm.litellm_core_utils.agentic_loop_settings import ( validated_max_agentic_loops, ) from litellm.litellm_core_utils.asyncify import run_async_function -from litellm.litellm_core_utils.audio_utils.subtitle_utils import synthesize_subtitle_document +from litellm.litellm_core_utils.audio_utils.subtitle_utils import ( + SUBTITLE_RESPONSE_FORMATS, + synthesize_subtitle_document, +) from litellm.litellm_core_utils.llm_request_utils import serialize_multipart_form_fields from litellm.litellm_core_utils.realtime_errors import realtime_error_event, websocket_close_reason from litellm.litellm_core_utils.realtime_streaming import RealTimeStreaming @@ -1303,16 +1306,16 @@ class BaseLLMHTTPHandler: if not provider_config.supports_subtitle_synthesis: return transformed requested_format: Final = optional_params.get("response_format") - if not isinstance(requested_format, str): + if not isinstance(requested_format, str) or requested_format not in SUBTITLE_RESPONSE_FORMATS: return transformed document: Final = synthesize_subtitle_document( words=transformed.get("words"), response_format=requested_format, ) - if document is None: - return transformed - transformed.text = document - delattr(transformed, "words") + if document is not None: + transformed.text = document + if "words" in transformed: + delattr(transformed, "words") return transformed def audio_transcriptions( diff --git a/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py b/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py index 4eaa18b5aa9..b37c0f466d2 100644 --- a/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py +++ b/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py @@ -1930,6 +1930,47 @@ def test_transform_audio_transcription_response_without_subtitle_opt_in_keeps_te assert response["words"] == words +class _SubtitleSynthesisAudioTranscriptionConfig(_JSONBodyAudioTranscriptionConfig): + @property + def supports_subtitle_synthesis(self) -> bool: + return True + + def transform_audio_transcription_response(self, raw_response): + payload = raw_response.json() + response = TranscriptionResponse(text=payload["text"]) + if "words" in payload: + response["words"] = payload["words"] + return response + + +def _transform_subtitle_response(payload): + return BaseLLMHTTPHandler()._transform_audio_transcription_response( + provider_config=_SubtitleSynthesisAudioTranscriptionConfig(), + model="test-model", + response=httpx.Response(200, json=payload), + model_response=TranscriptionResponse(), + logging_obj=Mock(), + optional_params={"response_format": "srt"}, + api_key=None, + ) + + +def test_subtitle_synthesis_fallback_without_timings_drops_words(): + response = _transform_subtitle_response( + {"text": "hello world", "words": [{"word": "hello"}, {"word": "world"}]} + ) + + assert response.text == "hello world" + assert "words" not in response + + +def test_subtitle_synthesis_without_words_keeps_plain_text(): + response = _transform_subtitle_response({"text": "hello world"}) + + assert response.text == "hello world" + assert "words" not in response + + @pytest.mark.asyncio async def test_async_retrieve_file_content_raises_on_http_error(): """