diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index c6653a722d..d777d34eaf 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -353,8 +353,10 @@ class LiteLLMRoutes(enum.Enum): # realtime "/realtime", "/v1/realtime", + "/openai/v1/realtime", "/realtime?{model}", "/v1/realtime?{model}", + "/openai/v1/realtime?{model}", # responses API "/responses", "/v1/responses", diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index a5905765c6..3f7745a54c 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -8805,6 +8805,7 @@ def _realtime_query_params_template( return tuple(params) +@app.websocket("/openai/v1/realtime") @app.websocket("/v1/realtime") @app.websocket("/realtime") async def realtime_websocket_endpoint( diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 00a7748309..400edcac88 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -825,6 +825,7 @@ API_ROUTE_TO_CALL_TYPES = { # Realtime API "/realtime": [CallTypes.arealtime], "/v1/realtime": [CallTypes.arealtime], + "/openai/v1/realtime": [CallTypes.arealtime], # Provider-specific routes "/anthropic/v1/messages": [CallTypes.anthropic_messages], # Google GenAI routes diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 3f19db36c3..6718f52cbf 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -6513,3 +6513,37 @@ async def test_get_current_spend_redis_error_falls_back_to_in_memory(): finally: ps.spend_counter_cache = orig_counter ps.prisma_client = orig_prisma + + +def test_realtime_websocket_route_aliases_registered(): + """Realtime sessions reach the proxy via three path aliases stacked on + `realtime_websocket_endpoint`. Dropping any of them silently 405s + WebSocket upgrades because the catch-all `/openai/{endpoint:path}` + HTTP passthrough only declares HTTP methods. The aliases must also be + in `LiteLLMRoutes.openai_routes` (so non-admin / team / key-scoped + auth allows them) and in `API_ROUTE_TO_CALL_TYPES` (so call-type-aware + logic such as guardrails can resolve the realtime call type).""" + from starlette.routing import WebSocketRoute + + from litellm.proxy._types import LiteLLMRoutes + from litellm.proxy.proxy_server import app + from litellm.types.utils import API_ROUTE_TO_CALL_TYPES, CallTypes + + websocket_paths = { + route.path for route in app.routes if isinstance(route, WebSocketRoute) + } + openai_routes = LiteLLMRoutes.openai_routes.value + + for expected in ("/openai/v1/realtime", "/v1/realtime", "/realtime"): + assert expected in websocket_paths, ( + f"{expected!r} missing from registered WebSocket routes; the " + f"realtime endpoint will 405 for clients hitting this path." + ) + assert expected in openai_routes, ( + f"{expected!r} missing from LiteLLMRoutes.openai_routes; " + f"non-admin / team / key-scoped users will get 403 on this path." + ) + assert API_ROUTE_TO_CALL_TYPES.get(expected) == [CallTypes.arealtime], ( + f"{expected!r} missing from API_ROUTE_TO_CALL_TYPES; call-type " + f"resolution will return None and break call-type-aware features." + )