From 7f626f3309847b2325bdd18331cf8747ced11249 Mon Sep 17 00:00:00 2001 From: AaronG Date: Tue, 3 Nov 2026 23:50:43 +0800 Subject: [PATCH] fix(audio): read PathLike content inside (filename, content) tuples calculate_request_duration() dropped pathlib.Path (PathLike) content when an audio upload was supplied as (filename, Path(...)) or (filename, Path(...), content_type). The tuple branch only handled bytes and file-like objects, so file_content stayed None and the local duration fallback returned None even though the upload machinery accepts PathLike. Add a PathLike branch in the tuple handling that reads the bytes from disk, mirroring the existing bare-Path branch. Add a regression test covering the tuple-with-Path and tuple-with-Path-and-content-type input shapes. Fixes #49805 --- .../litellm_core_utils/audio_utils/utils.py | 7 +++++ .../litellm_core_utils/test_audio_utils.py | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/litellm/litellm_core_utils/audio_utils/utils.py b/litellm/litellm_core_utils/audio_utils/utils.py index 29b10ac3c83..153c8813dbf 100644 --- a/litellm/litellm_core_utils/audio_utils/utils.py +++ b/litellm/litellm_core_utils/audio_utils/utils.py @@ -311,6 +311,13 @@ def calculate_request_duration(file: FileTypes) -> float | None: content: Final = file[1] if isinstance(content, bytes): file_content = content + elif isinstance(content, os.PathLike): + # pathlib.Path (or any PathLike) inside a (filename, content) + # tuple: read the bytes from disk, mirroring the bare-Path + # branch above. Without this the duration reader silently + # returns None for `(filename, Path(...))` uploads. + with open(str(content), "rb") as file_handle: + file_content = file_handle.read() elif hasattr(content, "read") and not isinstance(content, (str, os.PathLike)): # File-like object in tuple current_pos: Final = getattr(content, "tell", lambda: None)() diff --git a/tests/unit/litellm_core_utils/test_audio_utils.py b/tests/unit/litellm_core_utils/test_audio_utils.py index 122445e8e30..7e44ba60d08 100644 --- a/tests/unit/litellm_core_utils/test_audio_utils.py +++ b/tests/unit/litellm_core_utils/test_audio_utils.py @@ -340,6 +340,33 @@ class TestCalculateRequestDuration: wav_header ), "File position should be restored to original position" + def test_pathlib_path_inside_tuple(self, tmp_path): + """ + Regression test: calculate_request_duration must read PathLike content + inside a (filename, content[, content_type]) tuple. + + A transcription upload may be supplied as (filename, pathlib.Path(...)) + or (filename, pathlib.Path(...), content_type). The duration reader used + to ignore PathLike tuple contents and silently return None, even though + the upload machinery accepts them. + """ + import wave + + wav_path = tmp_path / "sample.wav" + with wave.open(str(wav_path), "wb") as audio: + audio.setnchannels(1) + audio.setsampwidth(2) + audio.setframerate(8000) + # 2 seconds of 16-bit mono audio at 8 kHz. + audio.writeframes(b"\0" * 32000) + + # Same file in three accepted shapes must all report ~2.0s. + assert calculate_request_duration(wav_path) == pytest.approx(2.0) + assert calculate_request_duration((str(wav_path), wav_path)) == pytest.approx(2.0) + assert calculate_request_duration( + (str(wav_path), wav_path, "audio/wav") + ) == pytest.approx(2.0) + class TestGetAudioFileContentHash: """Test the get_audio_file_content_hash function for cache key generation"""