Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/openai/lib/azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ def copy(
current_provider=self._azure_ad_token_provider,
)

return super().copy(
copied = super().copy(
api_key=api_key,
admin_api_key=admin_api_key,
workload_identity=workload_identity,
Expand All @@ -434,6 +434,14 @@ def copy(
**_extra_kwargs,
},
)
# `super().copy()` reconstructs the client from `base_url`, which does not carry the
# Azure endpoint/deployment context that `_prepare_url` relies on to route
# non-deployment endpoints (e.g. `/models`). Preserve it unless the caller overrides
# the base URL.
if base_url is None:
copied._azure_endpoint = self._azure_endpoint
copied._azure_deployment = self._azure_deployment
return copied

with_options = copy

Expand Down Expand Up @@ -762,7 +770,7 @@ def copy(
current_provider=self._azure_ad_token_provider,
)

return super().copy(
copied = super().copy(
api_key=api_key,
admin_api_key=admin_api_key,
workload_identity=workload_identity,
Expand All @@ -786,6 +794,14 @@ def copy(
**_extra_kwargs,
},
)
# `super().copy()` reconstructs the client from `base_url`, which does not carry the
# Azure endpoint/deployment context that `_prepare_url` relies on to route
# non-deployment endpoints (e.g. `/models`). Preserve it unless the caller overrides
# the base URL.
if base_url is None:
copied._azure_endpoint = self._azure_endpoint
copied._azure_deployment = self._azure_deployment
return copied

with_options = copy

Expand Down
50 changes: 50 additions & 0 deletions tests/lib/test_azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,56 @@ def test_client_copying_override_options(client: Client) -> None:
assert copied._custom_query == {"api-version": "2022-05-01"}


@pytest.mark.parametrize(
"client",
[
AzureOpenAI(
api_version="2024-02-01",
api_key="example API key",
azure_endpoint="https://example-resource.azure.openai.com",
azure_deployment="deployment-client",
),
AsyncAzureOpenAI(
api_version="2024-02-01",
api_key="example API key",
azure_endpoint="https://example-resource.azure.openai.com",
azure_deployment="deployment-client",
),
],
)
@pytest.mark.parametrize("method", ["copy", "with_options"])
@pytest.mark.parametrize("base_url", [None, "https://replacement.example.test/gateway"])
async def test_copy_preserves_deployment_routing(
client: Client, method: Literal["copy", "with_options"], base_url: str | None
) -> None:
copied = (
client.copy(timeout=5, base_url=base_url)
if method == "copy"
else client.with_options(timeout=5, base_url=base_url)
)
copied = copied.with_options(max_retries=0)
root = base_url or "https://example-resource.azure.openai.com/openai"
deployment = "body-model" if base_url else "deployment-client"

# Non-deployment endpoints use the root; deployment endpoints retain their routing.
req = copied._build_request(FinalRequestOptions.construct(method="get", url="/models"))
assert req.url == root + "/models?api-version=2024-02-01"
req = copied._build_request(
FinalRequestOptions.construct(method="post", url="/chat/completions", json_data={"model": "body-model"})
)
assert req.url == root + f"/deployments/{deployment}/chat/completions?api-version=2024-02-01"

if isinstance(copied, AsyncAzureOpenAI):
url, headers = await copied._configure_realtime("body-model", {})
else:
url, headers = copied._configure_realtime("body-model", {})
assert url == root.replace("https://", "wss://") + (f"/realtime?api-version=2024-02-01&deployment={deployment}")
assert headers == {"api-key": "example API key"}

# Changing a copy's destination must not change the original client.
assert client._prepare_url("/models") == "https://example-resource.azure.openai.com/openai/models"


@pytest.mark.parametrize("client", [sync_client, async_client])
@pytest.mark.parametrize("method", ["copy", "with_options"])
def test_client_copying_rejects_x509_workload_identity(client: Client, method: Literal["copy", "with_options"]) -> None:
Expand Down
Loading