fix(realtime): dial Azure's GA realtime upstream for GA clients

Azure realtime defaulted to the beta upstream whenever realtime_protocol
was not configured, so a GA client's session.update (session.type,
output_modalities, nested audio) was forwarded unchanged to
/openai/realtime and Azure rejected it with "Unknown parameter:
'session.type'" on gpt-realtime and gpt-realtime-1.5. The unset default
now follows the client the way the OpenAI handler already does: a client
that sends OpenAI-Beta: realtime=v1 keeps the beta upstream, any other
client gets /openai/v1/realtime. An explicit realtime_protocol in
litellm_params or LITELLM_AZURE_REALTIME_PROTOCOL still wins.
This commit is contained in:
mateo-berri 2026-09-10 13:18:52 -07:00
parent 692a311efb
commit 8deb465346
2 changed files with 74 additions and 7 deletions

View file

@ -14,6 +14,7 @@ from litellm.constants import (
request_timeout,
)
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
from litellm.litellm_core_utils.realtime_streaming import client_sent_openai_beta_realtime_header
from litellm.llms.base_llm.realtime.transformation import BaseRealtimeConfig
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
from litellm.llms.xai.common_utils import XAIModelInfo
@ -413,14 +414,14 @@ async def _arealtime(
api_version = api_version or litellm_params.api_version or "2024-10-01-preview"
realtime_protocol = (
configured_realtime_protocol: Final = (
kwargs.get("realtime_protocol")
or litellm_params.get("realtime_protocol")
or os.environ.get("LITELLM_AZURE_REALTIME_PROTOCOL")
)
if realtime_protocol is None and (query_params or {}).get("intent") == "transcription":
realtime_protocol = "GA"
realtime_protocol = realtime_protocol or "beta"
realtime_protocol: Final = _azure_realtime_protocol_for_client(
configured_realtime_protocol, query_params=query_params, websocket=websocket
)
resolved_azure_ad_token: Final = (
None if api_key else get_azure_ad_token(GenericLiteLLMParams(**kwargs, azure_ad_token=azure_ad_token))
)
@ -576,6 +577,19 @@ def _is_transcription_only_realtime_model(model: str, custom_llm_provider: str)
_TRANSCRIPTION_QUERY_PARAMS: Final[RealtimeQueryParams] = {"intent": "transcription"}
def _azure_realtime_protocol_for_client(
configured_protocol: object,
*,
query_params: RealtimeQueryParams | None,
websocket: "WebSocket",
) -> str:
if isinstance(configured_protocol, str) and configured_protocol:
return configured_protocol
if (query_params or {}).get("intent") == "transcription":
return "GA"
return "beta" if client_sent_openai_beta_realtime_header(websocket) else "GA"
def _azure_realtime_health_protocol(
model: str, realtime_protocol: str | None, model_params: Mapping[str, object]
) -> tuple[str, RealtimeQueryParams | None]:

View file

@ -327,7 +327,60 @@ async def test_arealtime_azure_ai_on_a_foundry_host_connects_to_the_azure_openai
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"
assert connect.url == "wss://my-project.services.ai.azure.com/openai/v1/realtime?model=gpt-realtime-mini"
class _ClientWebSocketWithHeaders:
def __init__(self, headers: tuple[tuple[bytes, bytes], ...]) -> None:
self.scope: Final = {"headers": headers}
_GA_CLIENT: Final = _ClientWebSocketWithHeaders(headers=())
_BETA_CLIENT: Final = _ClientWebSocketWithHeaders(headers=((b"openai-beta", b"realtime=v1"),))
async def _azure_backend_url_dialed_for(websocket: _ClientWebSocketWithHeaders, **kwargs: object) -> str | None:
connect: Final = _ConnectThatStopsAfterCapturingTheUrl()
with patch("websockets.connect", connect):
await realtime_main._arealtime.__wrapped__(
model="azure/gpt-realtime",
websocket=websocket,
api_base="https://my-endpoint.openai.azure.com",
api_key="fake-key",
litellm_logging_obj=FakeLogging(),
**kwargs,
)
return connect.url
@pytest.mark.asyncio
async def test_arealtime_azure_ga_client_without_beta_header_dials_the_ga_upstream(monkeypatch):
monkeypatch.delenv("LITELLM_AZURE_REALTIME_PROTOCOL", raising=False)
assert (
await _azure_backend_url_dialed_for(_GA_CLIENT)
== "wss://my-endpoint.openai.azure.com/openai/v1/realtime?model=gpt-realtime"
)
@pytest.mark.asyncio
async def test_arealtime_azure_beta_header_client_keeps_the_beta_upstream(monkeypatch):
monkeypatch.delenv("LITELLM_AZURE_REALTIME_PROTOCOL", raising=False)
assert await _azure_backend_url_dialed_for(_BETA_CLIENT) == (
"wss://my-endpoint.openai.azure.com/openai/realtime?api-version=2024-10-01-preview&deployment=gpt-realtime"
)
@pytest.mark.asyncio
async def test_arealtime_azure_explicit_beta_protocol_wins_over_a_ga_client(monkeypatch):
monkeypatch.delenv("LITELLM_AZURE_REALTIME_PROTOCOL", raising=False)
assert await _azure_backend_url_dialed_for(_GA_CLIENT, realtime_protocol="beta") == (
"wss://my-endpoint.openai.azure.com/openai/realtime?api-version=2024-10-01-preview&deployment=gpt-realtime"
)
@pytest.mark.asyncio
async def test_arealtime_azure_env_beta_protocol_wins_over_a_ga_client(monkeypatch):
monkeypatch.setenv("LITELLM_AZURE_REALTIME_PROTOCOL", "beta")
assert await _azure_backend_url_dialed_for(_GA_CLIENT) == (
"wss://my-endpoint.openai.azure.com/openai/realtime?api-version=2024-10-01-preview&deployment=gpt-realtime"
)