From ebf6395bc1f105b0fd863649737081263f10fcd2 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 27 Jun 2025 17:50:49 -0700 Subject: [PATCH] [Feat] Add Eleven Labs - Speech To Text Support on LiteLLM (#12119) * add ELEVENLABS as a provider * add deepgram to main.py * add ElevenLabsException * add ElevenLabsAudioTranscriptionConfig * add transform_audio_transcription_response * TestElevenLabsAudioTranscription * add elevenlabs/scribe_v1 to model cost map * add ElevenLabsAudioTranscriptionConfig * add AudioTranscriptionRequestData * add ElevenLabs transform * use AudioTranscriptionRequestData * refactoring fixes * add ProcessedAudioFile util for reading audio files * test_elevenlabs_diarize_parameter_passthrough * docs eleven labs * docs fixes * fix code qa checks * fixes - audio transcription * ui - add ElevenLabs logo * add elevenlabs logo * docs - ElevenLabs * test fix elevenlabs --- docs/my-website/docs/providers/elevenlabs.md | 231 ++++++++++++++++++ docs/my-website/sidebars.js | 1 + litellm/__init__.py | 5 + .../litellm_core_utils/audio_utils/utils.py | 100 ++++++++ .../get_supported_openai_params.py | 10 + .../audio_transcription/transformation.py | 94 ++++++- litellm/llms/custom_httpx/llm_http_handler.py | 69 +++--- .../audio_transcription/transformation.py | 108 ++------ .../audio_transcription/transformation.py | 197 +++++++++++++++ litellm/llms/elevenlabs/common_utils.py | 5 + litellm/llms/openai/transcriptions/handler.py | 2 +- litellm/main.py | 4 +- ...odel_prices_and_context_window_backup.json | 30 +++ .../out/assets/logos/elevenlabs.png | Bin 0 -> 35410 bytes litellm/types/utils.py | 1 + litellm/utils.py | 5 + model_prices_and_context_window.json | 30 +++ tests/llm_translation/test_elevenlabs.py | 111 +++++++++ .../litellm_core_utils/test_audio_utils.py | 208 ++++++++++++++++ ...gram_audio_transcription_transformation.py | 14 +- .../out/assets/logos/elevenlabs.png | Bin 0 -> 35410 bytes .../public/assets/logos/elevenlabs.png | Bin 0 -> 35410 bytes .../add_model/provider_specific_fields.tsx | 6 + .../src/components/provider_info_helpers.tsx | 9 +- 24 files changed, 1109 insertions(+), 131 deletions(-) create mode 100644 docs/my-website/docs/providers/elevenlabs.md create mode 100644 litellm/llms/elevenlabs/audio_transcription/transformation.py create mode 100644 litellm/llms/elevenlabs/common_utils.py create mode 100644 litellm/proxy/_experimental/out/assets/logos/elevenlabs.png create mode 100644 tests/llm_translation/test_elevenlabs.py create mode 100644 tests/test_litellm/litellm_core_utils/test_audio_utils.py create mode 100644 ui/litellm-dashboard/out/assets/logos/elevenlabs.png create mode 100644 ui/litellm-dashboard/public/assets/logos/elevenlabs.png 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 0000000000000000000000000000000000000000..634ddfa0542fddf0e9c5bcd041dad1d51f260a10 GIT binary patch literal 35410 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 CnCjyI literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..634ddfa0542fddf0e9c5bcd041dad1d51f260a10 GIT binary patch literal 35410 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 CnCjyI literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..634ddfa0542fddf0e9c5bcd041dad1d51f260a10 GIT binary patch literal 35410 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 CnCjyI literal 0 HcmV?d00001 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 } => {