fix(azure_ai): route audio and realtime calls on Foundry hosts through the Azure OpenAI handlers

This commit is contained in:
mateo-berri 2026-09-02 11:05:38 -07:00
parent bf22e6cd4a
commit 7b942fd983
5 changed files with 87 additions and 4 deletions

View file

@ -7,6 +7,7 @@ from litellm.litellm_core_utils.env_utils import get_env_int, get_env_int_in_ran
DEFAULT_HEALTH_CHECK_PROMPT: Final = str(os.getenv("DEFAULT_HEALTH_CHECK_PROMPT", "test from litellm"))
AZURE_DEFAULT_RESPONSES_API_VERSION: Final = str(os.getenv("AZURE_DEFAULT_RESPONSES_API_VERSION", "preview"))
AZURE_OPENAI_AUDIO_PROVIDERS: Final = frozenset({"azure", "azure_ai"})
ROUTER_MAX_FALLBACKS: Final = int(os.getenv("ROUTER_MAX_FALLBACKS", 5))
ROUTER_FALLBACK_ERROR_DETAIL_MAX_CHARS: Final = 2000
DEFAULT_BATCH_SIZE: Final = int(os.getenv("DEFAULT_BATCH_SIZE", 512))

View file

@ -60,6 +60,7 @@ if TYPE_CHECKING:
from litellm.types.utils import TokenCountResponse
from litellm.constants import (
AZURE_OPENAI_AUDIO_PROVIDERS,
DEFAULT_MOCK_RESPONSE_COMPLETION_TOKEN_COUNT,
DEFAULT_MOCK_RESPONSE_PROMPT_TOKEN_COUNT,
)
@ -7770,7 +7771,7 @@ def transcription(
provider=LlmProviders(custom_llm_provider),
)
if custom_llm_provider == "azure" and provider_config is None:
if custom_llm_provider in AZURE_OPENAI_AUDIO_PROVIDERS and provider_config is None:
# azure configs
api_base = api_base or litellm.api_base or get_secret_str("AZURE_API_BASE")
@ -8057,7 +8058,10 @@ def speech(
custom_llm_provider=custom_llm_provider,
)
response: HttpxBinaryResponseContent | Coroutine[object, object, HttpxBinaryResponseContent] | None = None
if custom_llm_provider == "openai" or custom_llm_provider in litellm.openai_compatible_providers:
if custom_llm_provider == "openai" or (
custom_llm_provider in litellm.openai_compatible_providers
and custom_llm_provider not in AZURE_OPENAI_AUDIO_PROVIDERS
):
if voice is None or not (isinstance(voice, str)):
raise litellm.BadRequestError(
message="'voice' is required to be passed as a string for OpenAI TTS",
@ -8111,7 +8115,7 @@ def speech(
aspeech=aspeech,
shared_session=shared_session,
)
elif custom_llm_provider == "azure":
elif custom_llm_provider in AZURE_OPENAI_AUDIO_PROVIDERS:
# Check if this is Azure Speech Service (Cognitive Services TTS)
if model.startswith("speech/"):
from litellm.llms.azure.text_to_speech.transformation import (

View file

@ -8,6 +8,7 @@ from typing import Any, Final, Literal, cast
import litellm
from litellm.constants import (
AZURE_OPENAI_AUDIO_PROVIDERS,
REALTIME_CREDENTIAL_RESOLUTION_TIMEOUT_SECONDS,
REALTIME_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES,
request_timeout,
@ -400,7 +401,7 @@ async def _arealtime(
litellm_metadata=_build_litellm_metadata(kwargs),
query_params=query_params,
)
elif _custom_llm_provider == "azure":
elif _custom_llm_provider in AZURE_OPENAI_AUDIO_PROVIDERS:
api_base = dynamic_api_base or litellm_params.api_base or litellm.api_base or get_secret_str("AZURE_API_BASE")
# set API KEY
api_key = dynamic_api_key or litellm.api_key or litellm.openai_key or get_secret_str("AZURE_API_KEY")

View file

@ -1,6 +1,7 @@
import asyncio
import time
from types import TracebackType
from typing import Final
from unittest.mock import MagicMock, patch
@ -294,3 +295,39 @@ async def test_azure_health_check_honors_deployment_realtime_protocol():
model_params={"realtime_protocol": "GA"},
)
assert connect.url == "wss://my-endpoint.openai.azure.com/openai/v1/realtime?model=gpt-4o-realtime-preview"
class _ConnectThatStopsAfterCapturingTheUrl:
url: str | None = None
def __call__(self, url: str, **kwargs: object) -> "_ConnectThatStopsAfterCapturingTheUrl":
self.url = url
return self
async def __aenter__(self) -> None:
raise RuntimeError("backend url captured, nothing to bridge")
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
return None
@pytest.mark.asyncio
async def test_arealtime_azure_ai_on_a_foundry_host_connects_to_the_azure_openai_realtime_route():
connect: Final = _ConnectThatStopsAfterCapturingTheUrl()
with patch("websockets.connect", connect):
await realtime_main._arealtime.__wrapped__(
model="azure_ai/gpt-realtime-mini",
websocket=MagicMock(),
api_base="https://my-project.services.ai.azure.com",
api_key="fake-key",
litellm_logging_obj=FakeLogging(),
)
assert connect.url == (
"wss://my-project.services.ai.azure.com/openai/realtime"
"?api-version=2024-10-01-preview&deployment=gpt-realtime-mini"
)

View file

@ -3181,3 +3181,43 @@ def test_stream_chunk_builder_defers_cost_to_logging_obj_when_usage_cost_absent(
assert response is not None
assert response._hidden_params.get("response_cost") is None
FOUNDRY_HOST: Final = "https://my-project.services.ai.azure.com"
def test_azure_ai_transcription_on_a_foundry_host_uses_the_azure_openai_deployment_route(
respx_mock: respx.MockRouter,
):
route: Final = respx_mock.post(
url__regex=r"https://my-project\.services\.ai\.azure\.com/openai/deployments/whisper-1/audio/transcriptions\?api-version=.+"
).mock(return_value=httpx.Response(200, json={"text": "hello"}))
response: Final = litellm.transcription(
model="azure_ai/whisper-1",
file=("tone.wav", b"RIFF\x00\x00\x00\x00WAVE", "audio/wav"),
api_base=FOUNDRY_HOST,
api_key="fake-key",
)
assert route.called
assert response.text == "hello"
def test_azure_ai_speech_on_a_foundry_host_uses_the_azure_openai_deployment_route(
respx_mock: respx.MockRouter,
):
route: Final = respx_mock.post(
url__regex=r"https://my-project\.services\.ai\.azure\.com/openai/deployments/tts-1/audio/speech\?api-version=.+"
).mock(return_value=httpx.Response(200, content=b"mp3-bytes"))
response: Final = litellm.speech(
model="azure_ai/tts-1",
input="hello",
voice="alloy",
api_base=FOUNDRY_HOST,
api_key="fake-key",
)
assert route.called
assert response.content == b"mp3-bytes"