Merge pull request #38390 from BerriAI/litellm_realtime_health_ga_protocol

fix(health): probe Azure GA realtime path for transcription-only models
This commit is contained in:
Mateo Wang 2026-08-26 12:53:05 -07:00 committed by GitHub
commit 724c5c2d96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 156 additions and 7 deletions

View file

@ -557,6 +557,34 @@ async def _arealtime(
raise ValueError(f"Unsupported model: {model}")
def _is_transcription_only_realtime_model(model: str, custom_llm_provider: str) -> bool:
try:
model_info: Final = litellm.get_model_info(model=model, custom_llm_provider=custom_llm_provider)
except Exception: # noqa: BLE001 # get_model_info raises bare Exception for unmapped models
return False
if model_info.get("mode") == "audio_transcription":
return True
return "/v1/realtime/transcription_sessions" in (model_info.get("supported_endpoints") or ())
_TRANSCRIPTION_QUERY_PARAMS: Final[RealtimeQueryParams] = {"intent": "transcription"}
def _azure_realtime_health_protocol(
model: str, realtime_protocol: str | None, model_params: Mapping[str, Any]
) -> tuple[str, RealtimeQueryParams | None]:
query_params: Final = _TRANSCRIPTION_QUERY_PARAMS if _is_transcription_only_realtime_model(model, "azure") else None
configured_raw: Final = (
realtime_protocol or model_params.get("realtime_protocol") or os.environ.get("LITELLM_AZURE_REALTIME_PROTOCOL")
)
configured: Final = configured_raw if isinstance(configured_raw, str) else None
if configured is not None:
return configured, query_params
if query_params is not None:
return "GA", query_params
return "beta", None
def _realtime_health_check_auth_headers(
custom_llm_provider: str, api_key: str | None, model_params: Mapping[str, Any]
) -> Mapping[str, str | None]:
@ -586,7 +614,9 @@ async def _realtime_health_check(
api_version: Optional[str] - api version
api_key: str - api key
custom_llm_provider: str - custom llm provider
realtime_protocol: Optional[str] - protocol version ("GA"/"v1" for GA path, "beta"/None for beta path)
realtime_protocol: Optional[str] - protocol version ("GA"/"v1" for GA path, "beta" for beta path);
None resolves it for Azure from model_params/env, with transcription-only models probing GA
plus intent=transcription the way real calls do
Returns:
bool - True if connection is successful, False otherwise
@ -602,11 +632,17 @@ async def _realtime_health_check(
model_params=model_params or _EMPTY_MODEL_PARAMS,
)
if custom_llm_provider == "azure":
resolved_protocol, azure_query_params = _azure_realtime_health_protocol(
model=model,
realtime_protocol=realtime_protocol,
model_params=model_params or _EMPTY_MODEL_PARAMS,
)
url = azure_realtime._construct_url(
api_base=api_base or "",
model=model,
api_version=api_version or "2024-10-01-preview",
realtime_protocol=realtime_protocol,
realtime_protocol=resolved_protocol,
query_params=azure_query_params,
)
elif custom_llm_provider == "openai":
url = openai_realtime._construct_url(

View file

@ -5845,6 +5845,7 @@ def _get_model_info_helper(
tiered_pricing=_model_info.get("tiered_pricing", None),
litellm_provider=_model_info.get("litellm_provider", custom_llm_provider),
mode=_model_info.get("mode"),
supported_endpoints=_model_info.get("supported_endpoints", None),
supports_system_messages=_model_info.get("supports_system_messages", None),
supports_response_schema=_model_info.get("supports_response_schema", None),
supports_vision=_model_info.get("supports_vision", None),

View file

@ -436,10 +436,9 @@ class TestGithubCopilotResponsesAPIRouting:
catalog entries that lack ``mode``).
Exercises the real ``_cached_get_model_info_helper`` plumbing via
``register_model`` (no mock). ``supported_endpoints`` is not carried on
the normalized ``ModelInfoBase`` the helper returns, so the gate must
read it from the raw ``litellm.model_cost`` entry; a mock-based test
would mask that.
``register_model`` (no mock). The gate reads ``supported_endpoints``
from the raw ``litellm.model_cost`` entry; a mock-based test would
mask that.
"""
litellm.register_model(
{

View file

@ -1,10 +1,12 @@
import asyncio
import time
from unittest.mock import MagicMock
from types import TracebackType
from unittest.mock import MagicMock, patch
import pytest
import litellm
from litellm.realtime_api import main as realtime_main
from litellm.realtime_api.main import _with_resolved_session_model
@ -190,3 +192,105 @@ def test_client_secret_forwards_nested_transcription_model_untouched(monkeypatch
session = captured["request_data"]["session"]
assert session["model"] == "gpt-4o-realtime-preview"
assert session["input_audio_transcription"]["model"] == "whisper-1"
class _CapturingConnect:
def __init__(self) -> None:
self.url: str | None = None
def __call__(self, url: str, **kwargs: object) -> "_CapturingConnect":
self.url = url
return self
async def __aenter__(self) -> MagicMock:
return MagicMock()
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
return None
@pytest.mark.asyncio
async def test_azure_health_check_probes_ga_transcription_url_for_transcription_model(local_model_cost_map):
"""Regression for LIT-6240: transcription-only models (mode audio_transcription
in the cost map) are GA-only and 400 on the beta path, so the health probe
must hit /openai/v1/realtime?intent=transcription like real calls do."""
connect = _CapturingConnect()
with patch("websockets.connect", connect):
assert await realtime_main._realtime_health_check(
model="gpt-realtime-whisper",
custom_llm_provider="azure",
api_key="fake-key",
api_base="https://my-endpoint.openai.azure.com",
api_version="2025-04-01-preview",
)
assert connect.url == "wss://my-endpoint.openai.azure.com/openai/v1/realtime?intent=transcription"
@pytest.mark.asyncio
async def test_azure_health_check_stays_on_ga_when_deployment_registration_overwrites_mode(
local_model_cost_map, monkeypatch
):
"""In a live proxy, Router._register_deployment_in_model_cost writes the
operator's deployment model_info (mode: realtime) over the catalog entry for
azure/gpt-realtime-whisper, so mode alone misreads the model as speech-capable
and the probe regresses to the beta path. supported_endpoints survives that
registration and must keep the probe on the GA transcription path."""
polluted = {**litellm.model_cost["azure/gpt-realtime-whisper"], "mode": "realtime"}
monkeypatch.setitem(litellm.model_cost, "azure/gpt-realtime-whisper", polluted)
connect = _CapturingConnect()
with patch("websockets.connect", connect):
assert await realtime_main._realtime_health_check(
model="gpt-realtime-whisper",
custom_llm_provider="azure",
api_key="fake-key",
api_base="https://my-endpoint.openai.azure.com",
api_version="2025-04-01-preview",
)
assert connect.url == "wss://my-endpoint.openai.azure.com/openai/v1/realtime?intent=transcription"
def test_transcription_only_detection_falls_back_to_mode(local_model_cost_map):
"""azure/whisper-1 declares mode audio_transcription but no supported_endpoints,
so only the mode signal can classify it as transcription-only."""
assert realtime_main._is_transcription_only_realtime_model("whisper-1", "azure") is True
def test_transcription_only_detection_rejects_speech_model(local_model_cost_map):
assert realtime_main._is_transcription_only_realtime_model("gpt-realtime-mini", "azure") is False
@pytest.mark.asyncio
async def test_azure_health_check_keeps_beta_path_for_speech_model():
connect = _CapturingConnect()
with patch("websockets.connect", connect):
assert await realtime_main._realtime_health_check(
model="gpt-4o-realtime-preview",
custom_llm_provider="azure",
api_key="fake-key",
api_base="https://my-endpoint.openai.azure.com",
api_version="2024-10-01-preview",
)
assert connect.url == (
"wss://my-endpoint.openai.azure.com/openai/realtime"
"?api-version=2024-10-01-preview&deployment=gpt-4o-realtime-preview"
)
@pytest.mark.asyncio
async def test_azure_health_check_honors_deployment_realtime_protocol():
connect = _CapturingConnect()
with patch("websockets.connect", connect):
assert await realtime_main._realtime_health_check(
model="gpt-4o-realtime-preview",
custom_llm_provider="azure",
api_key="fake-key",
api_base="https://my-endpoint.openai.azure.com",
api_version="2024-10-01-preview",
model_params={"realtime_protocol": "GA"},
)
assert connect.url == "wss://my-endpoint.openai.azure.com/openai/v1/realtime?model=gpt-4o-realtime-preview"

View file

@ -120,6 +120,15 @@ def test_get_model_info_surfaces_supports_adaptive_thinking(local_model_cost_map
assert generalized["supports_adaptive_thinking"] is True
def test_get_model_info_surfaces_supported_endpoints(local_model_cost_map):
"""supported_endpoints ships in the cost map and is declared on ModelInfoBase,
but the constructor never copied it, so get_model_info always returned None.
The realtime health check reads it to spot GA-only transcription models
(LIT-6240)."""
info = litellm.get_model_info(model="gpt-realtime-whisper", custom_llm_provider="azure")
assert info["supported_endpoints"] == ["/v1/realtime", "/v1/realtime/transcription_sessions"]
def test_potential_model_names_keeps_provider_prefixed_candidate():
"""A provider whose own model ids repeat the litellm provider name (Perplexity's
Agent API serves `perplexity/glm-5.2`, mapped as `perplexity/perplexity/glm-5.2`)