Skip to content

Commit 2c95dde

Browse files
committed
fix(azure): support v1/preview/latest API versions
When using api_version="v1", "preview", or "latest", the Azure OpenAI client now correctly constructs URLs using the new /openai/v1/ path format instead of the legacy /openai/deployments/{model}/ format. The new Azure OpenAI v1 API uses a different URL structure: - Old: /openai/deployments/{model}/chat/completions?api-version=2024-10-21 - New: /openai/v1/chat/completions?api-version=v1 Changes: - Add _is_v1_api flag to BaseAzureClient - Skip adding /deployments/{model}/ path for v1 API in _build_request - Use /openai/v1/ base URL for v1/preview/latest api_version values - Add comprehensive tests for v1 API support
1 parent dc76021 commit 2c95dde

File tree

2 files changed

+108
-6
lines changed

2 files changed

+108
-6
lines changed

src/openai/lib/azure.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def __init__(self) -> None:
5252
class BaseAzureClient(BaseClient[_HttpxClientT, _DefaultStreamT]):
5353
_azure_endpoint: httpx.URL | None
5454
_azure_deployment: str | None
55+
_is_v1_api: bool
5556

5657
@override
5758
def _build_request(
@@ -60,10 +61,12 @@ def _build_request(
6061
*,
6162
retries_taken: int = 0,
6263
) -> httpx.Request:
63-
if options.url in _deployments_endpoints and is_mapping(options.json_data):
64-
model = options.json_data.get("model")
65-
if model is not None and "/deployments" not in str(self.base_url.path):
66-
options.url = f"/deployments/{model}{options.url}"
64+
# v1 API doesn't use /deployments/{model}/ path - model is passed in body
65+
if not getattr(self, '_is_v1_api', False):
66+
if options.url in _deployments_endpoints and is_mapping(options.json_data):
67+
model = options.json_data.get("model")
68+
if model is not None and "/deployments" not in str(self.base_url.path):
69+
options.url = f"/deployments/{model}{options.url}"
6770

6871
return super()._build_request(options, retries_taken=retries_taken)
6972

@@ -208,6 +211,9 @@ def __init__(
208211
"Must provide either the `api_version` argument or the `OPENAI_API_VERSION` environment variable"
209212
)
210213

214+
# Check if using v1 API format (new Azure OpenAI API)
215+
_is_v1_api = api_version in ("v1", "latest", "preview")
216+
211217
if default_query is None:
212218
default_query = {"api-version": api_version}
213219
else:
@@ -222,7 +228,10 @@ def __init__(
222228
"Must provide one of the `base_url` or `azure_endpoint` arguments, or the `AZURE_OPENAI_ENDPOINT` environment variable"
223229
)
224230

225-
if azure_deployment is not None:
231+
if _is_v1_api:
232+
# v1 API uses /openai/v1/ path without /deployments/
233+
base_url = f"{azure_endpoint.rstrip('/')}/openai/v1"
234+
elif azure_deployment is not None:
226235
base_url = f"{azure_endpoint.rstrip('/')}/openai/deployments/{azure_deployment}"
227236
else:
228237
base_url = f"{azure_endpoint.rstrip('/')}/openai"
@@ -253,6 +262,7 @@ def __init__(
253262
self._azure_ad_token_provider = azure_ad_token_provider
254263
self._azure_deployment = azure_deployment if azure_endpoint else None
255264
self._azure_endpoint = httpx.URL(azure_endpoint) if azure_endpoint else None
265+
self._is_v1_api = _is_v1_api
256266

257267
@override
258268
def copy(
@@ -489,6 +499,9 @@ def __init__(
489499
"Must provide either the `api_version` argument or the `OPENAI_API_VERSION` environment variable"
490500
)
491501

502+
# Check if using v1 API format (new Azure OpenAI API)
503+
_is_v1_api = api_version in ("v1", "latest", "preview")
504+
492505
if default_query is None:
493506
default_query = {"api-version": api_version}
494507
else:
@@ -503,7 +516,10 @@ def __init__(
503516
"Must provide one of the `base_url` or `azure_endpoint` arguments, or the `AZURE_OPENAI_ENDPOINT` environment variable"
504517
)
505518

506-
if azure_deployment is not None:
519+
if _is_v1_api:
520+
# v1 API uses /openai/v1/ path without /deployments/
521+
base_url = f"{azure_endpoint.rstrip('/')}/openai/v1"
522+
elif azure_deployment is not None:
507523
base_url = f"{azure_endpoint.rstrip('/')}/openai/deployments/{azure_deployment}"
508524
else:
509525
base_url = f"{azure_endpoint.rstrip('/')}/openai"
@@ -534,6 +550,7 @@ def __init__(
534550
self._azure_ad_token_provider = azure_ad_token_provider
535551
self._azure_deployment = azure_deployment if azure_endpoint else None
536552
self._azure_endpoint = httpx.URL(azure_endpoint) if azure_endpoint else None
553+
self._is_v1_api = _is_v1_api
537554

538555
@override
539556
def copy(

tests/lib/test_azure.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,3 +802,88 @@ def test_client_sets_base_url(client: Client) -> None:
802802
)
803803
)
804804
assert req.url == "https://example-resource.azure.openai.com/openai/models?api-version=2024-02-01"
805+
806+
807+
# Tests for v1 API support
808+
class TestAzureV1API:
809+
"""Tests for Azure OpenAI v1/latest/preview API support."""
810+
811+
@pytest.mark.parametrize("api_version", ["v1", "latest", "preview"])
812+
@pytest.mark.parametrize("client_cls", [AzureOpenAI, AsyncAzureOpenAI])
813+
def test_v1_api_base_url(self, api_version: str, client_cls: type[Client]) -> None:
814+
"""v1 API should use /openai/v1/ base URL."""
815+
client = client_cls(
816+
api_version=api_version,
817+
api_key="test",
818+
azure_endpoint="https://example.azure.openai.com",
819+
)
820+
assert "/openai/v1" in str(client.base_url)
821+
assert "/deployments/" not in str(client.base_url)
822+
823+
@pytest.mark.parametrize("api_version", ["v1", "latest", "preview"])
824+
@pytest.mark.parametrize("client_cls", [AzureOpenAI, AsyncAzureOpenAI])
825+
def test_v1_api_no_deployments_path(self, api_version: str, client_cls: type[Client]) -> None:
826+
"""v1 API should NOT add /deployments/{model}/ to the path."""
827+
client = client_cls(
828+
api_version=api_version,
829+
api_key="test",
830+
azure_endpoint="https://example.azure.openai.com",
831+
)
832+
req = client._build_request(
833+
FinalRequestOptions.construct(
834+
method="post",
835+
url="/chat/completions",
836+
json_data={"model": "gpt-4o"},
837+
)
838+
)
839+
assert "/deployments/" not in str(req.url)
840+
assert "/openai/v1/chat/completions" in str(req.url)
841+
842+
@pytest.mark.parametrize("api_version", ["v1", "latest", "preview"])
843+
@pytest.mark.parametrize("client_cls", [AzureOpenAI, AsyncAzureOpenAI])
844+
def test_v1_api_has_query_param(self, api_version: str, client_cls: type[Client]) -> None:
845+
"""v1 API should still include ?api-version= query param."""
846+
client = client_cls(
847+
api_version=api_version,
848+
api_key="test",
849+
azure_endpoint="https://example.azure.openai.com",
850+
)
851+
req = client._build_request(
852+
FinalRequestOptions.construct(
853+
method="post",
854+
url="/chat/completions",
855+
json_data={"model": "gpt-4o"},
856+
)
857+
)
858+
assert f"api-version={api_version}" in str(req.url)
859+
860+
@pytest.mark.parametrize("client_cls", [AzureOpenAI, AsyncAzureOpenAI])
861+
def test_traditional_api_still_works(self, client_cls: type[Client]) -> None:
862+
"""Traditional API should still use /deployments/ path."""
863+
client = client_cls(
864+
api_version="2024-10-21",
865+
api_key="test",
866+
azure_endpoint="https://example.azure.openai.com",
867+
)
868+
req = client._build_request(
869+
FinalRequestOptions.construct(
870+
method="post",
871+
url="/chat/completions",
872+
json_data={"model": "gpt-4o"},
873+
)
874+
)
875+
assert "/deployments/gpt-4o/" in str(req.url)
876+
assert "api-version=2024-10-21" in str(req.url)
877+
878+
@pytest.mark.parametrize("api_version", ["v1", "latest", "preview"])
879+
def test_v1_api_ignores_azure_deployment_param(self, api_version: str) -> None:
880+
"""v1 API should ignore azure_deployment parameter since model is in body."""
881+
client = AzureOpenAI(
882+
api_version=api_version,
883+
api_key="test",
884+
azure_endpoint="https://example.azure.openai.com",
885+
azure_deployment="ignored-deployment",
886+
)
887+
# base_url should still be /openai/v1, not /openai/deployments/ignored-deployment
888+
assert "/openai/v1" in str(client.base_url)
889+
assert "/deployments/" not in str(client.base_url)

0 commit comments

Comments
 (0)