From 69129b0b5a5901249373997effbc0954007ce189 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Jun 2025 19:13:17 +0000 Subject: [PATCH 01/13] Checkpoint before follow-up message --- .../audio_transcription/transformation.py | 25 +++- ...seek_audio_transcription_transformation.py | 135 ++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) diff --git a/litellm/llms/deepgram/audio_transcription/transformation.py b/litellm/llms/deepgram/audio_transcription/transformation.py index f1b18808f79c..d7a143de54b2 100644 --- a/litellm/llms/deepgram/audio_transcription/transformation.py +++ b/litellm/llms/deepgram/audio_transcription/transformation.py @@ -163,7 +163,30 @@ def get_complete_url( ) api_base = api_base.rstrip("/") # Remove trailing slash if present - return f"{api_base}/listen?model={model}" + # Start with base URL and model parameter + url = f"{api_base}/listen?model={model}" + + # Add any additional query parameters from optional_params + # This supports parameters like punctuate, diarize, measurements, etc. + query_params = [] + for key, value in optional_params.items(): + # Skip parameters that are not meant for URL query strings + # These are OpenAI-specific params that we handle differently + if key in ["language"]: + continue + + # Convert boolean values to lowercase strings + if isinstance(value, bool): + value = str(value).lower() + + # Add the parameter to query string + query_params.append(f"{key}={value}") + + # Append query parameters to URL + if query_params: + url += "&" + "&".join(query_params) + + return url def validate_environment( self, diff --git a/tests/test_litellm/llms/deepgram/audio_transcription/test_deepseek_audio_transcription_transformation.py b/tests/test_litellm/llms/deepgram/audio_transcription/test_deepseek_audio_transcription_transformation.py index ea035db11958..af315ea751ef 100644 --- a/tests/test_litellm/llms/deepgram/audio_transcription/test_deepseek_audio_transcription_transformation.py +++ b/tests/test_litellm/llms/deepgram/audio_transcription/test_deepseek_audio_transcription_transformation.py @@ -55,3 +55,138 @@ def test_audio_file_handling(fixture_name, request): optional_params={}, litellm_params={}, ) + + +def test_get_complete_url_basic(): + """Test basic URL generation without optional parameters""" + handler = DeepgramAudioTranscriptionConfig() + url = handler.get_complete_url( + api_base=None, + api_key=None, + model="nova-2", + optional_params={}, + litellm_params={}, + ) + expected_url = "https://api.deepgram.com/v1/listen?model=nova-2" + assert url == expected_url + + +def test_get_complete_url_with_punctuate(): + """Test URL generation with punctuate parameter""" + handler = DeepgramAudioTranscriptionConfig() + url = handler.get_complete_url( + api_base=None, + api_key=None, + model="nova-2", + optional_params={"punctuate": True}, + litellm_params={}, + ) + expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true" + assert url == expected_url + + +def test_get_complete_url_with_diarize(): + """Test URL generation with diarize parameter""" + handler = DeepgramAudioTranscriptionConfig() + url = handler.get_complete_url( + api_base=None, + api_key=None, + model="nova-2", + optional_params={"diarize": True}, + litellm_params={}, + ) + expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&diarize=true" + assert url == expected_url + + +def test_get_complete_url_with_measurements(): + """Test URL generation with measurements parameter""" + handler = DeepgramAudioTranscriptionConfig() + url = handler.get_complete_url( + api_base=None, + api_key=None, + model="nova-2", + optional_params={"measurements": True}, + litellm_params={}, + ) + expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&measurements=true" + assert url == expected_url + + +def test_get_complete_url_with_multiple_params(): + """Test URL generation with multiple query parameters""" + handler = DeepgramAudioTranscriptionConfig() + url = handler.get_complete_url( + api_base=None, + api_key=None, + model="nova-2", + optional_params={ + "punctuate": True, + "diarize": False, + "measurements": True, + "smart_format": True, + }, + litellm_params={}, + ) + # URL should contain all parameters + assert "model=nova-2" in url + assert "punctuate=true" in url + assert "diarize=false" in url + assert "measurements=true" in url + assert "smart_format=true" in url + assert url.startswith("https://api.deepgram.com/v1/listen?") + + +def test_get_complete_url_with_language_parameter(): + """Test that language parameter is excluded from query string (handled separately)""" + handler = DeepgramAudioTranscriptionConfig() + url = handler.get_complete_url( + api_base=None, + api_key=None, + model="nova-2", + optional_params={ + "language": "en", + "punctuate": True, + }, + litellm_params={}, + ) + expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true" + assert url == expected_url + # Language should NOT appear in URL as it's handled separately + assert "language=" not in url + + +def test_get_complete_url_with_custom_api_base(): + """Test URL generation with custom API base""" + handler = DeepgramAudioTranscriptionConfig() + url = handler.get_complete_url( + api_base="https://custom.deepgram.com/v2", + api_key=None, + model="nova-2", + optional_params={"punctuate": True}, + litellm_params={}, + ) + expected_url = "https://custom.deepgram.com/v2/listen?model=nova-2&punctuate=true" + assert url == expected_url + + +def test_get_complete_url_with_string_values(): + """Test URL generation with string parameter values""" + handler = DeepgramAudioTranscriptionConfig() + url = handler.get_complete_url( + api_base=None, + api_key=None, + model="nova-2", + optional_params={ + "tier": "enhanced", + "version": "latest", + "punctuate": True, + }, + litellm_params={}, + ) + # URL should contain all parameters + assert "model=nova-2" in url + assert "tier=enhanced" in url + assert "version=latest" in url + assert "punctuate=true" in url + assert url.startswith("https://api.deepgram.com/v1/listen?") From 9f394bf00ccf6189dc880c985d92cbc8ce2aa0b8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Jun 2025 19:17:16 +0000 Subject: [PATCH 02/13] Add comprehensive tests for Deepgram transcription functionality --- .../test_deepgram_mock_transcription.py | 414 ++++++++++++++++++ 1 file changed, 414 insertions(+) create mode 100644 tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py diff --git a/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py b/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py new file mode 100644 index 000000000000..e6236a150e59 --- /dev/null +++ b/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py @@ -0,0 +1,414 @@ +import io +import json +import os +import sys +from unittest.mock import MagicMock, patch +from typing import Any + +import pytest + +sys.path.insert( + 0, os.path.abspath("../../../..") +) # Adds the parent directory to the system path + +import litellm +from litellm.types.utils import TranscriptionResponse + + +@pytest.fixture +def mock_deepgram_response(): + """Mock Deepgram API response""" + return { + "metadata": { + "transaction_key": "deprecated", + "request_id": "test-request-id", + "sha256": "test-sha", + "created": "2024-01-01T00:00:00.000Z", + "duration": 10.5, + "channels": 1, + "models": ["nova-2"], + }, + "results": { + "channels": [ + { + "alternatives": [ + { + "transcript": "Hello, this is a test transcription.", + "confidence": 0.99, + "words": [ + {"word": "Hello", "start": 0.0, "end": 0.5, "confidence": 0.99}, + {"word": "this", "start": 0.6, "end": 0.8, "confidence": 0.98}, + {"word": "is", "start": 0.9, "end": 1.1, "confidence": 0.97}, + {"word": "a", "start": 1.2, "end": 1.3, "confidence": 0.96}, + {"word": "test", "start": 1.4, "end": 1.8, "confidence": 0.95}, + {"word": "transcription", "start": 1.9, "end": 2.8, "confidence": 0.94}, + ] + } + ] + } + ] + } + } + + +@pytest.fixture +def test_audio_bytes(): + """Mock audio file bytes""" + return b"fake_audio_data_for_testing" + + +@pytest.fixture +def test_audio_file(): + """Mock audio file object""" + return io.BytesIO(b"fake_audio_data_for_testing") + + +class TestDeepgramMockTranscription: + """Test Deepgram transcription with mocked HTTP requests""" + + def test_basic_transcription(self, mock_deepgram_response, test_audio_bytes): + """Test basic transcription without additional parameters""" + + # Create mock response + mock_response = MagicMock() + mock_response.json.return_value = mock_deepgram_response + mock_response.status_code = 200 + mock_response.headers = {"Content-Type": "application/json"} + + with patch( + "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", + return_value=mock_response, + ) as mock_post: + + response: TranscriptionResponse = litellm.transcription( + model="deepgram/nova-2", + file=test_audio_bytes, + api_key="test-api-key", + ) + + # Verify the HTTP call was made + mock_post.assert_called_once() + call_kwargs = mock_post.call_args.kwargs + + # Verify URL (should be basic URL without extra params) + expected_url = "https://api.deepgram.com/v1/listen?model=nova-2" + assert call_kwargs["url"] == expected_url + + # Verify headers + assert "Authorization" in call_kwargs["headers"] + assert call_kwargs["headers"]["Authorization"] == "Token test-api-key" + + # Verify request data is the audio bytes + assert call_kwargs["data"] == test_audio_bytes + + # Verify response + assert response.text == "Hello, this is a test transcription." + assert hasattr(response, '_hidden_params') + + def test_transcription_with_punctuate(self, mock_deepgram_response, test_audio_bytes): + """Test transcription with punctuate=true parameter""" + + mock_response = MagicMock() + mock_response.json.return_value = mock_deepgram_response + mock_response.status_code = 200 + mock_response.headers = {"Content-Type": "application/json"} + + with patch( + "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", + return_value=mock_response, + ) as mock_post: + + response: TranscriptionResponse = litellm.transcription( + model="deepgram/nova-2", + file=test_audio_bytes, + api_key="test-api-key", + punctuate=True, + ) + + # Verify the HTTP call was made + mock_post.assert_called_once() + call_kwargs = mock_post.call_args.kwargs + + # Verify URL contains punctuate parameter + expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true" + assert call_kwargs["url"] == expected_url + + # Verify response + assert response.text == "Hello, this is a test transcription." + + def test_transcription_with_diarize(self, mock_deepgram_response, test_audio_bytes): + """Test transcription with diarize=true parameter""" + + mock_response = MagicMock() + mock_response.json.return_value = mock_deepgram_response + mock_response.status_code = 200 + mock_response.headers = {"Content-Type": "application/json"} + + with patch( + "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", + return_value=mock_response, + ) as mock_post: + + response: TranscriptionResponse = litellm.transcription( + model="deepgram/nova-2", + file=test_audio_bytes, + api_key="test-api-key", + diarize=True, + ) + + # Verify the HTTP call was made + mock_post.assert_called_once() + call_kwargs = mock_post.call_args.kwargs + + # Verify URL contains diarize parameter + expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&diarize=true" + assert call_kwargs["url"] == expected_url + + # Verify response + assert response.text == "Hello, this is a test transcription." + + def test_transcription_with_measurements(self, mock_deepgram_response, test_audio_bytes): + """Test transcription with measurements=true parameter""" + + mock_response = MagicMock() + mock_response.json.return_value = mock_deepgram_response + mock_response.status_code = 200 + mock_response.headers = {"Content-Type": "application/json"} + + with patch( + "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", + return_value=mock_response, + ) as mock_post: + + response: TranscriptionResponse = litellm.transcription( + model="deepgram/nova-2", + file=test_audio_bytes, + api_key="test-api-key", + measurements=True, + ) + + # Verify the HTTP call was made + mock_post.assert_called_once() + call_kwargs = mock_post.call_args.kwargs + + # Verify URL contains measurements parameter + expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&measurements=true" + assert call_kwargs["url"] == expected_url + + # Verify response + assert response.text == "Hello, this is a test transcription." + + def test_transcription_with_multiple_params(self, mock_deepgram_response, test_audio_bytes): + """Test transcription with multiple query parameters""" + + mock_response = MagicMock() + mock_response.json.return_value = mock_deepgram_response + mock_response.status_code = 200 + mock_response.headers = {"Content-Type": "application/json"} + + with patch( + "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", + return_value=mock_response, + ) as mock_post: + + response: TranscriptionResponse = litellm.transcription( + model="deepgram/nova-2", + file=test_audio_bytes, + api_key="test-api-key", + punctuate=True, + diarize=False, + measurements=True, + smart_format=True, + tier="enhanced", + ) + + # Verify the HTTP call was made + mock_post.assert_called_once() + call_kwargs = mock_post.call_args.kwargs + + # Verify URL contains all parameters + url = call_kwargs["url"] + assert "model=nova-2" in url + assert "punctuate=true" in url + assert "diarize=false" in url + assert "measurements=true" in url + assert "smart_format=true" in url + assert "tier=enhanced" in url + assert url.startswith("https://api.deepgram.com/v1/listen?") + + # Verify response + assert response.text == "Hello, this is a test transcription." + + def test_transcription_with_language_param(self, mock_deepgram_response, test_audio_bytes): + """Test that language parameter is handled separately (not in URL)""" + + mock_response = MagicMock() + mock_response.json.return_value = mock_deepgram_response + mock_response.status_code = 200 + mock_response.headers = {"Content-Type": "application/json"} + + with patch( + "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", + return_value=mock_response, + ) as mock_post: + + response: TranscriptionResponse = litellm.transcription( + model="deepgram/nova-2", + file=test_audio_bytes, + api_key="test-api-key", + language="en", + punctuate=True, + ) + + # Verify the HTTP call was made + mock_post.assert_called_once() + call_kwargs = mock_post.call_args.kwargs + + # Verify URL contains punctuate but NOT language + expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true" + assert call_kwargs["url"] == expected_url + assert "language=" not in call_kwargs["url"] + + # Verify response + assert response.text == "Hello, this is a test transcription." + + def test_transcription_with_custom_api_base(self, mock_deepgram_response, test_audio_bytes): + """Test transcription with custom API base URL""" + + mock_response = MagicMock() + mock_response.json.return_value = mock_deepgram_response + mock_response.status_code = 200 + mock_response.headers = {"Content-Type": "application/json"} + + with patch( + "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", + return_value=mock_response, + ) as mock_post: + + response: TranscriptionResponse = litellm.transcription( + model="deepgram/nova-2", + file=test_audio_bytes, + api_key="test-api-key", + api_base="https://custom.deepgram.com/v2", + punctuate=True, + ) + + # Verify the HTTP call was made + mock_post.assert_called_once() + call_kwargs = mock_post.call_args.kwargs + + # Verify custom API base is used + expected_url = "https://custom.deepgram.com/v2/listen?model=nova-2&punctuate=true" + assert call_kwargs["url"] == expected_url + + # Verify response + assert response.text == "Hello, this is a test transcription." + + @pytest.mark.asyncio + async def test_async_transcription_with_params(self, mock_deepgram_response, test_audio_bytes): + """Test async transcription with query parameters""" + + mock_response = MagicMock() + mock_response.json.return_value = mock_deepgram_response + mock_response.status_code = 200 + mock_response.headers = {"Content-Type": "application/json"} + + with patch( + "litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post", + return_value=mock_response, + ) as mock_post: + + response: TranscriptionResponse = await litellm.atranscription( + model="deepgram/nova-2", + file=test_audio_bytes, + api_key="test-api-key", + punctuate=True, + diarize=True, + measurements=False, + ) + + # Verify the HTTP call was made + mock_post.assert_called_once() + call_kwargs = mock_post.call_args.kwargs + + # Verify URL contains all parameters + url = call_kwargs["url"] + assert "model=nova-2" in url + assert "punctuate=true" in url + assert "diarize=true" in url + assert "measurements=false" in url + assert url.startswith("https://api.deepgram.com/v1/listen?") + + # Verify headers + assert "Authorization" in call_kwargs["headers"] + assert call_kwargs["headers"]["Authorization"] == "Token test-api-key" + + # Verify response + assert response.text == "Hello, this is a test transcription." + + def test_transcription_with_file_object(self, mock_deepgram_response, test_audio_file): + """Test transcription with file-like object""" + + mock_response = MagicMock() + mock_response.json.return_value = mock_deepgram_response + mock_response.status_code = 200 + mock_response.headers = {"Content-Type": "application/json"} + + with patch( + "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", + return_value=mock_response, + ) as mock_post: + + response: TranscriptionResponse = litellm.transcription( + model="deepgram/nova-2", + file=test_audio_file, + api_key="test-api-key", + punctuate=True, + ) + + # Verify the HTTP call was made + mock_post.assert_called_once() + call_kwargs = mock_post.call_args.kwargs + + # Verify URL contains punctuate parameter + expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true" + assert call_kwargs["url"] == expected_url + + # Verify request data contains the audio bytes + assert call_kwargs["data"] == b"fake_audio_data_for_testing" + + # Verify response + assert response.text == "Hello, this is a test transcription." + + def test_transcription_boolean_conversion(self, mock_deepgram_response, test_audio_bytes): + """Test that boolean values are correctly converted to lowercase strings""" + + mock_response = MagicMock() + mock_response.json.return_value = mock_deepgram_response + mock_response.status_code = 200 + mock_response.headers = {"Content-Type": "application/json"} + + with patch( + "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", + return_value=mock_response, + ) as mock_post: + + response: TranscriptionResponse = litellm.transcription( + model="deepgram/nova-2", + file=test_audio_bytes, + api_key="test-api-key", + punctuate=True, # Should become "true" + diarize=False, # Should become "false" + ) + + # Verify the HTTP call was made + mock_post.assert_called_once() + call_kwargs = mock_post.call_args.kwargs + + # Verify URL contains correctly formatted boolean values + url = call_kwargs["url"] + assert "punctuate=true" in url # lowercase "true" + assert "diarize=false" in url # lowercase "false" + # Should not contain Python-style "True" or "False" + assert "True" not in url + assert "False" not in url \ No newline at end of file From 2b2a91f5b1c1ff3b4e170d7ed6e74034f3bc097a Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 11 Jun 2025 12:20:02 -0700 Subject: [PATCH 03/13] clean up transform --- .../audio_transcription/transformation.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/litellm/llms/deepgram/audio_transcription/transformation.py b/litellm/llms/deepgram/audio_transcription/transformation.py index d7a143de54b2..866dc9a58a5c 100644 --- a/litellm/llms/deepgram/audio_transcription/transformation.py +++ b/litellm/llms/deepgram/audio_transcription/transformation.py @@ -126,9 +126,9 @@ def transform_audio_transcription_response( # Add additional metadata matching OpenAI format response["task"] = "transcribe" - response[ - "language" - ] = "english" # Deepgram auto-detects but doesn't return language + response["language"] = ( + "english" # Deepgram auto-detects but doesn't return language + ) response["duration"] = response_json["metadata"]["duration"] # Transform words to match OpenAI format @@ -165,27 +165,27 @@ def get_complete_url( # Start with base URL and model parameter url = f"{api_base}/listen?model={model}" - + # Add any additional query parameters from optional_params # This supports parameters like punctuate, diarize, measurements, etc. query_params = [] for key, value in optional_params.items(): # Skip parameters that are not meant for URL query strings # These are OpenAI-specific params that we handle differently - if key in ["language"]: + if key in self.get_supported_openai_params(model): continue - + # Convert boolean values to lowercase strings if isinstance(value, bool): value = str(value).lower() - + # Add the parameter to query string query_params.append(f"{key}={value}") - + # Append query parameters to URL if query_params: url += "&" + "&".join(query_params) - + return url def validate_environment( From 440cd05322297b4d04ff458623c31f75446415dc Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 11 Jun 2025 12:21:23 -0700 Subject: [PATCH 04/13] just use 1 test --- .../test_deepgram_mock_transcription.py | 415 +++++++----------- 1 file changed, 170 insertions(+), 245 deletions(-) diff --git a/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py b/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py index e6236a150e59..7237220aa201 100644 --- a/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py +++ b/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py @@ -2,8 +2,8 @@ import json import os import sys -from unittest.mock import MagicMock, patch from typing import Any +from unittest.mock import MagicMock, patch import pytest @@ -36,18 +36,48 @@ def mock_deepgram_response(): "transcript": "Hello, this is a test transcription.", "confidence": 0.99, "words": [ - {"word": "Hello", "start": 0.0, "end": 0.5, "confidence": 0.99}, - {"word": "this", "start": 0.6, "end": 0.8, "confidence": 0.98}, - {"word": "is", "start": 0.9, "end": 1.1, "confidence": 0.97}, - {"word": "a", "start": 1.2, "end": 1.3, "confidence": 0.96}, - {"word": "test", "start": 1.4, "end": 1.8, "confidence": 0.95}, - {"word": "transcription", "start": 1.9, "end": 2.8, "confidence": 0.94}, - ] + { + "word": "Hello", + "start": 0.0, + "end": 0.5, + "confidence": 0.99, + }, + { + "word": "this", + "start": 0.6, + "end": 0.8, + "confidence": 0.98, + }, + { + "word": "is", + "start": 0.9, + "end": 1.1, + "confidence": 0.97, + }, + { + "word": "a", + "start": 1.2, + "end": 1.3, + "confidence": 0.96, + }, + { + "word": "test", + "start": 1.4, + "end": 1.8, + "confidence": 0.95, + }, + { + "word": "transcription", + "start": 1.9, + "end": 2.8, + "confidence": 0.94, + }, + ], } ] } ] - } + }, } @@ -66,225 +96,145 @@ def test_audio_file(): class TestDeepgramMockTranscription: """Test Deepgram transcription with mocked HTTP requests""" - def test_basic_transcription(self, mock_deepgram_response, test_audio_bytes): - """Test basic transcription without additional parameters""" - + @pytest.mark.parametrize( + "optional_params,expected_url,test_id", + [ + # Basic transcription without parameters + ({}, "https://api.deepgram.com/v1/listen?model=nova-2", "basic"), + # Single parameters + ( + {"punctuate": True}, + "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true", + "punctuate_true", + ), + ( + {"diarize": True}, + "https://api.deepgram.com/v1/listen?model=nova-2&diarize=true", + "diarize_true", + ), + ( + {"measurements": True}, + "https://api.deepgram.com/v1/listen?model=nova-2&measurements=true", + "measurements_true", + ), + ( + {"diarize": False}, + "https://api.deepgram.com/v1/listen?model=nova-2&diarize=false", + "diarize_false", + ), + # String parameters + ( + {"tier": "enhanced"}, + "https://api.deepgram.com/v1/listen?model=nova-2&tier=enhanced", + "tier_enhanced", + ), + ( + {"version": "latest"}, + "https://api.deepgram.com/v1/listen?model=nova-2&version=latest", + "version_latest", + ), + # Language parameter should be excluded + ( + {"language": "en", "punctuate": True}, + "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true", + "language_excluded", + ), + # Multiple parameters with boolean conversion + ( + {"punctuate": True, "diarize": False}, + "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true&diarize=false", + "boolean_conversion", + ), + # Multiple mixed parameters + ( + { + "punctuate": True, + "diarize": False, + "measurements": True, + "smart_format": True, + "tier": "enhanced", + }, + None, + "multiple_params", + ), # We'll check contains for this one since order may vary + ], + ) + def test_transcription_url_generation( + self, + mock_deepgram_response, + test_audio_bytes, + optional_params, + expected_url, + test_id, + ): + """Test transcription URL generation with various parameters""" + # Create mock response mock_response = MagicMock() mock_response.json.return_value = mock_deepgram_response mock_response.status_code = 200 mock_response.headers = {"Content-Type": "application/json"} - + with patch( "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", return_value=mock_response, ) as mock_post: - + response: TranscriptionResponse = litellm.transcription( model="deepgram/nova-2", file=test_audio_bytes, api_key="test-api-key", + **optional_params, ) - + # Verify the HTTP call was made mock_post.assert_called_once() call_kwargs = mock_post.call_args.kwargs - - # Verify URL (should be basic URL without extra params) - expected_url = "https://api.deepgram.com/v1/listen?model=nova-2" - assert call_kwargs["url"] == expected_url - + + # Verify URL + actual_url = call_kwargs["url"] + if expected_url is None: + # For multiple params, check that all expected parts are present + assert "model=nova-2" in actual_url + assert "punctuate=true" in actual_url + assert "diarize=false" in actual_url + assert "measurements=true" in actual_url + assert "smart_format=true" in actual_url + assert "tier=enhanced" in actual_url + assert actual_url.startswith("https://api.deepgram.com/v1/listen?") + # Ensure language is not included even if it was in optional_params for other tests + assert "language=" not in actual_url + else: + assert ( + actual_url == expected_url + ), f"Test {test_id}: Expected {expected_url}, got {actual_url}" + # Verify headers assert "Authorization" in call_kwargs["headers"] assert call_kwargs["headers"]["Authorization"] == "Token test-api-key" - + # Verify request data is the audio bytes assert call_kwargs["data"] == test_audio_bytes - - # Verify response - assert response.text == "Hello, this is a test transcription." - assert hasattr(response, '_hidden_params') - def test_transcription_with_punctuate(self, mock_deepgram_response, test_audio_bytes): - """Test transcription with punctuate=true parameter""" - - mock_response = MagicMock() - mock_response.json.return_value = mock_deepgram_response - mock_response.status_code = 200 - mock_response.headers = {"Content-Type": "application/json"} - - with patch( - "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", - return_value=mock_response, - ) as mock_post: - - response: TranscriptionResponse = litellm.transcription( - model="deepgram/nova-2", - file=test_audio_bytes, - api_key="test-api-key", - punctuate=True, - ) - - # Verify the HTTP call was made - mock_post.assert_called_once() - call_kwargs = mock_post.call_args.kwargs - - # Verify URL contains punctuate parameter - expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true" - assert call_kwargs["url"] == expected_url - - # Verify response - assert response.text == "Hello, this is a test transcription." - - def test_transcription_with_diarize(self, mock_deepgram_response, test_audio_bytes): - """Test transcription with diarize=true parameter""" - - mock_response = MagicMock() - mock_response.json.return_value = mock_deepgram_response - mock_response.status_code = 200 - mock_response.headers = {"Content-Type": "application/json"} - - with patch( - "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", - return_value=mock_response, - ) as mock_post: - - response: TranscriptionResponse = litellm.transcription( - model="deepgram/nova-2", - file=test_audio_bytes, - api_key="test-api-key", - diarize=True, - ) - - # Verify the HTTP call was made - mock_post.assert_called_once() - call_kwargs = mock_post.call_args.kwargs - - # Verify URL contains diarize parameter - expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&diarize=true" - assert call_kwargs["url"] == expected_url - # Verify response assert response.text == "Hello, this is a test transcription." + assert hasattr(response, "_hidden_params") - def test_transcription_with_measurements(self, mock_deepgram_response, test_audio_bytes): - """Test transcription with measurements=true parameter""" - - mock_response = MagicMock() - mock_response.json.return_value = mock_deepgram_response - mock_response.status_code = 200 - mock_response.headers = {"Content-Type": "application/json"} - - with patch( - "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", - return_value=mock_response, - ) as mock_post: - - response: TranscriptionResponse = litellm.transcription( - model="deepgram/nova-2", - file=test_audio_bytes, - api_key="test-api-key", - measurements=True, - ) - - # Verify the HTTP call was made - mock_post.assert_called_once() - call_kwargs = mock_post.call_args.kwargs - - # Verify URL contains measurements parameter - expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&measurements=true" - assert call_kwargs["url"] == expected_url - - # Verify response - assert response.text == "Hello, this is a test transcription." + def test_transcription_with_custom_api_base( + self, mock_deepgram_response, test_audio_bytes + ): + """Test transcription with custom API base URL""" - def test_transcription_with_multiple_params(self, mock_deepgram_response, test_audio_bytes): - """Test transcription with multiple query parameters""" - mock_response = MagicMock() mock_response.json.return_value = mock_deepgram_response mock_response.status_code = 200 mock_response.headers = {"Content-Type": "application/json"} - - with patch( - "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", - return_value=mock_response, - ) as mock_post: - - response: TranscriptionResponse = litellm.transcription( - model="deepgram/nova-2", - file=test_audio_bytes, - api_key="test-api-key", - punctuate=True, - diarize=False, - measurements=True, - smart_format=True, - tier="enhanced", - ) - - # Verify the HTTP call was made - mock_post.assert_called_once() - call_kwargs = mock_post.call_args.kwargs - - # Verify URL contains all parameters - url = call_kwargs["url"] - assert "model=nova-2" in url - assert "punctuate=true" in url - assert "diarize=false" in url - assert "measurements=true" in url - assert "smart_format=true" in url - assert "tier=enhanced" in url - assert url.startswith("https://api.deepgram.com/v1/listen?") - - # Verify response - assert response.text == "Hello, this is a test transcription." - def test_transcription_with_language_param(self, mock_deepgram_response, test_audio_bytes): - """Test that language parameter is handled separately (not in URL)""" - - mock_response = MagicMock() - mock_response.json.return_value = mock_deepgram_response - mock_response.status_code = 200 - mock_response.headers = {"Content-Type": "application/json"} - with patch( "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", return_value=mock_response, ) as mock_post: - - response: TranscriptionResponse = litellm.transcription( - model="deepgram/nova-2", - file=test_audio_bytes, - api_key="test-api-key", - language="en", - punctuate=True, - ) - - # Verify the HTTP call was made - mock_post.assert_called_once() - call_kwargs = mock_post.call_args.kwargs - - # Verify URL contains punctuate but NOT language - expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true" - assert call_kwargs["url"] == expected_url - assert "language=" not in call_kwargs["url"] - - # Verify response - assert response.text == "Hello, this is a test transcription." - def test_transcription_with_custom_api_base(self, mock_deepgram_response, test_audio_bytes): - """Test transcription with custom API base URL""" - - mock_response = MagicMock() - mock_response.json.return_value = mock_deepgram_response - mock_response.status_code = 200 - mock_response.headers = {"Content-Type": "application/json"} - - with patch( - "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", - return_value=mock_response, - ) as mock_post: - response: TranscriptionResponse = litellm.transcription( model="deepgram/nova-2", file=test_audio_bytes, @@ -292,32 +242,36 @@ def test_transcription_with_custom_api_base(self, mock_deepgram_response, test_a api_base="https://custom.deepgram.com/v2", punctuate=True, ) - + # Verify the HTTP call was made mock_post.assert_called_once() call_kwargs = mock_post.call_args.kwargs - + # Verify custom API base is used - expected_url = "https://custom.deepgram.com/v2/listen?model=nova-2&punctuate=true" + expected_url = ( + "https://custom.deepgram.com/v2/listen?model=nova-2&punctuate=true" + ) assert call_kwargs["url"] == expected_url - + # Verify response assert response.text == "Hello, this is a test transcription." @pytest.mark.asyncio - async def test_async_transcription_with_params(self, mock_deepgram_response, test_audio_bytes): + async def test_async_transcription_with_params( + self, mock_deepgram_response, test_audio_bytes + ): """Test async transcription with query parameters""" - + mock_response = MagicMock() mock_response.json.return_value = mock_deepgram_response mock_response.status_code = 200 mock_response.headers = {"Content-Type": "application/json"} - + with patch( "litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post", return_value=mock_response, ) as mock_post: - + response: TranscriptionResponse = await litellm.atranscription( model="deepgram/nova-2", file=test_audio_bytes, @@ -326,11 +280,11 @@ async def test_async_transcription_with_params(self, mock_deepgram_response, tes diarize=True, measurements=False, ) - + # Verify the HTTP call was made mock_post.assert_called_once() call_kwargs = mock_post.call_args.kwargs - + # Verify URL contains all parameters url = call_kwargs["url"] assert "model=nova-2" in url @@ -338,77 +292,48 @@ async def test_async_transcription_with_params(self, mock_deepgram_response, tes assert "diarize=true" in url assert "measurements=false" in url assert url.startswith("https://api.deepgram.com/v1/listen?") - + # Verify headers assert "Authorization" in call_kwargs["headers"] assert call_kwargs["headers"]["Authorization"] == "Token test-api-key" - + # Verify response assert response.text == "Hello, this is a test transcription." - def test_transcription_with_file_object(self, mock_deepgram_response, test_audio_file): + def test_transcription_with_file_object( + self, mock_deepgram_response, test_audio_file + ): """Test transcription with file-like object""" - + mock_response = MagicMock() mock_response.json.return_value = mock_deepgram_response mock_response.status_code = 200 mock_response.headers = {"Content-Type": "application/json"} - + with patch( "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", return_value=mock_response, ) as mock_post: - + response: TranscriptionResponse = litellm.transcription( model="deepgram/nova-2", file=test_audio_file, api_key="test-api-key", punctuate=True, ) - + # Verify the HTTP call was made mock_post.assert_called_once() call_kwargs = mock_post.call_args.kwargs - + # Verify URL contains punctuate parameter - expected_url = "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true" + expected_url = ( + "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true" + ) assert call_kwargs["url"] == expected_url - + # Verify request data contains the audio bytes assert call_kwargs["data"] == b"fake_audio_data_for_testing" - + # Verify response assert response.text == "Hello, this is a test transcription." - - def test_transcription_boolean_conversion(self, mock_deepgram_response, test_audio_bytes): - """Test that boolean values are correctly converted to lowercase strings""" - - mock_response = MagicMock() - mock_response.json.return_value = mock_deepgram_response - mock_response.status_code = 200 - mock_response.headers = {"Content-Type": "application/json"} - - with patch( - "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", - return_value=mock_response, - ) as mock_post: - - response: TranscriptionResponse = litellm.transcription( - model="deepgram/nova-2", - file=test_audio_bytes, - api_key="test-api-key", - punctuate=True, # Should become "true" - diarize=False, # Should become "false" - ) - - # Verify the HTTP call was made - mock_post.assert_called_once() - call_kwargs = mock_post.call_args.kwargs - - # Verify URL contains correctly formatted boolean values - url = call_kwargs["url"] - assert "punctuate=true" in url # lowercase "true" - assert "diarize=false" in url # lowercase "false" - # Should not contain Python-style "True" or "False" - assert "True" not in url - assert "False" not in url \ No newline at end of file From fd674f33695e683dd3837bed1d13d28f20df5813 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 11 Jun 2025 12:31:21 -0700 Subject: [PATCH 05/13] test cleanup --- .../deepgram/test_deepgram_mock_transcription.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py b/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py index 7237220aa201..2a45a924c110 100644 --- a/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py +++ b/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py @@ -97,53 +97,45 @@ class TestDeepgramMockTranscription: """Test Deepgram transcription with mocked HTTP requests""" @pytest.mark.parametrize( - "optional_params,expected_url,test_id", + "optional_params,expected_url", [ # Basic transcription without parameters - ({}, "https://api.deepgram.com/v1/listen?model=nova-2", "basic"), + ({}, "https://api.deepgram.com/v1/listen?model=nova-2"), # Single parameters ( {"punctuate": True}, "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true", - "punctuate_true", ), ( {"diarize": True}, "https://api.deepgram.com/v1/listen?model=nova-2&diarize=true", - "diarize_true", ), ( {"measurements": True}, "https://api.deepgram.com/v1/listen?model=nova-2&measurements=true", - "measurements_true", ), ( {"diarize": False}, "https://api.deepgram.com/v1/listen?model=nova-2&diarize=false", - "diarize_false", ), # String parameters ( {"tier": "enhanced"}, "https://api.deepgram.com/v1/listen?model=nova-2&tier=enhanced", - "tier_enhanced", ), ( {"version": "latest"}, "https://api.deepgram.com/v1/listen?model=nova-2&version=latest", - "version_latest", ), # Language parameter should be excluded ( {"language": "en", "punctuate": True}, "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true", - "language_excluded", ), # Multiple parameters with boolean conversion ( {"punctuate": True, "diarize": False}, "https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true&diarize=false", - "boolean_conversion", ), # Multiple mixed parameters ( @@ -155,7 +147,6 @@ class TestDeepgramMockTranscription: "tier": "enhanced", }, None, - "multiple_params", ), # We'll check contains for this one since order may vary ], ) @@ -165,7 +156,6 @@ def test_transcription_url_generation( test_audio_bytes, optional_params, expected_url, - test_id, ): """Test transcription URL generation with various parameters""" @@ -207,7 +197,7 @@ def test_transcription_url_generation( else: assert ( actual_url == expected_url - ), f"Test {test_id}: Expected {expected_url}, got {actual_url}" + ), f"Expected {expected_url}, got {actual_url}" # Verify headers assert "Authorization" in call_kwargs["headers"] From 82474cdba6f1dbcdf14c71218e94d6daae71a0e9 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 11 Jun 2025 12:37:31 -0700 Subject: [PATCH 06/13] test fix get_complete_url --- .../audio_transcription/transformation.py | 10 +++- .../test_deepgram_mock_transcription.py | 50 ------------------- 2 files changed, 8 insertions(+), 52 deletions(-) diff --git a/litellm/llms/deepgram/audio_transcription/transformation.py b/litellm/llms/deepgram/audio_transcription/transformation.py index 866dc9a58a5c..9dd86795e30b 100644 --- a/litellm/llms/deepgram/audio_transcription/transformation.py +++ b/litellm/llms/deepgram/audio_transcription/transformation.py @@ -170,8 +170,14 @@ def get_complete_url( # This supports parameters like punctuate, diarize, measurements, etc. query_params = [] for key, value in optional_params.items(): - # Skip parameters that are not meant for URL query strings - # These are OpenAI-specific params that we handle differently + if value is None: + continue + if key in [ + "model", + "OPENAI_TRANSCRIPTION_PARAMS", + ]: + continue + if key in self.get_supported_openai_params(model): continue diff --git a/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py b/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py index 2a45a924c110..27a05199c93d 100644 --- a/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py +++ b/tests/test_litellm/llms/deepgram/test_deepgram_mock_transcription.py @@ -203,9 +203,6 @@ def test_transcription_url_generation( assert "Authorization" in call_kwargs["headers"] assert call_kwargs["headers"]["Authorization"] == "Token test-api-key" - # Verify request data is the audio bytes - assert call_kwargs["data"] == test_audio_bytes - # Verify response assert response.text == "Hello, this is a test transcription." assert hasattr(response, "_hidden_params") @@ -246,50 +243,6 @@ def test_transcription_with_custom_api_base( # Verify response assert response.text == "Hello, this is a test transcription." - @pytest.mark.asyncio - async def test_async_transcription_with_params( - self, mock_deepgram_response, test_audio_bytes - ): - """Test async transcription with query parameters""" - - mock_response = MagicMock() - mock_response.json.return_value = mock_deepgram_response - mock_response.status_code = 200 - mock_response.headers = {"Content-Type": "application/json"} - - with patch( - "litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post", - return_value=mock_response, - ) as mock_post: - - response: TranscriptionResponse = await litellm.atranscription( - model="deepgram/nova-2", - file=test_audio_bytes, - api_key="test-api-key", - punctuate=True, - diarize=True, - measurements=False, - ) - - # Verify the HTTP call was made - mock_post.assert_called_once() - call_kwargs = mock_post.call_args.kwargs - - # Verify URL contains all parameters - url = call_kwargs["url"] - assert "model=nova-2" in url - assert "punctuate=true" in url - assert "diarize=true" in url - assert "measurements=false" in url - assert url.startswith("https://api.deepgram.com/v1/listen?") - - # Verify headers - assert "Authorization" in call_kwargs["headers"] - assert call_kwargs["headers"]["Authorization"] == "Token test-api-key" - - # Verify response - assert response.text == "Hello, this is a test transcription." - def test_transcription_with_file_object( self, mock_deepgram_response, test_audio_file ): @@ -322,8 +275,5 @@ def test_transcription_with_file_object( ) assert call_kwargs["url"] == expected_url - # Verify request data contains the audio bytes - assert call_kwargs["data"] == b"fake_audio_data_for_testing" - # Verify response assert response.text == "Hello, this is a test transcription." From 4bf039a6c2c684e30b810218075a692e8c4ebbb2 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 11 Jun 2025 12:39:43 -0700 Subject: [PATCH 07/13] test rename file --- ...ion.py => test_deepgram_audio_transcription_transformation.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/test_litellm/llms/deepgram/audio_transcription/{test_deepseek_audio_transcription_transformation.py => test_deepgram_audio_transcription_transformation.py} (100%) diff --git a/tests/test_litellm/llms/deepgram/audio_transcription/test_deepseek_audio_transcription_transformation.py b/tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py similarity index 100% rename from tests/test_litellm/llms/deepgram/audio_transcription/test_deepseek_audio_transcription_transformation.py rename to tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py From 0b4d13a4c6c9667109d82ef89e50411a5514e783 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 11 Jun 2025 13:04:41 -0700 Subject: [PATCH 08/13] refactor deepgram URL construction --- .../audio_transcription/transformation.py | 104 ++++++++++++++---- 1 file changed, 82 insertions(+), 22 deletions(-) diff --git a/litellm/llms/deepgram/audio_transcription/transformation.py b/litellm/llms/deepgram/audio_transcription/transformation.py index 9dd86795e30b..97f3db238bd1 100644 --- a/litellm/llms/deepgram/audio_transcription/transformation.py +++ b/litellm/llms/deepgram/audio_transcription/transformation.py @@ -4,6 +4,7 @@ import io from typing import List, Optional, Union +from urllib.parse import urlencode, urlparse, urlunparse from httpx import Headers, Response @@ -163,36 +164,95 @@ def get_complete_url( ) api_base = api_base.rstrip("/") # Remove trailing slash if present - # Start with base URL and model parameter - url = f"{api_base}/listen?model={model}" + # Build query parameters including the model + all_query_params = {"model": model} + + # Add filtered optional parameters + additional_params = self._build_query_params(optional_params, model) + all_query_params.update(additional_params) + + # Construct URL with proper query string encoding + base_url = f"{api_base}/listen" + query_string = urlencode(all_query_params) + url = f"{base_url}?{query_string}" + + return url + + def _should_exclude_param( + self, + param_name: str, + model: str, + ) -> bool: + """ + Determines if a parameter should be excluded from the query string. + + Args: + param_name: Parameter name + model: Model name + + Returns: + True if the parameter should be excluded + """ + # Parameters that are handled elsewhere or not relevant to Deepgram API + excluded_params = { + "model", # Already in the URL path + "OPENAI_TRANSCRIPTION_PARAMS", # Internal litellm parameter + } + + # Skip if it's an excluded parameter + if param_name in excluded_params: + return True + + # Skip if it's an OpenAI-specific parameter that we handle separately + if param_name in self.get_supported_openai_params(model): + return True + + return False + + def _format_param_value(self, value) -> str: + """ + Formats a parameter value for use in query string. + + Args: + value: The parameter value to format + + Returns: + Formatted string value + """ + if isinstance(value, bool): + return str(value).lower() + return str(value) + + def _build_query_params(self, optional_params: dict, model: str) -> dict: + """ + Builds a dictionary of query parameters from optional_params. + + Args: + optional_params: Dictionary of optional parameters + model: Model name + + Returns: + Dictionary of filtered and formatted query parameters + """ + query_params = {} - # Add any additional query parameters from optional_params - # This supports parameters like punctuate, diarize, measurements, etc. - query_params = [] for key, value in optional_params.items(): + # Skip None values if value is None: continue - if key in [ - "model", - "OPENAI_TRANSCRIPTION_PARAMS", - ]: - continue - if key in self.get_supported_openai_params(model): + # Skip excluded parameters + if self._should_exclude_param( + param_name=key, + model=model, + ): continue - # Convert boolean values to lowercase strings - if isinstance(value, bool): - value = str(value).lower() - - # Add the parameter to query string - query_params.append(f"{key}={value}") + # Format and add the parameter + formatted_value = self._format_param_value(value) + query_params[key] = formatted_value - # Append query parameters to URL - if query_params: - url += "&" + "&".join(query_params) - - return url + return query_params def validate_environment( self, From dc5876aae5f0425630058e06ba359b4914ddf2cf Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 11 Jun 2025 13:13:22 -0700 Subject: [PATCH 09/13] add logging_obj.pre_call --- litellm/llms/custom_httpx/llm_http_handler.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index d1c68a6dccd5..fa0d4585923a 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -1038,6 +1038,17 @@ def audio_transcriptions( else: json_data = data + ## LOGGING + logging_obj.pre_call( + input=optional_params.get("query", ""), + api_key=api_key, + additional_args={ + "complete_input_dict": {}, + "api_base": complete_url, + "headers": headers, + }, + ) + try: # Make the POST request response = client.post( From 210b5db1c696eeb83639a706d2fd0ae8227b5cbb Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 11 Jun 2025 14:41:45 -0700 Subject: [PATCH 10/13] fix unused imports --- litellm/llms/deepgram/audio_transcription/transformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/deepgram/audio_transcription/transformation.py b/litellm/llms/deepgram/audio_transcription/transformation.py index 97f3db238bd1..0011196f4525 100644 --- a/litellm/llms/deepgram/audio_transcription/transformation.py +++ b/litellm/llms/deepgram/audio_transcription/transformation.py @@ -4,7 +4,7 @@ import io from typing import List, Optional, Union -from urllib.parse import urlencode, urlparse, urlunparse +from urllib.parse import urlencode from httpx import Headers, Response From dd69d5884c1c9bd064779168d4120a5a5d682741 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 11 Jun 2025 14:56:56 -0700 Subject: [PATCH 11/13] feat - add async deepgram support --- litellm/llms/custom_httpx/http_handler.py | 5 +- litellm/llms/custom_httpx/llm_http_handler.py | 191 +++++++++++++++--- 2 files changed, 169 insertions(+), 27 deletions(-) diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py index 87941aedea77..87eea27c4ad6 100644 --- a/litellm/llms/custom_httpx/http_handler.py +++ b/litellm/llms/custom_httpx/http_handler.py @@ -212,6 +212,7 @@ async def post( stream: bool = False, logging_obj: Optional[LiteLLMLoggingObject] = None, files: Optional[RequestFiles] = None, + content: Any = None, ): start_time = time.time() try: @@ -227,6 +228,7 @@ async def post( headers=headers, timeout=timeout, files=files, + content=content, ) response = await self.client.send(req, stream=stream) response.raise_for_status() @@ -452,6 +454,7 @@ async def single_connection_post_request( params: Optional[dict] = None, headers: Optional[dict] = None, stream: bool = False, + content: Any = None, ): """ Making POST request for a single connection client. @@ -459,7 +462,7 @@ async def single_connection_post_request( Used for retrying connection client errors. """ req = client.build_request( - "POST", url, data=data, json=json, params=params, headers=headers # type: ignore + "POST", url, data=data, json=json, params=params, headers=headers, content=content # type: ignore ) response = await client.send(req, stream=stream) response.raise_for_status() diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index fa0d4585923a..87c66f2e970f 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -982,28 +982,22 @@ async def arerank( request_data=request_data, ) - def audio_transcriptions( + def _prepare_audio_transcription_request( self, model: str, audio_file: FileTypes, optional_params: dict, litellm_params: dict, - model_response: TranscriptionResponse, - timeout: float, - max_retries: int, logging_obj: LiteLLMLoggingObj, api_key: Optional[str], api_base: Optional[str], - custom_llm_provider: str, - client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, - atranscription: bool = False, - headers: Optional[Dict[str, Any]] = None, - provider_config: Optional[BaseAudioTranscriptionConfig] = None, - ) -> TranscriptionResponse: - if provider_config is None: - raise ValueError( - f"No provider config found for model: {model} and provider: {custom_llm_provider}" - ) + headers: Optional[Dict[str, Any]], + provider_config: BaseAudioTranscriptionConfig, + ) -> tuple[dict, str, Optional[bytes], Optional[dict]]: + """ + Shared logic for preparing audio transcription requests. + Returns: (headers, complete_url, binary_data, json_data) + """ headers = provider_config.validate_environment( api_key=api_key, headers=headers or {}, @@ -1013,9 +1007,6 @@ def audio_transcriptions( litellm_params=litellm_params, ) - if client is None or not isinstance(client, HTTPHandler): - client = _get_httpx_client() - complete_url = provider_config.get_complete_url( api_base=api_base, api_key=api_key, @@ -1049,6 +1040,91 @@ def audio_transcriptions( }, ) + return headers, complete_url, binary_data, json_data + + def _transform_audio_transcription_response( + self, + provider_config: BaseAudioTranscriptionConfig, + model: str, + response: httpx.Response, + model_response: TranscriptionResponse, + logging_obj: LiteLLMLoggingObj, + optional_params: dict, + api_key: Optional[str], + ) -> TranscriptionResponse: + """Shared logic for transforming audio transcription responses.""" + if isinstance(provider_config, litellm.DeepgramAudioTranscriptionConfig): + return provider_config.transform_audio_transcription_response( + model=model, + raw_response=response, + model_response=model_response, + logging_obj=logging_obj, + request_data={}, + optional_params=optional_params, + litellm_params={}, + api_key=api_key, + ) + return model_response + + def audio_transcriptions( + self, + model: str, + audio_file: FileTypes, + optional_params: dict, + litellm_params: dict, + model_response: TranscriptionResponse, + timeout: float, + max_retries: int, + logging_obj: LiteLLMLoggingObj, + api_key: Optional[str], + api_base: Optional[str], + custom_llm_provider: str, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + atranscription: bool = False, + headers: Optional[Dict[str, Any]] = None, + provider_config: Optional[BaseAudioTranscriptionConfig] = None, + ) -> Union[TranscriptionResponse, Coroutine[Any, Any, TranscriptionResponse]]: + if provider_config is None: + raise ValueError( + f"No provider config found for model: {model} and provider: {custom_llm_provider}" + ) + + if atranscription is True: + return self.async_audio_transcriptions( # type: ignore + model=model, + audio_file=audio_file, + optional_params=optional_params, + litellm_params=litellm_params, + model_response=model_response, + timeout=timeout, + max_retries=max_retries, + logging_obj=logging_obj, + api_key=api_key, + api_base=api_base, + custom_llm_provider=custom_llm_provider, + client=client, + headers=headers, + provider_config=provider_config, + ) + + # Prepare the request + headers, complete_url, binary_data, json_data = ( + self._prepare_audio_transcription_request( + model=model, + audio_file=audio_file, + optional_params=optional_params, + litellm_params=litellm_params, + logging_obj=logging_obj, + api_key=api_key, + api_base=api_base, + headers=headers, + provider_config=provider_config, + ) + ) + + if client is None or not isinstance(client, HTTPHandler): + client = _get_httpx_client() + try: # Make the POST request response = client.post( @@ -1061,19 +1137,82 @@ def audio_transcriptions( except Exception as e: raise self._handle_error(e=e, provider_config=provider_config) - if isinstance(provider_config, litellm.DeepgramAudioTranscriptionConfig): - returned_response = provider_config.transform_audio_transcription_response( + return self._transform_audio_transcription_response( + provider_config=provider_config, + model=model, + response=response, + model_response=model_response, + logging_obj=logging_obj, + optional_params=optional_params, + api_key=api_key, + ) + + async def async_audio_transcriptions( + self, + model: str, + audio_file: FileTypes, + optional_params: dict, + litellm_params: dict, + model_response: TranscriptionResponse, + timeout: float, + max_retries: int, + logging_obj: LiteLLMLoggingObj, + api_key: Optional[str], + api_base: Optional[str], + custom_llm_provider: str, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + headers: Optional[Dict[str, Any]] = None, + provider_config: Optional[BaseAudioTranscriptionConfig] = None, + ) -> TranscriptionResponse: + if provider_config is None: + raise ValueError( + f"No provider config found for model: {model} and provider: {custom_llm_provider}" + ) + + # Prepare the request + headers, complete_url, binary_data, json_data = ( + self._prepare_audio_transcription_request( model=model, - raw_response=response, - model_response=model_response, - logging_obj=logging_obj, - request_data={}, + audio_file=audio_file, optional_params=optional_params, - litellm_params={}, + litellm_params=litellm_params, + logging_obj=logging_obj, api_key=api_key, + api_base=api_base, + headers=headers, + provider_config=provider_config, ) - return returned_response - return model_response + ) + + if client is None or not isinstance(client, AsyncHTTPHandler): + async_httpx_client = get_async_httpx_client( + llm_provider=litellm.LlmProviders(custom_llm_provider), + params={"ssl_verify": litellm_params.get("ssl_verify", None)}, + ) + else: + async_httpx_client = client + + try: + # Make the async POST request + response = await async_httpx_client.post( + url=complete_url, + headers=headers, + content=binary_data, + json=json_data, + timeout=timeout, + ) + except Exception as e: + raise self._handle_error(e=e, provider_config=provider_config) + + return self._transform_audio_transcription_response( + provider_config=provider_config, + model=model, + response=response, + model_response=model_response, + logging_obj=logging_obj, + optional_params=optional_params, + api_key=api_key, + ) async def async_anthropic_messages_handler( self, From 7a9bd2d74ecee4f608a976f223c3c9104bd32ab9 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 11 Jun 2025 15:34:50 -0700 Subject: [PATCH 12/13] test_audio_transcription_async --- .../base_audio_transcription_unit_tests.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/llm_translation/base_audio_transcription_unit_tests.py b/tests/llm_translation/base_audio_transcription_unit_tests.py index 689fb7dc1137..958a02d9f46f 100644 --- a/tests/llm_translation/base_audio_transcription_unit_tests.py +++ b/tests/llm_translation/base_audio_transcription_unit_tests.py @@ -52,6 +52,22 @@ def test_audio_transcription(self): assert transcript.text is not None + @pytest.mark.asyncio + async def test_audio_transcription_async(self): + """ + Test that the audio transcription is translated correctly. + """ + + litellm.set_verbose = True + litellm._turn_on_debug() + AUDIO_FILE = open(file_path, "rb") + transcription_call_args = self.get_base_audio_transcription_call_args() + transcript = await litellm.atranscription(**transcription_call_args, file=AUDIO_FILE) + print(f"transcript: {transcript.model_dump()}") + print(f"transcript hidden params: {transcript._hidden_params}") + + assert transcript.text is not None + def test_audio_transcription_optional_params(self): """ Test that the audio transcription is translated correctly. From 4ea18c4b60509a7283808ac04909b7b39ea6159f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 11 Jun 2025 15:50:32 -0700 Subject: [PATCH 13/13] fix python 3.8 test --- litellm/llms/custom_httpx/llm_http_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 87c66f2e970f..3a5490eee49c 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -993,7 +993,7 @@ def _prepare_audio_transcription_request( api_base: Optional[str], headers: Optional[Dict[str, Any]], provider_config: BaseAudioTranscriptionConfig, - ) -> tuple[dict, str, Optional[bytes], Optional[dict]]: + ) -> Tuple[dict, str, Optional[bytes], Optional[dict]]: """ Shared logic for preparing audio transcription requests. Returns: (headers, complete_url, binary_data, json_data)