Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions litellm/proxy/proxy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8805,6 +8805,7 @@ def _realtime_query_params_template(
return tuple(params)


@app.websocket("/openai/v1/realtime")
Comment thread
cursor[bot] marked this conversation as resolved.
@app.websocket("/v1/realtime")
@app.websocket("/realtime")
Comment thread
greptile-apps[bot] marked this conversation as resolved.
async def realtime_websocket_endpoint(
Expand Down
1 change: 1 addition & 0 deletions litellm/types/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,7 @@ class CallTypes(str, Enum):
# 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
Expand Down
34 changes: 34 additions & 0 deletions tests/test_litellm/proxy/test_proxy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)
Loading