From a85061d45d358fd8a196c1e25814957136682615 Mon Sep 17 00:00:00 2001 From: Andy Huynh Date: Tue, 21 Jul 2026 09:15:22 +0700 Subject: [PATCH 1/2] fix(elevenlabs): set duration on transcription response for cost tracking ElevenLabs returns no top-level duration, so the per-second cost path (input_cost_per_second x duration) always multiplied by 0 and every ElevenLabs transcription logged $0 spend. Derive duration from the last word/event end timestamp. --- .../audio_transcription/transformation.py | 13 ++++ ...labs_audio_transcription_transformation.py | 66 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 tests/test_litellm/llms/elevenlabs/audio_transcription/test_elevenlabs_audio_transcription_transformation.py diff --git a/litellm/llms/elevenlabs/audio_transcription/transformation.py b/litellm/llms/elevenlabs/audio_transcription/transformation.py index 68d1b5e16dd..ddce3eb4a8a 100644 --- a/litellm/llms/elevenlabs/audio_transcription/transformation.py +++ b/litellm/llms/elevenlabs/audio_transcription/transformation.py @@ -140,6 +140,19 @@ class ElevenLabsAudioTranscriptionConfig(BaseAudioTranscriptionConfig): } ) + # ElevenLabs does not return a top-level duration; derive it from + # the last timestamp so per-second cost tracking + # (input_cost_per_second x duration) bills the real audio length + # instead of 0. + duration = 0.0 + for word_data in response_json.get("words") or []: + try: + duration = max(duration, float(word_data.get("end") or 0.0)) + except (TypeError, ValueError): + continue + if duration > 0: + response["duration"] = duration + # Store full response in hidden params response._hidden_params = response_json diff --git a/tests/test_litellm/llms/elevenlabs/audio_transcription/test_elevenlabs_audio_transcription_transformation.py b/tests/test_litellm/llms/elevenlabs/audio_transcription/test_elevenlabs_audio_transcription_transformation.py new file mode 100644 index 00000000000..a38bace855f --- /dev/null +++ b/tests/test_litellm/llms/elevenlabs/audio_transcription/test_elevenlabs_audio_transcription_transformation.py @@ -0,0 +1,66 @@ +import os +import sys +from unittest.mock import MagicMock + +sys.path.insert( + 0, os.path.abspath("../../../../..") +) # Adds the parent directory to the system path + +from litellm.llms.elevenlabs.audio_transcription.transformation import ( + ElevenLabsAudioTranscriptionConfig, +) + + +def _mock_raw_response(response_json: dict) -> MagicMock: + raw = MagicMock() + raw.json.return_value = response_json + raw.text = str(response_json) + return raw + + +class TestElevenLabsTransformResponse: + def test_sets_duration_from_last_timestamp(self): + """Duration must be derived from word timestamps so per-second cost + tracking (input_cost_per_second x duration) bills real audio length — + without it every ElevenLabs transcription logs $0 spend.""" + config = ElevenLabsAudioTranscriptionConfig() + response = config.transform_audio_transcription_response( + _mock_raw_response( + { + "text": "hello world", + "language_code": "en", + "words": [ + {"type": "word", "text": "hello", "start": 0.1, "end": 0.5}, + {"type": "spacing", "text": " ", "start": 0.5, "end": 0.6}, + {"type": "word", "text": "world", "start": 0.6, "end": 1.9}, + {"type": "audio_event", "text": "(door)", "start": 2.0, "end": 3.4}, + ], + } + ) + ) + assert response.text == "hello world" + assert response.duration == 3.4 + + def test_no_words_leaves_duration_unset(self): + config = ElevenLabsAudioTranscriptionConfig() + response = config.transform_audio_transcription_response( + _mock_raw_response({"text": "hello", "language_code": "en"}) + ) + assert getattr(response, "duration", None) is None + + def test_malformed_word_timestamps_do_not_raise(self): + config = ElevenLabsAudioTranscriptionConfig() + response = config.transform_audio_transcription_response( + _mock_raw_response( + { + "text": "hello", + "language_code": "en", + "words": [ + {"type": "word", "text": "hello", "start": 0.0, "end": None}, + {"type": "word", "text": "x", "start": 0.0, "end": "bad"}, + {"type": "word", "text": "y", "start": 0.0, "end": 2.5}, + ], + } + ) + ) + assert response.duration == 2.5 From fd40828950a998d494b15e3ce2e0e50f7f2fd29e Mon Sep 17 00:00:00 2001 From: Andy Huynh Date: Tue, 21 Jul 2026 09:23:55 +0700 Subject: [PATCH 2/2] guard against non-dict entries in words array --- litellm/llms/elevenlabs/audio_transcription/transformation.py | 4 ++++ .../test_elevenlabs_audio_transcription_transformation.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/litellm/llms/elevenlabs/audio_transcription/transformation.py b/litellm/llms/elevenlabs/audio_transcription/transformation.py index ddce3eb4a8a..a27826c5049 100644 --- a/litellm/llms/elevenlabs/audio_transcription/transformation.py +++ b/litellm/llms/elevenlabs/audio_transcription/transformation.py @@ -130,6 +130,8 @@ class ElevenLabsAudioTranscriptionConfig(BaseAudioTranscriptionConfig): if "words" in response_json: response["words"] = [] for word_data in response_json["words"]: + if not isinstance(word_data, dict): + continue # Only include actual words, skip spacing and audio events if word_data.get("type") == "word": response["words"].append( @@ -146,6 +148,8 @@ class ElevenLabsAudioTranscriptionConfig(BaseAudioTranscriptionConfig): # instead of 0. duration = 0.0 for word_data in response_json.get("words") or []: + if not isinstance(word_data, dict): + continue try: duration = max(duration, float(word_data.get("end") or 0.0)) except (TypeError, ValueError): diff --git a/tests/test_litellm/llms/elevenlabs/audio_transcription/test_elevenlabs_audio_transcription_transformation.py b/tests/test_litellm/llms/elevenlabs/audio_transcription/test_elevenlabs_audio_transcription_transformation.py index a38bace855f..4b4bcc3689f 100644 --- a/tests/test_litellm/llms/elevenlabs/audio_transcription/test_elevenlabs_audio_transcription_transformation.py +++ b/tests/test_litellm/llms/elevenlabs/audio_transcription/test_elevenlabs_audio_transcription_transformation.py @@ -58,6 +58,8 @@ class TestElevenLabsTransformResponse: "words": [ {"type": "word", "text": "hello", "start": 0.0, "end": None}, {"type": "word", "text": "x", "start": 0.0, "end": "bad"}, + None, + "not-a-dict", {"type": "word", "text": "y", "start": 0.0, "end": 2.5}, ], }