[Feat] Add provider specific params for deepgram/ (#11638)

* Checkpoint before follow-up message

* Add comprehensive tests for Deepgram transcription functionality

* clean up transform

* just use 1 test

* test cleanup

* test fix get_complete_url

* test rename file

* refactor deepgram URL construction

* add logging_obj.pre_call

* fix unused imports

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Ishaan Jaff 2025-06-11 15:36:45 -07:00 • committed by GitHub
parent f3f7bc1537
commit 0cb7dd7577
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 575 additions and 61 deletions

View file

@ -1038,6 +1038,17 @@ class BaseLLMHTTPHandler:
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(

View file

@ -4,6 +4,7 @@ Translates from OpenAI's `/v1/audio/transcriptions` to Deepgram's `/v1/listen`
import io
from typing import List, Optional, Union
from urllib.parse import urlencode
from httpx import Headers, Response
@ -126,9 +127,9 @@ class DeepgramAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
# 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
@ -163,7 +164,95 @@ class DeepgramAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
)
api_base = api_base.rstrip("/") # Remove trailing slash if present
return 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 = {}
for key, value in optional_params.items():
# Skip None values
if value is None:
continue
# Skip excluded parameters
if self._should_exclude_param(
param_name=key,
model=model,
):
continue
# Format and add the parameter
formatted_value = self._format_param_value(value)
query_params[key] = formatted_value
return query_params
def validate_environment(
self,

View file

@ -0,0 +1,192 @@
import io
import os
import pathlib
import sys
import pytest
sys.path.insert(
0, os.path.abspath("../../../../..")
) # Adds the parent directory to the system path
import litellm
from litellm.llms.deepgram.audio_transcription.transformation import (
DeepgramAudioTranscriptionConfig,
)
@pytest.fixture
def test_bytes():
return b"litellm", b"litellm"
@pytest.fixture
def test_io_bytes(test_bytes):
return io.BytesIO(test_bytes[0]), test_bytes[1]
@pytest.fixture
def test_file():
pwd = os.path.dirname(os.path.realpath(__file__))
pwd_path = pathlib.Path(pwd)
test_root = pwd_path.parents[3]
print(f"test_root: {test_root}")
file_path = os.path.join(test_root, "gettysburg.wav")
f = open(file_path, "rb")
content = f.read()
f.seek(0)
return f, content
@pytest.mark.parametrize(
"fixture_name",
[
"test_bytes",
"test_io_bytes",
"test_file",
],
)
def test_audio_file_handling(fixture_name, request):
handler = DeepgramAudioTranscriptionConfig()
(audio_file, expected_output) = request.getfixturevalue(fixture_name)
assert expected_output == handler.transform_audio_transcription_request(
model="deepseek-audio-transcription",
audio_file=audio_file,
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?")

View file

@ -1,57 +0,0 @@
import io
import os
import pathlib
import sys
import pytest
sys.path.insert(
0, os.path.abspath("../../../../..")
) # Adds the parent directory to the system path
import litellm
from litellm.llms.deepgram.audio_transcription.transformation import (
DeepgramAudioTranscriptionConfig,
)
@pytest.fixture
def test_bytes():
return b"litellm", b"litellm"
@pytest.fixture
def test_io_bytes(test_bytes):
return io.BytesIO(test_bytes[0]), test_bytes[1]
@pytest.fixture
def test_file():
pwd = os.path.dirname(os.path.realpath(__file__))
pwd_path = pathlib.Path(pwd)
test_root = pwd_path.parents[3]
print(f"test_root: {test_root}")
file_path = os.path.join(test_root, "gettysburg.wav")
f = open(file_path, "rb")
content = f.read()
f.seek(0)
return f, content
@pytest.mark.parametrize(
"fixture_name",
[
"test_bytes",
"test_io_bytes",
"test_file",
],
)
def test_audio_file_handling(fixture_name, request):
handler = DeepgramAudioTranscriptionConfig()
(audio_file, expected_output) = request.getfixturevalue(fixture_name)
assert expected_output == handler.transform_audio_transcription_request(
model="deepseek-audio-transcription",
audio_file=audio_file,
optional_params={},
litellm_params={},
)

View file

@ -0,0 +1,279 @@
import io
import json
import os
import sys
from typing import Any
from unittest.mock import MagicMock, patch
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"""
@pytest.mark.parametrize(
"optional_params,expected_url",
[
# Basic transcription without parameters
({}, "https://api.deepgram.com/v1/listen?model=nova-2"),
# Single parameters
(
{"punctuate": True},
"https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true",
),
(
{"diarize": True},
"https://api.deepgram.com/v1/listen?model=nova-2&diarize=true",
),
(
{"measurements": True},
"https://api.deepgram.com/v1/listen?model=nova-2&measurements=true",
),
(
{"diarize": False},
"https://api.deepgram.com/v1/listen?model=nova-2&diarize=false",
),
# String parameters
(
{"tier": "enhanced"},
"https://api.deepgram.com/v1/listen?model=nova-2&tier=enhanced",
),
(
{"version": "latest"},
"https://api.deepgram.com/v1/listen?model=nova-2&version=latest",
),
# Language parameter should be excluded
(
{"language": "en", "punctuate": True},
"https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true",
),
# Multiple parameters with boolean conversion
(
{"punctuate": True, "diarize": False},
"https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true&diarize=false",
),
# Multiple mixed parameters
(
{
"punctuate": True,
"diarize": False,
"measurements": True,
"smart_format": True,
"tier": "enhanced",
},
None,
), # 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 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
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"Expected {expected_url}, got {actual_url}"
# 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."
assert hasattr(response, "_hidden_params")
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."
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 response
assert response.text == "Hello, this is a test transcription."