From aa2310db589e82502cf314f1036b529857ba8143 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sun, 1 Mar 2026 05:35:30 +0100 Subject: [PATCH 1/3] fix(azure): strip model from request body for deployment-based endpoints When using implicit deployments (model name as deployment), the Azure client correctly rewrites the URL to include /deployments/{model}/, but leaves the model parameter in the request body. Some Azure configurations reject requests where the body model doesn't match the actual model name behind the deployment. Create a new dict without model instead of using pop() to avoid mutating the shared json_data reference (model_copy is shallow), which would break request retries. Fixes #2892 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/openai/lib/azure.py | 1 + tests/lib/test_azure.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index ad64707261..a4290e0ab1 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -64,6 +64,7 @@ def _build_request( model = options.json_data.get("model") if model is not None and "/deployments" not in str(self.base_url.path): options.url = f"/deployments/{model}{options.url}" + options.json_data = {k: v for k, v in options.json_data.items() if k != "model"} # type: ignore[union-attr] return super()._build_request(options, retries_taken=retries_taken) diff --git a/tests/lib/test_azure.py b/tests/lib/test_azure.py index 52c24eba27..896079b31a 100644 --- a/tests/lib/test_azure.py +++ b/tests/lib/test_azure.py @@ -47,6 +47,23 @@ def test_implicit_deployment_path(client: Client) -> None: ) +@pytest.mark.parametrize("client", [sync_client, async_client]) +def test_implicit_deployment_strips_model_from_body(client: Client) -> None: + req = client._build_request( + FinalRequestOptions.construct( + method="post", + url="/images/generations", + json_data={"model": "gpt-image-1-5", "prompt": "sunset"}, + ) + ) + import json + + body = json.loads(req.content.decode()) + assert "model" not in body + assert body["prompt"] == "sunset" + assert "/deployments/gpt-image-1-5/" in str(req.url) + + @pytest.mark.parametrize( "client,method", [ From 8622ec922edf25665eb0632f22c7a5bea9c1e248 Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Sun, 1 Mar 2026 06:04:08 +0100 Subject: [PATCH 2/3] fix(review): address PR feedback - type narrowing, full URL assertion, parametrized test - Replace type: ignore[union-attr] with proper cast() type narrowing - Assert full URL instead of substring match in deployment test - Add parametrized test for /chat/completions endpoint - Move import json to module scope for consistency Refs: #2910 --- src/openai/lib/azure.py | 5 +++-- tests/lib/test_azure.py | 25 +++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index a4290e0ab1..7ca3fb844c 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -61,10 +61,11 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if options.url in _deployments_endpoints and is_mapping(options.json_data): - model = options.json_data.get("model") + json_data = cast(Mapping[str, Any], options.json_data) + model = json_data.get("model") if model is not None and "/deployments" not in str(self.base_url.path): options.url = f"/deployments/{model}{options.url}" - options.json_data = {k: v for k, v in options.json_data.items() if k != "model"} # type: ignore[union-attr] + options.json_data = {k: v for k, v in json_data.items() if k != "model"} return super()._build_request(options, retries_taken=retries_taken) diff --git a/tests/lib/test_azure.py b/tests/lib/test_azure.py index 896079b31a..d94918929c 100644 --- a/tests/lib/test_azure.py +++ b/tests/lib/test_azure.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import logging from typing import Union, cast from typing_extensions import Literal, Protocol @@ -56,12 +57,32 @@ def test_implicit_deployment_strips_model_from_body(client: Client) -> None: json_data={"model": "gpt-image-1-5", "prompt": "sunset"}, ) ) - import json body = json.loads(req.content.decode()) assert "model" not in body assert body["prompt"] == "sunset" - assert "/deployments/gpt-image-1-5/" in str(req.url) + assert ( + str(req.url) + == "https://example-resource.azure.openai.com/openai/deployments/gpt-image-1-5/images/generations?api-version=2023-07-01" + ) + + +@pytest.mark.parametrize("client", [sync_client, async_client]) +def test_implicit_deployment_strips_model_from_body_chat(client: Client) -> None: + req = client._build_request( + FinalRequestOptions.construct( + method="post", + url="/chat/completions", + json_data={"model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}]}, + ) + ) + body = json.loads(req.content.decode()) + assert "model" not in body + assert body["messages"] == [{"role": "user", "content": "hi"}] + assert ( + str(req.url) + == "https://example-resource.azure.openai.com/openai/deployments/gpt-4o/chat/completions?api-version=2023-07-01" + ) @pytest.mark.parametrize( From 33c8eb97d56f0b4a84ae6e650c272392b92e9900 Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Mon, 2 Mar 2026 17:28:48 +0100 Subject: [PATCH 3/3] fix(review): parametrize strip-model test across all deployment endpoints Replace two separate single-endpoint tests with one parametrized test covering all 8 deployment endpoints. json import was already at module scope and type: ignore was already removed. Refs: #2910 --- tests/lib/test_azure.py | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/tests/lib/test_azure.py b/tests/lib/test_azure.py index d94918929c..4bccf285bb 100644 --- a/tests/lib/test_azure.py +++ b/tests/lib/test_azure.py @@ -49,39 +49,34 @@ def test_implicit_deployment_path(client: Client) -> None: @pytest.mark.parametrize("client", [sync_client, async_client]) -def test_implicit_deployment_strips_model_from_body(client: Client) -> None: +@pytest.mark.parametrize( + "endpoint,model", + [ + ("/chat/completions", "gpt-4o"), + ("/completions", "gpt-4o"), + ("/embeddings", "text-embedding-ada-002"), + ("/images/generations", "gpt-image-1-5"), + ("/images/edits", "gpt-image-1-5"), + ("/audio/transcriptions", "whisper-1"), + ("/audio/translations", "whisper-1"), + ("/audio/speech", "tts-1"), + ], +) +def test_implicit_deployment_strips_model_from_body(client: Client, endpoint: str, model: str) -> None: req = client._build_request( FinalRequestOptions.construct( method="post", - url="/images/generations", - json_data={"model": "gpt-image-1-5", "prompt": "sunset"}, + url=endpoint, + json_data={"model": model, "extra": "value"}, ) ) body = json.loads(req.content.decode()) assert "model" not in body - assert body["prompt"] == "sunset" - assert ( - str(req.url) - == "https://example-resource.azure.openai.com/openai/deployments/gpt-image-1-5/images/generations?api-version=2023-07-01" - ) - - -@pytest.mark.parametrize("client", [sync_client, async_client]) -def test_implicit_deployment_strips_model_from_body_chat(client: Client) -> None: - req = client._build_request( - FinalRequestOptions.construct( - method="post", - url="/chat/completions", - json_data={"model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}]}, - ) - ) - body = json.loads(req.content.decode()) - assert "model" not in body - assert body["messages"] == [{"role": "user", "content": "hi"}] + assert body["extra"] == "value" assert ( str(req.url) - == "https://example-resource.azure.openai.com/openai/deployments/gpt-4o/chat/completions?api-version=2023-07-01" + == f"https://example-resource.azure.openai.com/openai/deployments/{model}{endpoint}?api-version=2023-07-01" )