fix(audio): measure duration for PathLike multipart tuple contents (#49806)

* fix(audio): measure duration for PathLike multipart tuple contents

* fix(audio): read a PathLike tuple's file directly instead of recursing

* fix(audio): read a tuple's path in the existing path branch

---------

Co-authored-by: ryan-crabbe-berri <ryan@berri.ai>
This commit is contained in:
Raunak Kumar Pandey 2026-11-09 16:54:57 -05:00 • committed by GitHub
parent 0b3558d924
commit 74cad3a2c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 24 additions and 12 deletions

View file

@ -301,9 +301,12 @@ def calculate_request_duration(file: FileTypes) -> float | None:
"Pass bytes, an open file handle, a (filename, content) "
"tuple, or a pathlib.Path."
)
elif isinstance(file, os.PathLike):
# File path (PathLike): SDK convenience.
with open(str(file), "rb") as f:
elif isinstance(file, os.PathLike) or (
isinstance(file, tuple) and len(file) >= 2 and isinstance(file[1], os.PathLike)
):
# File path (PathLike), bare or as the content of a (filename, path) tuple: SDK convenience.
path: Final = file if isinstance(file, os.PathLike) else file[1]
with open(str(path), "rb") as f:
file_content = f.read()
elif isinstance(file, tuple):
# Tuple format: (filename, content, optional content_type)

View file

@ -6,6 +6,7 @@ import base64
import io
import os
import tempfile
from pathlib import Path
from typing import Final
from unittest.mock import mock_open, patch
@ -329,16 +330,12 @@ class TestCalculateRequestDuration:
duration = calculate_request_duration(file_obj)
# Verify it succeeded (returns a duration, not None)
assert (
duration is not None
), "Duration should be calculated even when BytesIO is at end"
assert duration is not None, "Duration should be calculated even when BytesIO is at end"
assert isinstance(duration, float), "Duration should be a float"
assert duration > 0, "Duration should be positive"
# Verify the file position was restored
assert file_obj.tell() == len(
wav_header
), "File position should be restored to original position"
assert file_obj.tell() == len(wav_header), "File position should be restored to original position"
class TestGetAudioFileContentHash:
@ -364,9 +361,7 @@ class TestGetAudioFileContentHash:
hash1 = get_audio_file_content_hash((filename1, content))
hash2 = get_audio_file_content_hash((filename2, content))
assert (
hash1 == hash2
), "Same content should produce same hash regardless of filename"
assert hash1 == hash2, "Same content should produce same hash regardless of filename"
def test_bytes_input(self):
"""Test that raw bytes input works"""
@ -473,3 +468,17 @@ class TestSpeechMediaTypeFromAudioBytes:
from litellm.litellm_core_utils.audio_utils.utils import speech_media_type_from_audio_bytes
assert speech_media_type_from_audio_bytes(audio) == expected
@pytest.mark.parametrize("include_content_type", [False, True])
def test_duration_reads_pathlike_content_in_multipart_tuple(tmp_path: Path, include_content_type: bool) -> None:
audio_path: Final = tmp_path / "sample.wav"
audio_path.write_bytes(base64.b64decode(_encoded_audio(2, 8000, 1, "WAV")))
upload: Final = ("sample.wav", audio_path, "audio/wav") if include_content_type else ("sample.wav", audio_path)
assert calculate_request_duration(upload) == 2.0
def test_duration_does_not_open_bare_string_tuple_content(tmp_path: Path) -> None:
audio_path: Final = tmp_path / "sample.wav"
audio_path.write_bytes(base64.b64decode(_encoded_audio(2, 8000, 1, "WAV")))
assert calculate_request_duration(("sample.wav", str(audio_path))) is None