diff --git a/docs/my-website/docs/providers/elevenlabs.md b/docs/my-website/docs/providers/elevenlabs.md
new file mode 100644
index 00000000000..e80ea534f55
--- /dev/null
+++ b/docs/my-website/docs/providers/elevenlabs.md
@@ -0,0 +1,231 @@
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# ElevenLabs
+
+ElevenLabs provides high-quality AI voice technology, including speech-to-text capabilities through their transcription API.
+
+| Property | Details |
+|----------|---------|
+| Description | ElevenLabs offers advanced AI voice technology with speech-to-text transcription capabilities that support multiple languages and speaker diarization. |
+| Provider Route on LiteLLM | `elevenlabs/` |
+| Provider Doc | [ElevenLabs API ↗](https://elevenlabs.io/docs/api-reference) |
+| Supported Endpoints | `/audio/transcriptions` |
+
+## Quick Start
+
+### LiteLLM Python SDK
+
+
+
+
+```python showLineNumbers title="Basic audio transcription with ElevenLabs"
+import litellm
+
+# Transcribe audio file
+with open("audio.mp3", "rb") as audio_file:
+ response = litellm.transcription(
+ model="elevenlabs/scribe_v1",
+ file=audio_file,
+ api_key="your-elevenlabs-api-key" # or set ELEVENLABS_API_KEY env var
+ )
+
+print(response.text)
+```
+
+
+
+
+
+```python showLineNumbers title="Audio transcription with advanced features"
+import litellm
+
+# Transcribe with speaker diarization and language specification
+with open("audio.wav", "rb") as audio_file:
+ response = litellm.transcription(
+ model="elevenlabs/scribe_v1",
+ file=audio_file,
+ language="en", # Language hint (maps to language_code)
+ temperature=0.3, # Control randomness in transcription
+ diarize=True, # Enable speaker diarization
+ api_key="your-elevenlabs-api-key"
+ )
+
+print(f"Transcription: {response.text}")
+print(f"Language: {response.language}")
+
+# Access word-level timestamps if available
+if hasattr(response, 'words') and response.words:
+ for word_info in response.words:
+ print(f"Word: {word_info['word']}, Start: {word_info['start']}, End: {word_info['end']}")
+```
+
+
+
+
+
+```python showLineNumbers title="Async audio transcription"
+import litellm
+import asyncio
+
+async def transcribe_audio():
+ with open("audio.mp3", "rb") as audio_file:
+ response = await litellm.atranscription(
+ model="elevenlabs/scribe_v1",
+ file=audio_file,
+ api_key="your-elevenlabs-api-key"
+ )
+
+ return response.text
+
+# Run async transcription
+result = asyncio.run(transcribe_audio())
+print(result)
+```
+
+
+
+
+### LiteLLM Proxy
+
+#### 1. Configure your proxy
+
+
+
+
+```yaml showLineNumbers title="ElevenLabs configuration in config.yaml"
+model_list:
+ - model_name: elevenlabs-transcription
+ litellm_params:
+ model: elevenlabs/scribe_v1
+ api_key: os.environ/ELEVENLABS_API_KEY
+
+general_settings:
+ master_key: your-master-key
+```
+
+
+
+
+
+```bash showLineNumbers title="Required environment variables"
+export ELEVENLABS_API_KEY="your-elevenlabs-api-key"
+export LITELLM_MASTER_KEY="your-master-key"
+```
+
+
+
+
+#### 2. Start the proxy
+
+```bash showLineNumbers title="Start LiteLLM proxy server"
+litellm --config config.yaml
+
+# Proxy will be available at http://localhost:4000
+```
+
+#### 3. Make transcription requests
+
+
+
+
+```bash showLineNumbers title="Audio transcription with curl"
+curl http://localhost:4000/v1/audio/transcriptions \
+ -H "Authorization: Bearer $LITELLM_API_KEY" \
+ -H "Content-Type: multipart/form-data" \
+ -F file="@audio.mp3" \
+ -F model="elevenlabs-transcription" \
+ -F language="en" \
+ -F temperature="0.3"
+```
+
+
+
+
+
+```python showLineNumbers title="Using OpenAI SDK with LiteLLM proxy"
+from openai import OpenAI
+
+# Initialize client with your LiteLLM proxy URL
+client = OpenAI(
+ base_url="http://localhost:4000",
+ api_key="your-litellm-api-key"
+)
+
+# Transcribe audio file
+with open("audio.mp3", "rb") as audio_file:
+ response = client.audio.transcriptions.create(
+ model="elevenlabs-transcription",
+ file=audio_file,
+ language="en",
+ temperature=0.3,
+ # ElevenLabs-specific parameters
+ diarize=True,
+ speaker_boost=True,
+ custom_vocabulary="technical,AI,machine learning"
+ )
+
+print(response.text)
+```
+
+
+
+
+
+```javascript showLineNumbers title="Audio transcription with JavaScript"
+import OpenAI from 'openai';
+import fs from 'fs';
+
+const openai = new OpenAI({
+ baseURL: 'http://localhost:4000',
+ apiKey: 'your-litellm-api-key'
+});
+
+async function transcribeAudio() {
+ const response = await openai.audio.transcriptions.create({
+ file: fs.createReadStream('audio.mp3'),
+ model: 'elevenlabs-transcription',
+ language: 'en',
+ temperature: 0.3,
+ diarize: true,
+ speaker_boost: true
+ });
+
+ console.log(response.text);
+}
+
+transcribeAudio();
+```
+
+
+
+
+## Response Format
+
+ElevenLabs returns transcription responses in OpenAI-compatible format:
+
+```json showLineNumbers title="Example transcription response"
+{
+ "text": "Hello, this is a sample transcription with multiple speakers.",
+ "task": "transcribe",
+ "language": "en",
+ "words": [
+ {
+ "word": "Hello",
+ "start": 0.0,
+ "end": 0.5
+ },
+ {
+ "word": "this",
+ "start": 0.5,
+ "end": 0.8
+ }
+ ]
+}
+```
+
+### Common Issues
+
+1. **Invalid API Key**: Ensure `ELEVENLABS_API_KEY` is set correctly
+
+
diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js
index 022ab3b3cfd..80c68fe7c59 100644
--- a/docs/my-website/sidebars.js
+++ b/docs/my-website/sidebars.js
@@ -415,6 +415,7 @@ const sidebars = {
"providers/groq",
"providers/github",
"providers/deepseek",
+ "providers/elevenlabs",
"providers/fireworks_ai",
"providers/clarifai",
"providers/vllm",
diff --git a/litellm/__init__.py b/litellm/__init__.py
index 1ba7d663d88..4ebb6fb936d 100644
--- a/litellm/__init__.py
+++ b/litellm/__init__.py
@@ -478,6 +478,7 @@ nscale_models: List = []
nebius_models: List = []
nebius_embedding_models: List = []
deepgram_models: List = []
+elevenlabs_models: List = []
def is_bedrock_pricing_only_model(key: str) -> bool:
@@ -651,6 +652,8 @@ def add_known_models():
featherless_ai_models.append(key)
elif value.get("litellm_provider") == "deepgram":
deepgram_models.append(key)
+ elif value.get("litellm_provider") == "elevenlabs":
+ elevenlabs_models.append(key)
add_known_models()
@@ -733,6 +736,7 @@ model_list = (
+ featherless_ai_models
+ nscale_models
+ deepgram_models
+ + elevenlabs_models
)
model_list_set = set(model_list)
@@ -797,6 +801,7 @@ models_by_provider: dict = {
"nscale": nscale_models,
"featherless_ai": featherless_ai_models,
"deepgram": deepgram_models,
+ "elevenlabs": elevenlabs_models,
}
# mapping for those models which have larger equivalents
diff --git a/litellm/litellm_core_utils/audio_utils/utils.py b/litellm/litellm_core_utils/audio_utils/utils.py
index 8018fe11537..fc0c8aca842 100644
--- a/litellm/litellm_core_utils/audio_utils/utils.py
+++ b/litellm/litellm_core_utils/audio_utils/utils.py
@@ -3,10 +3,110 @@ Utils used for litellm.transcription() and litellm.atranscription()
"""
import os
+from dataclasses import dataclass
+from litellm.types.files import get_file_mime_type_from_extension
from litellm.types.utils import FileTypes
+@dataclass
+class ProcessedAudioFile:
+ """
+ Processed audio file data.
+
+ Attributes:
+ file_content: The binary content of the audio file
+ filename: The filename (extracted or generated)
+ content_type: The MIME type of the audio file
+ """
+ file_content: bytes
+ filename: str
+ content_type: str
+
+
+def process_audio_file(audio_file: FileTypes) -> ProcessedAudioFile:
+ """
+ Common utility function to process audio files for audio transcription APIs.
+
+ Handles various input types:
+ - File paths (str, os.PathLike)
+ - Raw bytes/bytearray
+ - Tuples (filename, content, optional content_type)
+ - File-like objects with read() method
+
+ Args:
+ audio_file: The audio file input in various formats
+
+ Returns:
+ ProcessedAudioFile: Structured data with file content, filename, and content type
+
+ Raises:
+ ValueError: If audio_file type is unsupported or content cannot be extracted
+ """
+ file_content = None
+ filename = None
+
+ if isinstance(audio_file, (bytes, bytearray)):
+ # Raw bytes
+ filename = 'audio.wav'
+ file_content = bytes(audio_file)
+ elif isinstance(audio_file, (str, os.PathLike)):
+ # File path or PathLike
+ file_path = str(audio_file)
+ with open(file_path, 'rb') as f:
+ file_content = f.read()
+ filename = file_path.split('/')[-1]
+ elif isinstance(audio_file, tuple):
+ # Tuple format: (filename, content, content_type) or (filename, content)
+ if len(audio_file) >= 2:
+ filename = audio_file[0] or 'audio.wav'
+ content = audio_file[1]
+ if isinstance(content, (bytes, bytearray)):
+ file_content = bytes(content)
+ elif isinstance(content, (str, os.PathLike)):
+ # File path or PathLike
+ with open(str(content), 'rb') as f:
+ file_content = f.read()
+ elif hasattr(content, 'read'):
+ # File-like object
+ file_content = content.read()
+ if hasattr(content, 'seek'):
+ content.seek(0)
+ else:
+ raise ValueError(f"Unsupported content type in tuple: {type(content)}")
+ else:
+ raise ValueError("Tuple must have at least 2 elements: (filename, content)")
+ elif hasattr(audio_file, 'read') and not isinstance(audio_file, (str, bytes, bytearray, tuple, os.PathLike)):
+ # File-like object (IO) - check this after all other types
+ filename = getattr(audio_file, 'name', 'audio.wav')
+ file_content = audio_file.read() # type: ignore
+ # Reset file pointer if possible
+ if hasattr(audio_file, 'seek'):
+ audio_file.seek(0) # type: ignore
+ else:
+ raise ValueError(f"Unsupported audio_file type: {type(audio_file)}")
+
+ if file_content is None:
+ raise ValueError("Could not extract file content from audio_file")
+
+ # Determine content type using LiteLLM's file type utilities
+ content_type = 'audio/wav' # Default fallback
+ if filename:
+ try:
+ # Extract extension from filename
+ extension = filename.split('.')[-1].lower() if '.' in filename else 'wav'
+ content_type = get_file_mime_type_from_extension(extension)
+ except ValueError:
+ # If extension is not recognized, fallback to audio/wav
+ content_type = 'audio/wav'
+
+ return ProcessedAudioFile(
+ file_content=file_content,
+ filename=filename,
+ content_type=content_type
+ )
+
+
def get_audio_file_name(file_obj: FileTypes) -> str:
"""
Safely get the name of a file-like object or return its string representation.
diff --git a/litellm/litellm_core_utils/get_supported_openai_params.py b/litellm/litellm_core_utils/get_supported_openai_params.py
index 461b962dbc1..f1901fa2ce9 100644
--- a/litellm/litellm_core_utils/get_supported_openai_params.py
+++ b/litellm/litellm_core_utils/get_supported_openai_params.py
@@ -252,6 +252,16 @@ def get_supported_openai_params( # noqa: PLR0915
model=model
)
)
+ elif custom_llm_provider == "elevenlabs":
+ if request_type == "transcription":
+ from litellm.llms.elevenlabs.audio_transcription.transformation import (
+ ElevenLabsAudioTranscriptionConfig,
+ )
+ return (
+ ElevenLabsAudioTranscriptionConfig().get_supported_openai_params(
+ model=model
+ )
+ )
elif custom_llm_provider in litellm._custom_providers:
if request_type == "chat_completion":
provider_config = litellm.ProviderConfigManager.get_provider_chat_config(
diff --git a/litellm/llms/base_llm/audio_transcription/transformation.py b/litellm/llms/base_llm/audio_transcription/transformation.py
index cf88fed30d2..179b8d0fb02 100644
--- a/litellm/llms/base_llm/audio_transcription/transformation.py
+++ b/litellm/llms/base_llm/audio_transcription/transformation.py
@@ -1,5 +1,6 @@
from abc import ABC, abstractmethod
-from typing import TYPE_CHECKING, Any, List, Optional, Union
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
import httpx
@@ -8,7 +9,7 @@ from litellm.types.llms.openai import (
AllMessageValues,
OpenAIAudioTranscriptionOptionalParams,
)
-from litellm.types.utils import FileTypes, ModelResponse
+from litellm.types.utils import FileTypes, ModelResponse, TranscriptionResponse
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
@@ -18,6 +19,21 @@ else:
LiteLLMLoggingObj = Any
+@dataclass
+class AudioTranscriptionRequestData:
+ """
+ Structured data for audio transcription requests.
+
+ Attributes:
+ data: The request data (form data for multipart, json data for regular requests)
+ files: Optional files dict for multipart form data
+ content_type: Optional content type override
+ """
+ data: Union[dict, bytes]
+ files: Optional[dict] = None
+ content_type: Optional[str] = None
+
+
class BaseAudioTranscriptionConfig(BaseConfig, ABC):
@abstractmethod
def get_supported_openai_params(
@@ -50,11 +66,21 @@ class BaseAudioTranscriptionConfig(BaseConfig, ABC):
audio_file: FileTypes,
optional_params: dict,
litellm_params: dict,
- ) -> Union[dict, bytes]:
+ ) -> Union[AudioTranscriptionRequestData, Dict]:
raise NotImplementedError(
"AudioTranscriptionConfig needs a request transformation for audio transcription models"
)
+
+
+ def transform_audio_transcription_response(
+ self,
+ raw_response: httpx.Response,
+ ) -> TranscriptionResponse:
+ raise NotImplementedError(
+ "AudioTranscriptionConfig does not need a response transformation for audio transcription models"
+ )
+
def transform_request(
self,
model: str,
@@ -84,3 +110,65 @@ class BaseAudioTranscriptionConfig(BaseConfig, ABC):
raise NotImplementedError(
"AudioTranscriptionConfig does not need a response transformation for audio transcription models"
)
+
+
+ def get_provider_specific_params(
+ self,
+ model: str,
+ optional_params: dict,
+ openai_params: List[OpenAIAudioTranscriptionOptionalParams],
+ ) -> dict:
+ """
+ Get provider specific parameters that are not OpenAI compatible
+
+ eg. if user passes `diarize=True`, we need to pass `diarize` to the provider
+ but `diarize` is not an OpenAI parameter, so we need to handle it here
+ """
+ provider_specific_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
+
+ # Add the parameter to the provider specific params
+ provider_specific_params[key] = value
+
+ return provider_specific_params
+
+ 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
diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py
index dbabdaa1490..75013aea83c 100644
--- a/litellm/llms/custom_httpx/llm_http_handler.py
+++ b/litellm/llms/custom_httpx/llm_http_handler.py
@@ -1004,11 +1004,16 @@ class BaseLLMHTTPHandler:
api_base: Optional[str],
headers: Optional[Dict[str, Any]],
provider_config: BaseAudioTranscriptionConfig,
- ) -> Tuple[dict, str, Optional[bytes], Optional[dict]]:
+ ) -> Tuple[dict, str, Union[dict, bytes, None], Optional[dict]]:
"""
Shared logic for preparing audio transcription requests.
- Returns: (headers, complete_url, binary_data, json_data)
- """
+ Returns: (headers, complete_url, data, files)
+ """
+ # Handle the response based on type
+ from litellm.llms.base_llm.audio_transcription.transformation import (
+ AudioTranscriptionRequestData,
+ )
+
headers = provider_config.validate_environment(
api_key=api_key,
headers=headers or {},
@@ -1026,32 +1031,33 @@ class BaseLLMHTTPHandler:
litellm_params=litellm_params,
)
- # Handle the audio file based on type
- data = provider_config.transform_audio_transcription_request(
+ # Transform the request to get data
+ transformed_result = provider_config.transform_audio_transcription_request(
model=model,
audio_file=audio_file,
optional_params=optional_params,
litellm_params=litellm_params,
)
- binary_data: Optional[bytes] = None
- json_data: Optional[dict] = None
- if isinstance(data, bytes):
- binary_data = data
- else:
- json_data = data
+
+ # All providers now return AudioTranscriptionRequestData
+ if not isinstance(transformed_result, AudioTranscriptionRequestData):
+ raise ValueError(f"Provider {provider_config.__class__.__name__} must return AudioTranscriptionRequestData")
+
+ data = transformed_result.data
+ files = transformed_result.files
## LOGGING
logging_obj.pre_call(
input=optional_params.get("query", ""),
api_key=api_key,
additional_args={
- "complete_input_dict": {},
+ "complete_input_dict": data or {},
"api_base": complete_url,
"headers": headers,
},
)
- return headers, complete_url, binary_data, json_data
+ return headers, complete_url, data, files
def _transform_audio_transcription_response(
self,
@@ -1064,18 +1070,9 @@ class BaseLLMHTTPHandler:
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
+ return provider_config.transform_audio_transcription_response(
+ raw_response=response,
+ )
def audio_transcriptions(
self,
@@ -1122,8 +1119,8 @@ class BaseLLMHTTPHandler:
(
headers,
complete_url,
- binary_data,
- json_data,
+ data,
+ files,
) = self._prepare_audio_transcription_request(
model=model,
audio_file=audio_file,
@@ -1140,12 +1137,13 @@ class BaseLLMHTTPHandler:
client = _get_httpx_client()
try:
- # Make the POST request
+ # Make the POST request - clean and simple, always use data and files
response = client.post(
url=complete_url,
headers=headers,
- content=binary_data,
- json=json_data,
+ data=data,
+ files=files,
+ json=data if files is None and isinstance(data, dict) else None, # Use json param only when no files and data is dict
timeout=timeout,
)
except Exception as e:
@@ -1187,8 +1185,8 @@ class BaseLLMHTTPHandler:
(
headers,
complete_url,
- binary_data,
- json_data,
+ data,
+ files,
) = self._prepare_audio_transcription_request(
model=model,
audio_file=audio_file,
@@ -1210,12 +1208,13 @@ class BaseLLMHTTPHandler:
async_httpx_client = client
try:
- # Make the async POST request
+ # Make the async POST request - clean and simple, always use data and files
response = await async_httpx_client.post(
url=complete_url,
headers=headers,
- content=binary_data,
- json=json_data,
+ data=data,
+ files=files,
+ json=data if files is None and isinstance(data, dict) else None, # Use json param only when no files and data is dict
timeout=timeout,
)
except Exception as e:
diff --git a/litellm/llms/deepgram/audio_transcription/transformation.py b/litellm/llms/deepgram/audio_transcription/transformation.py
index 0011196f452..0cdfd734de7 100644
--- a/litellm/llms/deepgram/audio_transcription/transformation.py
+++ b/litellm/llms/deepgram/audio_transcription/transformation.py
@@ -2,12 +2,12 @@
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
+from litellm.litellm_core_utils.audio_utils.utils import process_audio_file
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import (
@@ -17,8 +17,8 @@ from litellm.types.llms.openai import (
from litellm.types.utils import FileTypes, TranscriptionResponse
from ...base_llm.audio_transcription.transformation import (
+ AudioTranscriptionRequestData,
BaseAudioTranscriptionConfig,
- LiteLLMLoggingObj,
)
from ..common_utils import DeepgramException
@@ -55,59 +55,31 @@ class DeepgramAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
audio_file: FileTypes,
optional_params: dict,
litellm_params: dict,
- ) -> Union[dict, bytes]:
+ ) -> AudioTranscriptionRequestData:
"""
- Processes the audio file input based on its type and returns the binary data.
+ Processes the audio file input based on its type and returns AudioTranscriptionRequestData.
+
+ For Deepgram, the binary audio data is sent directly as the request body.
Args:
audio_file: Can be a file path (str), a tuple (filename, file_content), or binary data (bytes).
Returns:
- The binary data of the audio file.
+ AudioTranscriptionRequestData with binary data and no files.
"""
- binary_data: bytes # Explicitly declare the type
-
- # Handle the audio file based on type
- if isinstance(audio_file, str):
- # If it's a file path
- with open(audio_file, "rb") as f:
- binary_data = f.read() # `f.read()` always returns `bytes`
- elif isinstance(audio_file, tuple):
- # Handle tuple case
- _, file_content = audio_file[:2]
- if isinstance(file_content, str):
- with open(file_content, "rb") as f:
- binary_data = f.read() # `f.read()` always returns `bytes`
- elif isinstance(file_content, bytes):
- binary_data = file_content
- else:
- raise TypeError(
- f"Unexpected type in tuple: {type(file_content)}. Expected str or bytes."
- )
- elif isinstance(audio_file, bytes):
- # Assume it's already binary data
- binary_data = audio_file
- elif isinstance(audio_file, io.BufferedReader) or isinstance(
- audio_file, io.BytesIO
- ):
- # Handle file-like objects
- binary_data = audio_file.read()
-
- else:
- raise TypeError(f"Unsupported type for audio_file: {type(audio_file)}")
-
- return binary_data
+ # Use common utility to process the audio file
+ processed_audio = process_audio_file(audio_file)
+
+ # Return structured data with binary content and no files
+ # For Deepgram, we send binary data directly as request body
+ return AudioTranscriptionRequestData(
+ data=processed_audio.file_content,
+ files=None
+ )
def transform_audio_transcription_response(
self,
- model: str,
raw_response: Response,
- model_response: TranscriptionResponse,
- logging_obj: LiteLLMLoggingObj,
- request_data: dict,
- optional_params: dict,
- litellm_params: dict,
- api_key: Optional[str] = None,
) -> TranscriptionResponse:
"""
Transforms the raw response from Deepgram to the TranscriptionResponse format
@@ -178,36 +150,6 @@ class DeepgramAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
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:
"""
@@ -235,19 +177,13 @@ class DeepgramAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
Dictionary of filtered and formatted query parameters
"""
query_params = {}
+ provider_specific_params = self.get_provider_specific_params(
+ optional_params=optional_params,
+ model=model,
+ openai_params=self.get_supported_openai_params(model)
+ )
- 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
-
+ for key, value in provider_specific_params.items():
# Format and add the parameter
formatted_value = self._format_param_value(value)
query_params[key] = formatted_value
diff --git a/litellm/llms/elevenlabs/audio_transcription/transformation.py b/litellm/llms/elevenlabs/audio_transcription/transformation.py
new file mode 100644
index 00000000000..e56e83b4dec
--- /dev/null
+++ b/litellm/llms/elevenlabs/audio_transcription/transformation.py
@@ -0,0 +1,197 @@
+"""
+Translates from OpenAI's `/v1/audio/transcriptions` to ElevenLabs's `/v1/speech-to-text`
+"""
+
+from typing import List, Optional, Union
+
+from httpx import Headers, Response
+
+import litellm
+from litellm.litellm_core_utils.audio_utils.utils import process_audio_file
+from litellm.llms.base_llm.chat.transformation import BaseLLMException
+from litellm.secret_managers.main import get_secret_str
+from litellm.types.llms.openai import (
+ AllMessageValues,
+ OpenAIAudioTranscriptionOptionalParams,
+)
+from litellm.types.utils import FileTypes, TranscriptionResponse
+
+from ...base_llm.audio_transcription.transformation import (
+ AudioTranscriptionRequestData,
+ BaseAudioTranscriptionConfig,
+)
+from ..common_utils import ElevenLabsException
+
+
+class ElevenLabsAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
+ @property
+ def custom_llm_provider(self) -> str:
+ return litellm.LlmProviders.ELEVENLABS.value
+
+ def get_supported_openai_params(
+ self, model: str
+ ) -> List[OpenAIAudioTranscriptionOptionalParams]:
+ return ["language", "temperature"]
+
+ def map_openai_params(
+ self,
+ non_default_params: dict,
+ optional_params: dict,
+ model: str,
+ drop_params: bool,
+ ) -> dict:
+ supported_params = self.get_supported_openai_params(model)
+ for k, v in non_default_params.items():
+ if k in supported_params:
+ if k == "language":
+ # Map OpenAI language format to ElevenLabs language_code
+ optional_params["language_code"] = v
+ else:
+ optional_params[k] = v
+ return optional_params
+
+ def get_error_class(
+ self, error_message: str, status_code: int, headers: Union[dict, Headers]
+ ) -> BaseLLMException:
+ return ElevenLabsException(
+ message=error_message, status_code=status_code, headers=headers
+ )
+
+ def transform_audio_transcription_request(
+ self,
+ model: str,
+ audio_file: FileTypes,
+ optional_params: dict,
+ litellm_params: dict,
+ ) -> AudioTranscriptionRequestData:
+ """
+ Transforms the audio transcription request for ElevenLabs API.
+
+ Returns AudioTranscriptionRequestData with both form data and files.
+
+ Returns:
+ AudioTranscriptionRequestData: Structured data with form data and files
+ """
+
+ # Use common utility to process the audio file
+ processed_audio = process_audio_file(audio_file)
+
+ # Prepare form data
+ form_data = {"model_id": model}
+
+
+ #########################################################
+ # Add OpenAI Compatible Parameters
+ #########################################################
+ for key, value in optional_params.items():
+ if key in self.get_supported_openai_params(model) and value is not None:
+ # Convert values to strings for form data, but skip None values
+ form_data[key] = str(value)
+
+ #########################################################
+ # Add Provider Specific Parameters
+ #########################################################
+ provider_specific_params = self.get_provider_specific_params(
+ model=model,
+ optional_params=optional_params,
+ openai_params=self.get_supported_openai_params(model)
+ )
+
+ for key, value in provider_specific_params.items():
+ form_data[key] = str(value)
+ #########################################################
+ #########################################################
+
+ # Prepare files
+ files = {"file": (processed_audio.filename, processed_audio.file_content, processed_audio.content_type)}
+
+ return AudioTranscriptionRequestData(
+ data=form_data,
+ files=files
+ )
+
+
+ def transform_audio_transcription_response(
+ self,
+ raw_response: Response,
+ ) -> TranscriptionResponse:
+ """
+ Transforms the raw response from ElevenLabs to the TranscriptionResponse format
+ """
+ try:
+ response_json = raw_response.json()
+
+ # Extract the main transcript text
+ text = response_json.get("text", "")
+
+ # Create TranscriptionResponse object
+ response = TranscriptionResponse(text=text)
+
+ # Add additional metadata matching OpenAI format
+ response["task"] = "transcribe"
+ response["language"] = response_json.get("language_code", "unknown")
+
+ # Map ElevenLabs words to OpenAI format
+ if "words" in response_json:
+ response["words"] = []
+ for word_data in response_json["words"]:
+ # Only include actual words, skip spacing and audio events
+ if word_data.get("type") == "word":
+ response["words"].append({
+ "word": word_data.get("text", ""),
+ "start": word_data.get("start", 0),
+ "end": word_data.get("end", 0)
+ })
+
+ # Store full response in hidden params
+ response._hidden_params = response_json
+
+ return response
+
+ except Exception as e:
+ raise ValueError(
+ f"Error transforming ElevenLabs response: {str(e)}\nResponse: {raw_response.text}"
+ )
+
+ def get_complete_url(
+ self,
+ api_base: Optional[str],
+ api_key: Optional[str],
+ model: str,
+ optional_params: dict,
+ litellm_params: dict,
+ stream: Optional[bool] = None,
+ ) -> str:
+ if api_base is None:
+ api_base = (
+ get_secret_str("ELEVENLABS_API_BASE") or "https://api.elevenlabs.io"
+ )
+ api_base = api_base.rstrip("/") # Remove trailing slash if present
+
+ # ElevenLabs speech-to-text endpoint
+ url = f"{api_base}/v1/speech-to-text"
+
+ return url
+
+ def validate_environment(
+ self,
+ headers: dict,
+ model: str,
+ messages: List[AllMessageValues],
+ optional_params: dict,
+ litellm_params: dict,
+ api_key: Optional[str] = None,
+ api_base: Optional[str] = None,
+ ) -> dict:
+ api_key = api_key or get_secret_str("ELEVENLABS_API_KEY")
+ if api_key is None:
+ raise ValueError(
+ "ElevenLabs API key is required. Set ELEVENLABS_API_KEY environment variable."
+ )
+
+ auth_header = {
+ "xi-api-key": api_key,
+ }
+
+ headers.update(auth_header)
+ return headers
\ No newline at end of file
diff --git a/litellm/llms/elevenlabs/common_utils.py b/litellm/llms/elevenlabs/common_utils.py
new file mode 100644
index 00000000000..c1421b619f3
--- /dev/null
+++ b/litellm/llms/elevenlabs/common_utils.py
@@ -0,0 +1,5 @@
+from litellm.llms.base_llm.chat.transformation import BaseLLMException
+
+
+class ElevenLabsException(BaseLLMException):
+ pass
\ No newline at end of file
diff --git a/litellm/llms/openai/transcriptions/handler.py b/litellm/llms/openai/transcriptions/handler.py
index c2747222fc0..4fe48dd3c6c 100644
--- a/litellm/llms/openai/transcriptions/handler.py
+++ b/litellm/llms/openai/transcriptions/handler.py
@@ -100,7 +100,7 @@ class OpenAIAudioTranscription(OpenAIChatCompletion):
litellm_params=litellm_params,
)
- if isinstance(data, bytes):
+ if not isinstance(data, dict):
raise ValueError("OpenAI transformation route requires a dict")
else:
data = {"model": model, "file": audio_file, **optional_params}
diff --git a/litellm/main.py b/litellm/main.py
index 2a2ed517ebe..1f45894d769 100644
--- a/litellm/main.py
+++ b/litellm/main.py
@@ -4937,7 +4937,7 @@ def transcription(
provider_config=provider_config,
litellm_params=litellm_params_dict,
)
- elif custom_llm_provider == "deepgram":
+ elif custom_llm_provider in [LlmProviders.DEEPGRAM.value, LlmProviders.ELEVENLABS.value]:
response = base_llm_http_handler.audio_transcriptions(
model=model,
audio_file=file,
@@ -4959,7 +4959,7 @@ def transcription(
logging_obj=litellm_logging_obj,
api_base=api_base,
api_key=api_key,
- custom_llm_provider="deepgram",
+ custom_llm_provider=custom_llm_provider,
headers={},
provider_config=provider_config,
)
diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json
index f45aa54da19..e6eb3372956 100644
--- a/litellm/model_prices_and_context_window_backup.json
+++ b/litellm/model_prices_and_context_window_backup.json
@@ -15704,5 +15704,35 @@
"metadata": {
"notes": "Deepgram's hosted OpenAI Whisper models - pricing may differ from native Deepgram models"
}
+ },
+ "elevenlabs/scribe_v1": {
+ "mode": "audio_transcription",
+ "input_cost_per_second": 0.0000611,
+ "output_cost_per_second": 0.0,
+ "litellm_provider": "elevenlabs",
+ "supported_endpoints": [
+ "/v1/audio/transcriptions"
+ ],
+ "source": "https://elevenlabs.io/pricing",
+ "metadata": {
+ "original_pricing_per_hour": 0.22,
+ "calculation": "$0.22/hour = $0.00366/minute = $0.0000611 per second (enterprise pricing)",
+ "notes": "ElevenLabs Scribe v1 - state-of-the-art speech recognition model with 99 language support"
+ }
+ },
+ "elevenlabs/scribe_v1_experimental": {
+ "mode": "audio_transcription",
+ "input_cost_per_second": 0.0000611,
+ "output_cost_per_second": 0.0,
+ "litellm_provider": "elevenlabs",
+ "supported_endpoints": [
+ "/v1/audio/transcriptions"
+ ],
+ "source": "https://elevenlabs.io/pricing",
+ "metadata": {
+ "original_pricing_per_hour": 0.22,
+ "calculation": "$0.22/hour = $0.00366/minute = $0.0000611 per second (enterprise pricing)",
+ "notes": "ElevenLabs Scribe v1 experimental - enhanced version of the main Scribe model"
+ }
}
}
\ No newline at end of file
diff --git a/litellm/proxy/_experimental/out/assets/logos/elevenlabs.png b/litellm/proxy/_experimental/out/assets/logos/elevenlabs.png
new file mode 100644
index 00000000000..634ddfa0542
Binary files /dev/null and b/litellm/proxy/_experimental/out/assets/logos/elevenlabs.png differ
diff --git a/litellm/types/utils.py b/litellm/types/utils.py
index 733335ef548..b28977e7e58 100644
--- a/litellm/types/utils.py
+++ b/litellm/types/utils.py
@@ -2300,6 +2300,7 @@ class LlmProviders(str, Enum):
NEBIUS = "nebius"
INFINITY = "infinity"
DEEPGRAM = "deepgram"
+ ELEVENLABS = "elevenlabs"
NOVITA = "novita"
AIOHTTP_OPENAI = "aiohttp_openai"
LANGFUSE = "langfuse"
diff --git a/litellm/utils.py b/litellm/utils.py
index 592c626aeeb..43f2b6c3f9f 100644
--- a/litellm/utils.py
+++ b/litellm/utils.py
@@ -6864,6 +6864,11 @@ class ProviderConfigManager:
return litellm.FireworksAIAudioTranscriptionConfig()
elif litellm.LlmProviders.DEEPGRAM == provider:
return litellm.DeepgramAudioTranscriptionConfig()
+ elif litellm.LlmProviders.ELEVENLABS == provider:
+ from litellm.llms.elevenlabs.audio_transcription.transformation import (
+ ElevenLabsAudioTranscriptionConfig,
+ )
+ return ElevenLabsAudioTranscriptionConfig()
elif litellm.LlmProviders.OPENAI == provider:
if "gpt-4o" in model:
return litellm.OpenAIGPTAudioTranscriptionConfig()
diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json
index f45aa54da19..e6eb3372956 100644
--- a/model_prices_and_context_window.json
+++ b/model_prices_and_context_window.json
@@ -15704,5 +15704,35 @@
"metadata": {
"notes": "Deepgram's hosted OpenAI Whisper models - pricing may differ from native Deepgram models"
}
+ },
+ "elevenlabs/scribe_v1": {
+ "mode": "audio_transcription",
+ "input_cost_per_second": 0.0000611,
+ "output_cost_per_second": 0.0,
+ "litellm_provider": "elevenlabs",
+ "supported_endpoints": [
+ "/v1/audio/transcriptions"
+ ],
+ "source": "https://elevenlabs.io/pricing",
+ "metadata": {
+ "original_pricing_per_hour": 0.22,
+ "calculation": "$0.22/hour = $0.00366/minute = $0.0000611 per second (enterprise pricing)",
+ "notes": "ElevenLabs Scribe v1 - state-of-the-art speech recognition model with 99 language support"
+ }
+ },
+ "elevenlabs/scribe_v1_experimental": {
+ "mode": "audio_transcription",
+ "input_cost_per_second": 0.0000611,
+ "output_cost_per_second": 0.0,
+ "litellm_provider": "elevenlabs",
+ "supported_endpoints": [
+ "/v1/audio/transcriptions"
+ ],
+ "source": "https://elevenlabs.io/pricing",
+ "metadata": {
+ "original_pricing_per_hour": 0.22,
+ "calculation": "$0.22/hour = $0.00366/minute = $0.0000611 per second (enterprise pricing)",
+ "notes": "ElevenLabs Scribe v1 experimental - enhanced version of the main Scribe model"
+ }
}
}
\ No newline at end of file
diff --git a/tests/llm_translation/test_elevenlabs.py b/tests/llm_translation/test_elevenlabs.py
new file mode 100644
index 00000000000..4227c3f3c62
--- /dev/null
+++ b/tests/llm_translation/test_elevenlabs.py
@@ -0,0 +1,111 @@
+import os
+import sys
+
+import pytest
+from unittest.mock import patch, MagicMock
+import httpx
+
+sys.path.insert(
+ 0, os.path.abspath("../..")
+) # Adds the parent directory to the system path
+import litellm
+from base_audio_transcription_unit_tests import BaseLLMAudioTranscriptionTest
+
+
+class TestElevenLabsAudioTranscription(BaseLLMAudioTranscriptionTest):
+ def get_base_audio_transcription_call_args(self) -> dict:
+ return {
+ "model": "elevenlabs/scribe_v1",
+ }
+
+ def get_custom_llm_provider(self) -> litellm.LlmProviders:
+ return litellm.LlmProviders.ELEVENLABS
+
+ def test_elevenlabs_diarize_parameter_passthrough(self):
+ """
+ Test that provider-specific parameters like diarize=True get passed through
+ to the ElevenLabs request form data.
+ """
+ # Mock successful response
+ mock_response = MagicMock()
+ mock_response.status_code = 200
+ mock_response.text = '{"text": "Four score and seven years ago", "language_code": "en"}'
+ mock_response.json.return_value = {
+ "text": "Four score and seven years ago",
+ "language_code": "en",
+ "words": [
+ {"type": "word", "text": "Four", "start": 0.0, "end": 0.5},
+ {"type": "word", "text": "score", "start": 0.5, "end": 1.0}
+ ]
+ }
+
+ # Create a mock audio file
+ audio_content = b"fake audio data"
+
+ captured_request_data = {}
+
+ def mock_post(*args, **kwargs):
+ # Capture the request data for verification
+ captured_request_data.update({
+ 'url': kwargs.get('url'),
+ 'data': kwargs.get('data'),
+ 'files': kwargs.get('files'),
+ 'headers': kwargs.get('headers'),
+ 'json': kwargs.get('json')
+ })
+ return mock_response
+
+ # Mock the HTTPHandler.post method which is what actually makes the request
+ from litellm.llms.custom_httpx.http_handler import HTTPHandler
+
+ with patch.object(HTTPHandler, 'post', side_effect=mock_post):
+ try:
+ result = litellm.transcription(
+ model="elevenlabs/scribe_v1",
+ file=audio_content,
+ diarize=True, # This should be passed through to the form data
+ language="en", # This should be mapped to language_code
+ temperature=0.5, # This should also be passed through
+ custom_param="test_value" # This should also be passed through
+ )
+
+ # Verify the request was made with correct form data
+ assert 'speech-to-text' in captured_request_data['url']
+
+ # Check that form data contains the expected parameters
+ form_data = captured_request_data['data']
+ assert form_data is not None, "Form data should not be None"
+
+ print(f"✅ Captured form data: {form_data}")
+
+ # Check basic required parameters
+ assert 'model_id' in form_data, "model_id should be in form data"
+ assert form_data['model_id'] == 'scribe_v1', f"Expected model_id 'scribe_v1', got {form_data['model_id']}"
+
+ # Check that diarize parameter is passed through
+ assert 'diarize' in form_data, f"diarize should be in form data. Got: {list(form_data.keys())}"
+ assert form_data['diarize'] == 'True', f"Expected diarize='True', got {form_data['diarize']}"
+
+ # Check that OpenAI language parameter is mapped correctly
+ assert 'language_code' in form_data, "language_code should be in form data"
+ assert form_data['language_code'] == 'en', f"Expected language_code='en', got {form_data['language_code']}"
+
+ # Check that temperature is passed through
+ assert 'temperature' in form_data, "temperature should be in form data"
+ assert form_data['temperature'] == '0.5', f"Expected temperature='0.5', got {form_data['temperature']}"
+
+ # Check that custom parameters are passed through
+ assert 'custom_param' in form_data, "custom_param should be in form data"
+ assert form_data['custom_param'] == 'test_value', f"Expected custom_param='test_value', got {form_data['custom_param']}"
+
+ # Check that files are included
+ files = captured_request_data['files']
+ assert files is not None, "Files should not be None"
+ assert 'file' in files, "file should be in files"
+
+ print("✅ All parameter passthrough tests passed!")
+
+ except Exception as e:
+ print(f"❌ Test failed: {e}")
+ print(f"Captured request data: {captured_request_data}")
+ raise
\ No newline at end of file
diff --git a/tests/test_litellm/litellm_core_utils/test_audio_utils.py b/tests/test_litellm/litellm_core_utils/test_audio_utils.py
new file mode 100644
index 00000000000..9efabdede0a
--- /dev/null
+++ b/tests/test_litellm/litellm_core_utils/test_audio_utils.py
@@ -0,0 +1,208 @@
+"""
+Test the audio utils functionality in litellm_core_utils/audio_utils/utils.py
+"""
+
+import io
+import os
+import tempfile
+from unittest.mock import mock_open, patch
+
+import pytest
+
+from litellm.litellm_core_utils.audio_utils.utils import (
+ ProcessedAudioFile,
+ get_audio_file_for_health_check,
+ get_audio_file_name,
+ process_audio_file,
+)
+
+
+class TestProcessAudioFile:
+ """Test the process_audio_file function with various input types"""
+
+ def test_process_bytes_input(self):
+ """Test processing raw bytes input"""
+ audio_data = b"fake audio data"
+ result = process_audio_file(audio_data)
+
+ assert isinstance(result, ProcessedAudioFile)
+ assert result.file_content == audio_data
+ assert result.filename == "audio.wav"
+ assert result.content_type == "audio/wav"
+
+ def test_process_bytearray_input(self):
+ """Test processing bytearray input"""
+ audio_data = bytearray(b"fake audio data")
+ result = process_audio_file(audio_data)
+
+ assert isinstance(result, ProcessedAudioFile)
+ assert result.file_content == bytes(audio_data)
+ assert result.filename == "audio.wav"
+ assert result.content_type == "audio/wav"
+
+ def test_process_file_path_input(self):
+ """Test processing file path input"""
+ test_content = b"test audio content"
+
+ with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as temp_file:
+ temp_file.write(test_content)
+ temp_file_path = temp_file.name
+
+ try:
+ result = process_audio_file(temp_file_path)
+
+ assert isinstance(result, ProcessedAudioFile)
+ assert result.file_content == test_content
+ assert result.filename == os.path.basename(temp_file_path)
+ assert result.content_type == "audio/mpeg" # .mp3 should map to audio/mpeg
+ finally:
+ os.unlink(temp_file_path)
+
+ def test_process_tuple_input_with_bytes(self):
+ """Test processing tuple input with bytes content"""
+ filename = "test.wav"
+ audio_data = b"fake audio data"
+ audio_tuple = (filename, audio_data)
+
+ result = process_audio_file(audio_tuple)
+
+ assert isinstance(result, ProcessedAudioFile)
+ assert result.file_content == audio_data
+ assert result.filename == filename
+ assert result.content_type == "audio/wav"
+
+ def test_process_tuple_input_with_file_path(self):
+ """Test processing tuple input with file path content"""
+ test_content = b"test audio content"
+
+ with tempfile.NamedTemporaryFile(suffix=".flac", delete=False) as temp_file:
+ temp_file.write(test_content)
+ temp_file_path = temp_file.name
+
+ try:
+ filename = "custom_name.flac"
+ audio_tuple = (filename, temp_file_path)
+
+ result = process_audio_file(audio_tuple)
+
+ assert isinstance(result, ProcessedAudioFile)
+ assert result.file_content == test_content
+ assert result.filename == filename
+ assert result.content_type == "audio/flac"
+ finally:
+ os.unlink(temp_file_path)
+
+ def test_process_file_like_object(self):
+ """Test processing file-like object input"""
+ test_content = b"test audio content"
+ file_obj = io.BytesIO(test_content)
+ file_obj.name = "test_audio.ogg"
+
+ result = process_audio_file(file_obj)
+
+ assert isinstance(result, ProcessedAudioFile)
+ assert result.file_content == test_content
+ assert result.filename == "test_audio.ogg"
+ assert result.content_type == "audio/ogg"
+
+ # Verify file pointer was reset
+ assert file_obj.tell() == 0
+
+ def test_process_file_like_object_without_name(self):
+ """Test processing file-like object without name attribute"""
+ test_content = b"test audio content"
+ file_obj = io.BytesIO(test_content)
+
+ result = process_audio_file(file_obj)
+
+ assert isinstance(result, ProcessedAudioFile)
+ assert result.file_content == test_content
+ assert result.filename == "audio.wav"
+ assert result.content_type == "audio/wav"
+
+ def test_process_tuple_with_file_like_object(self):
+ """Test processing tuple with file-like object as content"""
+ test_content = b"test audio content"
+ file_obj = io.BytesIO(test_content)
+
+ filename = "custom.mp3"
+ audio_tuple = (filename, file_obj)
+
+ result = process_audio_file(audio_tuple)
+
+ assert isinstance(result, ProcessedAudioFile)
+ assert result.file_content == test_content
+ assert result.filename == filename
+ assert result.content_type == "audio/mpeg"
+
+ # Verify file pointer was reset
+ assert file_obj.tell() == 0
+
+ def test_mime_type_detection_various_extensions(self):
+ """Test MIME type detection for various audio file extensions"""
+ test_cases = [
+ ("test.wav", "audio/wav"),
+ ("test.mp3", "audio/mpeg"),
+ ("test.flac", "audio/flac"),
+ ("test.ogg", "audio/ogg"),
+ ("test.aac", "audio/aac"),
+ ("test.m4a", "audio/x-m4a"),
+ ]
+
+ for filename, expected_mime_type in test_cases:
+ audio_tuple = (filename, b"fake content")
+ result = process_audio_file(audio_tuple)
+ assert result.content_type == expected_mime_type, f"Failed for {filename}"
+
+ def test_mime_type_fallback_for_unknown_extension(self):
+ """Test MIME type fallback for unknown file extensions"""
+ audio_tuple = ("test.unknown", b"fake content")
+ result = process_audio_file(audio_tuple)
+
+ assert result.content_type == "audio/wav" # Should fallback to default
+
+ def test_process_pathlike_object(self):
+ """Test processing os.PathLike object"""
+ test_content = b"test audio content"
+
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file:
+ temp_file.write(test_content)
+ temp_file_path = temp_file.name
+
+ try:
+ # Convert to pathlib.Path
+ from pathlib import Path
+ path_obj = Path(temp_file_path)
+
+ result = process_audio_file(path_obj)
+
+ assert isinstance(result, ProcessedAudioFile)
+ assert result.file_content == test_content
+ assert result.filename == os.path.basename(temp_file_path)
+ assert result.content_type == "audio/wav"
+ finally:
+ os.unlink(temp_file_path)
+
+ def test_invalid_input_type(self):
+ """Test that invalid input types raise ValueError"""
+ with pytest.raises(ValueError, match="Unsupported audio_file type"):
+ process_audio_file(123) # Invalid type
+
+ def test_invalid_tuple_length(self):
+ """Test that tuple with less than 2 elements raises ValueError"""
+ with pytest.raises(ValueError, match="Tuple must have at least 2 elements"):
+ process_audio_file(("only_one_element",))
+
+ def test_invalid_tuple_content_type(self):
+ """Test that tuple with unsupported content type raises ValueError"""
+ with pytest.raises(ValueError, match="Unsupported content type in tuple"):
+ process_audio_file(("filename", 123)) # Invalid content type
+
+ def test_tuple_with_none_filename(self):
+ """Test tuple with None filename gets default name"""
+ audio_tuple = (None, b"fake content")
+ result = process_audio_file(audio_tuple)
+
+ assert result.filename == "audio.wav"
+ assert result.content_type == "audio/wav"
+
diff --git a/tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py b/tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py
index af315ea751e..5ca94dd93fa 100644
--- a/tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py
+++ b/tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py
@@ -10,6 +10,9 @@ sys.path.insert(
) # Adds the parent directory to the system path
import litellm
+from litellm.llms.base_llm.audio_transcription.transformation import (
+ AudioTranscriptionRequestData,
+)
from litellm.llms.deepgram.audio_transcription.transformation import (
DeepgramAudioTranscriptionConfig,
)
@@ -49,12 +52,21 @@ def 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(
+ result = handler.transform_audio_transcription_request(
model="deepseek-audio-transcription",
audio_file=audio_file,
optional_params={},
litellm_params={},
)
+
+ # Check that result is AudioTranscriptionRequestData
+ assert isinstance(result, AudioTranscriptionRequestData)
+
+ # Check that data matches expected output
+ assert result.data == expected_output
+
+ # Check that files is None for Deepgram (binary data)
+ assert result.files is None
def test_get_complete_url_basic():
diff --git a/ui/litellm-dashboard/out/assets/logos/elevenlabs.png b/ui/litellm-dashboard/out/assets/logos/elevenlabs.png
new file mode 100644
index 00000000000..634ddfa0542
Binary files /dev/null and b/ui/litellm-dashboard/out/assets/logos/elevenlabs.png differ
diff --git a/ui/litellm-dashboard/public/assets/logos/elevenlabs.png b/ui/litellm-dashboard/public/assets/logos/elevenlabs.png
new file mode 100644
index 00000000000..634ddfa0542
Binary files /dev/null and b/ui/litellm-dashboard/public/assets/logos/elevenlabs.png differ
diff --git a/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx b/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx
index 05defef30a5..3c6784bf85a 100644
--- a/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx
@@ -266,6 +266,12 @@ const PROVIDER_CREDENTIAL_FIELDS: Record =
type: "password",
required: true
}],
+ [Providers.ElevenLabs]: [{
+ key: "api_key",
+ label: "API Key",
+ type: "password",
+ required: true
+ }],
[Providers.Google_AI_Studio]: [{
key: "api_key",
label: "API Key",
diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx
index 856525c6479..0e02a92770a 100644
--- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx
+++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx
@@ -27,7 +27,8 @@ export enum Providers {
Openrouter = "Openrouter",
FireworksAI = "Fireworks AI",
Triton = "Triton",
- Deepgram = "Deepgram"
+ Deepgram = "Deepgram",
+ ElevenLabs = "ElevenLabs"
}
@@ -57,7 +58,8 @@ export const provider_map: Record = {
Openrouter: "openrouter",
FireworksAI: "fireworks_ai",
Triton: "triton",
- Deepgram: "deepgram"
+ Deepgram: "deepgram",
+ ElevenLabs: "elevenlabs"
};
const asset_logos_folder = '/ui/assets/logos/';
@@ -88,7 +90,8 @@ export const providerLogoMap: Record = {
[Providers.Vertex_AI]: `${asset_logos_folder}google.svg`,
[Providers.xAI]: `${asset_logos_folder}xai.svg`,
[Providers.Triton]: `${asset_logos_folder}nvidia_triton.png`,
- [Providers.Deepgram]: `${asset_logos_folder}deepgram.png`
+ [Providers.Deepgram]: `${asset_logos_folder}deepgram.png`,
+ [Providers.ElevenLabs]: `${asset_logos_folder}elevenlabs.png`
};
export const getProviderLogoAndName = (providerValue: string): { logo: string, displayName: string } => {