diff --git a/litellm/llms/soniox/audio_transcription/handler.py b/litellm/llms/soniox/audio_transcription/handler.py index 1725132829e..cfb3ec75311 100644 --- a/litellm/llms/soniox/audio_transcription/handler.py +++ b/litellm/llms/soniox/audio_transcription/handler.py @@ -39,7 +39,9 @@ from litellm.llms.custom_httpx.http_handler import ( from litellm.llms.soniox.audio_transcription.transformation import ( SONIOX_HANDLER_ONLY_PARAMS, SonioxAudioTranscriptionConfig, + SonioxInvalidBoolParam, decode_soniox_form_params, + raise_soniox_form_error, ) from litellm.llms.soniox.common_utils import ( SONIOX_DEFAULT_CLEANUP, @@ -228,6 +230,8 @@ class SonioxAudioTranscriptionHandler: base_url: Final = get_soniox_api_base(api_base) decoded: Final = decode_soniox_form_params(optional_params) + if isinstance(decoded, SonioxInvalidBoolParam): + raise_soniox_form_error(decoded) # Server-side clamps. Caller-supplied poll settings (from request kwargs) # are bounded so an authenticated caller cannot force a worker into a diff --git a/litellm/llms/soniox/audio_transcription/transformation.py b/litellm/llms/soniox/audio_transcription/transformation.py index 9f224944073..dbe5d1cd746 100644 --- a/litellm/llms/soniox/audio_transcription/transformation.py +++ b/litellm/llms/soniox/audio_transcription/transformation.py @@ -10,8 +10,9 @@ contract of `base_llm_http_handler.audio_transcriptions`. """ from collections.abc import Mapping +from dataclasses import dataclass from types import MappingProxyType -from typing import Any, Final +from typing import Any, Final, NoReturn from httpx import Headers, Response from pydantic import TypeAdapter, ValidationError @@ -68,7 +69,19 @@ SONIOX_BOOL_PARAMS: Final[frozenset[str]] = frozenset( _JSON_CONTAINER: Final = TypeAdapter[dict[str, object] | list[object]](dict[str, object] | list[object]) -def _decode_form_value(key: str, value: object) -> object: +@dataclass(frozen=True, slots=True) +class SonioxInvalidBoolParam: + key: str + value: str + + +def raise_soniox_form_error(error: SonioxInvalidBoolParam) -> NoReturn: + raise SonioxException( + message=f"`{error.key}` must be a boolean, got {error.value!r}", status_code=400, headers=None + ) + + +def _decode_form_value(key: str, value: object) -> object | SonioxInvalidBoolParam: if not isinstance(value, str): return value if key in SONIOX_BOOL_PARAMS: @@ -77,7 +90,7 @@ def _decode_form_value(key: str, value: object) -> object: return True if lowered in ("false", "0"): return False - raise SonioxException(message=f"`{key}` must be a boolean, got {value!r}", status_code=400, headers=None) + return SonioxInvalidBoolParam(key=key, value=value) if key in SONIOX_JSON_PARAMS and value.lstrip()[:1] in ("{", "["): try: return _JSON_CONTAINER.validate_json(value) @@ -88,9 +101,15 @@ def _decode_form_value(key: str, value: object) -> object: return value -def decode_soniox_form_params(optional_params: Mapping[str, object]) -> Mapping[str, object]: +def decode_soniox_form_params( + optional_params: Mapping[str, object], +) -> Mapping[str, object] | SonioxInvalidBoolParam: """Multipart form fields reach the proxy as strings; restore the JSON types Soniox expects.""" - return MappingProxyType({key: _decode_form_value(key, value) for key, value in optional_params.items()}) + decoded: Final = {key: _decode_form_value(key, value) for key, value in optional_params.items()} + return next( + (value for value in decoded.values() if isinstance(value, SonioxInvalidBoolParam)), + MappingProxyType(decoded), + ) class SonioxAudioTranscriptionConfig(BaseAudioTranscriptionConfig): diff --git a/tests/test_litellm/llms/soniox/audio_transcription/test_soniox_audio_transcription_handler.py b/tests/test_litellm/llms/soniox/audio_transcription/test_soniox_audio_transcription_handler.py index 077b9142d6e..d3b219c5f02 100644 --- a/tests/test_litellm/llms/soniox/audio_transcription/test_soniox_audio_transcription_handler.py +++ b/tests/test_litellm/llms/soniox/audio_transcription/test_soniox_audio_transcription_handler.py @@ -339,6 +339,24 @@ class TestPollLimitsClamping: a worker on tight poll loops. """ + def test_should_raise_400_when_form_bool_param_is_invalid(self): + handler = SonioxAudioTranscriptionHandler() + with pytest.raises(SonioxException) as exc_info: + handler._prepare( + audio_file=None, + optional_params={ + "enable_speaker_diarization": "yes", + "audio_url": "https://example.com/a.wav", + }, + litellm_params={}, + api_key="sk-test", + api_base=None, + provider_config=SonioxAudioTranscriptionConfig(), + headers={}, + ) + assert exc_info.value.status_code == 400 + assert "enable_speaker_diarization" in str(exc_info.value) + def test_should_clamp_poll_interval_to_minimum(self): from litellm.llms.soniox.common_utils import SONIOX_MIN_POLL_INTERVAL diff --git a/tests/test_litellm/llms/soniox/audio_transcription/test_soniox_audio_transcription_transformation.py b/tests/test_litellm/llms/soniox/audio_transcription/test_soniox_audio_transcription_transformation.py index 99d7c3d4411..2b938f0bb9b 100644 --- a/tests/test_litellm/llms/soniox/audio_transcription/test_soniox_audio_transcription_transformation.py +++ b/tests/test_litellm/llms/soniox/audio_transcription/test_soniox_audio_transcription_transformation.py @@ -9,7 +9,9 @@ import pytest from litellm.llms.soniox.audio_transcription.transformation import ( SonioxAudioTranscriptionConfig, + SonioxInvalidBoolParam, decode_soniox_form_params, + raise_soniox_form_error, ) from litellm.llms.soniox.common_utils import SonioxException from litellm.llms.soniox.types import ( @@ -751,11 +753,16 @@ class TestDecodeSonioxFormParams: "language_hints_strict": expected, } - def test_should_raise_400_on_non_boolean_string(self): + def test_should_return_error_value_on_non_boolean_string(self): + result = decode_soniox_form_params({"context": '{"general": []}', "enable_speaker_diarization": "yes"}) + assert result == SonioxInvalidBoolParam(key="enable_speaker_diarization", value="yes") + + def test_should_map_invalid_bool_error_to_400(self): with pytest.raises(SonioxException) as exc_info: - decode_soniox_form_params({"enable_speaker_diarization": "yes"}) + raise_soniox_form_error(SonioxInvalidBoolParam(key="enable_speaker_diarization", value="yes")) assert exc_info.value.status_code == 400 assert "enable_speaker_diarization" in str(exc_info.value) + assert "yes" in str(exc_info.value) @pytest.mark.parametrize( "raw, expected",