From 5cbd98d473ad6ea53f935f1e77ca74bae709e392 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Fri, 23 Jan 2026 09:16:39 -0800 Subject: [PATCH 1/2] openai chat and responses to v1 ga api --- .../agent_framework/azure/_chat_client.py | 17 ++-- .../azure/_responses_client.py | 35 ++------ .../core/agent_framework/azure/_shared.py | 83 ++++++++++++------- python/packages/core/tests/azure/conftest.py | 2 +- .../tests/azure/test_azure_chat_client.py | 15 ++-- .../azure/test_azure_responses_client.py | 16 ++++ .../agents/azure_openai/README.md | 2 +- ...zure_chat_client_with_explicit_settings.py | 4 +- .../azure_responses_client_image_analysis.py | 8 +- ...responses_client_with_explicit_settings.py | 4 +- ...azure_responses_client_with_file_search.py | 6 +- .../azure_responses_client_with_hosted_mcp.py | 10 ++- .../azure_responses_client_with_local_mcp.py | 3 +- .../devui/azure_responses_agent/agent.py | 1 - .../getting_started/devui/in_memory_mode.py | 5 +- .../multimodal_input/README.md | 3 +- 16 files changed, 118 insertions(+), 96 deletions(-) diff --git a/python/packages/core/agent_framework/azure/_chat_client.py b/python/packages/core/agent_framework/azure/_chat_client.py index b60054165f..8800d56019 100644 --- a/python/packages/core/agent_framework/azure/_chat_client.py +++ b/python/packages/core/agent_framework/azure/_chat_client.py @@ -3,11 +3,11 @@ import json import logging import sys -from collections.abc import Mapping +from collections.abc import Awaitable, Callable, Mapping from typing import Any, Generic, TypedDict from azure.core.credentials import TokenCredential -from openai.lib.azure import AsyncAzureADTokenProvider, AsyncAzureOpenAI +from openai import AsyncOpenAI from openai.types.chat.chat_completion import Choice from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice from pydantic import ValidationError @@ -152,13 +152,12 @@ def __init__( deployment_name: str | None = None, endpoint: str | None = None, base_url: str | None = None, - api_version: str | None = None, ad_token: str | None = None, - ad_token_provider: AsyncAzureADTokenProvider | None = None, + ad_token_provider: Callable[[], str | Awaitable[str]] | None = None, token_endpoint: str | None = None, credential: TokenCredential | None = None, default_headers: Mapping[str, str] | None = None, - async_client: AsyncAzureOpenAI | None = None, + async_client: AsyncOpenAI | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, instruction_role: str | None = None, @@ -176,11 +175,9 @@ def __init__( in the env vars or .env file. Can also be set via environment variable AZURE_OPENAI_ENDPOINT. base_url: The deployment base URL. If provided will override the value - in the env vars or .env file. + in the env vars or .env file. For standard Azure endpoints, /openai/v1/ is + appended automatically. Can also be set via environment variable AZURE_OPENAI_BASE_URL. - api_version: The deployment API version. If provided will override the value - in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_API_VERSION. ad_token: The Azure Active Directory token. ad_token_provider: The Azure Active Directory token provider. token_endpoint: The token endpoint to request an Azure token. @@ -236,7 +233,6 @@ class MyOptions(AzureOpenAIChatOptions, total=False): base_url=base_url, # type: ignore endpoint=endpoint, # type: ignore chat_deployment_name=deployment_name, - api_version=api_version, env_file_path=env_file_path, env_file_encoding=env_file_encoding, token_endpoint=token_endpoint, @@ -254,7 +250,6 @@ class MyOptions(AzureOpenAIChatOptions, total=False): deployment_name=azure_openai_settings.chat_deployment_name, endpoint=azure_openai_settings.endpoint, base_url=azure_openai_settings.base_url, - api_version=azure_openai_settings.api_version, # type: ignore api_key=azure_openai_settings.api_key.get_secret_value() if azure_openai_settings.api_key else None, ad_token=ad_token, ad_token_provider=ad_token_provider, diff --git a/python/packages/core/agent_framework/azure/_responses_client.py b/python/packages/core/agent_framework/azure/_responses_client.py index e4f6989fa0..feb42e5392 100644 --- a/python/packages/core/agent_framework/azure/_responses_client.py +++ b/python/packages/core/agent_framework/azure/_responses_client.py @@ -1,12 +1,11 @@ # Copyright (c) Microsoft. All rights reserved. import sys -from collections.abc import Mapping +from collections.abc import Awaitable, Callable, Mapping from typing import TYPE_CHECKING, Any, Generic, TypedDict -from urllib.parse import urljoin from azure.core.credentials import TokenCredential -from openai.lib.azure import AsyncAzureADTokenProvider, AsyncAzureOpenAI +from openai import AsyncOpenAI from pydantic import ValidationError from agent_framework import use_chat_middleware, use_function_invocation @@ -59,13 +58,12 @@ def __init__( deployment_name: str | None = None, endpoint: str | None = None, base_url: str | None = None, - api_version: str | None = None, ad_token: str | None = None, - ad_token_provider: AsyncAzureADTokenProvider | None = None, + ad_token_provider: Callable[[], str | Awaitable[str]] | None = None, token_endpoint: str | None = None, credential: TokenCredential | None = None, default_headers: Mapping[str, str] | None = None, - async_client: AsyncAzureOpenAI | None = None, + async_client: AsyncOpenAI | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, instruction_role: str | None = None, @@ -83,11 +81,9 @@ def __init__( in the env vars or .env file. Can also be set via environment variable AZURE_OPENAI_ENDPOINT. base_url: The deployment base URL. If provided will override the value - in the env vars or .env file. Currently, the base_url must end with "/openai/v1/". + in the env vars or .env file. For standard Azure endpoints, /openai/v1/ is + appended automatically. Can also be set via environment variable AZURE_OPENAI_BASE_URL. - api_version: The deployment API version. If provided will override the value - in the env vars or .env file. Currently, the api_version must be "preview". - Can also be set via environment variable AZURE_OPENAI_API_VERSION. ad_token: The Azure Active Directory token. ad_token_provider: The Azure Active Directory token provider. token_endpoint: The token endpoint to request an Azure token. @@ -142,22 +138,10 @@ class MyOptions(AzureOpenAIResponsesOptions, total=False): base_url=base_url, # type: ignore endpoint=endpoint, # type: ignore responses_deployment_name=deployment_name, - api_version=api_version, env_file_path=env_file_path, env_file_encoding=env_file_encoding, token_endpoint=token_endpoint, - default_api_version="preview", ) - # TODO(peterychang): This is a temporary hack to ensure that the base_url is set correctly - # while this feature is in preview. - # But we should only do this if we're on azure. Private deployments may not need this. - if ( - not azure_openai_settings.base_url - and azure_openai_settings.endpoint - and azure_openai_settings.endpoint.host - and azure_openai_settings.endpoint.host.endswith(".openai.azure.com") - ): - azure_openai_settings.base_url = urljoin(str(azure_openai_settings.endpoint), "/openai/v1/") # type: ignore except ValidationError as exc: raise ServiceInitializationError(f"Failed to validate settings: {exc}") from exc @@ -171,7 +155,6 @@ class MyOptions(AzureOpenAIResponsesOptions, total=False): deployment_name=azure_openai_settings.responses_deployment_name, endpoint=azure_openai_settings.endpoint, base_url=azure_openai_settings.base_url, - api_version=azure_openai_settings.api_version, # type: ignore api_key=azure_openai_settings.api_key.get_secret_value() if azure_openai_settings.api_key else None, ad_token=ad_token, ad_token_provider=ad_token_provider, @@ -183,8 +166,8 @@ class MyOptions(AzureOpenAIResponsesOptions, total=False): ) @override - def _check_model_presence(self, run_options: dict[str, Any]) -> None: - if not run_options.get("model"): + def _check_model_presence(self, options: dict[str, Any]) -> None: + if not options.get("model"): if not self.model_id: raise ValueError("deployment_name must be a non-empty string") - run_options["model"] = self.model_id + options["model"] = self.model_id diff --git a/python/packages/core/agent_framework/azure/_shared.py b/python/packages/core/agent_framework/azure/_shared.py index e3eb37b26e..d8ec64124b 100644 --- a/python/packages/core/agent_framework/azure/_shared.py +++ b/python/packages/core/agent_framework/azure/_shared.py @@ -5,9 +5,10 @@ from collections.abc import Awaitable, Callable, Mapping from copy import copy from typing import Any, ClassVar, Final +from urllib.parse import urljoin from azure.core.credentials import TokenCredential -from openai.lib.azure import AsyncAzureOpenAI +from openai import AsyncOpenAI from pydantic import SecretStr, model_validator from .._pydantic import AFBaseSettings, HTTPsUrl @@ -142,6 +143,29 @@ def _validate_fields(self) -> Self: return self +def _construct_v1_base_url(endpoint: HTTPsUrl | None, base_url: HTTPsUrl | None) -> str | None: + """Construct the v1 API base URL from endpoint if not explicitly provided. + + For standard Azure OpenAI endpoints, automatically appends /openai/v1/ path. + Custom/private deployments can provide their own base_url. + + Args: + endpoint: The Azure OpenAI endpoint URL. + base_url: Explicit base URL if provided by user. + + Returns: + The base URL to use, or None if neither endpoint nor base_url is valid. + """ + if base_url: + return str(base_url) + + # Standard Azure OpenAI endpoints + if endpoint and endpoint.host and endpoint.host.endswith((".openai.azure.com", ".services.ai.azure.com")): + return urljoin(str(endpoint), "/openai/v1/") + + return None + + class AzureOpenAIConfigMixin(OpenAIBase): """Internal class for configuring a connection to an Azure OpenAI service.""" @@ -153,31 +177,27 @@ def __init__( deployment_name: str, endpoint: HTTPsUrl | None = None, base_url: HTTPsUrl | None = None, - api_version: str = DEFAULT_AZURE_API_VERSION, api_key: str | None = None, ad_token: str | None = None, ad_token_provider: Callable[[], str | Awaitable[str]] | None = None, token_endpoint: str | None = None, credential: TokenCredential | None = None, default_headers: Mapping[str, str] | None = None, - client: AsyncAzureOpenAI | None = None, + client: AsyncOpenAI | None = None, instruction_role: str | None = None, **kwargs: Any, ) -> None: """Internal class for configuring a connection to an Azure OpenAI service. - The `validate_call` decorator is used with a configuration that allows arbitrary types. - This is necessary for types like `HTTPsUrl` and `OpenAIModelTypes`. - Args: deployment_name: Name of the deployment. endpoint: The specific endpoint URL for the deployment. - base_url: The base URL for Azure services. - api_version: Azure API version. Defaults to the defined DEFAULT_AZURE_API_VERSION. - api_key: API key for Azure services. + base_url: The base URL for Azure services. If not provided and endpoint is a + standard Azure OpenAI endpoint, /openai/v1/ will be appended automatically. + api_key: API key for Azure services. Can also be a token provider callable. ad_token: Azure AD token for authentication. ad_token_provider: A callable or coroutine function providing Azure AD tokens. - token_endpoint: Azure AD token endpoint use to get the token. + token_endpoint: Azure AD token endpoint used to get the token. credential: Azure credential for authentication. default_headers: Default headers for HTTP requests. client: An existing client to use. @@ -191,7 +211,11 @@ def __init__( if APP_INFO: merged_headers.update(APP_INFO) merged_headers = prepend_agent_framework_to_user_agent(merged_headers) + if not client: + # Construct v1 base URL from endpoint if not explicitly provided + v1_base_url = _construct_v1_base_url(endpoint, base_url) + # If the client is None, the api_key is none, the ad_token is none, and the ad_token_provider is none, # then we will attempt to get the ad_token using the default endpoint specified in the Azure OpenAI # settings. @@ -203,35 +227,34 @@ def __init__( "Please provide either api_key, ad_token or ad_token_provider or a client." ) - if not endpoint and not base_url: - raise ServiceInitializationError("Please provide an endpoint or a base_url") + if not v1_base_url: + raise ServiceInitializationError( + "Please provide an endpoint or a base_url. " + "For standard Azure OpenAI endpoints, the v1 API path will be appended automatically." + ) + + # Determine the effective api_key for AsyncOpenAI + effective_api_key: str | Callable[[], str | Awaitable[str]] | None = None + if api_key: + effective_api_key = api_key + elif ad_token_provider: + effective_api_key = ad_token_provider + elif ad_token: + effective_api_key = ad_token args: dict[str, Any] = { + "base_url": v1_base_url, + "api_key": effective_api_key, "default_headers": merged_headers, } - if api_version: - args["api_version"] = api_version - if ad_token: - args["azure_ad_token"] = ad_token - if ad_token_provider: - args["azure_ad_token_provider"] = ad_token_provider - if api_key: - args["api_key"] = api_key - if base_url: - args["base_url"] = str(base_url) - if endpoint and not base_url: - args["azure_endpoint"] = str(endpoint) - if deployment_name: - args["azure_deployment"] = deployment_name if "websocket_base_url" in kwargs: args["websocket_base_url"] = kwargs.pop("websocket_base_url") - client = AsyncAzureOpenAI(**args) + client = AsyncOpenAI(**args) # Store configuration as instance attributes for serialization - self.endpoint = str(endpoint) - self.base_url = str(base_url) - self.api_version = api_version + self.endpoint = str(endpoint) if endpoint else None + self.base_url = str(base_url) if base_url else None self.deployment_name = deployment_name self.instruction_role = instruction_role # Store default_headers but filter out USER_AGENT_KEY for serialization diff --git a/python/packages/core/tests/azure/conftest.py b/python/packages/core/tests/azure/conftest.py index a9c03cd664..6b672aef61 100644 --- a/python/packages/core/tests/azure/conftest.py +++ b/python/packages/core/tests/azure/conftest.py @@ -31,7 +31,7 @@ def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dic override_env_param_dict = {} env_vars = { - "AZURE_OPENAI_ENDPOINT": "https://test-endpoint.com", + "AZURE_OPENAI_ENDPOINT": "https://test-endpoint.openai.azure.com", "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "test_chat_deployment", "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME": "test_chat_deployment", "AZURE_OPENAI_TEXT_DEPLOYMENT_NAME": "test_text_deployment", diff --git a/python/packages/core/tests/azure/test_azure_chat_client.py b/python/packages/core/tests/azure/test_azure_chat_client.py index 84d7d897ff..73db56b3ea 100644 --- a/python/packages/core/tests/azure/test_azure_chat_client.py +++ b/python/packages/core/tests/azure/test_azure_chat_client.py @@ -8,7 +8,7 @@ import pytest from azure.identity import AzureCliCredential from httpx import Request, Response -from openai import AsyncAzureOpenAI, AsyncStream +from openai import AsyncOpenAI, AsyncStream from openai.resources.chat.completions import AsyncCompletions as AsyncChatCompletions from openai.types.chat import ChatCompletion, ChatCompletionChunk from openai.types.chat.chat_completion import Choice @@ -51,18 +51,18 @@ def test_init(azure_openai_unit_test_env: dict[str, str]) -> None: azure_chat_client = AzureOpenAIChatClient() assert azure_chat_client.client is not None - assert isinstance(azure_chat_client.client, AsyncAzureOpenAI) + assert isinstance(azure_chat_client.client, AsyncOpenAI) assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] assert isinstance(azure_chat_client, BaseChatClient) def test_init_client(azure_openai_unit_test_env: dict[str, str]) -> None: # Test successful initialization with client - client = MagicMock(spec=AsyncAzureOpenAI) + client = MagicMock(spec=AsyncOpenAI) azure_chat_client = AzureOpenAIChatClient(async_client=client) assert azure_chat_client.client is not None - assert isinstance(azure_chat_client.client, AsyncAzureOpenAI) + assert isinstance(azure_chat_client.client, AsyncOpenAI) def test_init_base_url(azure_openai_unit_test_env: dict[str, str]) -> None: @@ -74,7 +74,7 @@ def test_init_base_url(azure_openai_unit_test_env: dict[str, str]) -> None: ) assert azure_chat_client.client is not None - assert isinstance(azure_chat_client.client, AsyncAzureOpenAI) + assert isinstance(azure_chat_client.client, AsyncOpenAI) assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] assert isinstance(azure_chat_client, BaseChatClient) for key, value in default_headers.items(): @@ -87,7 +87,7 @@ def test_init_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None: azure_chat_client = AzureOpenAIChatClient() assert azure_chat_client.client is not None - assert isinstance(azure_chat_client.client, AsyncAzureOpenAI) + assert isinstance(azure_chat_client.client, AsyncOpenAI) assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] assert isinstance(azure_chat_client, BaseChatClient) @@ -122,7 +122,6 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None: "deployment_name": azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], "endpoint": azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"], "api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"], - "api_version": azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"], "default_headers": default_headers, "env_file_path": "test.env", } @@ -132,7 +131,7 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None: assert dumped_settings["model_id"] == settings["deployment_name"] assert str(settings["endpoint"]) in str(dumped_settings["endpoint"]) assert str(settings["deployment_name"]) == str(dumped_settings["deployment_name"]) - assert settings["api_version"] == dumped_settings["api_version"] + # Note: api_version is no longer used - v1 API doesn't require it assert "api_key" not in dumped_settings # Assert that the default header we added is present in the dumped_settings default headers diff --git a/python/packages/core/tests/azure/test_azure_responses_client.py b/python/packages/core/tests/azure/test_azure_responses_client.py index b2d4a59ab7..1afeb5e369 100644 --- a/python/packages/core/tests/azure/test_azure_responses_client.py +++ b/python/packages/core/tests/azure/test_azure_responses_client.py @@ -119,6 +119,22 @@ def test_init_with_empty_model_id(azure_openai_unit_test_env: dict[str, str]) -> ) +def test_check_model_presence_sets_model(azure_openai_unit_test_env: dict[str, str]) -> None: + """Test that _check_model_presence sets model from deployment_name.""" + client = AzureOpenAIResponsesClient() + options: dict[str, Any] = {} + client._check_model_presence(options) # type: ignore + assert options["model"] == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] + + +def test_check_model_presence_preserves_existing_model(azure_openai_unit_test_env: dict[str, str]) -> None: + """Test that _check_model_presence does not override an existing model.""" + client = AzureOpenAIResponsesClient() + options: dict[str, Any] = {"model": "custom-model"} + client._check_model_presence(options) # type: ignore + assert options["model"] == "custom-model" + + def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None: default_headers = {"X-Unit-Test": "test-guid"} diff --git a/python/samples/getting_started/agents/azure_openai/README.md b/python/samples/getting_started/agents/azure_openai/README.md index 466860de3e..57d9be0d1e 100644 --- a/python/samples/getting_started/agents/azure_openai/README.md +++ b/python/samples/getting_started/agents/azure_openai/README.md @@ -35,7 +35,7 @@ Make sure to set the following environment variables before running the examples - `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`: The name of your Azure OpenAI Responses deployment Optionally, you can set: -- `AZURE_OPENAI_API_VERSION`: The API version to use (default is `2024-02-15-preview`) +- `AZURE_OPENAI_API_VERSION`: The API version to use for Assistants client (default is `2024-05-01-preview`). Not needed for Chat or Responses clients. - `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key (if not using `AzureCliCredential`) - `AZURE_OPENAI_BASE_URL`: Your Azure OpenAI base URL (if different from the endpoint) diff --git a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_explicit_settings.py b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_explicit_settings.py index 8d39b4b035..40f04df944 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_explicit_settings.py +++ b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_explicit_settings.py @@ -39,7 +39,9 @@ async def main() -> None: tools=get_weather, ) - result = await agent.run("What's the weather like in New York?") + query = "What's the weather like in New York?" + print(f"User: {query}") + result = await agent.run(query) print(f"Result: {result}\n") diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_image_analysis.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_image_analysis.py index ebfb81dada..8c271ffe7c 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_image_analysis.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_image_analysis.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatMessage, TextContent, UriContent +from agent_framework import ChatMessage, Content from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential @@ -27,9 +27,9 @@ async def main(): user_message = ChatMessage( role="user", contents=[ - TextContent(text="What do you see in this image?"), - UriContent( - uri="https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", + Content.from_text("What do you see in this image?"), + Content.from_uri( + uri="https://www.w3schools.com/css/img_5terre.jpg", media_type="image/jpeg", ), ], diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py index 16960401bb..ba70949080 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py @@ -39,7 +39,9 @@ async def main() -> None: tools=get_weather, ) - result = await agent.run("What's the weather like in New York?") + query = "What's the weather like in New York?" + print(f"User: {query}") + result = await agent.run(query) print(f"Result: {result}\n") diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_file_search.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_file_search.py index b42c7acf2f..08f35eb659 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_file_search.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_file_search.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatAgent, HostedFileSearchTool, HostedVectorStoreContent +from agent_framework import ChatAgent, Content, HostedFileSearchTool from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential @@ -22,7 +22,7 @@ # Helper functions -async def create_vector_store(client: AzureOpenAIResponsesClient) -> tuple[str, HostedVectorStoreContent]: +async def create_vector_store(client: AzureOpenAIResponsesClient) -> tuple[str, Content]: """Create a vector store with sample documents.""" file = await client.client.files.create( file=("todays_weather.txt", b"The weather today is sunny with a high of 75F."), purpose="assistants" @@ -35,7 +35,7 @@ async def create_vector_store(client: AzureOpenAIResponsesClient) -> tuple[str, if result.last_error is not None: raise Exception(f"Vector store file processing failed with status: {result.last_error.message}") - return file.id, HostedVectorStoreContent(vector_store_id=vector_store.id) + return file.id, Content.from_hosted_vector_store(vector_store_id=vector_store.id) async def delete_vector_store(client: AzureOpenAIResponsesClient, file_id: str, vector_store_id: str) -> None: diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py index 9ed1d74e16..aac5a62887 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py @@ -33,7 +33,10 @@ async def handle_approvals_without_thread(query: str, agent: "AgentProtocol"): new_inputs.append(ChatMessage(role="assistant", contents=[user_input_needed])) user_approval = input("Approve function call? (y/n): ") new_inputs.append( - ChatMessage(role="user", contents=[user_input_needed.create_response(user_approval.lower() == "y")]) + ChatMessage( + role="user", + contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], + ) ) result = await agent.run(new_inputs) @@ -56,7 +59,7 @@ async def handle_approvals_with_thread(query: str, agent: "AgentProtocol", threa new_input.append( ChatMessage( role="user", - contents=[user_input_needed.create_response(user_approval.lower() == "y")], + contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) ) result = await agent.run(new_input, thread=thread, store=True) @@ -82,7 +85,8 @@ async def handle_approvals_with_thread_streaming(query: str, agent: "AgentProtoc user_approval = input("Approve function call? (y/n): ") new_input.append( ChatMessage( - role="user", contents=[user_input_needed.create_response(user_approval.lower() == "y")] + role="user", + contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) ) new_input_added = True diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_local_mcp.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_local_mcp.py index 4958a64b44..18a88f674e 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_local_mcp.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_local_mcp.py @@ -23,7 +23,6 @@ # Environment variables for Azure OpenAI Responses authentication # AZURE_OPENAI_ENDPOINT="" # AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME="" -# AZURE_OPENAI_API_VERSION="" # e.g. "2025-03-01-preview" async def main(): @@ -32,7 +31,7 @@ async def main(): credential = AzureCliCredential() # Build an agent backed by Azure OpenAI Responses - # (endpoint/deployment/api_version can also come from env vars above) + # (endpoint/deployment can also come from env vars above) responses_client = AzureOpenAIResponsesClient( credential=credential, ) diff --git a/python/samples/getting_started/devui/azure_responses_agent/agent.py b/python/samples/getting_started/devui/azure_responses_agent/agent.py index a2a8dbf054..c4b5079a9d 100644 --- a/python/samples/getting_started/devui/azure_responses_agent/agent.py +++ b/python/samples/getting_started/devui/azure_responses_agent/agent.py @@ -87,7 +87,6 @@ def extract_key_points( chat_client=AzureOpenAIResponsesClient( deployment_name=_deployment_name, endpoint=_endpoint, - api_version="2025-03-01-preview", # Required for Responses API ), tools=[summarize_document, extract_key_points], ) diff --git a/python/samples/getting_started/devui/in_memory_mode.py b/python/samples/getting_started/devui/in_memory_mode.py index 12bb63864c..b4030bf3e8 100644 --- a/python/samples/getting_started/devui/in_memory_mode.py +++ b/python/samples/getting_started/devui/in_memory_mode.py @@ -66,9 +66,8 @@ def main(): # Create Azure OpenAI chat client chat_client = AzureOpenAIChatClient( api_key=os.environ.get("AZURE_OPENAI_API_KEY"), - azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"), - api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-10-21"), - model_id=os.environ.get("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", "gpt-4o"), + endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"), + deployment_name=os.environ.get("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", "gpt-4o"), ) # Create agents diff --git a/python/samples/getting_started/multimodal_input/README.md b/python/samples/getting_started/multimodal_input/README.md index 2254fe89f7..328f1be340 100644 --- a/python/samples/getting_started/multimodal_input/README.md +++ b/python/samples/getting_started/multimodal_input/README.md @@ -36,9 +36,10 @@ Set the following environment variables before running the examples: - `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`: The name of your Azure OpenAI responses model deployment Optionally for Azure OpenAI: -- `AZURE_OPENAI_API_VERSION`: The API version to use (default is `2024-10-21`) - `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key (if not using `AzureCliCredential`) +> **Note:** For Chat and Responses clients, the v1 API is used which does not require an `api_version` parameter. + **Note:** You can also provide configuration directly in code instead of using environment variables: ```python # Example: Pass deployment_name directly From 93a960016e8c834c76d29cf6b299923e1475f6cf Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Fri, 23 Jan 2026 10:14:48 -0800 Subject: [PATCH 2/2] added tests --- .../core/agent_framework/azure/_shared.py | 4 +- .../tests/azure/test_azure_chat_client.py | 50 +++++++++++++++++ .../azure/test_azure_responses_client.py | 54 +++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/azure/_shared.py b/python/packages/core/agent_framework/azure/_shared.py index d8ec64124b..60ad210731 100644 --- a/python/packages/core/agent_framework/azure/_shared.py +++ b/python/packages/core/agent_framework/azure/_shared.py @@ -230,7 +230,9 @@ def __init__( if not v1_base_url: raise ServiceInitializationError( "Please provide an endpoint or a base_url. " - "For standard Azure OpenAI endpoints, the v1 API path will be appended automatically." + "For standard Azure OpenAI endpoints (*.openai.azure.com and *.services.ai.azure.com), " + "the v1 API path will be appended automatically; for non-standard or private deployments, " + "you must provide a base_url that already includes the desired API path." ) # Determine the effective api_key for AsyncOpenAI diff --git a/python/packages/core/tests/azure/test_azure_chat_client.py b/python/packages/core/tests/azure/test_azure_chat_client.py index 73db56b3ea..69c8160938 100644 --- a/python/packages/core/tests/azure/test_azure_chat_client.py +++ b/python/packages/core/tests/azure/test_azure_chat_client.py @@ -630,6 +630,56 @@ async def test_streaming_with_none_delta( assert any(msg.contents for msg in results) +def test_client_uses_custom_base_url_when_provided() -> None: + """Test that a custom base_url is used directly when provided.""" + custom_base_url = "https://custom.example.com/my/path/" + + client = AzureOpenAIChatClient( + deployment_name="gpt-4o", + base_url=custom_base_url, + api_key="test-key", + ) + + assert client.client is not None + assert str(client.client.base_url) == custom_base_url + + +def test_client_constructs_v1_url_for_openai_azure_com_endpoint() -> None: + """Test v1 URL construction for .openai.azure.com endpoints.""" + client = AzureOpenAIChatClient( + deployment_name="gpt-4o", + endpoint="https://my-resource.openai.azure.com", + api_key="test-key", + ) + + assert client.client is not None + assert str(client.client.base_url) == "https://my-resource.openai.azure.com/openai/v1/" + + +def test_client_constructs_v1_url_for_services_ai_azure_com_endpoint() -> None: + """Test v1 URL construction for .services.ai.azure.com endpoints.""" + client = AzureOpenAIChatClient( + deployment_name="gpt-4o", + endpoint="https://my-resource.services.ai.azure.com", + api_key="test-key", + ) + + assert client.client is not None + assert str(client.client.base_url) == "https://my-resource.services.ai.azure.com/openai/v1/" + + +def test_client_raises_error_for_non_standard_endpoint() -> None: + """Test that non-standard endpoints raise an error.""" + with pytest.raises(ServiceInitializationError) as exc_info: + AzureOpenAIChatClient( + deployment_name="gpt-4o", + endpoint="https://api.openai.com", + api_key="test-key", + ) + + assert "Please provide an endpoint or a base_url" in str(exc_info.value) + + @ai_function def get_story_text() -> str: """Returns a story about Emily and David.""" diff --git a/python/packages/core/tests/azure/test_azure_responses_client.py b/python/packages/core/tests/azure/test_azure_responses_client.py index 1afeb5e369..f0d4bdf823 100644 --- a/python/packages/core/tests/azure/test_azure_responses_client.py +++ b/python/packages/core/tests/azure/test_azure_responses_client.py @@ -6,6 +6,7 @@ import pytest from azure.identity import AzureCliCredential +from openai import AsyncOpenAI from pydantic import BaseModel from pytest import param @@ -156,6 +157,59 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None: assert "User-Agent" not in dumped_settings["default_headers"] +def test_responses_client_uses_custom_base_url_when_provided() -> None: + """Test that a custom base_url is used directly when provided.""" + custom_base_url = "https://custom.example.com/my/path/" + + client = AzureOpenAIResponsesClient( + deployment_name="gpt-4o", + base_url=custom_base_url, + api_key="test-key", + ) + + assert client.client is not None + assert isinstance(client.client, AsyncOpenAI) + assert str(client.client.base_url) == custom_base_url + + +def test_responses_client_constructs_v1_url_for_openai_azure_com_endpoint() -> None: + """Test v1 URL construction for .openai.azure.com endpoints.""" + client = AzureOpenAIResponsesClient( + deployment_name="gpt-4o", + endpoint="https://my-resource.openai.azure.com", + api_key="test-key", + ) + + assert client.client is not None + assert isinstance(client.client, AsyncOpenAI) + assert str(client.client.base_url) == "https://my-resource.openai.azure.com/openai/v1/" + + +def test_responses_client_constructs_v1_url_for_services_ai_azure_com_endpoint() -> None: + """Test v1 URL construction for .services.ai.azure.com endpoints.""" + client = AzureOpenAIResponsesClient( + deployment_name="gpt-4o", + endpoint="https://my-resource.services.ai.azure.com", + api_key="test-key", + ) + + assert client.client is not None + assert isinstance(client.client, AsyncOpenAI) + assert str(client.client.base_url) == "https://my-resource.services.ai.azure.com/openai/v1/" + + +def test_responses_client_raises_error_for_non_standard_endpoint() -> None: + """Test that non-standard endpoints raise an error.""" + with pytest.raises(ServiceInitializationError) as exc_info: + AzureOpenAIResponsesClient( + deployment_name="gpt-4o", + endpoint="https://api.openai.com", + api_key="test-key", + ) + + assert "Please provide an endpoint or a base_url" in str(exc_info.value) + + # region Integration Tests