fix(azure): preserve deployment routing across copy/with_options (#3593)
- [x] I understand that this repository is auto-generated and my pull
request may not be merged
## Changes being requested
Fixes a routing regression in `AzureOpenAI` / `AsyncAzureOpenAI` after
`.copy()` / `.with_options()`.
This change is in **hand-maintained** code — `src/openai/lib/azure.py`
has no `File generated from our OpenAPI spec by Stainless` header, and
`copy()`/`with_options()` there is custom Azure logic, so it is not
affected by codegen.
### Problem
`AzureOpenAI.copy()` (aliased as `with_options()`) delegates to the base
`OpenAI.copy()`, which reconstructs the client from `base_url` and does
**not** pass `azure_endpoint` / `azure_deployment` back to
`AzureOpenAI.__init__`. Because `_azure_endpoint` and
`_azure_deployment` are only set from those constructor arguments, they
get reset to `None` on the copied client.
`_prepare_url()` uses those attributes to bypass the deployment path for
**non-deployment** endpoints:
```python
if self._azure_deployment and self._azure_endpoint and url not in _deployments_endpoints:
# -> {endpoint}/openai/{url} (no /deployments/<name>/)
```
Once they are `None`, that bypass no longer runs, so a client configured
with `azure_deployment` starts routing non-deployment endpoints (e.g.
`/models`) under the deployment path, which 404s.
### Reproduction (no network / key required)
```python
from openai import AzureOpenAI
c = AzureOpenAI(
azure_endpoint="https://example.openai.azure.com",
azure_deployment="my-deploy",
api_version="2024-06-01",
azure_ad_token="fake-token",
)
print(c._prepare_url("/models"))
# https://example.openai.azure.com/openai/models ✅
c2 = c.with_options(timeout=30)
print(c2._azure_endpoint, c2._azure_deployment) # None None
print(c2._prepare_url("/models"))
# https://example.openai.azure.com/openai/deployments/my-deploy/models ❌ (404)
```
### Fix
Preserve `_azure_endpoint` / `_azure_deployment` on the copied client,
unless the caller overrides `base_url` in the copy (in which case the
old endpoint context is intentionally not carried over). Applied
symmetrically to the sync and async clients.
### Why minimal
- Only `copy()` in each of the two Azure clients changes; two guarded
lines each.
- Public API, credential handling, and the mutually-exclusive `base_url`
/ `azure_endpoint` constructor contract are untouched (the fix
deliberately avoids passing `azure_endpoint` alongside `base_url`).
- Deployment endpoints (e.g. `/chat/completions`) are unaffected —
`base_url` already encodes the deployment and `_build_request` continues
to guard on `"/deployments" in base_url.path`.
### Tests
Added `test_copy_preserves_deployment_routing` in
`tests/lib/test_azure.py` (sync + async × `copy` / `with_options`),
asserting that after a copy:
- `/models` → `{endpoint}/openai/models` (not nested under
`/deployments/<name>/`), and
- `/chat/completions` still keeps the deployment path.
Fails on `main` (wrong `/models` URL), passes with this change.
### Validation
- `rye run pytest tests/lib/test_azure.py` → 63 passed
- `rye run pytest tests/lib/` → all pass except a pre-existing,
unrelated failure
(`test_bedrock_auth_conformance.py::test_retry_signing_fixture`, which
fails identically on clean `main` in this environment)
- `ruff check` / `ruff format` clean on both files
- `pyright` and `mypy` clean on `src/openai/lib/azure.py`
### Compatibility
No public API change; behavior only changes for the previously-broken
post-copy case. Copies that override `base_url` keep their current
behavior.
## Additional context & links
Discovered by auditing sibling copy/state-preservation logic; no
existing issue or PR covers this. `.with_options()` is a common pattern
(per-request timeouts/headers), so Azure users combining it with
`azure_deployment` are likely to hit this.
---------
Co-authored-by: Marcus Wood <marcuswood@openai.com> H
Henry Su committed
b3c04f4e701e07726f320295a38928421946e530
Parent: ccc10f5
Committed by GitHub <noreply@github.com>
on 9/15/2026, 9:11:34 PM