Skip to content

Commit c056990

Browse files
Python: fix OpenAI Azure routing and provider samples
Prefer OpenAI when OPENAI_API_KEY is present unless Azure is explicitly requested. Clarify constructor docs, keep deprecated Azure wrappers compatible with stricter settings validation, and refresh the provider samples and tests to use the current client patterns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent efb14ce commit c056990

60 files changed

Lines changed: 1581 additions & 1924 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

python/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ FOUNDRY_PROJECT_ENDPOINT=...
5757
FOUNDRY_MODEL=...
5858
```
5959

60+
For the generic OpenAI clients (`OpenAIChatClient` and `OpenAIChatCompletionClient`), configuration
61+
resolves in this order:
62+
63+
1. Explicit Azure inputs such as `credential`, `azure_endpoint`, or `api_version`
64+
2. `OPENAI_API_KEY` / explicit OpenAI API-key parameters
65+
3. Azure environment fallback such as `AZURE_OPENAI_ENDPOINT` and `AZURE_OPENAI_API_KEY`
66+
67+
This means mixed shells default to OpenAI when `OPENAI_API_KEY` is present. To force Azure routing,
68+
pass an explicit Azure input such as `credential=AzureCliCredential()`.
69+
6070
You can also override environment variables by explicitly passing configuration parameters to the chat client constructor:
6171

6272
```python

python/packages/azure-ai/agent_framework_azure_ai/_deprecated_azure_openai.py

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111

