From c98d7400785011e0f5de2e33c9bf4dec95332847 Mon Sep 17 00:00:00 2001 From: Krista Pratico Date: Fri, 28 Feb 2025 06:36:08 -0800 Subject: [PATCH] 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 --- src/openai/lib/azure.py | 67 ++- .../resources/beta/realtime/realtime.py | 36 +- tests/lib/test_azure.py | 563 ++++++++++++++++++ 3 files changed, 637 insertions(+), 29 deletions(-) diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index f857d76e51..ea7bd20d99 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -49,6 +49,9 @@ def __init__(self) -> None: class BaseAzureClient(BaseClient[_HttpxClientT, _DefaultStreamT]): + _azure_endpoint: httpx.URL | None + _azure_deployment: str | None + @override def _build_request( self, @@ -58,11 +61,29 @@ def _build_request( ) -> 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 @@ def __init__( 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 @@ def __init__( 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 @@ def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions: 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 != "": auth_headers = {"api-key": self.api_key} @@ -320,7 +343,17 @@ def _configure_realtime(self, model: str, extra_query: Query) -> tuple[Query, di 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 @@ def __init__( 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 @@ def __init__( 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 @@ async def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOp 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 != "": auth_headers = {"api-key": self.api_key} @@ -584,4 +619,14 @@ async def _configure_realtime(self, model: str, extra_query: Query) -> tuple[Que 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 diff --git a/src/openai/resources/beta/realtime/realtime.py b/src/openai/resources/beta/realtime/realtime.py index 0cf7c85799..cd610d9089 100644 --- a/src/openai/resources/beta/realtime/realtime.py +++ b/src/openai/resources/beta/realtime/realtime.py @@ -324,15 +324,15 @@ async def __aenter__(self) -> AsyncRealtimeConnection: 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 @@ def __enter__(self) -> RealtimeConnection: 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) diff --git a/tests/lib/test_azure.py b/tests/lib/test_azure.py index a28aa8c2f6..52c24eba27 100644 --- a/tests/lib/test_azure.py +++ b/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 @@ async def test_azure_bearer_token_redacted_async( 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"] == "" + + +@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"