From 03af235ad5af7f6c30e80035e67073d96e86b173 Mon Sep 17 00:00:00 2001 From: Maxime BRUN Date: Mon, 17 Aug 2026 11:53:49 +0200 Subject: [PATCH] fix(realtime): preserve custom api base paths --- litellm/llms/openai/realtime/handler.py | 23 +++++++------ .../realtime/test_openai_realtime_handler.py | 33 +++++++++++++------ 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/litellm/llms/openai/realtime/handler.py b/litellm/llms/openai/realtime/handler.py index 0343f22e7d1..3406414a29d 100644 --- a/litellm/llms/openai/realtime/handler.py +++ b/litellm/llms/openai/realtime/handler.py @@ -84,17 +84,20 @@ class OpenAIRealtime(OpenAIChatCompletion): """ Construct the backend websocket URL with all query parameters (including 'model'). """ - from httpx import URL + from httpx import URL, QueryParams - api_base = api_base.replace("https://", "wss://") - api_base = api_base.replace("http://", "ws://") - url = URL(api_base) - # Set the correct path - url = url.copy_with(path="/v1/realtime") - # Include all query parameters including 'model' - if query_params: - url = url.copy_with(params=query_params) - return str(url) + api_base_url: Final = URL(api_base) + websocket_scheme: Final = {"https": "wss", "http": "ws"}.get(api_base_url.scheme, api_base_url.scheme) + base_path: Final = api_base_url.path.rstrip("/") + realtime_path: Final = ( + base_path + if base_path.endswith("/v1/realtime") + else f"{base_path}/realtime" + if base_path.endswith("/v1") + else f"{base_path}/v1/realtime" + ) + merged_params: Final = QueryParams(query_params).merge(api_base_url.params) + return str(api_base_url.copy_with(scheme=websocket_scheme, path=realtime_path, params=merged_params)) def _make_event_normalizer(self) -> RealtimeEventNormalizer | None: """Return a per-session GA event normalizer, or None for passthrough. diff --git a/tests/test_litellm/llms/openai/realtime/test_openai_realtime_handler.py b/tests/test_litellm/llms/openai/realtime/test_openai_realtime_handler.py index e9798f45dce..47a7dccc9e4 100644 --- a/tests/test_litellm/llms/openai/realtime/test_openai_realtime_handler.py +++ b/tests/test_litellm/llms/openai/realtime/test_openai_realtime_handler.py @@ -14,9 +14,16 @@ sys.path.insert( @pytest.mark.parametrize( - "api_base", ["https://api.openai.com/v1", "https://api.openai.com"] + ("api_base", "expected_scheme", "expected_path"), + [ + ("https://api.openai.com", "wss", "/v1/realtime"), + ("https://api.openai.com/v1", "wss", "/v1/realtime"), + ("https://api.openai.com/v1/realtime", "wss", "/v1/realtime"), + ("http://localhost:8000/gateway/openai", "ws", "/gateway/openai/v1/realtime"), + ("https://gateway.example.com/tenant/openai/v1/", "wss", "/tenant/openai/v1/realtime"), + ], ) -def test_openai_realtime_handler_url_construction(api_base): +def test_openai_realtime_handler_url_construction(api_base, expected_scheme, expected_path): from litellm.llms.openai.realtime.handler import OpenAIRealtime handler = OpenAIRealtime() @@ -26,9 +33,10 @@ def test_openai_realtime_handler_url_construction(api_base): "model": "gpt-4o-realtime-preview-2024-10-01", }, ) - # Model parameter should be included in the URL - assert url.startswith("wss://api.openai.com/v1/realtime?") - assert "model=gpt-4o-realtime-preview-2024-10-01" in url + parsed_url = httpx.URL(url) + assert parsed_url.scheme == expected_scheme + assert parsed_url.path == expected_path + assert parsed_url.params["model"] == "gpt-4o-realtime-preview-2024-10-01" def test_openai_realtime_handler_url_with_extra_params(): @@ -36,16 +44,21 @@ def test_openai_realtime_handler_url_with_extra_params(): from litellm.types.realtime import RealtimeQueryParams handler = OpenAIRealtime() - api_base = "https://api.openai.com/v1" + api_base = ( + "https://gateway.example.com/tenant/openai/v1" + "?gateway_route=europe&gateway_route=backup" + "&model=gateway-model&intent=gateway-intent" + ) query_params: RealtimeQueryParams = { "model": "gpt-4o-realtime-preview-2024-10-01", "intent": "chat", } url = handler._construct_url(api_base=api_base, query_params=query_params) - # Both 'model' and other params should be included in the query string - assert url.startswith("wss://api.openai.com/v1/realtime?") - assert "model=gpt-4o-realtime-preview-2024-10-01" in url - assert "intent=chat" in url + parsed_url = httpx.URL(url) + assert parsed_url.path == "/tenant/openai/v1/realtime" + assert parsed_url.params.get_list("gateway_route") == ["europe", "backup"] + assert parsed_url.params["model"] == "gateway-model" + assert parsed_url.params["intent"] == "gateway-intent" def test_openai_realtime_handler_model_parameter_inclusion():