From 70f8bcee9e66f564c24a134898a251138bf9895b Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Thu, 10 Sep 2026 22:16:27 +0200 Subject: [PATCH] refactor(soniox): derive handler options and provider params without mutating a local dict --- .../soniox/audio_transcription/handler.py | 78 +++++++++++-------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/litellm/llms/soniox/audio_transcription/handler.py b/litellm/llms/soniox/audio_transcription/handler.py index da0ac7d7a06..7832ffa9f54 100644 --- a/litellm/llms/soniox/audio_transcription/handler.py +++ b/litellm/llms/soniox/audio_transcription/handler.py @@ -19,6 +19,7 @@ import asyncio import math import time from collections.abc import Coroutine, Mapping, Sequence +from types import MappingProxyType from typing import TYPE_CHECKING, Any, Final import httpx @@ -36,6 +37,7 @@ from litellm.llms.custom_httpx.http_handler import ( get_async_httpx_client, ) from litellm.llms.soniox.audio_transcription.transformation import ( + SONIOX_HANDLER_ONLY_PARAMS, SonioxAudioTranscriptionConfig, decode_soniox_form_params, ) @@ -61,12 +63,32 @@ else: _CLEANUP_TARGETS: Final = TypeAdapter(tuple[str, ...]) +# `language` is translated to `language_hints` upstream; `audio_url`/`file_id`/`response_format` +# are placed by the handler, everything else here is a LiteLLM-only knob. +_NOT_FORWARDED_TO_SONIOX: Final[frozenset[str]] = frozenset( + (*SONIOX_HANDLER_ONLY_PARAMS, "audio_url", "file_id", "response_format", "language") +) def _optional_str(value: object) -> str | None: return None if value is None else str(value) +def _float_or_default(value: object, default: float) -> float: + try: + parsed: Final = float(str(value)) + except ValueError: + return default + return parsed if math.isfinite(parsed) else default + + +def _int_or_default(value: object, default: int) -> int: + try: + return int(str(value)) + except (ValueError, OverflowError): + return default + + class _TranscriptionMeta(TypedDict, total=False): """Fields the handler reads from a Soniox transcription object.""" @@ -191,7 +213,7 @@ class SonioxAudioTranscriptionHandler: ) -> tuple[ dict[str, str], # auth headers str, # api_base (no trailing slash) - dict[str, object], # body for POST /v1/transcriptions (without file_id/audio_url) + Mapping[str, object], # body for POST /v1/transcriptions (without file_id/audio_url) _HandlerOptions, # handler-only options (poll interval, cleanup, ...) ]: # Validate env -> auth headers. @@ -207,18 +229,18 @@ class SonioxAudioTranscriptionHandler: base_url: Final = get_soniox_api_base(api_base) - # Decoded copy so the caller's dict is never mutated - # (the caller may reuse `optional_params` for retries or logging). - params: Final = dict(decode_soniox_form_params(optional_params)) + decoded: Final = decode_soniox_form_params(optional_params) - # Pull handler-only kwargs out of params so they aren't sent - # to Soniox. - poll_interval = float(str(params.pop("soniox_polling_interval", SONIOX_DEFAULT_POLL_INTERVAL))) - try: - max_attempts = int(str(params.pop("soniox_max_polling_attempts", SONIOX_DEFAULT_MAX_POLL_ATTEMPTS))) - except (ValueError, OverflowError): - max_attempts = SONIOX_DEFAULT_MAX_POLL_ATTEMPTS - cleanup_raw: Final = params.pop("soniox_cleanup", SONIOX_DEFAULT_CLEANUP) + # Server-side clamps. Caller-supplied poll settings (from request kwargs) + # are bounded so an authenticated caller cannot force a worker into a + # tight poll loop (zero interval) or pin it indefinitely (huge attempt + # count). Total polling time is bounded by + # SONIOX_MAX_POLL_ATTEMPTS * SONIOX_MAX_POLL_INTERVAL. + poll_interval: Final = _float_or_default(decoded.get("soniox_polling_interval"), SONIOX_DEFAULT_POLL_INTERVAL) + max_attempts: Final = _int_or_default( + decoded.get("soniox_max_polling_attempts"), SONIOX_DEFAULT_MAX_POLL_ATTEMPTS + ) + cleanup_raw: Final = decoded.get("soniox_cleanup", SONIOX_DEFAULT_CLEANUP) cleanup: Final[tuple[str, ...]] = ( () if cleanup_raw is None @@ -226,34 +248,22 @@ class SonioxAudioTranscriptionHandler: if isinstance(cleanup_raw, str) else _CLEANUP_TARGETS.validate_python(cleanup_raw) ) - filename_override: Final = _optional_str(params.pop("filename", None)) - - # Server-side clamps. Caller-supplied poll settings (from request kwargs) - # are bounded so an authenticated caller cannot force a worker into a - # tight poll loop (zero interval) or pin it indefinitely (huge attempt - # count). Total polling time is bounded by - # SONIOX_MAX_POLL_ATTEMPTS * SONIOX_MAX_POLL_INTERVAL. - if not math.isfinite(poll_interval): - poll_interval = SONIOX_DEFAULT_POLL_INTERVAL - clamped_poll_interval: Final = max(SONIOX_MIN_POLL_INTERVAL, min(poll_interval, SONIOX_MAX_POLL_INTERVAL)) - clamped_max_attempts: Final = max(1, min(max_attempts, SONIOX_MAX_POLL_ATTEMPTS)) # response_format is handled by LiteLLM post-processing, not Soniox. handler_opts: Final[_HandlerOptions] = { - "poll_interval": clamped_poll_interval, - "max_attempts": clamped_max_attempts, + "poll_interval": max(SONIOX_MIN_POLL_INTERVAL, min(poll_interval, SONIOX_MAX_POLL_INTERVAL)), + "max_attempts": max(1, min(max_attempts, SONIOX_MAX_POLL_ATTEMPTS)), "cleanup": cleanup, - "filename_override": filename_override, - "audio_url": _optional_str(params.pop("audio_url", None)), - "file_id": _optional_str(params.pop("file_id", None)), - "response_format": _optional_str(params.pop("response_format", None)), + "filename_override": _optional_str(decoded.get("filename")), + "audio_url": _optional_str(decoded.get("audio_url")), + "file_id": _optional_str(decoded.get("file_id")), + "response_format": _optional_str(decoded.get("response_format")), } - # Soniox does not accept `language` directly; map_openai_params should - # already have translated it, but drop any leftover to be safe. - params.pop("language", None) - - return auth_headers, base_url, params, handler_opts + provider_params: Final = MappingProxyType( + {key: value for key, value in decoded.items() if key not in _NOT_FORWARDED_TO_SONIOX} + ) + return auth_headers, base_url, provider_params, handler_opts def _build_create_body( self,