Commit c98d7400

Krista Pratico <krpratic@microsoft.com>
2025-02-28 22:36:08
fix(azure): azure_deployment use with realtime + non-deployment-based APIs (#2154)
* support realtime with azure_deployment * lint * use rsplit * switch approach: save copy of the original url * save azure_endpoint as it was given * docstring * format * remove unnecessary check + add test * fix for websocket_base_url * add another test
1 parent ba2a8a0
Changed files (3)
src
openai
lib
resources
beta
realtime
tests
src/openai/lib/azure.py
@@ -49,6 +49,9 @@ class MutuallyExclusiveAuthError(OpenAIError):
 
 
 class BaseAzureClient(BaseClient[_HttpxClientT, _DefaultStreamT]):
+    _azure_endpoint: httpx.URL | None
+    _azure_deployment: str | None
+
     @override
     def _build_request(
         self,
@@ -58,11 +61,29 @@ class BaseAzureClient(BaseClient[_HttpxClientT, _DefaultStreamT]):
     ) -> httpx.Request:
         if options.url in _deployments_endpoints and is_mapping(options.json_data):
             model = options.json_data.get("model")
-            if model is not None and not "/deployments" in str(self.base_url):
+            if model is not None and "/deployments" not in str(self.base_url.path):
                 options.url = f"/deployments/{model}{options.url}"
 
         return super()._build_request(options, retries_taken=retries_taken)
 
+    @override
+    def _prepare_url(self, url: str) -> httpx.URL:
+        """Adjust the URL if the client was configured with an Azure endpoint + deployment
+        and the API feature being called is **not** a deployments-based endpoint
+        (i.e. requires /deployments/deployment-name in the URL path).
+        """
+        if self._azure_deployment and self._azure_endpoint and url not in _deployments_endpoints:
+            merge_url = httpx.URL(url)
+            if merge_url.is_relative_url:
+                merge_raw_path = (
+                    self._azure_endpoint.raw_path.rstrip(b"/") + b"/openai/" + merge_url.raw_path.lstrip(b"/")
+                )
+                return self._azure_endpoint.copy_with(raw_path=merge_raw_path)
+
+            return merge_url
+
+        return super()._prepare_url(url)
+
 
 class AzureOpenAI(BaseAzureClient[httpx.Client, Stream[Any]], OpenAI):
     @overload
@@ -160,8 +181,8 @@ class AzureOpenAI(BaseAzureClient[httpx.Client, Stream[Any]], OpenAI):
 
             azure_ad_token_provider: A function that returns an Azure Active Directory token, will be invoked on every request.
 
-            azure_deployment: A model deployment, if given sets the base client URL to include `/deployments/{azure_deployment}`.
-                Note: this means you won't be able to use non-deployment endpoints. Not supported with Assistants APIs.
+            azure_deployment: A model deployment, if given with `azure_endpoint`, sets the base client URL to include `/deployments/{azure_deployment}`.
+                Not supported with Assistants APIs.
         """
         if api_key is None:
             api_key = os.environ.get("AZURE_OPENAI_API_KEY")
@@ -224,6 +245,8 @@ class AzureOpenAI(BaseAzureClient[httpx.Client, Stream[Any]], OpenAI):
         self._api_version = api_version
         self._azure_ad_token = azure_ad_token
         self._azure_ad_token_provider = azure_ad_token_provider
+        self._azure_deployment = azure_deployment if azure_endpoint else None
+        self._azure_endpoint = httpx.URL(azure_endpoint) if azure_endpoint else None
 
     @override
     def copy(
@@ -307,12 +330,12 @@ class AzureOpenAI(BaseAzureClient[httpx.Client, Stream[Any]], OpenAI):
 
         return options
 
-    def _configure_realtime(self, model: str, extra_query: Query) -> tuple[Query, dict[str, str]]:
+    def _configure_realtime(self, model: str, extra_query: Query) -> tuple[httpx.URL, dict[str, str]]:
         auth_headers = {}
         query = {
             **extra_query,
             "api-version": self._api_version,
-            "deployment": model,
+            "deployment": self._azure_deployment or model,
         }
         if self.api_key != "<missing API key>":
             auth_headers = {"api-key": self.api_key}
@@ -320,7 +343,17 @@ class AzureOpenAI(BaseAzureClient[httpx.Client, Stream[Any]], OpenAI):
             token = self._get_azure_ad_token()
             if token:
                 auth_headers = {"Authorization": f"Bearer {token}"}
-        return query, auth_headers
+
+        if self.websocket_base_url is not None:
+            base_url = httpx.URL(self.websocket_base_url)
+            merge_raw_path = base_url.raw_path.rstrip(b"/") + b"/realtime"
+            realtime_url = base_url.copy_with(raw_path=merge_raw_path)
+        else:
+            base_url = self._prepare_url("/realtime")
+            realtime_url = base_url.copy_with(scheme="wss")
+
+        url = realtime_url.copy_with(params={**query})
+        return url, auth_headers
 
 
 class AsyncAzureOpenAI(BaseAzureClient[httpx.AsyncClient, AsyncStream[Any]], AsyncOpenAI):
@@ -422,8 +455,8 @@ class AsyncAzureOpenAI(BaseAzureClient[httpx.AsyncClient, AsyncStream[Any]], Asy
 
             azure_ad_token_provider: A function that returns an Azure Active Directory token, will be invoked on every request.
 
-            azure_deployment: A model deployment, if given sets the base client URL to include `/deployments/{azure_deployment}`.
-                Note: this means you won't be able to use non-deployment endpoints. Not supported with Assistants APIs.
+            azure_deployment: A model deployment, if given with `azure_endpoint`, sets the base client URL to include `/deployments/{azure_deployment}`.
+                Not supported with Assistants APIs.
         """
         if api_key is None:
             api_key = os.environ.get("AZURE_OPENAI_API_KEY")
@@ -486,6 +519,8 @@ class AsyncAzureOpenAI(BaseAzureClient[httpx.AsyncClient, AsyncStream[Any]], Asy
         self._api_version = api_version
         self._azure_ad_token = azure_ad_token
         self._azure_ad_token_provider = azure_ad_token_provider
+        self._azure_deployment = azure_deployment if azure_endpoint else None
+        self._azure_endpoint = httpx.URL(azure_endpoint) if azure_endpoint else None
 
     @override
     def copy(
@@ -571,12 +606,12 @@ class AsyncAzureOpenAI(BaseAzureClient[httpx.AsyncClient, AsyncStream[Any]], Asy
 
         return options
 
-    async def _configure_realtime(self, model: str, extra_query: Query) -> tuple[Query, dict[str, str]]:
+    async def _configure_realtime(self, model: str, extra_query: Query) -> tuple[httpx.URL, dict[str, str]]:
         auth_headers = {}
         query = {
             **extra_query,
             "api-version": self._api_version,
-            "deployment": model,
+            "deployment": self._azure_deployment or model,
         }
         if self.api_key != "<missing API key>":
             auth_headers = {"api-key": self.api_key}
@@ -584,4 +619,14 @@ class AsyncAzureOpenAI(BaseAzureClient[httpx.AsyncClient, AsyncStream[Any]], Asy
             token = await self._get_azure_ad_token()
             if token:
                 auth_headers = {"Authorization": f"Bearer {token}"}
-        return query, auth_headers
+
+        if self.websocket_base_url is not None:
+            base_url = httpx.URL(self.websocket_base_url)
+            merge_raw_path = base_url.raw_path.rstrip(b"/") + b"/realtime"
+            realtime_url = base_url.copy_with(raw_path=merge_raw_path)
+        else:
+            base_url = self._prepare_url("/realtime")
+            realtime_url = base_url.copy_with(scheme="wss")
+
+        url = realtime_url.copy_with(params={**query})
+        return url, auth_headers
src/openai/resources/beta/realtime/realtime.py
@@ -324,15 +324,15 @@ class AsyncRealtimeConnectionManager:
         extra_query = self.__extra_query
         auth_headers = self.__client.auth_headers
         if is_async_azure_client(self.__client):
-            extra_query, auth_headers = await self.__client._configure_realtime(self.__model, extra_query)
-
-        url = self._prepare_url().copy_with(
-            params={
-                **self.__client.base_url.params,
-                "model": self.__model,
-                **extra_query,
-            },
-        )
+            url, auth_headers = await self.__client._configure_realtime(self.__model, extra_query)
+        else:
+            url = self._prepare_url().copy_with(
+                params={
+                    **self.__client.base_url.params,
+                    "model": self.__model,
+                    **extra_query,
+                },
+            )
         log.debug("Connecting to %s", url)
         if self.__websocket_connection_options:
             log.debug("Connection options: %s", self.__websocket_connection_options)
@@ -506,15 +506,15 @@ class RealtimeConnectionManager:
         extra_query = self.__extra_query
         auth_headers = self.__client.auth_headers
         if is_azure_client(self.__client):
-            extra_query, auth_headers = self.__client._configure_realtime(self.__model, extra_query)
-
-        url = self._prepare_url().copy_with(
-            params={
-                **self.__client.base_url.params,
-                "model": self.__model,
-                **extra_query,
-            },
-        )
+            url, auth_headers = self.__client._configure_realtime(self.__model, extra_query)
+        else:
+            url = self._prepare_url().copy_with(
+                params={
+                    **self.__client.base_url.params,
+                    "model": self.__model,
+                    **extra_query,
+                },
+            )
         log.debug("Connecting to %s", url)
         if self.__websocket_connection_options:
             log.debug("Connection options: %s", self.__websocket_connection_options)
tests/lib/test_azure.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import logging
 from typing import Union, cast
 from typing_extensions import Literal, Protocol
@@ -239,3 +241,564 @@ class TestAzureLogging:
         for record in caplog.records:
             if is_dict(record.args) and record.args.get("headers") and is_dict(record.args["headers"]):
                 assert record.args["headers"]["Authorization"] == "<redacted>"
+
+
+@pytest.mark.parametrize(
+    "client,base_url,api,json_data,expected",
+    [
+        # Deployment-based endpoints
+        # AzureOpenAI: No deployment specified
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+            ),
+            "https://example-resource.azure.openai.com/openai/",
+            "/chat/completions",
+            {"model": "deployment-body"},
+            "https://example-resource.azure.openai.com/openai/deployments/deployment-body/chat/completions?api-version=2024-02-01",
+        ),
+        # AzureOpenAI: Deployment specified
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployment-client",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployment-client/",
+            "/chat/completions",
+            {"model": "deployment-body"},
+            "https://example-resource.azure.openai.com/openai/deployments/deployment-client/chat/completions?api-version=2024-02-01",
+        ),
+        # AzureOpenAI: "deployments" in the DNS name
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://deployments.example-resource.azure.openai.com",
+            ),
+            "https://deployments.example-resource.azure.openai.com/openai/",
+            "/chat/completions",
+            {"model": "deployment-body"},
+            "https://deployments.example-resource.azure.openai.com/openai/deployments/deployment-body/chat/completions?api-version=2024-02-01",
+        ),
+        # AzureOpenAI: Deployment called deployments
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployments",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployments/",
+            "/chat/completions",
+            {"model": "deployment-body"},
+            "https://example-resource.azure.openai.com/openai/deployments/deployments/chat/completions?api-version=2024-02-01",
+        ),
+        # AzureOpenAI: base_url and azure_deployment specified; ignored b/c not supported
+        (
+            AzureOpenAI(  # type: ignore
+                api_version="2024-02-01",
+                api_key="example API key",
+                base_url="https://example.azure-api.net/PTU/",
+                azure_deployment="deployment-client",
+            ),
+            "https://example.azure-api.net/PTU/",
+            "/chat/completions",
+            {"model": "deployment-body"},
+            "https://example.azure-api.net/PTU/deployments/deployment-body/chat/completions?api-version=2024-02-01",
+        ),
+        # AsyncAzureOpenAI: No deployment specified
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+            ),
+            "https://example-resource.azure.openai.com/openai/",
+            "/chat/completions",
+            {"model": "deployment-body"},
+            "https://example-resource.azure.openai.com/openai/deployments/deployment-body/chat/completions?api-version=2024-02-01",
+        ),
+        # AsyncAzureOpenAI: Deployment specified
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployment-client",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployment-client/",
+            "/chat/completions",
+            {"model": "deployment-body"},
+            "https://example-resource.azure.openai.com/openai/deployments/deployment-client/chat/completions?api-version=2024-02-01",
+        ),
+        # AsyncAzureOpenAI: "deployments" in the DNS name
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://deployments.example-resource.azure.openai.com",
+            ),
+            "https://deployments.example-resource.azure.openai.com/openai/",
+            "/chat/completions",
+            {"model": "deployment-body"},
+            "https://deployments.example-resource.azure.openai.com/openai/deployments/deployment-body/chat/completions?api-version=2024-02-01",
+        ),
+        # AsyncAzureOpenAI: Deployment called deployments
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployments",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployments/",
+            "/chat/completions",
+            {"model": "deployment-body"},
+            "https://example-resource.azure.openai.com/openai/deployments/deployments/chat/completions?api-version=2024-02-01",
+        ),
+        # AsyncAzureOpenAI: base_url and azure_deployment specified; azure_deployment ignored b/c not supported
+        (
+            AsyncAzureOpenAI(  # type: ignore
+                api_version="2024-02-01",
+                api_key="example API key",
+                base_url="https://example.azure-api.net/PTU/",
+                azure_deployment="deployment-client",
+            ),
+            "https://example.azure-api.net/PTU/",
+            "/chat/completions",
+            {"model": "deployment-body"},
+            "https://example.azure-api.net/PTU/deployments/deployment-body/chat/completions?api-version=2024-02-01",
+        ),
+    ],
+)
+def test_prepare_url_deployment_endpoint(
+    client: Client, base_url: str, api: str, json_data: dict[str, str], expected: str
+) -> None:
+    req = client._build_request(
+        FinalRequestOptions.construct(
+            method="post",
+            url=api,
+            json_data=json_data,
+        )
+    )
+    assert req.url == expected
+    assert client.base_url == base_url
+
+
+@pytest.mark.parametrize(
+    "client,base_url,api,json_data,expected",
+    [
+        # Non-deployment endpoints
+        # AzureOpenAI: No deployment specified
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+            ),
+            "https://example-resource.azure.openai.com/openai/",
+            "/models",
+            {},
+            "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01",
+        ),
+        # AzureOpenAI: No deployment specified
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+            ),
+            "https://example-resource.azure.openai.com/openai/",
+            "/assistants",
+            {"model": "deployment-body"},
+            "https://example-resource.azure.openai.com/openai/assistants?api-version=2024-02-01",
+        ),
+        # AzureOpenAI: Deployment specified
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployment-client",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployment-client/",
+            "/models",
+            {},
+            "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01",
+        ),
+        # AzureOpenAI: Deployment specified
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployment-client",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployment-client/",
+            "/assistants",
+            {"model": "deployment-body"},
+            "https://example-resource.azure.openai.com/openai/assistants?api-version=2024-02-01",
+        ),
+        # AzureOpenAI: "deployments" in the DNS name
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://deployments.example-resource.azure.openai.com",
+            ),
+            "https://deployments.example-resource.azure.openai.com/openai/",
+            "/models",
+            {},
+            "https://deployments.example-resource.azure.openai.com/openai/models?api-version=2024-02-01",
+        ),
+        # AzureOpenAI: Deployment called "deployments"
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployments",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployments/",
+            "/models",
+            {},
+            "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01",
+        ),
+        # AzureOpenAI: base_url and azure_deployment specified; azure_deployment ignored b/c not supported
+        (
+            AzureOpenAI(  # type: ignore
+                api_version="2024-02-01",
+                api_key="example API key",
+                base_url="https://example.azure-api.net/PTU/",
+                azure_deployment="deployment-client",
+            ),
+            "https://example.azure-api.net/PTU/",
+            "/models",
+            {},
+            "https://example.azure-api.net/PTU/models?api-version=2024-02-01",
+        ),
+        # AsyncAzureOpenAI: No deployment specified
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+            ),
+            "https://example-resource.azure.openai.com/openai/",
+            "/models",
+            {},
+            "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01",
+        ),
+        # AsyncAzureOpenAI: No deployment specified
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+            ),
+            "https://example-resource.azure.openai.com/openai/",
+            "/assistants",
+            {"model": "deployment-body"},
+            "https://example-resource.azure.openai.com/openai/assistants?api-version=2024-02-01",
+        ),
+        # AsyncAzureOpenAI: Deployment specified
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployment-client",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployment-client/",
+            "/models",
+            {},
+            "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01",
+        ),
+        # AsyncAzureOpenAI: Deployment specified
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployment-client",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployment-client/",
+            "/assistants",
+            {"model": "deployment-body"},
+            "https://example-resource.azure.openai.com/openai/assistants?api-version=2024-02-01",
+        ),
+        # AsyncAzureOpenAI: "deployments" in the DNS name
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://deployments.example-resource.azure.openai.com",
+            ),
+            "https://deployments.example-resource.azure.openai.com/openai/",
+            "/models",
+            {},
+            "https://deployments.example-resource.azure.openai.com/openai/models?api-version=2024-02-01",
+        ),
+        # AsyncAzureOpenAI: Deployment called "deployments"
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployments",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployments/",
+            "/models",
+            {},
+            "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01",
+        ),
+        # AsyncAzureOpenAI: base_url and azure_deployment specified; azure_deployment ignored b/c not supported
+        (
+            AsyncAzureOpenAI(  # type: ignore
+                api_version="2024-02-01",
+                api_key="example API key",
+                base_url="https://example.azure-api.net/PTU/",
+                azure_deployment="deployment-client",
+            ),
+            "https://example.azure-api.net/PTU/",
+            "/models",
+            {},
+            "https://example.azure-api.net/PTU/models?api-version=2024-02-01",
+        ),
+    ],
+)
+def test_prepare_url_nondeployment_endpoint(
+    client: Client, base_url: str, api: str, json_data: dict[str, str], expected: str
+) -> None:
+    req = client._build_request(
+        FinalRequestOptions.construct(
+            method="post",
+            url=api,
+            json_data=json_data,
+        )
+    )
+    assert req.url == expected
+    assert client.base_url == base_url
+
+
+@pytest.mark.parametrize(
+    "client,base_url,json_data,expected",
+    [
+        # Realtime endpoint
+        # AzureOpenAI: No deployment specified
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+            ),
+            "https://example-resource.azure.openai.com/openai/",
+            {"model": "deployment-body"},
+            "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body",
+        ),
+        # AzureOpenAI: Deployment specified
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployment-client",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployment-client/",
+            {"model": "deployment-body"},
+            "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-client",
+        ),
+        # AzureOpenAI: "deployments" in the DNS name
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://deployments.azure.openai.com",
+            ),
+            "https://deployments.azure.openai.com/openai/",
+            {"model": "deployment-body"},
+            "wss://deployments.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body",
+        ),
+        # AzureOpenAI: Deployment called "deployments"
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployments",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployments/",
+            {"model": "deployment-body"},
+            "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployments",
+        ),
+        # AzureOpenAI: base_url and azure_deployment specified; azure_deployment ignored b/c not supported
+        (
+            AzureOpenAI(  # type: ignore
+                api_version="2024-02-01",
+                api_key="example API key",
+                base_url="https://example.azure-api.net/PTU/",
+                azure_deployment="my-deployment",
+            ),
+            "https://example.azure-api.net/PTU/",
+            {"model": "deployment-body"},
+            "wss://example.azure-api.net/PTU/realtime?api-version=2024-02-01&deployment=deployment-body",
+        ),
+        # AzureOpenAI: websocket_base_url specified
+        (
+            AzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                websocket_base_url="wss://example-resource.azure.openai.com/base",
+            ),
+            "https://example-resource.azure.openai.com/openai/",
+            {"model": "deployment-body"},
+            "wss://example-resource.azure.openai.com/base/realtime?api-version=2024-02-01&deployment=deployment-body",
+        ),
+    ],
+)
+def test_prepare_url_realtime(client: AzureOpenAI, base_url: str, json_data: dict[str, str], expected: str) -> None:
+    url, _ = client._configure_realtime(json_data["model"], {})
+    assert str(url) == expected
+    assert client.base_url == base_url
+
+
+@pytest.mark.parametrize(
+    "client,base_url,json_data,expected",
+    [
+        # AsyncAzureOpenAI: No deployment specified
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+            ),
+            "https://example-resource.azure.openai.com/openai/",
+            {"model": "deployment-body"},
+            "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body",
+        ),
+        # AsyncAzureOpenAI: Deployment specified
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployment-client",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployment-client/",
+            {"model": "deployment-body"},
+            "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-client",
+        ),
+        # AsyncAzureOpenAI: "deployments" in the DNS name
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://deployments.azure.openai.com",
+            ),
+            "https://deployments.azure.openai.com/openai/",
+            {"model": "deployment-body"},
+            "wss://deployments.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body",
+        ),
+        # AsyncAzureOpenAI: Deployment called "deployments"
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                azure_deployment="deployments",
+            ),
+            "https://example-resource.azure.openai.com/openai/deployments/deployments/",
+            {"model": "deployment-body"},
+            "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployments",
+        ),
+        # AsyncAzureOpenAI: base_url and azure_deployment specified; azure_deployment ignored b/c not supported
+        (
+            AsyncAzureOpenAI(  # type: ignore
+                api_version="2024-02-01",
+                api_key="example API key",
+                base_url="https://example.azure-api.net/PTU/",
+                azure_deployment="deployment-client",
+            ),
+            "https://example.azure-api.net/PTU/",
+            {"model": "deployment-body"},
+            "wss://example.azure-api.net/PTU/realtime?api-version=2024-02-01&deployment=deployment-body",
+        ),
+        # AsyncAzureOpenAI: websocket_base_url specified
+        (
+            AsyncAzureOpenAI(
+                api_version="2024-02-01",
+                api_key="example API key",
+                azure_endpoint="https://example-resource.azure.openai.com",
+                websocket_base_url="wss://example-resource.azure.openai.com/base",
+            ),
+            "https://example-resource.azure.openai.com/openai/",
+            {"model": "deployment-body"},
+            "wss://example-resource.azure.openai.com/base/realtime?api-version=2024-02-01&deployment=deployment-body",
+        ),
+    ],
+)
+async def test_prepare_url_realtime_async(
+    client: AsyncAzureOpenAI, base_url: str, json_data: dict[str, str], expected: str
+) -> None:
+    url, _ = await client._configure_realtime(json_data["model"], {})
+    assert str(url) == expected
+    assert client.base_url == base_url
+
+
+def test_client_sets_base_url(client: Client) -> None:
+    client = AzureOpenAI(
+        api_version="2024-02-01",
+        api_key="example API key",
+        azure_endpoint="https://example-resource.azure.openai.com",
+        azure_deployment="my-deployment",
+    )
+    assert client.base_url == "https://example-resource.azure.openai.com/openai/deployments/my-deployment/"
+
+    # (not recommended) user sets base_url to target different deployment
+    client.base_url = "https://example-resource.azure.openai.com/openai/deployments/different-deployment/"
+    req = client._build_request(
+        FinalRequestOptions.construct(
+            method="post",
+            url="/chat/completions",
+            json_data={"model": "placeholder"},
+        )
+    )
+    assert (
+        req.url
+        == "https://example-resource.azure.openai.com/openai/deployments/different-deployment/chat/completions?api-version=2024-02-01"
+    )
+    req = client._build_request(
+        FinalRequestOptions.construct(
+            method="post",
+            url="/models",
+            json_data={},
+        )
+    )
+    assert req.url == "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01"
+
+    # (not recommended) user sets base_url to remove deployment
+    client.base_url = "https://example-resource.azure.openai.com/openai/"
+    req = client._build_request(
+        FinalRequestOptions.construct(
+            method="post",
+            url="/chat/completions",
+            json_data={"model": "deployment"},
+        )
+    )
+    assert (
+        req.url
+        == "https://example-resource.azure.openai.com/openai/deployments/deployment/chat/completions?api-version=2024-02-01"
+    )
+    req = client._build_request(
+        FinalRequestOptions.construct(
+            method="post",
+            url="/models",
+            json_data={},
+        )
+    )
+    assert req.url == "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01"