From 11dccec19833c6f1690c26e29dfc4a5574b7187d Mon Sep 17 00:00:00 2001 From: Krista Pratico Date: Fri, 14 Feb 2025 14:36:01 -0800 Subject: [PATCH 01/10] support realtime with azure_deployment --- src/openai/lib/azure.py | 56 +- .../resources/beta/realtime/realtime.py | 36 +- tests/lib/test_azure.py | 487 ++++++++++++++++++ 3 files changed, 550 insertions(+), 29 deletions(-) diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index f857d76e51..23085d6ddb 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -58,11 +58,26 @@ 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: + if not self._azure_deployment: + return super()._prepare_url(url) + + deployment_segment = f"/deployments/{self._azure_deployment}" + if deployment_segment in str(self.base_url.path) and url not in _deployments_endpoints: + merge_url = httpx.URL(url) + if merge_url.is_relative_url: + base_path = self.base_url.path.split(deployment_segment)[0] + merge_path = f"{base_path}/{merge_url.path.lstrip('/')}" + return self.base_url.copy_with(path=merge_path) + + return super()._prepare_url(url) + class AzureOpenAI(BaseAzureClient[httpx.Client, Stream[Any]], OpenAI): @overload @@ -160,8 +175,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 +239,7 @@ 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 @override def copy( @@ -307,12 +323,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 +336,16 @@ 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 + + realtime_url = self._prepare_url("/realtime") + if self.websocket_base_url is not None: + base_url = httpx.URL(self.websocket_base_url) + else: + base_url = realtime_url.copy_with(scheme="wss") + + url = base_url.copy_with(params={**query}) + return url, auth_headers + class AsyncAzureOpenAI(BaseAzureClient[httpx.AsyncClient, AsyncStream[Any]], AsyncOpenAI): @@ -422,8 +447,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 +511,7 @@ 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 @override def copy( @@ -571,12 +597,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 +610,12 @@ 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 + + realtime_url = self._prepare_url("/realtime") + if self.websocket_base_url is not None: + base_url = httpx.URL(self.websocket_base_url) + else: + base_url = realtime_url.copy_with(scheme="wss") + + url = base_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 235790a9f5..c746af510a 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..a9d728f1ae 100644 --- a/tests/lib/test_azure.py +++ b/tests/lib/test_azure.py @@ -239,3 +239,490 @@ 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( + 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( + 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( + 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( + 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,api,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/", + "/realtime", + {"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/", + "/realtime", + {"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/", + "/realtime", + {"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/", + "/realtime", + {"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( + 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/", + "/realtime", + {"model": "deployment-body"}, + "wss://example.azure-api.net/PTU/realtime?api-version=2024-02-01&deployment=deployment-body" + ), + ], +) +def test_prepare_url_realtime(client: AzureOpenAI, base_url: str, api: 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,api,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/", + "/realtime", + {"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/", + "/realtime", + {"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/", + "/realtime", + {"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/", + "/realtime", + {"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( + 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/", + "/realtime", + {"model": "deployment-body"}, + "wss://example.azure-api.net/PTU/realtime?api-version=2024-02-01&deployment=deployment-body" + ), + ], +) +async def test_prepare_url_realtime_async(client: AsyncAzureOpenAI, base_url: str, api: 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 From 57bacea547f2807fd9c166e3ceaf903f2d8fe0e5 Mon Sep 17 00:00:00 2001 From: Krista Pratico Date: Fri, 14 Feb 2025 15:24:03 -0800 Subject: [PATCH 02/10] lint --- src/openai/lib/azure.py | 2 ++ tests/lib/test_azure.py | 32 ++++++++++++-------------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index 23085d6ddb..9c23dab26d 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -49,6 +49,8 @@ def __init__(self) -> None: class BaseAzureClient(BaseClient[_HttpxClientT, _DefaultStreamT]): + _azure_deployment: str | None + @override def _build_request( self, diff --git a/tests/lib/test_azure.py b/tests/lib/test_azure.py index a9d728f1ae..5f44a50c21 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 @@ -297,7 +299,7 @@ async def test_azure_bearer_token_redacted_async( ), # AzureOpenAI: base_url and azure_deployment specified; ignored b/c not supported ( - AzureOpenAI( + AzureOpenAI( # type: ignore api_version="2024-02-01", api_key="example API key", base_url="https://example.azure-api.net/PTU/", @@ -360,7 +362,7 @@ async def test_azure_bearer_token_redacted_async( ), # AsyncAzureOpenAI: base_url and azure_deployment specified; azure_deployment ignored b/c not supported ( - AsyncAzureOpenAI( + AsyncAzureOpenAI( # type: ignore api_version="2024-02-01", api_key="example API key", base_url="https://example.azure-api.net/PTU/", @@ -466,7 +468,7 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str ), # AzureOpenAI: base_url and azure_deployment specified; azure_deployment ignored b/c not supported ( - AzureOpenAI( + AzureOpenAI( # type: ignore api_version="2024-02-01", api_key="example API key", base_url="https://example.azure-api.net/PTU/", @@ -554,7 +556,7 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str ), # AsyncAzureOpenAI: base_url and azure_deployment specified; azure_deployment ignored b/c not supported ( - AsyncAzureOpenAI( + AsyncAzureOpenAI( # type: ignore api_version="2024-02-01", api_key="example API key", base_url="https://example.azure-api.net/PTU/", @@ -580,7 +582,7 @@ def test_prepare_url_nondeployment_endpoint(client: Client, base_url: str, api: @pytest.mark.parametrize( - "client,base_url,api,json_data,expected", + "client,base_url,json_data,expected", [ # Realtime endpoint # AzureOpenAI: No deployment specified @@ -591,7 +593,6 @@ def test_prepare_url_nondeployment_endpoint(client: Client, base_url: str, api: azure_endpoint="https://example-resource.azure.openai.com", ), "https://example-resource.azure.openai.com/openai/", - "/realtime", {"model": "deployment-body"}, "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body" ), @@ -604,7 +605,6 @@ def test_prepare_url_nondeployment_endpoint(client: Client, base_url: str, api: azure_deployment="deployment-client" ), "https://example-resource.azure.openai.com/openai/deployments/deployment-client/", - "/realtime", {"model": "deployment-body"}, "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-client" ), @@ -616,7 +616,6 @@ def test_prepare_url_nondeployment_endpoint(client: Client, base_url: str, api: azure_endpoint="https://deployments.azure.openai.com", ), "https://deployments.azure.openai.com/openai/", - "/realtime", {"model": "deployment-body"}, "wss://deployments.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body" ), @@ -629,33 +628,31 @@ def test_prepare_url_nondeployment_endpoint(client: Client, base_url: str, api: azure_deployment="deployments" ), "https://example-resource.azure.openai.com/openai/deployments/deployments/", - "/realtime", {"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( + 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/", - "/realtime", {"model": "deployment-body"}, "wss://example.azure-api.net/PTU/realtime?api-version=2024-02-01&deployment=deployment-body" ), ], ) -def test_prepare_url_realtime(client: AzureOpenAI, base_url: str, api: str, json_data: dict[str, str], expected: str) -> None: +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,api,json_data,expected", + "client,base_url,json_data,expected", [ # AsyncAzureOpenAI: No deployment specified ( @@ -665,7 +662,6 @@ def test_prepare_url_realtime(client: AzureOpenAI, base_url: str, api: str, json azure_endpoint="https://example-resource.azure.openai.com", ), "https://example-resource.azure.openai.com/openai/", - "/realtime", {"model": "deployment-body"}, "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body" ), @@ -678,7 +674,6 @@ def test_prepare_url_realtime(client: AzureOpenAI, base_url: str, api: str, json azure_deployment="deployment-client" ), "https://example-resource.azure.openai.com/openai/deployments/deployment-client/", - "/realtime", {"model": "deployment-body"}, "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-client" ), @@ -690,7 +685,6 @@ def test_prepare_url_realtime(client: AzureOpenAI, base_url: str, api: str, json azure_endpoint="https://deployments.azure.openai.com", ), "https://deployments.azure.openai.com/openai/", - "/realtime", {"model": "deployment-body"}, "wss://deployments.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body" ), @@ -703,26 +697,24 @@ def test_prepare_url_realtime(client: AzureOpenAI, base_url: str, api: str, json azure_deployment="deployments" ), "https://example-resource.azure.openai.com/openai/deployments/deployments/", - "/realtime", {"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( + 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/", - "/realtime", {"model": "deployment-body"}, "wss://example.azure-api.net/PTU/realtime?api-version=2024-02-01&deployment=deployment-body" ), ], ) -async def test_prepare_url_realtime_async(client: AsyncAzureOpenAI, base_url: str, api: str, json_data: dict[str, str], expected: str) -> None: +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 From 2c445b8f675633a2b68dea2930e78883682c2e7d Mon Sep 17 00:00:00 2001 From: Krista Pratico Date: Tue, 18 Feb 2025 10:22:00 -0800 Subject: [PATCH 03/10] use rsplit --- src/openai/lib/azure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index 9c23dab26d..5bdbf9be42 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -74,7 +74,7 @@ def _prepare_url(self, url: str) -> httpx.URL: if deployment_segment in str(self.base_url.path) and url not in _deployments_endpoints: merge_url = httpx.URL(url) if merge_url.is_relative_url: - base_path = self.base_url.path.split(deployment_segment)[0] + base_path = self.base_url.path.rsplit(deployment_segment, maxsplit=1)[0] merge_path = f"{base_path}/{merge_url.path.lstrip('/')}" return self.base_url.copy_with(path=merge_path) From 8b20ec41838d8df63e7fd9a84ddbf1c340b722cb Mon Sep 17 00:00:00 2001 From: Krista Pratico Date: Thu, 20 Feb 2025 11:29:34 -0800 Subject: [PATCH 04/10] switch approach: save copy of the original url --- src/openai/lib/azure.py | 26 ++++++--- tests/lib/test_azure.py | 120 +++++++++++++++++++++------------------- 2 files changed, 80 insertions(+), 66 deletions(-) diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index 5bdbf9be42..766298c933 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -49,6 +49,7 @@ def __init__(self) -> None: class BaseAzureClient(BaseClient[_HttpxClientT, _DefaultStreamT]): + _original_url: httpx.URL | None _azure_deployment: str | None @override @@ -67,16 +68,21 @@ def _build_request( @override def _prepare_url(self, url: str) -> httpx.URL: - if not self._azure_deployment: - return super()._prepare_url(url) - - deployment_segment = f"/deployments/{self._azure_deployment}" - if deployment_segment in str(self.base_url.path) and url not in _deployments_endpoints: + """Adjust the URL if the client was configured with an Azure deployment + and the API feature being called is **not** a deployments-based endpoint + (aka requires /deployments/deployment-name in the URL path). + """ + if ( + self._azure_deployment + and f"/deployments/{self._azure_deployment}" in str(self.base_url.path) + and url not in _deployments_endpoints + ): merge_url = httpx.URL(url) - if merge_url.is_relative_url: - base_path = self.base_url.path.rsplit(deployment_segment, maxsplit=1)[0] - merge_path = f"{base_path}/{merge_url.path.lstrip('/')}" - return self.base_url.copy_with(path=merge_path) + if merge_url.is_relative_url and self._original_url: + merge_raw_path = self._original_url.raw_path + merge_url.raw_path.lstrip(b"/") + return self._original_url.copy_with(raw_path=merge_raw_path) + + return merge_url return super()._prepare_url(url) @@ -242,6 +248,7 @@ def __init__( 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._original_url = httpx.URL(f"{azure_endpoint.rstrip('/')}/openai/") if azure_endpoint else None @override def copy( @@ -514,6 +521,7 @@ def __init__( 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._original_url = httpx.URL(f"{azure_endpoint.rstrip('/')}/openai/") if azure_endpoint else None @override def copy( diff --git a/tests/lib/test_azure.py b/tests/lib/test_azure.py index 5f44a50c21..87db5221fb 100644 --- a/tests/lib/test_azure.py +++ b/tests/lib/test_azure.py @@ -257,7 +257,7 @@ async def test_azure_bearer_token_redacted_async( "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" + "https://example-resource.azure.openai.com/openai/deployments/deployment-body/chat/completions?api-version=2024-02-01", ), # AzureOpenAI: Deployment specified ( @@ -265,12 +265,12 @@ async def test_azure_bearer_token_redacted_async( api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployment-client" + 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" + "https://example-resource.azure.openai.com/openai/deployments/deployment-client/chat/completions?api-version=2024-02-01", ), # AzureOpenAI: "deployments" in the DNS name ( @@ -282,7 +282,7 @@ async def test_azure_bearer_token_redacted_async( "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" + "https://deployments.example-resource.azure.openai.com/openai/deployments/deployment-body/chat/completions?api-version=2024-02-01", ), # AzureOpenAI: Deployment called deployments ( @@ -290,12 +290,12 @@ async def test_azure_bearer_token_redacted_async( api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployments" + 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" + "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 ( @@ -303,12 +303,12 @@ async def test_azure_bearer_token_redacted_async( api_version="2024-02-01", api_key="example API key", base_url="https://example.azure-api.net/PTU/", - azure_deployment="deployment-client" + 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" + "https://example.azure-api.net/PTU/deployments/deployment-body/chat/completions?api-version=2024-02-01", ), # AsyncAzureOpenAI: No deployment specified ( @@ -320,7 +320,7 @@ async def test_azure_bearer_token_redacted_async( "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" + "https://example-resource.azure.openai.com/openai/deployments/deployment-body/chat/completions?api-version=2024-02-01", ), # AsyncAzureOpenAI: Deployment specified ( @@ -328,12 +328,12 @@ async def test_azure_bearer_token_redacted_async( api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployment-client" + 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" + "https://example-resource.azure.openai.com/openai/deployments/deployment-client/chat/completions?api-version=2024-02-01", ), # AsyncAzureOpenAI: "deployments" in the DNS name ( @@ -345,7 +345,7 @@ async def test_azure_bearer_token_redacted_async( "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" + "https://deployments.example-resource.azure.openai.com/openai/deployments/deployment-body/chat/completions?api-version=2024-02-01", ), # AsyncAzureOpenAI: Deployment called deployments ( @@ -353,12 +353,12 @@ async def test_azure_bearer_token_redacted_async( api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployments" + 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" + "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 ( @@ -366,16 +366,18 @@ async def test_azure_bearer_token_redacted_async( api_version="2024-02-01", api_key="example API key", base_url="https://example.azure-api.net/PTU/", - azure_deployment="deployment-client" + 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" + "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: +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", @@ -401,7 +403,7 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str "https://example-resource.azure.openai.com/openai/", "/models", {}, - "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01" + "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01", ), # AzureOpenAI: No deployment specified ( @@ -413,7 +415,7 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str "https://example-resource.azure.openai.com/openai/", "/assistants", {"model": "deployment-body"}, - "https://example-resource.azure.openai.com/openai/assistants?api-version=2024-02-01" + "https://example-resource.azure.openai.com/openai/assistants?api-version=2024-02-01", ), # AzureOpenAI: Deployment specified ( @@ -421,12 +423,12 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployment-client" + 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" + "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01", ), # AzureOpenAI: Deployment specified ( @@ -434,12 +436,12 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployment-client" + 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" + "https://example-resource.azure.openai.com/openai/assistants?api-version=2024-02-01", ), # AzureOpenAI: "deployments" in the DNS name ( @@ -451,7 +453,7 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str "https://deployments.example-resource.azure.openai.com/openai/", "/models", {}, - "https://deployments.example-resource.azure.openai.com/openai/models?api-version=2024-02-01" + "https://deployments.example-resource.azure.openai.com/openai/models?api-version=2024-02-01", ), # AzureOpenAI: Deployment called "deployments" ( @@ -459,12 +461,12 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployments" + 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" + "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 ( @@ -472,12 +474,12 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str api_version="2024-02-01", api_key="example API key", base_url="https://example.azure-api.net/PTU/", - azure_deployment="deployment-client" + azure_deployment="deployment-client", ), "https://example.azure-api.net/PTU/", "/models", {}, - "https://example.azure-api.net/PTU/models?api-version=2024-02-01" + "https://example.azure-api.net/PTU/models?api-version=2024-02-01", ), # AsyncAzureOpenAI: No deployment specified ( @@ -489,7 +491,7 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str "https://example-resource.azure.openai.com/openai/", "/models", {}, - "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01" + "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01", ), # AsyncAzureOpenAI: No deployment specified ( @@ -501,7 +503,7 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str "https://example-resource.azure.openai.com/openai/", "/assistants", {"model": "deployment-body"}, - "https://example-resource.azure.openai.com/openai/assistants?api-version=2024-02-01" + "https://example-resource.azure.openai.com/openai/assistants?api-version=2024-02-01", ), # AsyncAzureOpenAI: Deployment specified ( @@ -509,12 +511,12 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployment-client" + 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" + "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01", ), # AsyncAzureOpenAI: Deployment specified ( @@ -522,12 +524,12 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployment-client" + 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" + "https://example-resource.azure.openai.com/openai/assistants?api-version=2024-02-01", ), # AsyncAzureOpenAI: "deployments" in the DNS name ( @@ -539,7 +541,7 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str "https://deployments.example-resource.azure.openai.com/openai/", "/models", {}, - "https://deployments.example-resource.azure.openai.com/openai/models?api-version=2024-02-01" + "https://deployments.example-resource.azure.openai.com/openai/models?api-version=2024-02-01", ), # AsyncAzureOpenAI: Deployment called "deployments" ( @@ -547,12 +549,12 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployments" + 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" + "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 ( @@ -560,16 +562,18 @@ def test_prepare_url_deployment_endpoint(client: Client, base_url: str, api: str api_version="2024-02-01", api_key="example API key", base_url="https://example.azure-api.net/PTU/", - azure_deployment="deployment-client" + azure_deployment="deployment-client", ), "https://example.azure-api.net/PTU/", "/models", {}, - "https://example.azure-api.net/PTU/models?api-version=2024-02-01" + "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: +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", @@ -594,7 +598,7 @@ def test_prepare_url_nondeployment_endpoint(client: Client, base_url: str, api: ), "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" + "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body", ), # AzureOpenAI: Deployment specified ( @@ -602,11 +606,11 @@ def test_prepare_url_nondeployment_endpoint(client: Client, base_url: str, api: api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployment-client" + 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" + "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-client", ), # AzureOpenAI: "deployments" in the DNS name ( @@ -617,7 +621,7 @@ def test_prepare_url_nondeployment_endpoint(client: Client, base_url: str, api: ), "https://deployments.azure.openai.com/openai/", {"model": "deployment-body"}, - "wss://deployments.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body" + "wss://deployments.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body", ), # AzureOpenAI: Deployment called "deployments" ( @@ -625,11 +629,11 @@ def test_prepare_url_nondeployment_endpoint(client: Client, base_url: str, api: api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployments" + 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" + "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 ( @@ -637,11 +641,11 @@ def test_prepare_url_nondeployment_endpoint(client: Client, base_url: str, api: api_version="2024-02-01", api_key="example API key", base_url="https://example.azure-api.net/PTU/", - azure_deployment="my-deployment" + 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" + "wss://example.azure-api.net/PTU/realtime?api-version=2024-02-01&deployment=deployment-body", ), ], ) @@ -663,7 +667,7 @@ def test_prepare_url_realtime(client: AzureOpenAI, base_url: str, json_data: dic ), "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" + "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body", ), # AsyncAzureOpenAI: Deployment specified ( @@ -671,11 +675,11 @@ def test_prepare_url_realtime(client: AzureOpenAI, base_url: str, json_data: dic api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployment-client" + 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" + "wss://example-resource.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-client", ), # AsyncAzureOpenAI: "deployments" in the DNS name ( @@ -686,7 +690,7 @@ def test_prepare_url_realtime(client: AzureOpenAI, base_url: str, json_data: dic ), "https://deployments.azure.openai.com/openai/", {"model": "deployment-body"}, - "wss://deployments.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body" + "wss://deployments.azure.openai.com/openai/realtime?api-version=2024-02-01&deployment=deployment-body", ), # AsyncAzureOpenAI: Deployment called "deployments" ( @@ -694,11 +698,11 @@ def test_prepare_url_realtime(client: AzureOpenAI, base_url: str, json_data: dic api_version="2024-02-01", api_key="example API key", azure_endpoint="https://example-resource.azure.openai.com", - azure_deployment="deployments" + 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" + "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 ( @@ -706,15 +710,17 @@ def test_prepare_url_realtime(client: AzureOpenAI, base_url: str, json_data: dic api_version="2024-02-01", api_key="example API key", base_url="https://example.azure-api.net/PTU/", - azure_deployment="deployment-client" + 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" + "wss://example.azure-api.net/PTU/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: +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 From 9ef5e2a7e07fbe7b2d4d13a1677280fc4645341d Mon Sep 17 00:00:00 2001 From: Krista Pratico Date: Wed, 26 Feb 2025 16:23:35 -0800 Subject: [PATCH 05/10] save azure_endpoint as it was given --- src/openai/lib/azure.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index 766298c933..ddf14599b5 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -49,7 +49,7 @@ def __init__(self) -> None: class BaseAzureClient(BaseClient[_HttpxClientT, _DefaultStreamT]): - _original_url: httpx.URL | None + _azure_endpoint: httpx.URL | None _azure_deployment: str | None @override @@ -78,9 +78,9 @@ def _prepare_url(self, url: str) -> httpx.URL: and url not in _deployments_endpoints ): merge_url = httpx.URL(url) - if merge_url.is_relative_url and self._original_url: - merge_raw_path = self._original_url.raw_path + merge_url.raw_path.lstrip(b"/") - return self._original_url.copy_with(raw_path=merge_raw_path) + if merge_url.is_relative_url and self._azure_endpoint: + 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 @@ -248,7 +248,7 @@ def __init__( 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._original_url = httpx.URL(f"{azure_endpoint.rstrip('/')}/openai/") if azure_endpoint else None + self._azure_endpoint = httpx.URL(azure_endpoint) if azure_endpoint else None @override def copy( @@ -521,7 +521,7 @@ def __init__( 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._original_url = httpx.URL(f"{azure_endpoint.rstrip('/')}/openai/") if azure_endpoint else None + self._azure_endpoint = httpx.URL(azure_endpoint) if azure_endpoint else None @override def copy( From c1b9d60bf23a9ee91e1bacd38b393512c0f7672b Mon Sep 17 00:00:00 2001 From: Krista Pratico Date: Wed, 26 Feb 2025 16:31:31 -0800 Subject: [PATCH 06/10] docstring --- src/openai/lib/azure.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index ddf14599b5..54bf0f7701 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -68,9 +68,9 @@ def _build_request( @override def _prepare_url(self, url: str) -> httpx.URL: - """Adjust the URL if the client was configured with an Azure deployment + """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 - (aka requires /deployments/deployment-name in the URL path). + (i.e. requires /deployments/deployment-name in the URL path). """ if ( self._azure_deployment From ae88c9452ccf692ed04c2f8dc68182ddfe3bd40f Mon Sep 17 00:00:00 2001 From: Krista Pratico Date: Wed, 26 Feb 2025 16:35:50 -0800 Subject: [PATCH 07/10] format --- src/openai/lib/azure.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index 54bf0f7701..3c099902ba 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -79,7 +79,9 @@ def _prepare_url(self, url: str) -> httpx.URL: ): merge_url = httpx.URL(url) if merge_url.is_relative_url and self._azure_endpoint: - merge_raw_path = self._azure_endpoint.raw_path.rstrip(b"/") + b"/openai/" + merge_url.raw_path.lstrip(b"/") + 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 @@ -356,7 +358,6 @@ def _configure_realtime(self, model: str, extra_query: Query) -> tuple[httpx.URL return url, auth_headers - class AsyncAzureOpenAI(BaseAzureClient[httpx.AsyncClient, AsyncStream[Any]], AsyncOpenAI): @overload def __init__( From 62b24bf3d3fb5a7a231dd860e42c6492f55423e0 Mon Sep 17 00:00:00 2001 From: Krista Pratico Date: Wed, 26 Feb 2025 18:05:21 -0800 Subject: [PATCH 08/10] remove unnecessary check + add test --- src/openai/lib/azure.py | 8 ++------ tests/lib/test_azure.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index 3c099902ba..ce88a85448 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -72,13 +72,9 @@ def _prepare_url(self, url: str) -> httpx.URL: 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 f"/deployments/{self._azure_deployment}" in str(self.base_url.path) - and url not in _deployments_endpoints - ): + 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 and self._azure_endpoint: + if merge_url.is_relative_url: merge_raw_path = ( self._azure_endpoint.raw_path.rstrip(b"/") + b"/openai/" + merge_url.raw_path.lstrip(b"/") ) diff --git a/tests/lib/test_azure.py b/tests/lib/test_azure.py index 87db5221fb..e403f5b796 100644 --- a/tests/lib/test_azure.py +++ b/tests/lib/test_azure.py @@ -724,3 +724,35 @@ async def test_prepare_url_realtime_async( 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" From 33ec2e7982ba64c903dd19492f68c3a370f4d35b Mon Sep 17 00:00:00 2001 From: Krista Pratico Date: Wed, 26 Feb 2025 18:53:38 -0800 Subject: [PATCH 09/10] fix for websocket_base_url --- src/openai/lib/azure.py | 16 ++++++++++------ tests/lib/test_azure.py | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index ce88a85448..ea7bd20d99 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -344,13 +344,15 @@ def _configure_realtime(self, model: str, extra_query: Query) -> tuple[httpx.URL if token: auth_headers = {"Authorization": f"Bearer {token}"} - realtime_url = self._prepare_url("/realtime") 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 = realtime_url.copy_with(scheme="wss") + base_url = self._prepare_url("/realtime") + realtime_url = base_url.copy_with(scheme="wss") - url = base_url.copy_with(params={**query}) + url = realtime_url.copy_with(params={**query}) return url, auth_headers @@ -618,11 +620,13 @@ async def _configure_realtime(self, model: str, extra_query: Query) -> tuple[htt if token: auth_headers = {"Authorization": f"Bearer {token}"} - realtime_url = self._prepare_url("/realtime") 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 = realtime_url.copy_with(scheme="wss") + base_url = self._prepare_url("/realtime") + realtime_url = base_url.copy_with(scheme="wss") - url = base_url.copy_with(params={**query}) + url = realtime_url.copy_with(params={**query}) return url, auth_headers diff --git a/tests/lib/test_azure.py b/tests/lib/test_azure.py index e403f5b796..3026388dd3 100644 --- a/tests/lib/test_azure.py +++ b/tests/lib/test_azure.py @@ -647,6 +647,18 @@ def test_prepare_url_nondeployment_endpoint( {"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: @@ -716,6 +728,18 @@ def test_prepare_url_realtime(client: AzureOpenAI, base_url: str, json_data: dic {"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( From 45e15c0e1e71fe0013c780ed92f75f4609023982 Mon Sep 17 00:00:00 2001 From: Krista Pratico Date: Thu, 27 Feb 2025 09:27:33 -0800 Subject: [PATCH 10/10] add another test --- tests/lib/test_azure.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/lib/test_azure.py b/tests/lib/test_azure.py index 3026388dd3..52c24eba27 100644 --- a/tests/lib/test_azure.py +++ b/tests/lib/test_azure.py @@ -780,3 +780,25 @@ def test_client_sets_base_url(client: Client) -> None: ) ) 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"