1212
import json
1313
import logging
14+
import os
1415
import sys
1516
from collections.abc import Mapping, Sequence
17+
from contextlib import contextmanager
1618
from copy import copy
1719
from typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, cast
1820
from urllib.parse import urljoin, urlparse
@@ -109,6 +111,39 @@ def _apply_azure_defaults(
109111
settings["token_endpoint"] = default_token_endpoint
110112

111113

114+
@contextmanager
115+
def _prefer_single_azure_endpoint_env(*, endpoint: str | None, base_url: str | None) -> Any:
116+
"""Temporarily expose only the Azure endpoint setting that raw OpenAI clients accept.
117+
118+
The deprecated Azure wrappers have historically tolerated both
119+
``AZURE_OPENAI_BASE_URL`` and ``AZURE_OPENAI_ENDPOINT`` being present and prefer
120+
``base_url`` when both are available. The raw OpenAI constructors now validate
121+
that exactly one is set, so we temporarily hide the unused env var while
122+
delegating to those constructors.
123+
"""
124+
original_base_url = os.environ.get("AZURE_OPENAI_BASE_URL")
125+
original_endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT")
126+
127+
try:
128+
if base_url:
129+
os.environ["AZURE_OPENAI_BASE_URL"] = str(base_url)
130+
os.environ.pop("AZURE_OPENAI_ENDPOINT", None)
131+
elif endpoint:
132+
os.environ["AZURE_OPENAI_ENDPOINT"] = str(endpoint)
133+
os.environ.pop("AZURE_OPENAI_BASE_URL", None)
134+
yield
135+
finally:
136+
if original_base_url is None:
137+
os.environ.pop("AZURE_OPENAI_BASE_URL", None)
138+
else:
139+
os.environ["AZURE_OPENAI_BASE_URL"] = original_base_url
140+
141+
if original_endpoint is None:
142+
os.environ.pop("AZURE_OPENAI_ENDPOINT", None)
143+
else:
144+
os.environ["AZURE_OPENAI_ENDPOINT"] = original_endpoint
145+
146+
112147
# endregion
113148

114149

@@ -315,6 +350,8 @@ def __init__(
315350
"or 'AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME' environment variable."
316351
)
317352

353+
endpoint_value = azure_openai_settings.get("endpoint")
354+
client_base_url = azure_openai_settings.get("base_url")
318355
if not async_client:
319356
# Create the Azure OpenAI client directly
320357
merged_headers = dict(copy(default_headers)) if default_headers else {}
@@ -332,9 +369,7 @@ def __init__(
332369
if not api_key_secret and not ad_token_provider:
333370
raise ValueError("Please provide either api_key, credential, or a client.")
334371

335-
client_endpoint = azure_openai_settings.get("endpoint")
336-
client_base_url = azure_openai_settings.get("base_url")
337-
if not client_endpoint and not client_base_url:
372+
if not endpoint_value and not client_base_url:
338373
raise ValueError("Please provide an endpoint or a base_url")
339374

340375
client_args: dict[str, Any] = {"default_headers": merged_headers}
@@ -346,8 +381,8 @@ def __init__(
346381
client_args["api_key"] = api_key_secret.get_secret_value()
347382
if client_base_url:
348383
client_args["base_url"] = str(client_base_url)
349-
if client_endpoint and not client_base_url:
350-
client_args["azure_endpoint"] = str(client_endpoint)
384+
if endpoint_value and not client_base_url:
385+
client_args["azure_endpoint"] = str(endpoint_value)
351386
if responses_deployment_name:
352387
client_args["azure_deployment"] = responses_deployment_name
353388
if "websocket_base_url" in kwargs:
@@ -360,16 +395,17 @@ def __init__(
360395
self.api_version = azure_openai_settings.get("api_version") or ""
361396
self.deployment_name = responses_deployment_name
362397

363-
super().__init__(
364-
async_client=async_client,
365-
model=responses_deployment_name,
366-
api_version=azure_openai_settings.get("api_version"),
367-
instruction_role=instruction_role,
368-
default_headers=default_headers,
369-
middleware=middleware, # type: ignore[arg-type]
370-
function_invocation_configuration=function_invocation_configuration,
371-
**kwargs,
372-
)
398+
with _prefer_single_azure_endpoint_env(endpoint=endpoint_value, base_url=client_base_url):
399+
super().__init__(
400+
async_client=async_client,
401+
model=responses_deployment_name,
402+
api_version=azure_openai_settings.get("api_version"),
403+
instruction_role=instruction_role,
404+
default_headers=default_headers,
405+
middleware=middleware, # type: ignore[arg-type]
406+
function_invocation_configuration=function_invocation_configuration,
407+
**kwargs,
408+
)
373409

374410
@staticmethod
375411
def _create_client_from_project(
@@ -530,6 +566,8 @@ def __init__(
530566
"or 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' environment variable."
531567
)
532568

569+
endpoint_value = azure_openai_settings.get("endpoint")
570+
base_url_value = azure_openai_settings.get("base_url")
533571
if not async_client:
534572
# Create the Azure OpenAI client directly
535573
merged_headers = dict(copy(default_headers)) if default_headers else {}
@@ -547,8 +585,6 @@ def __init__(
547585
if not api_key_secret and not ad_token_provider:
548586
raise ValueError("Please provide either api_key, credential, or a client.")
549587

550-
endpoint_value = azure_openai_settings.get("endpoint")
551-
base_url_value = azure_openai_settings.get("base_url")
552588
if not endpoint_value and not base_url_value:
553589
raise ValueError("Please provide an endpoint or a base_url")
554590

@@ -573,16 +609,17 @@ def __init__(
573609
self.api_version = azure_openai_settings.get("api_version") or ""
574610
self.deployment_name = chat_deployment_name
575611

576-
super().__init__(
577-
async_client=async_client,
578-
model=chat_deployment_name,
579-
api_version=azure_openai_settings.get("api_version"),
580-
instruction_role=instruction_role,
581-
default_headers=default_headers,
582-
additional_properties=additional_properties,
583-
middleware=middleware, # type: ignore[arg-type]
584-
function_invocation_configuration=function_invocation_configuration,
585-
)
612+
with _prefer_single_azure_endpoint_env(endpoint=endpoint_value, base_url=base_url_value):
613+
super().__init__(
614+
async_client=async_client,
615+
model=chat_deployment_name,
616+
api_version=azure_openai_settings.get("api_version"),
617+
instruction_role=instruction_role,
618+
default_headers=default_headers,
619+
additional_properties=additional_properties,
620+
middleware=middleware, # type: ignore[arg-type]
621+
function_invocation_configuration=function_invocation_configuration,
622+
)
586623

587624
@override
588625
def _parse_text_from_openai(self, choice: Choice | ChunkChoice) -> Content | None:

python/packages/openai/AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ agent_framework_openai/
2727

2828
All clients follow the Raw + Full-Featured pattern (e.g., `RawOpenAIChatClient` + `OpenAIChatClient`).
2929

30+
The generic OpenAI chat clients support both OpenAI and Azure OpenAI routing. Precedence is:
31+
explicit Azure inputs (`credential`, `azure_endpoint`, `api_version`) → OpenAI API key
32+
(`OPENAI_API_KEY`) → Azure environment fallback (`AZURE_OPENAI_*`).
33+
3034
## Dependencies
3135

3236
- `agent-framework-core` — core abstractions

python/packages/openai/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ from agent_framework.openai import OpenAIChatClient
1515

1616
client = OpenAIChatClient(model_id="gpt-4o")
1717
```
18+
19+
When both OpenAI and Azure environment variables are present, the generic OpenAI clients prefer
20+
OpenAI whenever `OPENAI_API_KEY` is configured. To force Azure routing, pass an explicit Azure input
21+
such as `credential`, `azure_endpoint`, or `api_version`.

0 commit comments

Comments
 (0)