refactor(soniox): derive handler options and provider params without mutating a local dict

This commit is contained in:
Dan Lemon 2026-09-10 22:16:27 +02:00
parent 3d9e261c20
commit 70f8bcee9e
No known key found for this signature in database
GPG key ID: 63D20454AD4DAA0A

View file

@ -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,