fix(transcription): stop billing a FLAC with no sample count as 576 trillion seconds (#50842)

* fix(transcription): treat libsndfile's unknown frame count as no duration

* refactor(transcription): inline the unknown frame count check

* fix(transcription): read the frame count once to keep the type gate flat

* test(transcription): annotate the new passthrough test's locals as Final

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Kerry Lu 2026-11-09 22:19:33 -08:00 • committed by GitHub
parent 84b0a4297a
commit 3daeb8daf2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 71 additions and 3 deletions

View file

@ -31,6 +31,9 @@ def normalized_audio_duration_seconds(value: object) -> float | None:
return seconds if math.isfinite(seconds) and seconds >= 0 else None
LIBSNDFILE_UNKNOWN_FRAME_COUNT: Final = 2**63 - 1
@dataclass
class ProcessedAudioFile:
"""
@ -340,7 +343,10 @@ def calculate_request_duration(file: FileTypes) -> float | None:
# Extract duration using soundfile
file_object: Final = io.BytesIO(file_content)
with sf.SoundFile(file_object) as audio:
duration: Final = len(audio) / audio.samplerate
frames: Final = len(audio)
if frames >= LIBSNDFILE_UNKNOWN_FRAME_COUNT:
return None
duration: Final = frames / audio.samplerate
return duration
except Exception:

View file

@ -29,6 +29,7 @@ from litellm.constants import (
TRANSCRIBE_MEDIA_FETCH_ATTEMPTS,
TRANSCRIBE_MEDIA_LAST_MODIFIED_TOLERANCE_SECONDS,
)
from litellm.litellm_core_utils.audio_utils.utils import LIBSNDFILE_UNKNOWN_FRAME_COUNT
from litellm.litellm_core_utils.aws_partition import get_aws_dns_suffix
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.litellm_logging import (
@ -523,7 +524,10 @@ async def write_media_within_limit(response: httpx.Response, media_file: IO[byte
def media_file_seconds(path: Path) -> float | None:
try:
with soundfile.SoundFile(str(path)) as audio:
return len(audio) / audio.samplerate
frames: Final = len(audio)
if frames >= LIBSNDFILE_UNKNOWN_FRAME_COUNT:
return None
return frames / audio.samplerate
except (RuntimeError, ValueError, OSError) as e:
verbose_proxy_logger.warning("Transcribe media could not be decoded for its duration: %s", e)
return None

View file

@ -20,6 +20,7 @@ from integration.cost_calculation.cost_tracking_case import (
RecountExpected,
RecountRates,
SseResponse,
UnsizedFlacUpload,
WavUpload,
)
from pydantic import JsonValue
@ -8103,6 +8104,18 @@ WHISPER_NEXT_TRANSCRIPTIONS_PER_SECOND: Final = CostTrackingTestCase(
)
WHISPER_NEXT_TRANSCRIPTIONS_UNSIZED_FLAC_NO_USAGE: Final = CostTrackingTestCase(
name="whisper-next-transcriptions-unsized-flac-no-usage",
covers="quota_management.spend_tracking.cost_matrix.logs_cost",
model="whisper-next",
endpoint="/v1/audio/transcriptions",
upload=UnsizedFlacUpload(kind="unsized_flac"),
request={"response_format": "json"},
response=JsonResponse(content_type="application/json", body={"text": "hello"}),
expected=ExactExpected(spend=0, input_cost=0, output_cost=0, prompt_tokens=0, completion_tokens=0),
)
# whisper-verbose-next
WHISPER_VERBOSE_NEXT_TRANSCRIPTIONS_DURATION: Final = CostTrackingTestCase(
name="whisper-verbose-next-transcriptions-duration",
@ -12206,6 +12219,7 @@ CASES: Final[tuple[CostTrackingTestCase, ...]] = (
GPT_5_6_UPSTREAM_500_ZERO_SPEND,
GPT_5_6_UPSTREAM_429_ZERO_SPEND,
WHISPER_NEXT_TRANSCRIPTIONS_PER_SECOND,
WHISPER_NEXT_TRANSCRIPTIONS_UNSIZED_FLAC_NO_USAGE,
WHISPER_VERBOSE_NEXT_TRANSCRIPTIONS_DURATION,
GPT_4O_TRANSCRIBE_NEXT_TRANSCRIPTIONS_TOKENS,
TTS_NEXT_SPEECH_PER_CHARACTER,

View file

@ -180,7 +180,13 @@ class PngUpload(BaseModel):
kind: Literal["png"]
Upload: TypeAlias = Annotated[WavUpload | PngUpload, Field(discriminator="kind")]
class UnsizedFlacUpload(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
kind: Literal["unsized_flac"]
Upload: TypeAlias = Annotated[WavUpload | PngUpload | UnsizedFlacUpload, Field(discriminator="kind")]
class JsonResponse(BaseModel):

View file

@ -72,6 +72,19 @@ def _wav_bytes(seconds: float) -> bytes:
return output.getvalue()
def _unsized_flac_bytes() -> bytes:
"""A FLAC whose STREAMINFO total-samples field is 0 (unknown), as an encoder writing to a pipe leaves it."""
sample_rate: Final = 16000
bits_per_sample: Final = 16
streaminfo: Final = (
struct.pack(">HH", 4096, 4096)
+ bytes(6)
+ ((sample_rate << 44) | ((bits_per_sample - 1) << 36)).to_bytes(8, "big")
+ bytes(16)
)
return b"fLaC" + b"\x80" + len(streaminfo).to_bytes(3, "big") + streaminfo
def _png_bytes() -> bytes:
def chunk(kind: bytes, payload: bytes) -> bytes:
return (
@ -97,6 +110,8 @@ def _multipart_request(gateway: Gateway, case: CostTrackingTestCase, model_name:
}
if case.upload.kind == "wav":
files: Final = {"file": ("audio.wav", _wav_bytes(case.upload.seconds), "audio/wav")}
elif case.upload.kind == "unsized_flac":
files = {"file": ("audio.flac", _unsized_flac_bytes(), "audio/flac")}
else:
files = {"image": ("image.png", _png_bytes(), "image/png")}
return gateway.request_multipart(case.endpoint, fields, files, key=key)

View file

@ -5,6 +5,7 @@ Test the audio utils functionality in litellm_core_utils/audio_utils/utils.py
import base64
import io
import os
import struct
import tempfile
from pathlib import Path
from typing import Final
@ -338,6 +339,17 @@ class TestCalculateRequestDuration:
assert file_obj.tell() == len(wav_header), "File position should be restored to original position"
def _unsized_flac() -> bytes:
streaminfo: Final = (
struct.pack(">HH", 4096, 4096) + bytes(6) + ((16000 << 44) | (15 << 36)).to_bytes(8, "big") + bytes(16)
)
return b"fLaC" + b"\x80" + len(streaminfo).to_bytes(3, "big") + streaminfo
def test_duration_is_unknown_for_a_flac_that_does_not_state_its_length() -> None:
assert calculate_request_duration(_unsized_flac()) is None
class TestGetAudioFileContentHash:
"""Test the get_audio_file_content_hash function for cache key generation"""

View file

@ -1,9 +1,11 @@
import asyncio
import io
import json
import struct
import wave
from datetime import datetime
from pathlib import Path
from typing import Final
from unittest.mock import MagicMock
import httpx
@ -453,6 +455,15 @@ class TestMediaFileSeconds:
assert media_file_seconds(media) == pytest.approx(1.5)
def test_flac_that_does_not_state_its_length_yields_no_duration(self, tmp_path: Path):
streaminfo: Final = (
struct.pack(">HH", 4096, 4096) + bytes(6) + ((16000 << 44) | (15 << 36)).to_bytes(8, "big") + bytes(16)
)
media: Final = tmp_path / "a.flac"
_ = media.write_bytes(b"fLaC" + b"\x80" + len(streaminfo).to_bytes(3, "big") + streaminfo)
assert media_file_seconds(media) is None
def test_undecodable_media_yields_no_duration(self, tmp_path: Path):
media = tmp_path / "a.wav"
_ = media.write_bytes(b"not audio at all")