diff --git a/litellm/llms/azure/audio_transcription/transformation.py b/litellm/llms/azure/audio_transcription/transformation.py index 1e3433f2499..cda4f238263 100644 --- a/litellm/llms/azure/audio_transcription/transformation.py +++ b/litellm/llms/azure/audio_transcription/transformation.py @@ -20,6 +20,7 @@ from litellm.types.llms.openai import ( AllMessageValues, OpenAIAudioTranscriptionOptionalParams, ) +from litellm.secret_managers.main import get_secret_str from litellm.types.utils import FileTypes, TranscriptionResponse @@ -68,6 +69,7 @@ class AzureSpeechAudioTranscriptionConfig(BaseAudioTranscriptionConfig): api_key: Optional[str] = None, api_base: Optional[str] = None, ) -> dict: + api_key = api_key or get_secret_str("AZURE_SPEECH_API_KEY") if not api_key: raise AzureSpeechAudioTranscriptionException( message="api_key is required for Azure AI Speech transcription.", @@ -91,6 +93,7 @@ class AzureSpeechAudioTranscriptionConfig(BaseAudioTranscriptionConfig): litellm_params: dict, stream: Optional[bool] = None, ) -> str: + api_base = api_base or get_secret_str("AZURE_SPEECH_API_BASE") if api_base is None: raise AzureSpeechAudioTranscriptionException( message=( @@ -158,6 +161,15 @@ class AzureSpeechAudioTranscriptionConfig(BaseAudioTranscriptionConfig): if self._is_stt_endpoint(hostname=hostname): return f"{parsed_url.scheme}://{hostname}" + if self._is_azure_openai_endpoint(hostname=hostname): + raise AzureSpeechAudioTranscriptionException( + message=( + "Azure AI Speech transcription requires a Cognitive Services " + "or STT Speech endpoint, not an Azure OpenAI endpoint." + ), + status_code=400, + ) + return api_base def _is_cognitive_services_endpoint(self, hostname: str) -> bool: @@ -170,6 +182,9 @@ class AzureSpeechAudioTranscriptionConfig(BaseAudioTranscriptionConfig): f".{self.STT_SPEECH_DOMAIN}" ) + def _is_azure_openai_endpoint(self, hostname: str) -> bool: + return hostname.endswith(".openai.azure.com") + def _extract_region_from_hostname(self, hostname: str, domain: str) -> str: if hostname.endswith(f".{domain}"): return hostname[: -len(f".{domain}")] diff --git a/litellm/main.py b/litellm/main.py index 9b75de3960a..520d61d1e8e 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -94,11 +94,7 @@ from litellm.litellm_core_utils.mock_functions import ( from litellm.litellm_core_utils.prompt_templates.common_utils import ( get_content_from_model_response, ) -from litellm.llms.base_llm import ( - BaseAudioTranscriptionConfig, - BaseConfig, - BaseImageGenerationConfig, -) +from litellm.llms.base_llm import BaseConfig, BaseImageGenerationConfig from litellm.llms.base_llm.base_model_iterator import ( convert_model_response_to_streaming, ) @@ -6442,65 +6438,6 @@ async def atranscription(*args, **kwargs) -> TranscriptionResponse: ) -def _azure_speech_audio_transcriptions( - model: str, - file: FileTypes, - optional_params: dict, - litellm_params_dict: dict, - model_response: TranscriptionResponse, - atranscription: bool, - client: Optional[ - Union[ - openai.AsyncOpenAI, - openai.OpenAI, - openai.AzureOpenAI, - openai.AsyncAzureOpenAI, - ] - ], - timeout: float, - max_retries: int, - litellm_logging_obj: LiteLLMLoggingObj, - api_base: Optional[str], - api_key: Optional[str], - custom_llm_provider: str, - provider_config: BaseAudioTranscriptionConfig, - shared_session: Optional["ClientSession"], -) -> Union[TranscriptionResponse, Coroutine[Any, Any, TranscriptionResponse]]: - api_base = api_base or litellm.api_base or get_secret_str("AZURE_API_BASE") - api_key = ( - api_key - or litellm.api_key - or litellm.azure_key - or get_secret_str("AZURE_API_KEY") - ) - - return base_llm_http_handler.audio_transcriptions( - model=model, - audio_file=file, - optional_params=optional_params, - litellm_params=litellm_params_dict, - model_response=model_response, - atranscription=atranscription, - client=( - client - if client is not None - and ( - isinstance(client, HTTPHandler) or isinstance(client, AsyncHTTPHandler) - ) - else None - ), - timeout=timeout, - max_retries=max_retries, - logging_obj=litellm_logging_obj, - api_base=api_base, - api_key=api_key, - custom_llm_provider=custom_llm_provider, - headers={}, - provider_config=provider_config, - shared_session=shared_session, - ) - - @client def transcription( model: str, @@ -6603,29 +6540,7 @@ def transcription( provider=LlmProviders(custom_llm_provider), ) - if ( - custom_llm_provider == "azure" - and provider_config is not None - and model.startswith("speech/") - ): - response = _azure_speech_audio_transcriptions( - model=model, - file=file, - optional_params=optional_params, - litellm_params_dict=litellm_params_dict, - model_response=model_response, - atranscription=atranscription, - client=client, - timeout=timeout, - max_retries=max_retries, - litellm_logging_obj=litellm_logging_obj, - api_base=api_base, - api_key=api_key, - custom_llm_provider=custom_llm_provider, - provider_config=provider_config, - shared_session=shared_session, - ) - elif custom_llm_provider == "azure": + if custom_llm_provider == "azure" and provider_config is None: # azure configs api_base = api_base or litellm.api_base or get_secret_str("AZURE_API_BASE") diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 708d8c51920..461d26b63bb 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -5652,7 +5652,7 @@ "source": "https://azure.microsoft.com/en-us/pricing/calculator/" }, "azure/speech/azure-stt": { - "input_cost_per_second": 0.0, + "input_cost_per_second": 0.0002777778, "litellm_provider": "azure", "mode": "audio_transcription", "output_cost_per_second": 0.0, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 01f76c745c5..215b55d4e2d 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -5652,7 +5652,7 @@ "source": "https://azure.microsoft.com/en-us/pricing/calculator/" }, "azure/speech/azure-stt": { - "input_cost_per_second": 0.0, + "input_cost_per_second": 0.0002777778, "litellm_provider": "azure", "mode": "audio_transcription", "output_cost_per_second": 0.0, diff --git a/tests/test_litellm/llms/azure/test_azure_speech_audio_transcription.py b/tests/test_litellm/llms/azure/test_azure_speech_audio_transcription.py index e743ad6f375..7192835c678 100644 --- a/tests/test_litellm/llms/azure/test_azure_speech_audio_transcription.py +++ b/tests/test_litellm/llms/azure/test_azure_speech_audio_transcription.py @@ -1,17 +1,20 @@ import io +import json +from pathlib import Path from unittest.mock import MagicMock import httpx import pytest import litellm +from litellm.llms.azure.audio_transcription.transformation import ( + AzureSpeechAudioTranscriptionConfig, + AzureSpeechAudioTranscriptionException, +) from litellm.llms.base_llm.audio_transcription.transformation import ( AudioTranscriptionRequestData, BaseAudioTranscriptionConfig, ) -from litellm.llms.azure.audio_transcription.transformation import ( - AzureSpeechAudioTranscriptionConfig, -) from litellm.types.utils import TranscriptionResponse from litellm.utils import ProviderConfigManager @@ -60,6 +63,48 @@ def test_azure_speech_audio_transcription_accepts_stt_endpoint_base(): ) +def test_azure_speech_audio_transcription_uses_dedicated_api_base_env(monkeypatch): + config = AzureSpeechAudioTranscriptionConfig() + + monkeypatch.setattr( + "litellm.llms.azure.audio_transcription.transformation.get_secret_str", + lambda key: ( + "https://centralus.api.cognitive.microsoft.com" + if key == "AZURE_SPEECH_API_BASE" + else None + ), + ) + + url = config.get_complete_url( + api_base=None, + api_key="test-key", + model="speech/azure-stt", + optional_params={}, + litellm_params={}, + ) + + assert ( + url + == "https://centralus.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1?language=en-US&format=simple" + ) + + +def test_azure_speech_audio_transcription_rejects_azure_openai_endpoint(): + config = AzureSpeechAudioTranscriptionConfig() + + with pytest.raises( + AzureSpeechAudioTranscriptionException, + match="not an Azure OpenAI endpoint", + ): + config.get_complete_url( + api_base="https://example.openai.azure.com", + api_key="test-key", + model="speech/azure-stt", + optional_params={}, + litellm_params={}, + ) + + def test_azure_speech_audio_transcription_validate_environment(): config = AzureSpeechAudioTranscriptionConfig() @@ -77,6 +122,26 @@ def test_azure_speech_audio_transcription_validate_environment(): assert headers["Accept"] == "application/json" +def test_azure_speech_audio_transcription_uses_dedicated_api_key_env(monkeypatch): + config = AzureSpeechAudioTranscriptionConfig() + + monkeypatch.setattr( + "litellm.llms.azure.audio_transcription.transformation.get_secret_str", + lambda key: "speech-key" if key == "AZURE_SPEECH_API_KEY" else None, + ) + + headers = config.validate_environment( + headers={}, + model="speech/azure-stt", + messages=[], + optional_params={}, + litellm_params={}, + api_key=None, + ) + + assert headers["Ocp-Apim-Subscription-Key"] == "speech-key" + + def test_azure_speech_audio_transcription_request_transform(): config = AzureSpeechAudioTranscriptionConfig() audio = io.BytesIO(b"RIFF....WAVE") @@ -143,3 +208,10 @@ def test_azure_speech_transcription_routes_through_provider_config(monkeypatch): AzureSpeechAudioTranscriptionConfig, ) assert audio_handler.call_args.kwargs["custom_llm_provider"] == "azure" + + +def test_azure_speech_stt_has_non_zero_input_pricing(): + pricing_path = Path(__file__).parents[4] / "model_prices_and_context_window.json" + pricing = json.loads(pricing_path.read_text()) + + assert pricing["azure/speech/azure-stt"]["input_cost_per_second"] > 0