From de0a3604cafde2f0d3475c3887e0561ad43a80dc Mon Sep 17 00:00:00 2001 From: Darren Cohen <39422044+dargilco@users.noreply.github.com> Date: Tue, 17 Mar 2026 08:57:40 -0700 Subject: [PATCH 01/11] Re-emit --- .../azure/ai/projects/_client.py | 15 ++++++-- .../azure/ai/projects/_configuration.py | 26 +++++++++---- .../azure/ai/projects/_utils/model_base.py | 6 +++ .../azure/ai/projects/aio/_client.py | 15 ++++++-- .../azure/ai/projects/aio/_configuration.py | 26 +++++++++---- .../ai/projects/aio/operations/_operations.py | 31 +++++++++++++-- .../azure/ai/projects/models/_enums.py | 2 + .../azure/ai/projects/models/_models.py | 2 +- .../ai/projects/operations/_operations.py | 38 +++++++++++++++++-- .../foundry_features_header_test_base.py | 9 ++--- .../test_foundry_features_header.py | 5 +-- .../test_foundry_features_header_async.py | 3 +- .../test_foundry_features_header_optional.py | 9 +++-- ..._foundry_features_header_optional_async.py | 21 +++++----- 14 files changed, 152 insertions(+), 56 deletions(-) diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/_client.py b/sdk/ai/azure-ai-projects/azure/ai/projects/_client.py index c9b4ba135893..635a13982e7b 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/_client.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/_client.py @@ -7,10 +7,11 @@ # -------------------------------------------------------------------------- from copy import deepcopy -from typing import Any, Optional, TYPE_CHECKING +from typing import Any, Optional, TYPE_CHECKING, Union from typing_extensions import Self from azure.core import PipelineClient +from azure.core.credentials import AzureKeyCredential from azure.core.pipeline import policies from azure.core.rest import HttpRequest, HttpResponse @@ -53,8 +54,10 @@ class AIProjectClient: # pylint: disable=too-many-instance-attributes the form "https://{ai-services-account-name}.services.ai.azure.com/api/projects/_project". Required. :type endpoint: str - :param credential: Credential used to authenticate requests to the service. Required. - :type credential: ~azure.core.credentials.TokenCredential + :param credential: Credential used to authenticate requests to the service. Is either a key + credential type or a token credential type. Required. + :type credential: ~azure.core.credentials.AzureKeyCredential or + ~azure.core.credentials.TokenCredential :param allow_preview: Whether to enable preview features. Must be specified and set to True to enable preview features. Default value is None. :type allow_preview: bool @@ -65,7 +68,11 @@ class AIProjectClient: # pylint: disable=too-many-instance-attributes """ def __init__( - self, endpoint: str, credential: "TokenCredential", allow_preview: Optional[bool] = None, **kwargs: Any + self, + endpoint: str, + credential: Union[AzureKeyCredential, "TokenCredential"], + allow_preview: Optional[bool] = None, + **kwargs: Any ) -> None: _endpoint = "{endpoint}" self._config = AIProjectClientConfiguration( diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/_configuration.py b/sdk/ai/azure-ai-projects/azure/ai/projects/_configuration.py index 1b9d513e9220..25c0f861fa8b 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/_configuration.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/_configuration.py @@ -6,8 +6,9 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -from typing import Any, Optional, TYPE_CHECKING +from typing import Any, Optional, TYPE_CHECKING, Union +from azure.core.credentials import AzureKeyCredential from azure.core.pipeline import policies from ._version import VERSION @@ -28,8 +29,10 @@ class AIProjectClientConfiguration: # pylint: disable=too-many-instance-attribu the form "https://{ai-services-account-name}.services.ai.azure.com/api/projects/_project". Required. :type endpoint: str - :param credential: Credential used to authenticate requests to the service. Required. - :type credential: ~azure.core.credentials.TokenCredential + :param credential: Credential used to authenticate requests to the service. Is either a key + credential type or a token credential type. Required. + :type credential: ~azure.core.credentials.AzureKeyCredential or + ~azure.core.credentials.TokenCredential :param allow_preview: Whether to enable preview features. Must be specified and set to True to enable preview features. Default value is None. :type allow_preview: bool @@ -40,7 +43,11 @@ class AIProjectClientConfiguration: # pylint: disable=too-many-instance-attribu """ def __init__( - self, endpoint: str, credential: "TokenCredential", allow_preview: Optional[bool] = None, **kwargs: Any + self, + endpoint: str, + credential: Union[AzureKeyCredential, "TokenCredential"], + allow_preview: Optional[bool] = None, + **kwargs: Any, ) -> None: api_version: str = kwargs.pop("api_version", "v1") @@ -58,6 +65,13 @@ def __init__( self.polling_interval = kwargs.get("polling_interval", 30) self._configure(**kwargs) + def _infer_policy(self, **kwargs): + if isinstance(self.credential, AzureKeyCredential): + return policies.AzureKeyCredentialPolicy(self.credential, "api-key", **kwargs) + if hasattr(self.credential, "get_token"): + return policies.BearerTokenCredentialPolicy(self.credential, *self.credential_scopes, **kwargs) + raise TypeError(f"Unsupported credential: {self.credential}") + def _configure(self, **kwargs: Any) -> None: self.user_agent_policy = kwargs.get("user_agent_policy") or policies.UserAgentPolicy(**kwargs) self.headers_policy = kwargs.get("headers_policy") or policies.HeadersPolicy(**kwargs) @@ -69,6 +83,4 @@ def _configure(self, **kwargs: Any) -> None: self.retry_policy = kwargs.get("retry_policy") or policies.RetryPolicy(**kwargs) self.authentication_policy = kwargs.get("authentication_policy") if self.credential and not self.authentication_policy: - self.authentication_policy = policies.BearerTokenCredentialPolicy( - self.credential, *self.credential_scopes, **kwargs - ) + self.authentication_policy = self._infer_policy(**kwargs) diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/_utils/model_base.py b/sdk/ai/azure-ai-projects/azure/ai/projects/_utils/model_base.py index 9616929f7415..a75a22adbb97 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/_utils/model_base.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/_utils/model_base.py @@ -630,6 +630,9 @@ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: if len(items) > 0: existed_attr_keys.append(xml_name) dict_to_pass[rf._rest_name] = _deserialize(rf._type, items) + elif not rf._is_optional: + existed_attr_keys.append(xml_name) + dict_to_pass[rf._rest_name] = [] continue # text element is primitive type @@ -905,6 +908,8 @@ def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-retur # is it optional? try: if any(a is _NONE_TYPE for a in annotation.__args__): # pyright: ignore + if rf: + rf._is_optional = True if len(annotation.__args__) <= 2: # pyright: ignore if_obj_deserializer = _get_deserialize_callable_from_annotation( next(a for a in annotation.__args__ if a is not _NONE_TYPE), module, rf # pyright: ignore @@ -1084,6 +1089,7 @@ def __init__( self._is_discriminator = is_discriminator self._visibility = visibility self._is_model = False + self._is_optional = False self._default = default self._format = format self._is_multipart_file_input = is_multipart_file_input diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_client.py b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_client.py index 086ddbab3fd9..3fadfc6efc2f 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_client.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_client.py @@ -7,10 +7,11 @@ # -------------------------------------------------------------------------- from copy import deepcopy -from typing import Any, Awaitable, Optional, TYPE_CHECKING +from typing import Any, Awaitable, Optional, TYPE_CHECKING, Union from typing_extensions import Self from azure.core import AsyncPipelineClient +from azure.core.credentials import AzureKeyCredential from azure.core.pipeline import policies from azure.core.rest import AsyncHttpResponse, HttpRequest @@ -53,8 +54,10 @@ class AIProjectClient: # pylint: disable=too-many-instance-attributes the form "https://{ai-services-account-name}.services.ai.azure.com/api/projects/_project". Required. :type endpoint: str - :param credential: Credential used to authenticate requests to the service. Required. - :type credential: ~azure.core.credentials_async.AsyncTokenCredential + :param credential: Credential used to authenticate requests to the service. Is either a key + credential type or a token credential type. Required. + :type credential: ~azure.core.credentials.AzureKeyCredential or + ~azure.core.credentials_async.AsyncTokenCredential :param allow_preview: Whether to enable preview features. Must be specified and set to True to enable preview features. Default value is None. :type allow_preview: bool @@ -65,7 +68,11 @@ class AIProjectClient: # pylint: disable=too-many-instance-attributes """ def __init__( - self, endpoint: str, credential: "AsyncTokenCredential", allow_preview: Optional[bool] = None, **kwargs: Any + self, + endpoint: str, + credential: Union[AzureKeyCredential, "AsyncTokenCredential"], + allow_preview: Optional[bool] = None, + **kwargs: Any ) -> None: _endpoint = "{endpoint}" self._config = AIProjectClientConfiguration( diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_configuration.py b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_configuration.py index 7fd12934a2a8..bd8d890b071b 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_configuration.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_configuration.py @@ -6,8 +6,9 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -from typing import Any, Optional, TYPE_CHECKING +from typing import Any, Optional, TYPE_CHECKING, Union +from azure.core.credentials import AzureKeyCredential from azure.core.pipeline import policies from .._version import VERSION @@ -28,8 +29,10 @@ class AIProjectClientConfiguration: # pylint: disable=too-many-instance-attribu the form "https://{ai-services-account-name}.services.ai.azure.com/api/projects/_project". Required. :type endpoint: str - :param credential: Credential used to authenticate requests to the service. Required. - :type credential: ~azure.core.credentials_async.AsyncTokenCredential + :param credential: Credential used to authenticate requests to the service. Is either a key + credential type or a token credential type. Required. + :type credential: ~azure.core.credentials.AzureKeyCredential or + ~azure.core.credentials_async.AsyncTokenCredential :param allow_preview: Whether to enable preview features. Must be specified and set to True to enable preview features. Default value is None. :type allow_preview: bool @@ -40,7 +43,11 @@ class AIProjectClientConfiguration: # pylint: disable=too-many-instance-attribu """ def __init__( - self, endpoint: str, credential: "AsyncTokenCredential", allow_preview: Optional[bool] = None, **kwargs: Any + self, + endpoint: str, + credential: Union[AzureKeyCredential, "AsyncTokenCredential"], + allow_preview: Optional[bool] = None, + **kwargs: Any, ) -> None: api_version: str = kwargs.pop("api_version", "v1") @@ -58,6 +65,13 @@ def __init__( self.polling_interval = kwargs.get("polling_interval", 30) self._configure(**kwargs) + def _infer_policy(self, **kwargs): + if isinstance(self.credential, AzureKeyCredential): + return policies.AzureKeyCredentialPolicy(self.credential, "api-key", **kwargs) + if hasattr(self.credential, "get_token"): + return policies.AsyncBearerTokenCredentialPolicy(self.credential, *self.credential_scopes, **kwargs) + raise TypeError(f"Unsupported credential: {self.credential}") + def _configure(self, **kwargs: Any) -> None: self.user_agent_policy = kwargs.get("user_agent_policy") or policies.UserAgentPolicy(**kwargs) self.headers_policy = kwargs.get("headers_policy") or policies.HeadersPolicy(**kwargs) @@ -69,6 +83,4 @@ def _configure(self, **kwargs: Any) -> None: self.retry_policy = kwargs.get("retry_policy") or policies.AsyncRetryPolicy(**kwargs) self.authentication_policy = kwargs.get("authentication_policy") if self.credential and not self.authentication_policy: - self.authentication_policy = policies.AsyncBearerTokenCredentialPolicy( - self.credential, *self.credential_scopes, **kwargs - ) + self.authentication_policy = self._infer_policy(**kwargs) diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py index 5daa5483b071..792361d3c9fc 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py @@ -393,6 +393,9 @@ async def create_version( agent_name: str, *, definition: _models.AgentDefinition, + foundry_features: Optional[ + Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] + ] = None, content_type: str = "application/json", metadata: Optional[dict[str, str]] = None, description: Optional[str] = None, @@ -410,6 +413,7 @@ async def create_version( :keyword definition: The agent definition. This can be a workflow, hosted agent, or a simple agent definition. Required. :paramtype definition: ~azure.ai.projects.models.AgentDefinition + or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str @@ -429,7 +433,15 @@ async def create_version( @overload async def create_version( - self, agent_name: str, body: JSON, *, content_type: str = "application/json", **kwargs: Any + self, + agent_name: str, + body: JSON, + *, + foundry_features: Optional[ + Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] + ] = None, + content_type: str = "application/json", + **kwargs: Any ) -> _models.AgentVersionDetails: """Create a new agent version. @@ -442,6 +454,7 @@ async def create_version( :type agent_name: str :param body: Required. :type body: JSON + or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str @@ -452,7 +465,15 @@ async def create_version( @overload async def create_version( - self, agent_name: str, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + self, + agent_name: str, + body: IO[bytes], + *, + foundry_features: Optional[ + Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] + ] = None, + content_type: str = "application/json", + **kwargs: Any ) -> _models.AgentVersionDetails: """Create a new agent version. @@ -465,6 +486,7 @@ async def create_version( :type agent_name: str :param body: Required. :type body: IO[bytes] + or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str @@ -480,6 +502,9 @@ async def create_version( body: Union[JSON, IO[bytes]] = _Unset, *, definition: _models.AgentDefinition = _Unset, + foundry_features: Optional[ + Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] + ] = None, metadata: Optional[dict[str, str]] = None, description: Optional[str] = None, **kwargs: Any @@ -498,6 +523,7 @@ async def create_version( :keyword definition: The agent definition. This can be a workflow, hosted agent, or a simple agent definition. Required. :paramtype definition: ~azure.ai.projects.models.AgentDefinition + or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard. @@ -511,7 +537,6 @@ async def create_version( :rtype: ~azure.ai.projects.models.AgentVersionDetails :raises ~azure.core.exceptions.HttpResponseError: """ - _foundry_features: Optional[str] = _get_agent_definition_opt_in_keys if self._config.allow_preview else None # type: ignore error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/models/_enums.py b/sdk/ai/azure-ai-projects/azure/ai/projects/models/_enums.py index 923dd800ef60..fff2adb637c4 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/models/_enums.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/models/_enums.py @@ -373,6 +373,8 @@ class _FoundryFeaturesOptInKeys(str, Enum, metaclass=CaseInsensitiveEnumMeta): """MEMORY_STORES_V1_PREVIEW.""" TOOLSET_V1_PREVIEW = "Toolsets=V1Preview" """TOOLSET_V1_PREVIEW.""" + AGENT_ENDPOINT_V1_PREVIEW = "AgentEndpoints=V1Preview" + """AGENT_ENDPOINT_V1_PREVIEW.""" class FunctionShellToolParamEnvironmentType(str, Enum, metaclass=CaseInsensitiveEnumMeta): diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/models/_models.py b/sdk/ai/azure-ai-projects/azure/ai/projects/models/_models.py index f009264826d7..2974e60e06a3 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/models/_models.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/models/_models.py @@ -2792,7 +2792,7 @@ class ContainerNetworkPolicyAllowlistParam(ContainerNetworkPolicyParam, discrimi allowed_domains: list[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) """A list of allowed domains when type is ``allowlist``. Required.""" domain_secrets: Optional[list["_models.ContainerNetworkPolicyDomainSecretParam"]] = rest_field( - visibility=["read", "create", "update", "delete", "query"] + visibility=["create"] ) """Optional domain-scoped secrets for allowlisted domains.""" diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py b/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py index 5a9b2fc628c9..8e1c7c828fc8 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py @@ -142,7 +142,12 @@ def build_agents_list_request( def build_agents_create_version_request( - agent_name: str, *, foundry_features: Optional[Union[str, _AgentDefinitionOptInKeys]] = None, **kwargs: Any + agent_name: str, + *, + foundry_features: Optional[ + Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] + ] = None, + **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) @@ -2229,6 +2234,9 @@ def create_version( agent_name: str, *, definition: _models.AgentDefinition, + foundry_features: Optional[ + Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] + ] = None, content_type: str = "application/json", metadata: Optional[dict[str, str]] = None, description: Optional[str] = None, @@ -2246,6 +2254,7 @@ def create_version( :keyword definition: The agent definition. This can be a workflow, hosted agent, or a simple agent definition. Required. :paramtype definition: ~azure.ai.projects.models.AgentDefinition + or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str @@ -2265,7 +2274,15 @@ def create_version( @overload def create_version( - self, agent_name: str, body: JSON, *, content_type: str = "application/json", **kwargs: Any + self, + agent_name: str, + body: JSON, + *, + foundry_features: Optional[ + Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] + ] = None, + content_type: str = "application/json", + **kwargs: Any ) -> _models.AgentVersionDetails: """Create a new agent version. @@ -2278,6 +2295,7 @@ def create_version( :type agent_name: str :param body: Required. :type body: JSON + or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str @@ -2288,7 +2306,15 @@ def create_version( @overload def create_version( - self, agent_name: str, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + self, + agent_name: str, + body: IO[bytes], + *, + foundry_features: Optional[ + Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] + ] = None, + content_type: str = "application/json", + **kwargs: Any ) -> _models.AgentVersionDetails: """Create a new agent version. @@ -2301,6 +2327,7 @@ def create_version( :type agent_name: str :param body: Required. :type body: IO[bytes] + or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str @@ -2316,6 +2343,9 @@ def create_version( body: Union[JSON, IO[bytes]] = _Unset, *, definition: _models.AgentDefinition = _Unset, + foundry_features: Optional[ + Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] + ] = None, metadata: Optional[dict[str, str]] = None, description: Optional[str] = None, **kwargs: Any @@ -2334,6 +2364,7 @@ def create_version( :keyword definition: The agent definition. This can be a workflow, hosted agent, or a simple agent definition. Required. :paramtype definition: ~azure.ai.projects.models.AgentDefinition + or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard. @@ -2347,7 +2378,6 @@ def create_version( :rtype: ~azure.ai.projects.models.AgentVersionDetails :raises ~azure.core.exceptions.HttpResponseError: """ - _foundry_features: Optional[str] = _get_agent_definition_opt_in_keys if self._config.allow_preview else None # type: ignore error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, diff --git a/sdk/ai/azure-ai-projects/tests/foundry_features_header/foundry_features_header_test_base.py b/sdk/ai/azure-ai-projects/tests/foundry_features_header/foundry_features_header_test_base.py index 22cf12076aa6..e9b11ed7efcb 100644 --- a/sdk/ai/azure-ai-projects/tests/foundry_features_header/foundry_features_header_test_base.py +++ b/sdk/ai/azure-ai-projects/tests/foundry_features_header/foundry_features_header_test_base.py @@ -147,9 +147,7 @@ def _make_fake_call(cls, method: Any) -> Any: if param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD): continue - is_required = ( - param.default is inspect.Parameter.empty or param.default in _UNSET_SENTINELS - ) + is_required = param.default is inspect.Parameter.empty or param.default in _UNSET_SENTINELS if not is_required: continue @@ -176,8 +174,7 @@ def _record_header_assertion(cls, label: str, request: Any, expected_value: str) f"missing or empty.\nActual headers: {dict(request.headers)}" ) assert header_value == expected_value, ( - f"{label}: expected '{FOUNDRY_FEATURES_HEADER}: {expected_value}' " - f"but got '{header_value}'" + f"{label}: expected '{FOUNDRY_FEATURES_HEADER}: {expected_value}' " f"but got '{header_value}'" ) cls._report_max_label_len = max(cls._report_max_label_len, len(label)) cls._report.append((label, header_value)) @@ -192,6 +189,6 @@ def _record_header_absence_assertion(cls, label: str, request: Any) -> None: f"{label}: expected '{FOUNDRY_FEATURES_HEADER}' header to be absent.\n" f"Actual headers: {dict(request.headers)}" ) - absence_note = f'\'{FOUNDRY_FEATURES_HEADER}\' header not present (as expected)' + absence_note = f"'{FOUNDRY_FEATURES_HEADER}' header not present (as expected)" cls._report_absent_max_label_len = max(cls._report_absent_max_label_len, len(label)) cls._report_absent.append((label, absence_note)) diff --git a/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header.py b/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header.py index 71fdf4fd4212..b9890940a9da 100644 --- a/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header.py +++ b/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header.py @@ -45,7 +45,6 @@ _RequestCaptured, ) - # --------------------------------------------------------------------------- # Sync-specific transport # --------------------------------------------------------------------------- @@ -146,7 +145,7 @@ def _print_report() -> Iterator[None]: if report: print("\n\nFoundry-Features header report (sync):") for label, header_value in sorted(report): - print(f"{label:<{max_len}} | \"{header_value}\"") + print(f'{label:<{max_len}} | "{header_value}"') # --------------------------------------------------------------------------- @@ -209,4 +208,4 @@ def test_foundry_features_header( """Assert that *method_name* on .beta. sends the expected Foundry-Features value.""" sc = getattr(client.beta, subclient_name) method = getattr(sc, method_name) - self._assert_header(label, self._make_fake_call(method), expected_header_value) \ No newline at end of file + self._assert_header(label, self._make_fake_call(method), expected_header_value) diff --git a/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_async.py b/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_async.py index 73db8c64491d..9cadf09a6910 100644 --- a/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_async.py +++ b/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_async.py @@ -47,7 +47,6 @@ _RequestCaptured, ) - # --------------------------------------------------------------------------- # Async-specific transport # --------------------------------------------------------------------------- @@ -156,7 +155,7 @@ def _print_report_async() -> Iterator[None]: if report: print("\n\nFoundry-Features header report (async):") for label, header_value in sorted(report): - print(f"{label:<{max_len}} | \"{header_value}\"") + print(f'{label:<{max_len}} | "{header_value}"') # --------------------------------------------------------------------------- diff --git a/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional.py b/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional.py index ef32e04421f7..e3817cd74013 100644 --- a/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional.py +++ b/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional.py @@ -24,7 +24,6 @@ _RequestCaptured, ) - _NON_BETA_OPTIONAL_TEST_CASES = [ # Each pytest.param entry has the following positional arguments: # 1. method_name (str) – "." on AIProjectClient, e.g. "agents.create_version" @@ -94,14 +93,16 @@ def _print_report_optional() -> Iterator[None]: max_len = TestFoundryFeaturesHeaderOptional._report_max_label_len print("\n\nFoundry-Features optional header report (sync) — test_optional_header_present_when_preview_enabled:") for label, header_value in sorted(present_report): - print(f"{label:<{max_len}} | \"{header_value}\"") + print(f'{label:<{max_len}} | "{header_value}"') absent_report = TestFoundryFeaturesHeaderOptional._report_absent if absent_report: max_len = TestFoundryFeaturesHeaderOptional._report_absent_max_label_len - print("\n\nFoundry-Features optional header report (sync) — test_optional_header_absent_when_preview_not_enabled:") + print( + "\n\nFoundry-Features optional header report (sync) — test_optional_header_absent_when_preview_not_enabled:" + ) for label, header_value in sorted(absent_report): - print(f"{label:<{max_len}} | \"{header_value}\"") + print(f'{label:<{max_len}} | "{header_value}"') class TestFoundryFeaturesHeaderOptional(FoundryFeaturesHeaderTestBase): diff --git a/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional_async.py b/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional_async.py index 06e2c21637f0..07409e086d1e 100644 --- a/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional_async.py +++ b/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional_async.py @@ -25,7 +25,6 @@ _RequestCaptured, ) - _NON_BETA_OPTIONAL_ASYNC_TEST_CASES = [ # Each pytest.param entry has the following positional arguments: # 1. method_name (str) – "." on AIProjectClient, e.g. "agents.create_version" @@ -91,16 +90,20 @@ def _print_report_optional_async() -> Iterator[None]: present_report = TestFoundryFeaturesHeaderOptionalAsync._report if present_report: max_len = TestFoundryFeaturesHeaderOptionalAsync._report_max_label_len - print("\n\nFoundry-Features optional header report (async) — test_optional_header_present_when_preview_enabled_async:") + print( + "\n\nFoundry-Features optional header report (async) — test_optional_header_present_when_preview_enabled_async:" + ) for label, header_value in sorted(present_report): - print(f"{label:<{max_len}} | \"{header_value}\"") + print(f'{label:<{max_len}} | "{header_value}"') absent_report = TestFoundryFeaturesHeaderOptionalAsync._report_absent if absent_report: max_len = TestFoundryFeaturesHeaderOptionalAsync._report_absent_max_label_len - print("\n\nFoundry-Features optional header report (async) — test_optional_header_absent_when_preview_not_enabled_async:") + print( + "\n\nFoundry-Features optional header report (async) — test_optional_header_absent_when_preview_not_enabled_async:" + ) for label, header_value in sorted(absent_report): - print(f"{label:<{max_len}} | \"{header_value}\"") + print(f'{label:<{max_len}} | "{header_value}"') class TestFoundryFeaturesHeaderOptionalAsync(FoundryFeaturesHeaderTestBase): @@ -144,9 +147,7 @@ async def _assert_header_absent_async(cls, label: str, call: Any) -> None: cls._record_header_absence_assertion(label, request) @pytest.mark.asyncio - @pytest.mark.parametrize( - "method_name,expected_header_value", _NON_BETA_OPTIONAL_ASYNC_TEST_CASES - ) + @pytest.mark.parametrize("method_name,expected_header_value", _NON_BETA_OPTIONAL_ASYNC_TEST_CASES) async def test_optional_header_present_when_preview_enabled_async( self, async_client_preview_enabled: AsyncAIProjectClient, @@ -159,9 +160,7 @@ async def test_optional_header_present_when_preview_enabled_async( await self._assert_header_present_async(method_name, self._make_fake_call(method), expected_header_value) @pytest.mark.asyncio - @pytest.mark.parametrize( - "method_name,_expected_header_value", _NON_BETA_OPTIONAL_ASYNC_TEST_CASES - ) + @pytest.mark.parametrize("method_name,_expected_header_value", _NON_BETA_OPTIONAL_ASYNC_TEST_CASES) async def test_optional_header_absent_when_preview_not_enabled_async( self, async_client_preview_disabled: AsyncAIProjectClient, From c43acc4cccb7ed0414ad4fdcc64ad8dc0ac8785a Mon Sep 17 00:00:00 2001 From: Darren Cohen <39422044+dargilco@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:10:18 -0700 Subject: [PATCH 02/11] manual fix --- .../ai/projects/aio/operations/_operations.py | 18 ++---------------- .../ai/projects/aio/operations/_patch.py | 2 ++ .../ai/projects/operations/_operations.py | 19 +++---------------- .../azure/ai/projects/operations/_patch.py | 2 ++ 4 files changed, 9 insertions(+), 32 deletions(-) diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py index 792361d3c9fc..691bc738bd0a 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py @@ -393,9 +393,6 @@ async def create_version( agent_name: str, *, definition: _models.AgentDefinition, - foundry_features: Optional[ - Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] - ] = None, content_type: str = "application/json", metadata: Optional[dict[str, str]] = None, description: Optional[str] = None, @@ -413,7 +410,6 @@ async def create_version( :keyword definition: The agent definition. This can be a workflow, hosted agent, or a simple agent definition. Required. :paramtype definition: ~azure.ai.projects.models.AgentDefinition - or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str @@ -437,9 +433,6 @@ async def create_version( agent_name: str, body: JSON, *, - foundry_features: Optional[ - Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] - ] = None, content_type: str = "application/json", **kwargs: Any ) -> _models.AgentVersionDetails: @@ -454,7 +447,6 @@ async def create_version( :type agent_name: str :param body: Required. :type body: JSON - or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str @@ -469,9 +461,6 @@ async def create_version( agent_name: str, body: IO[bytes], *, - foundry_features: Optional[ - Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] - ] = None, content_type: str = "application/json", **kwargs: Any ) -> _models.AgentVersionDetails: @@ -486,7 +475,6 @@ async def create_version( :type agent_name: str :param body: Required. :type body: IO[bytes] - or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str @@ -502,9 +490,6 @@ async def create_version( body: Union[JSON, IO[bytes]] = _Unset, *, definition: _models.AgentDefinition = _Unset, - foundry_features: Optional[ - Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] - ] = None, metadata: Optional[dict[str, str]] = None, description: Optional[str] = None, **kwargs: Any @@ -523,7 +508,6 @@ async def create_version( :keyword definition: The agent definition. This can be a workflow, hosted agent, or a simple agent definition. Required. :paramtype definition: ~azure.ai.projects.models.AgentDefinition - or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard. @@ -537,6 +521,8 @@ async def create_version( :rtype: ~azure.ai.projects.models.AgentVersionDetails :raises ~azure.core.exceptions.HttpResponseError: """ + _foundry_features: Optional[str] = _get_agent_definition_opt_in_keys if self._config.allow_preview else None # type: ignore + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_patch.py b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_patch.py index fd4e68774f3b..66fb4c5d49c2 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_patch.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_patch.py @@ -47,6 +47,8 @@ class BetaOperations(GeneratedBetaOperations): """:class:`~azure.ai.projects.aio.operations.BetaRedTeamsOperations` operations""" schedules: BetaSchedulesOperations """:class:`~azure.ai.projects.aio.operations.BetaSchedulesOperations` operations""" + toolsets: BetaToolsetsOperations + """:class:`~azure.ai.projects.operations.BetaToolsetsOperations` operations""" def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py b/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py index 8e1c7c828fc8..a8245fce86c8 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py @@ -43,6 +43,7 @@ [ _AgentDefinitionOptInKeys.HOSTED_AGENTS_V1_PREVIEW.value, _AgentDefinitionOptInKeys.WORKFLOW_AGENTS_V1_PREVIEW.value, + _FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW.value, ] ) @@ -2234,9 +2235,6 @@ def create_version( agent_name: str, *, definition: _models.AgentDefinition, - foundry_features: Optional[ - Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] - ] = None, content_type: str = "application/json", metadata: Optional[dict[str, str]] = None, description: Optional[str] = None, @@ -2254,7 +2252,6 @@ def create_version( :keyword definition: The agent definition. This can be a workflow, hosted agent, or a simple agent definition. Required. :paramtype definition: ~azure.ai.projects.models.AgentDefinition - or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str @@ -2278,9 +2275,6 @@ def create_version( agent_name: str, body: JSON, *, - foundry_features: Optional[ - Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] - ] = None, content_type: str = "application/json", **kwargs: Any ) -> _models.AgentVersionDetails: @@ -2295,7 +2289,6 @@ def create_version( :type agent_name: str :param body: Required. :type body: JSON - or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str @@ -2310,9 +2303,6 @@ def create_version( agent_name: str, body: IO[bytes], *, - foundry_features: Optional[ - Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] - ] = None, content_type: str = "application/json", **kwargs: Any ) -> _models.AgentVersionDetails: @@ -2327,7 +2317,6 @@ def create_version( :type agent_name: str :param body: Required. :type body: IO[bytes] - or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str @@ -2343,9 +2332,6 @@ def create_version( body: Union[JSON, IO[bytes]] = _Unset, *, definition: _models.AgentDefinition = _Unset, - foundry_features: Optional[ - Union[str, _AgentDefinitionOptInKeys, Literal[_FoundryFeaturesOptInKeys.AGENT_ENDPOINT_V1_PREVIEW]] - ] = None, metadata: Optional[dict[str, str]] = None, description: Optional[str] = None, **kwargs: Any @@ -2364,7 +2350,6 @@ def create_version( :keyword definition: The agent definition. This can be a workflow, hosted agent, or a simple agent definition. Required. :paramtype definition: ~azure.ai.projects.models.AgentDefinition - or ~azure.ai.projects.models.AGENT_ENDPOINT_V1_PREVIEW :keyword metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard. @@ -2378,6 +2363,8 @@ def create_version( :rtype: ~azure.ai.projects.models.AgentVersionDetails :raises ~azure.core.exceptions.HttpResponseError: """ + _foundry_features: Optional[str] = _get_agent_definition_opt_in_keys if self._config.allow_preview else None # type: ignore + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_patch.py b/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_patch.py index bc78f4d6baf8..fdf038b764c5 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_patch.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_patch.py @@ -47,6 +47,8 @@ class BetaOperations(GeneratedBetaOperations): """:class:`~azure.ai.projects.operations.BetaRedTeamsOperations` operations""" schedules: BetaSchedulesOperations """:class:`~azure.ai.projects.operations.BetaSchedulesOperations` operations""" + toolsets: BetaToolsetsOperations + """:class:`~azure.ai.projects.operations.BetaToolsetsOperations` operations""" def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) From 381ed16a85b4a247a928c41c0ba348365ed2b833 Mon Sep 17 00:00:00 2001 From: Darren Cohen <39422044+dargilco@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:16:56 -0700 Subject: [PATCH 03/11] Add tests --- .../ai/projects/aio/operations/_operations.py | 14 ++---------- .../ai/projects/operations/_operations.py | 14 ++---------- .../foundry_features_header_test_base.py | 22 ++++++++++++++++++- .../test_foundry_features_header_optional.py | 18 +-------------- ..._foundry_features_header_optional_async.py | 22 +++---------------- 5 files changed, 29 insertions(+), 61 deletions(-) diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py index 691bc738bd0a..3e78f121e181 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py @@ -429,12 +429,7 @@ async def create_version( @overload async def create_version( - self, - agent_name: str, - body: JSON, - *, - content_type: str = "application/json", - **kwargs: Any + self, agent_name: str, body: JSON, *, content_type: str = "application/json", **kwargs: Any ) -> _models.AgentVersionDetails: """Create a new agent version. @@ -457,12 +452,7 @@ async def create_version( @overload async def create_version( - self, - agent_name: str, - body: IO[bytes], - *, - content_type: str = "application/json", - **kwargs: Any + self, agent_name: str, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any ) -> _models.AgentVersionDetails: """Create a new agent version. diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py b/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py index a8245fce86c8..55f80af3bb5c 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py @@ -2271,12 +2271,7 @@ def create_version( @overload def create_version( - self, - agent_name: str, - body: JSON, - *, - content_type: str = "application/json", - **kwargs: Any + self, agent_name: str, body: JSON, *, content_type: str = "application/json", **kwargs: Any ) -> _models.AgentVersionDetails: """Create a new agent version. @@ -2299,12 +2294,7 @@ def create_version( @overload def create_version( - self, - agent_name: str, - body: IO[bytes], - *, - content_type: str = "application/json", - **kwargs: Any + self, agent_name: str, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any ) -> _models.AgentVersionDetails: """Create a new agent version. diff --git a/sdk/ai/azure-ai-projects/tests/foundry_features_header/foundry_features_header_test_base.py b/sdk/ai/azure-ai-projects/tests/foundry_features_header/foundry_features_header_test_base.py index e9b11ed7efcb..927eff3d40b6 100644 --- a/sdk/ai/azure-ai-projects/tests/foundry_features_header/foundry_features_header_test_base.py +++ b/sdk/ai/azure-ai-projects/tests/foundry_features_header/foundry_features_header_test_base.py @@ -16,6 +16,7 @@ """ import inspect +import pytest from typing import Any, ClassVar, List, Tuple, Union, get_origin from azure.core.credentials import AccessToken @@ -41,11 +42,30 @@ "toolsets": "Toolsets=V1Preview", } +# Shared test cases for non-beta methods that optionally send the Foundry-Features header. +# Used by both test_foundry_features_header_optional.py (sync) and +# test_foundry_features_header_optional_async.py (async). +_NON_BETA_OPTIONAL_TEST_CASES = [ + # Each pytest.param entry has the following positional arguments: + # 1. method_name (str) – "." on AIProjectClient, e.g. "agents.create_version" + # The subclient and method names are parsed automatically from this string. + # 2. expected_header_value (str) – Expected value of the Foundry-Features header when allow_preview=True. + # Use a comma-separated list of feature=version pairs, e.g. "FeatureA=V1Preview,FeatureB=V1Preview". + # The test id is derived automatically from method_name. + pytest.param( + "agents.create_version", + "HostedAgents=V1Preview,WorkflowAgents=V1Preview,AgentEndpoint=V1Preview", + ), + pytest.param( + "evaluation_rules.create_or_update", + "Evaluations=V1Preview", + ), +] + # Both sentinel values – used by _make_fake_call to detect required parameters # whose defaults are the internal _Unset object (rather than inspect.Parameter.empty). _UNSET_SENTINELS: frozenset = frozenset({_SyncUnset, _AsyncUnset}) - # --------------------------------------------------------------------------- # Sentinel exception raised by capturing transports # --------------------------------------------------------------------------- diff --git a/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional.py b/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional.py index e3817cd74013..b4d51f49842c 100644 --- a/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional.py +++ b/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional.py @@ -21,26 +21,10 @@ FAKE_ENDPOINT, FakeCredential, FoundryFeaturesHeaderTestBase, + _NON_BETA_OPTIONAL_TEST_CASES, _RequestCaptured, ) -_NON_BETA_OPTIONAL_TEST_CASES = [ - # Each pytest.param entry has the following positional arguments: - # 1. method_name (str) – "." on AIProjectClient, e.g. "agents.create_version" - # The subclient and method names are parsed automatically from this string. - # 2. expected_header_value (str) – Expected value of the Foundry-Features header when allow_preview=True. - # Use a comma-separated list of feature=version pairs, e.g. "FeatureA=V1Preview,FeatureB=V1Preview". - # The test id is derived automatically from method_name. - pytest.param( - "agents.create_version", - "HostedAgents=V1Preview,WorkflowAgents=V1Preview", - ), - pytest.param( - "evaluation_rules.create_or_update", - "Evaluations=V1Preview", - ), -] - class CapturingTransport(HttpTransport): """Sync transport that captures the outgoing request and raises _RequestCaptured.""" diff --git a/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional_async.py b/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional_async.py index 07409e086d1e..711af5221896 100644 --- a/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional_async.py +++ b/sdk/ai/azure-ai-projects/tests/foundry_features_header/test_foundry_features_header_optional_async.py @@ -22,26 +22,10 @@ FAKE_ENDPOINT, AsyncFakeCredential, FoundryFeaturesHeaderTestBase, + _NON_BETA_OPTIONAL_TEST_CASES, _RequestCaptured, ) -_NON_BETA_OPTIONAL_ASYNC_TEST_CASES = [ - # Each pytest.param entry has the following positional arguments: - # 1. method_name (str) – "." on AIProjectClient, e.g. "agents.create_version" - # The subclient and method names are parsed automatically from this string. - # 2. expected_header_value (str) – Expected value of the Foundry-Features header when allow_preview=True. - # Use a comma-separated list of feature=version pairs, e.g. "FeatureA=V1Preview,FeatureB=V1Preview". - # The test id is derived automatically from method_name. - pytest.param( - "agents.create_version", - "HostedAgents=V1Preview,WorkflowAgents=V1Preview", - ), - pytest.param( - "evaluation_rules.create_or_update", - "Evaluations=V1Preview", - ), -] - class CapturingAsyncTransport(AsyncHttpTransport): """Async transport that captures the outgoing request and raises _RequestCaptured.""" @@ -147,7 +131,7 @@ async def _assert_header_absent_async(cls, label: str, call: Any) -> None: cls._record_header_absence_assertion(label, request) @pytest.mark.asyncio - @pytest.mark.parametrize("method_name,expected_header_value", _NON_BETA_OPTIONAL_ASYNC_TEST_CASES) + @pytest.mark.parametrize("method_name,expected_header_value", _NON_BETA_OPTIONAL_TEST_CASES) async def test_optional_header_present_when_preview_enabled_async( self, async_client_preview_enabled: AsyncAIProjectClient, @@ -160,7 +144,7 @@ async def test_optional_header_present_when_preview_enabled_async( await self._assert_header_present_async(method_name, self._make_fake_call(method), expected_header_value) @pytest.mark.asyncio - @pytest.mark.parametrize("method_name,_expected_header_value", _NON_BETA_OPTIONAL_ASYNC_TEST_CASES) + @pytest.mark.parametrize("method_name,_expected_header_value", _NON_BETA_OPTIONAL_TEST_CASES) async def test_optional_header_absent_when_preview_not_enabled_async( self, async_client_preview_disabled: AsyncAIProjectClient, From 31c9b8316189ff2b45766fbe665968879a5cfc9a Mon Sep 17 00:00:00 2001 From: Darren Cohen <39422044+dargilco@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:37:03 -0700 Subject: [PATCH 04/11] Fixes --- .../azure/ai/projects/aio/operations/_patch.py | 6 ++++-- .../azure/ai/projects/operations/_patch.py | 6 ++++-- .../foundry_features_header_test_base.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_patch.py b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_patch.py index 66fb4c5d49c2..9e5b1872d76d 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_patch.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_patch.py @@ -19,9 +19,10 @@ BetaEvaluationTaxonomiesOperations, BetaEvaluatorsOperations, BetaInsightsOperations, + BetaOperations as GeneratedBetaOperations, BetaRedTeamsOperations, BetaSchedulesOperations, - BetaOperations as GeneratedBetaOperations, + BetaToolsetsOperations, ) @@ -59,15 +60,16 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: __all__: List[str] = [ "AgentsOperations", "BetaEvaluationTaxonomiesOperations", - "EvaluationRulesOperations", "BetaEvaluatorsOperations", "BetaInsightsOperations", "BetaMemoryStoresOperations", "BetaOperations", "BetaRedTeamsOperations", "BetaSchedulesOperations", + "BetaToolsetsOperations", "ConnectionsOperations", "DatasetsOperations", + "EvaluationRulesOperations", "TelemetryOperations", ] # Add all objects you want publicly available to users at this package level diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_patch.py b/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_patch.py index fdf038b764c5..de30f901a967 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_patch.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_patch.py @@ -19,9 +19,10 @@ BetaEvaluationTaxonomiesOperations, BetaEvaluatorsOperations, BetaInsightsOperations, + BetaOperations as GeneratedBetaOperations, BetaRedTeamsOperations, BetaSchedulesOperations, - BetaOperations as GeneratedBetaOperations, + BetaToolsetsOperations, ) @@ -59,15 +60,16 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: __all__: List[str] = [ "AgentsOperations", "BetaEvaluationTaxonomiesOperations", - "EvaluationRulesOperations", "BetaEvaluatorsOperations", "BetaInsightsOperations", "BetaMemoryStoresOperations", "BetaOperations", "BetaRedTeamsOperations", "BetaSchedulesOperations", + "BetaToolsetsOperations", "ConnectionsOperations", "DatasetsOperations", + "EvaluationRulesOperations", "TelemetryOperations", ] # Add all objects you want publicly available to users at this package level diff --git a/sdk/ai/azure-ai-projects/tests/foundry_features_header/foundry_features_header_test_base.py b/sdk/ai/azure-ai-projects/tests/foundry_features_header/foundry_features_header_test_base.py index 927eff3d40b6..efb53002ddf2 100644 --- a/sdk/ai/azure-ai-projects/tests/foundry_features_header/foundry_features_header_test_base.py +++ b/sdk/ai/azure-ai-projects/tests/foundry_features_header/foundry_features_header_test_base.py @@ -54,7 +54,7 @@ # The test id is derived automatically from method_name. pytest.param( "agents.create_version", - "HostedAgents=V1Preview,WorkflowAgents=V1Preview,AgentEndpoint=V1Preview", + "HostedAgents=V1Preview,WorkflowAgents=V1Preview,AgentEndpoints=V1Preview", ), pytest.param( "evaluation_rules.create_or_update", From 76cfd6bb5fbb0fdbad25f9da6911f3a7d875e3d4 Mon Sep 17 00:00:00 2001 From: Darren Cohen <39422044+dargilco@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:49:00 -0700 Subject: [PATCH 05/11] Add asserts.json --- sdk/ai/azure-ai-projects/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-projects/assets.json b/sdk/ai/azure-ai-projects/assets.json index 4d184f12bdb4..3461a08c9534 100644 --- a/sdk/ai/azure-ai-projects/assets.json +++ b/sdk/ai/azure-ai-projects/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/ai/azure-ai-projects", - "Tag": "python/ai/azure-ai-projects_5b25ba9450" + "Tag": "python/ai/azure-ai-projects_62ca3a2d9b" } From 1fb1f22c49f4a4f56a62c7c33ab3e0d4f011def7 Mon Sep 17 00:00:00 2001 From: Darren Cohen <39422044+dargilco@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:32:13 -0700 Subject: [PATCH 06/11] api-key samples --- sdk/ai/azure-ai-projects/README.md | 52 +++++++++-- .../azure/ai/projects/_patch.py | 50 +++++++---- .../azure/ai/projects/aio/_patch.py | 42 ++++++--- .../samples/agents/sample_agent_basic.py | 3 +- .../agents/sample_agent_basic_api_key_auth.py | 81 +++++++++++++++++ .../sample_agent_basic_api_key_auth_async.py | 88 +++++++++++++++++++ .../agents/sample_agent_basic_async.py | 3 +- 7 files changed, 284 insertions(+), 35 deletions(-) create mode 100644 sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth.py create mode 100644 sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth_async.py diff --git a/sdk/ai/azure-ai-projects/README.md b/sdk/ai/azure-ai-projects/README.md index 93d3cd788fb8..8bb065bc3893 100644 --- a/sdk/ai/azure-ai-projects/README.md +++ b/sdk/ai/azure-ai-projects/README.md @@ -55,7 +55,8 @@ To report an issue with the client library, or request additional features, plea * An [Azure subscription][azure_sub]. * A [project in Microsoft Foundry](https://learn.microsoft.com/azure/foundry/how-to/create-projects). * A Foundry project endpoint URL of the form `https://your-ai-services-account-name.services.ai.azure.com/api/projects/your-project-name`. It can be found in your Microsoft Foundry Project home page. Below we will assume the environment variable `FOUNDRY_PROJECT_ENDPOINT` was defined to hold this value. -* An Entra ID token for authentication. Your application needs an object that implements the [TokenCredential](https://learn.microsoft.com/python/api/azure-core/azure.core.credentials.tokencredential) interface. Code samples here use [DefaultAzureCredential](https://learn.microsoft.com/python/api/azure-identity/azure.identity.defaultazurecredential). To get that working, you will need: +* To authenticate using API key, you will need the "Project API key" as shown in your Microsoft Foundry Project home page. +* To authenticate using Entra ID, your application needs an object that implements the [TokenCredential](https://learn.microsoft.com/python/api/azure-core/azure.core.credentials.tokencredential) interface. Code samples here use [DefaultAzureCredential](https://learn.microsoft.com/python/api/azure-identity/azure.identity.defaultazurecredential). To get that working, you will need: * An appropriate role assignment. See [Role-based access control in Microsoft Foundry portal](https://learn.microsoft.com/azure/foundry/concepts/rbac-foundry). Role assignment can be done via the "Access Control (IAM)" tab of your Azure AI Project resource in the Azure portal. * [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) installed. * You are logged into your Azure account by running `az login`. @@ -74,9 +75,46 @@ pip show azure-ai-projects ## Key concepts -### Create and authenticate the client with Entra ID +### Create and authenticate the client with API key + +To construct a synchronous client using a context manager: + +```python +import os +from azure.core.credentials import AzureKeyCredential +from azure.ai.projects import AIProjectClient + +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] +api_key = os.environ["FOUNDRY_PROJECT_API_KEY"] + +with ( + AIProjectClient(endpoint=endpoint, credential=AzureKeyCredential(api_key)) as project_client +): +``` -Entra ID is the only authentication method supported at the moment by the client. +To construct an asynchronous client, install the additional package [aiohttp](https://pypi.org/project/aiohttp/): + +```bash +pip install aiohttp +``` + +and run: + +```python +import os +import asyncio +from azure.core.credentials import AzureKeyCredential +from azure.ai.projects.aio import AIProjectClient + +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] +api_key = os.environ["FOUNDRY_PROJECT_API_KEY"] + +async with ( + AIProjectClient(endpoint=endpoint, credential=AzureKeyCredential(api_key)) as project_client +): +``` + +### Create and authenticate the client with Entra ID To construct a synchronous client using a context manager: @@ -85,9 +123,11 @@ import os from azure.ai.projects import AIProjectClient from azure.identity import DefaultAzureCredential +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + with ( DefaultAzureCredential() as credential, - AIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], credential=credential) as project_client, + AIProjectClient(endpoint=endpoint, credential=credential) as project_client, ): ``` @@ -105,9 +145,11 @@ import asyncio from azure.ai.projects.aio import AIProjectClient from azure.identity.aio import DefaultAzureCredential +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + async with ( DefaultAzureCredential() as credential, - AIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], credential=credential) as project_client, + AIProjectClient(endpoint=endpoint, credential=credential) as project_client, ): ``` diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py b/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py index 98c3e388bb92..e4359d73f6fc 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py @@ -14,6 +14,7 @@ from typing import List, Any import httpx # pylint: disable=networking-import-outside-azure-core-transport from openai import OpenAI +from azure.core.credentials import AzureKeyCredential from azure.core.tracing.decorator import distributed_trace from azure.core.credentials import TokenCredential from azure.identity import get_bearer_token_provider @@ -25,7 +26,6 @@ class AIProjectClient(AIProjectClientGenerated): # pylint: disable=too-many-instance-attributes """AIProjectClient. - :ivar beta: BetaOperations operations :vartype beta: azure.ai.projects.operations.BetaOperations :ivar agents: AgentsOperations operations @@ -46,8 +46,10 @@ class AIProjectClient(AIProjectClientGenerated): # pylint: disable=too-many-ins the form "https://{ai-services-account-name}.services.ai.azure.com/api/projects/_project". Required. :type endpoint: str - :param credential: Credential used to authenticate requests to the service. Required. - :type credential: ~azure.core.credentials.TokenCredential + :param credential: Credential used to authenticate requests to the service. Is either a key + credential type or a token credential type. Required. + :type credential: ~azure.core.credentials.AzureKeyCredential or + ~azure.core.credentials.TokenCredential :param allow_preview: Whether to enable preview features. Optional, default is False. Set this to True to create a Hosted Agent (using :class:`~azure.ai.projects.models.HostedAgentDefinition`) or a Workflow Agent (using :class:`~azure.ai.projects.models.WorkflowAgentDefinition`). @@ -64,7 +66,12 @@ class AIProjectClient(AIProjectClientGenerated): # pylint: disable=too-many-ins """ def __init__( - self, endpoint: str, credential: TokenCredential, *, allow_preview: bool = False, **kwargs: Any + self, + endpoint: str, + credential: Union[AzureKeyCredential, "TokenCredential"], + *, + allow_preview: bool = False, + **kwargs: Any, ) -> None: self._console_logging_enabled: bool = ( @@ -78,7 +85,7 @@ def __init__( azure_logger = logging.getLogger("azure") azure_logger.setLevel(logging.DEBUG) console_handler = logging.StreamHandler(stream=sys.stdout) - console_handler.addFilter(_BearerTokenRedactionFilter()) + console_handler.addFilter(_AuthSecretsFilter()) azure_logger.addHandler(console_handler) # Exclude detailed logs for network calls associated with getting Entra ID token. logging.getLogger("azure.identity").setLevel(logging.ERROR) @@ -105,8 +112,12 @@ def get_openai_client(self, **kwargs: Any) -> "OpenAI": # type: ignore[name-def The OpenAI client constructor is called with: * ``base_url`` set to the endpoint provided to the AIProjectClient constructor, with "/openai/v1" appended. Can be overridden by passing ``base_url`` as a keyword argument. - * ``api_key`` set to a get_bearer_token_provider() callable that uses the TokenCredential provided to the - AIProjectClient constructor, with scope "https://ai.azure.com/.default". + * If :class:`~azure.ai.projects.AIProjectClient` was constructed with a bearer token, ``api_key`` is set + to a get_bearer_token_provider() callable that uses the TokenCredential provided to the AIProjectClient + constructor, with scope ``https://ai.azure.com/.default``. + Can be overridden by passing ``api_key`` as a keyword argument. + * If :class:`~azure.ai.projects.AIProjectClient` was constructed with ``api-key``, it is passed to the + OpenAI constructor as is. Can be overridden by passing ``api_key`` as a keyword argument. .. note:: The packages ``openai`` and ``azure.identity`` must be installed prior to calling this method. @@ -130,13 +141,17 @@ def get_openai_client(self, **kwargs: Any) -> "OpenAI": # type: ignore[name-def base_url, ) - # Allow caller to override api_key, otherwise use token provider + # Allow caller to override api_key, otherwise use api-key or token provider given during AIProjectClient constructor if "api_key" in kwargs: api_key = kwargs.pop("api_key") else: - api_key = get_bearer_token_provider( - self._config.credential, # pylint: disable=protected-access - "https://ai.azure.com/.default", + api_key = ( + self._config.credential.key # pylint: disable=protected-access + if isinstance(self._config.credential, AzureKeyCredential) + else get_bearer_token_provider( + self._config.credential, # pylint: disable=protected-access + "https://ai.azure.com/.default", + ) ) if "http_client" in kwargs: @@ -178,16 +193,21 @@ def _create_openai_client(**kwargs) -> OpenAI: return client -class _BearerTokenRedactionFilter(logging.Filter): - """Redact bearer tokens in azure.core log messages before they are emitted to console.""" +class _AuthSecretsFilter(logging.Filter): + """Redact bearer tokens and api-key values in azure.core log messages before they are emitted to console.""" _AUTH_HEADER_DICT_PATTERN = re.compile( - r"(?i)(['\"]authorization['\"]\s*:\s*['\"])bearer\s+[^'\"]+(['\"])", + r"(?i)(['\"]authorization['\"]\ *:\ *['\"])bearer\s+[^'\"]+(['\"])", + ) + + _API_KEY_HEADER_DICT_PATTERN = re.compile( + r"(?i)(['\"]api-key['\"]\ *:\ *['\"])[^'\"]+(['\"])", ) def filter(self, record: logging.LogRecord) -> bool: rendered = record.getMessage() redacted = self._AUTH_HEADER_DICT_PATTERN.sub(r"\1Bearer \2", rendered) + redacted = self._API_KEY_HEADER_DICT_PATTERN.sub(r"\1\2", redacted) if redacted != rendered: # Replace the pre-formatted content so handlers emit sanitized output. record.msg = redacted @@ -208,7 +228,7 @@ class OpenAILoggingTransport(httpx.HTTPTransport): """ def _sanitize_auth_header(self, headers) -> None: - """Sanitize authorization header by redacting sensitive information. + """Sanitize authorization and api-key headers by redacting sensitive information. :param headers: Dictionary of HTTP headers to sanitize :type headers: dict diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py index 4f8312c6996e..85e365a02d1e 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py @@ -13,10 +13,11 @@ from typing import List, Any import httpx # pylint: disable=networking-import-outside-azure-core-transport from openai import AsyncOpenAI +from azure.core.credentials import AzureKeyCredential from azure.core.tracing.decorator import distributed_trace from azure.core.credentials_async import AsyncTokenCredential from azure.identity.aio import get_bearer_token_provider -from .._patch import _BearerTokenRedactionFilter +from .._patch import _AuthSecretsFilter from ._client import AIProjectClient as AIProjectClientGenerated from .operations import TelemetryOperations @@ -25,7 +26,6 @@ class AIProjectClient(AIProjectClientGenerated): # pylint: disable=too-many-instance-attributes """AIProjectClient. - :ivar beta: BetaOperations operations :vartype beta: azure.ai.projects.aio.operations.BetaOperations :ivar agents: AgentsOperations operations @@ -46,8 +46,10 @@ class AIProjectClient(AIProjectClientGenerated): # pylint: disable=too-many-ins the form "https://{ai-services-account-name}.services.ai.azure.com/api/projects/_project". Required. :type endpoint: str - :param credential: Credential used to authenticate requests to the service. Required. - :type credential: ~azure.core.credentials_async.AsyncTokenCredential + :param credential: Credential used to authenticate requests to the service. Is either a key + credential type or a token credential type. Required. + :type credential: ~azure.core.credentials.AzureKeyCredential or + ~azure.core.credentials_async.AsyncTokenCredential :param allow_preview: Whether to enable preview features. Optional, default is False. Set this to True to create a Hosted Agent (using :class:`~azure.ai.projects.models.HostedAgentDefinition`) or a Workflow Agent (using :class:`~azure.ai.projects.models.WorkflowAgentDefinition`). @@ -56,6 +58,7 @@ class AIProjectClient(AIProjectClientGenerated): # pylint: disable=too-many-ins are all in preview, but do not require setting `allow_preview=True` since it's implied by the sub-client name. When preview features are enabled, the client libraries sends the HTTP request header `Foundry-Features` with the appropriate value in all relevant calls to the service. + :type allow_preview: bool :keyword api_version: The API version to use for this operation. Known values are "v1" and None. Default value is "v1". Note that overriding this default value may result in unsupported behavior. @@ -63,7 +66,12 @@ class AIProjectClient(AIProjectClientGenerated): # pylint: disable=too-many-ins """ def __init__( - self, endpoint: str, credential: AsyncTokenCredential, *, allow_preview: bool = False, **kwargs: Any + self, + endpoint: str, + credential: Union[AzureKeyCredential, "AsyncTokenCredential"], + *, + allow_preview: bool = False, + **kwargs: Any, ) -> None: self._console_logging_enabled: bool = ( @@ -77,7 +85,7 @@ def __init__( azure_logger = logging.getLogger("azure") azure_logger.setLevel(logging.DEBUG) console_handler = logging.StreamHandler(stream=sys.stdout) - console_handler.addFilter(_BearerTokenRedactionFilter()) + console_handler.addFilter(_AuthSecretsFilter()) azure_logger.addHandler(console_handler) # Exclude detailed logs for network calls associated with getting Entra ID token. logging.getLogger("azure.identity").setLevel(logging.ERROR) @@ -104,8 +112,12 @@ def get_openai_client(self, **kwargs: Any) -> "AsyncOpenAI": # type: ignore[nam The AsyncOpenAI client constructor is called with: * ``base_url`` set to the endpoint provided to the AIProjectClient constructor, with "/openai/v1" appended. Can be overridden by passing ``base_url`` as a keyword argument. - * ``api_key`` set to a get_bearer_token_provider() callable that uses the TokenCredential provided to the - AIProjectClient constructor, with scope "https://ai.azure.com/.default". + * If :class:`~azure.ai.projects.aio.AIProjectClient` was constructed with a bearer token, ``api_key`` is set + to a get_bearer_token_provider() callable that uses the TokenCredential provided to the AIProjectClient + constructor, with scope ``https://ai.azure.com/.default``. + Can be overridden by passing ``api_key`` as a keyword argument. + * If :class:`~azure.ai.projects.aio.AIProjectClient` was constructed with ``api-key``, it is passed to the + OpenAI constructor as is. Can be overridden by passing ``api_key`` as a keyword argument. .. note:: The packages ``openai`` and ``azure.identity`` must be installed prior to calling this method. @@ -129,13 +141,17 @@ def get_openai_client(self, **kwargs: Any) -> "AsyncOpenAI": # type: ignore[nam base_url, ) - # Allow caller to override api_key, otherwise use token provider + # Allow caller to override api_key, otherwise use api-key or token provider given during AIProjectClient constructor if "api_key" in kwargs: api_key = kwargs.pop("api_key") else: - api_key = get_bearer_token_provider( - self._config.credential, # pylint: disable=protected-access - "https://ai.azure.com/.default", + api_key = ( + self._config.credential.key # pylint: disable=protected-access + if isinstance(self._config.credential, AzureKeyCredential) + else get_bearer_token_provider( + self._config.credential, # pylint: disable=protected-access + "https://ai.azure.com/.default", + ) ) if "http_client" in kwargs: @@ -190,7 +206,7 @@ class OpenAILoggingTransport(httpx.AsyncHTTPTransport): """ def _sanitize_auth_header(self, headers): - """Sanitize authorization header by redacting sensitive information. + """Sanitize authorization and api-key headers by redacting sensitive information. :param headers: Dictionary of HTTP headers to sanitize :type headers: dict diff --git a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic.py b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic.py index 2f097665de87..64b3161d4af6 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic.py +++ b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic.py @@ -6,7 +6,8 @@ """ DESCRIPTION: This sample demonstrates how to run basic Prompt Agent operations - using the synchronous AIProjectClient. + using the synchronous AIProjectClient. It uses Entra ID authentication to + connect to the Microsoft Foundry service. The OpenAI compatible Responses and Conversation calls in this sample are made using the OpenAI client from the `openai` package. See https://platform.openai.com/docs/api-reference diff --git a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth.py b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth.py new file mode 100644 index 000000000000..d04fa2e34576 --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth.py @@ -0,0 +1,81 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +""" +DESCRIPTION: + This sample demonstrates how to run basic Prompt Agent operations + using the synchronous AIProjectClient. It uses API key authentication to + connect to the Microsoft Foundry service. + + The OpenAI compatible Responses and Conversation calls in this sample are made using + the OpenAI client from the `openai` package. See https://platform.openai.com/docs/api-reference + for more information. + +USAGE: + python sample_agent_basic_api_key_auth.py + + Before running the sample: + + pip install "azure-ai-projects>=2.0.2" python-dotenv + + Set these environment variables with your own values: + 2) FOUNDRY_PROJECT_API_KEY - The Project API key as found in your Foundry project home page. + 3) FOUNDRY_MODEL_NAME - The deployment name of the AI model, as found under the "Name" column in + the "Models + endpoints" tab in your Microsoft Foundry project. +""" + +import os +from dotenv import load_dotenv +from azure.core.credentials import AzureKeyCredential +from azure.ai.projects import AIProjectClient +from azure.ai.projects.models import PromptAgentDefinition + +load_dotenv() + +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] +api_key = os.environ["FOUNDRY_PROJECT_API_KEY"] + +with ( + AIProjectClient(endpoint=endpoint, credential=AzureKeyCredential(api_key)) as project_client, + project_client.get_openai_client() as openai_client, +): + + agent = project_client.agents.create_version( + agent_name="MyAgent", + definition=PromptAgentDefinition( + model=os.environ["FOUNDRY_MODEL_NAME"], + instructions="You are a helpful assistant that answers general questions", + ), + ) + print(f"Agent created (id: {agent.id}, name: {agent.name}, version: {agent.version})") + + conversation = openai_client.conversations.create( + items=[{"type": "message", "role": "user", "content": "What is the size of France in square miles?"}], + ) + print(f"Created conversation with initial user message (id: {conversation.id})") + + response = openai_client.responses.create( + conversation=conversation.id, + extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + ) + print(f"Response output: {response.output_text}") + + openai_client.conversations.items.create( + conversation_id=conversation.id, + items=[{"type": "message", "role": "user", "content": "And what is the capital city?"}], + ) + print("Added a second user message to the conversation") + + response = openai_client.responses.create( + conversation=conversation.id, + extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + ) + print(f"Response output: {response.output_text}") + + openai_client.conversations.delete(conversation_id=conversation.id) + print("Conversation deleted") + + project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + print("Agent deleted") diff --git a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth_async.py b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth_async.py new file mode 100644 index 000000000000..1327b837156c --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth_async.py @@ -0,0 +1,88 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +""" +DESCRIPTION: + This sample demonstrates how to run basic Prompt Agent operations + using the asynchronous AIProjectClient. It uses API key authentication to + connect to the Microsoft Foundry service. + + The OpenAI compatible Responses and Conversation calls in this sample are made using + the OpenAI client from the `openai` package. See https://platform.openai.com/docs/api-reference + for more information. + +USAGE: + python sample_agent_basic_async.py + + Before running the sample: + + pip install "azure-ai-projects>=2.0.2" python-dotenv aiohttp + + Set these environment variables with your own values: + 2) FOUNDRY_PROJECT_API_KEY - The Project API key as found in your Foundry project home page. + 3) FOUNDRY_MODEL_NAME - The deployment name of the AI model, as found under the "Name" column in + the "Models + endpoints" tab in your Microsoft Foundry project. +""" + +import asyncio +import os +from dotenv import load_dotenv +from azure.core.credentials import AzureKeyCredential +from azure.ai.projects.aio import AIProjectClient +from azure.ai.projects.models import PromptAgentDefinition + +load_dotenv() + +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] +api_key = os.environ["FOUNDRY_PROJECT_API_KEY"] + + +async def main() -> None: + async with ( + AIProjectClient(endpoint=endpoint, credential=AzureKeyCredential(api_key)) as project_client, + project_client.get_openai_client() as openai_client, + ): + + agent = await project_client.agents.create_version( + agent_name="MyAgent", + definition=PromptAgentDefinition( + model=os.environ["FOUNDRY_MODEL_NAME"], + instructions="You are a helpful assistant that answers general questions.", + ), + ) + print(f"Agent created (name: {agent.name}, id: {agent.id}, version: {agent.version})") + + conversation = await openai_client.conversations.create( + items=[{"type": "message", "role": "user", "content": "What is the size of France in square miles?"}], + ) + print(f"Created conversation with initial user message (id: {conversation.id})") + + response = await openai_client.responses.create( + conversation=conversation.id, + extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + ) + print(f"Response output: {response.output_text}") + + await openai_client.conversations.items.create( + conversation_id=conversation.id, + items=[{"type": "message", "role": "user", "content": "And what is the capital city?"}], + ) + print("Added a second user message to the conversation") + + response = await openai_client.responses.create( + conversation=conversation.id, + extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + ) + print(f"Response output: {response.output_text}") + + await openai_client.conversations.delete(conversation_id=conversation.id) + print("Conversation deleted") + + await project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + print("Agent deleted") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_async.py b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_async.py index 0f9d39bc6685..3936a1aec2e6 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_async.py +++ b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_async.py @@ -6,7 +6,8 @@ """ DESCRIPTION: This sample demonstrates how to run basic Prompt Agent operations - using the asynchronous AIProjectClient. + using the asynchronous AIProjectClient. It uses Entra ID authentication to + connect to the Microsoft Foundry service. The OpenAI compatible Responses and Conversation calls in this sample are made using the OpenAI client from the `openai` package. See https://platform.openai.com/docs/api-reference From b70b76caa36fce29931b78583056ac93a8041a46 Mon Sep 17 00:00:00 2001 From: Darren Cohen <39422044+dargilco@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:36:33 -0700 Subject: [PATCH 07/11] Add note to samples --- .../samples/agents/sample_agent_basic_api_key_auth.py | 3 +++ .../samples/agents/sample_agent_basic_api_key_auth_async.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth.py b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth.py index d04fa2e34576..0b6fd228f13d 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth.py +++ b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth.py @@ -9,6 +9,9 @@ using the synchronous AIProjectClient. It uses API key authentication to connect to the Microsoft Foundry service. + Note that API key authentication is available starting with version + 2.0.2 of the client library. + The OpenAI compatible Responses and Conversation calls in this sample are made using the OpenAI client from the `openai` package. See https://platform.openai.com/docs/api-reference for more information. diff --git a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth_async.py b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth_async.py index 1327b837156c..bdf7ea738457 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth_async.py +++ b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic_api_key_auth_async.py @@ -9,6 +9,9 @@ using the asynchronous AIProjectClient. It uses API key authentication to connect to the Microsoft Foundry service. + Note that API key authentication is available starting with version + 2.0.2 of the client library. + The OpenAI compatible Responses and Conversation calls in this sample are made using the OpenAI client from the `openai` package. See https://platform.openai.com/docs/api-reference for more information. From f63437b7927f60fc9d683379a62b5f401772c2ea Mon Sep 17 00:00:00 2001 From: Darren Cohen <39422044+dargilco@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:41:41 -0700 Subject: [PATCH 08/11] mypy --- sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py | 2 +- sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py b/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py index e4359d73f6fc..b3a2d45b4512 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py @@ -11,7 +11,7 @@ import os import re import logging -from typing import List, Any +from typing import List, Any, Union import httpx # pylint: disable=networking-import-outside-azure-core-transport from openai import OpenAI from azure.core.credentials import AzureKeyCredential diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py index 85e365a02d1e..386e813d1743 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py @@ -10,7 +10,7 @@ import os import logging -from typing import List, Any +from typing import List, Any, Union import httpx # pylint: disable=networking-import-outside-azure-core-transport from openai import AsyncOpenAI from azure.core.credentials import AzureKeyCredential From 49b52a0fa97d6f1812535830c5fac67f9c7c76ac Mon Sep 17 00:00:00 2001 From: Darren Cohen <39422044+dargilco@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:53:46 -0700 Subject: [PATCH 09/11] new test asserts --- sdk/ai/azure-ai-projects/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-projects/assets.json b/sdk/ai/azure-ai-projects/assets.json index 3461a08c9534..f55c57223d1f 100644 --- a/sdk/ai/azure-ai-projects/assets.json +++ b/sdk/ai/azure-ai-projects/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/ai/azure-ai-projects", - "Tag": "python/ai/azure-ai-projects_62ca3a2d9b" + "Tag": "python/ai/azure-ai-projects_256abfb29c" } From 9cea9250ff74b50f9a63926dc904668c88eca2fa Mon Sep 17 00:00:00 2001 From: Darren Cohen <39422044+dargilco@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:08:00 -0700 Subject: [PATCH 10/11] Fix --- sdk/ai/azure-ai-projects/.env.template | 1 + sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py | 1 + sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py | 1 + sdk/ai/azure-ai-projects/tests/conftest.py | 6 +++++- sdk/ai/azure-ai-projects/tests/test_base.py | 1 + 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-projects/.env.template b/sdk/ai/azure-ai-projects/.env.template index 620367d29578..6ffe61b712d7 100644 --- a/sdk/ai/azure-ai-projects/.env.template +++ b/sdk/ai/azure-ai-projects/.env.template @@ -21,6 +21,7 @@ AZURE_AI_PROJECTS_CONSOLE_LOGGING= # Project endpoint has the format: # `https://.services.ai.azure.com/api/projects/` FOUNDRY_PROJECT_ENDPOINT= +FOUNDRY_PROJECT_API_KEY= FOUNDRY_MODEL_NAME= FOUNDRY_AGENT_NAME= CONVERSATION_ID= diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py b/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py index b3a2d45b4512..9d5ca36cce62 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py @@ -26,6 +26,7 @@ class AIProjectClient(AIProjectClientGenerated): # pylint: disable=too-many-instance-attributes """AIProjectClient. + :ivar beta: BetaOperations operations :vartype beta: azure.ai.projects.operations.BetaOperations :ivar agents: AgentsOperations operations diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py index 386e813d1743..7172df9225b7 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py @@ -26,6 +26,7 @@ class AIProjectClient(AIProjectClientGenerated): # pylint: disable=too-many-instance-attributes """AIProjectClient. + :ivar beta: BetaOperations operations :vartype beta: azure.ai.projects.aio.operations.BetaOperations :ivar agents: AgentsOperations operations diff --git a/sdk/ai/azure-ai-projects/tests/conftest.py b/sdk/ai/azure-ai-projects/tests/conftest.py index 483f6fd7f4eb..a6dd76036377 100644 --- a/sdk/ai/azure-ai-projects/tests/conftest.py +++ b/sdk/ai/azure-ai-projects/tests/conftest.py @@ -48,6 +48,7 @@ class SanitizedValues: PROJECT_NAME = "sanitized-project-name" COMPONENT_NAME = "sanitized-component-name" AGENTS_API_VERSION = "sanitized-api-version" + API_KEY = "sanitized-api-key" @pytest.fixture(scope="session") @@ -59,6 +60,7 @@ def sanitized_values(): "account_name": f"{SanitizedValues.ACCOUNT_NAME}", "component_name": f"{SanitizedValues.COMPONENT_NAME}", "agents_api_version": f"{SanitizedValues.AGENTS_API_VERSION}", + "api_key": f"{SanitizedValues.API_KEY}", } @@ -153,6 +155,8 @@ def sanitize_url_paths(): ) add_body_string_sanitizer(target=image_generation_model, value="sanitized-gpt-image") + add_header_regex_sanitizer(key="api-key", value=SanitizedValues.API_KEY) + # Deterministic fallback sanitization for image generation deployment/model values. # These do not depend on environment variables and ensure recordings are redacted even # when runtime values come from unexpected sources. @@ -167,7 +171,7 @@ def sanitize_url_paths(): ) # Sanitize API key from service response (this includes Application Insights connection string) - add_body_key_sanitizer(json_path="credentials.key", value="sanitized-api-key") + add_body_key_sanitizer(json_path="credentials.key", value=SanitizedValues.API_KEY) # Sanitize GitHub personal access tokens that may appear in connection credentials add_general_regex_sanitizer(regex=r"github_pat_[A-Za-z0-9_]+", value="sanitized-github-pat") diff --git a/sdk/ai/azure-ai-projects/tests/test_base.py b/sdk/ai/azure-ai-projects/tests/test_base.py index af3d3e3f30a3..bf114404b34d 100644 --- a/sdk/ai/azure-ai-projects/tests/test_base.py +++ b/sdk/ai/azure-ai-projects/tests/test_base.py @@ -40,6 +40,7 @@ EnvironmentVariableLoader, "", foundry_project_endpoint="https://sanitized-account-name.services.ai.azure.com/api/projects/sanitized-project-name", + foundry_project_api_key="sanitized-api-key", foundry_model_name="sanitized-model-deployment-name", image_generation_model_deployment_name="sanitized-gpt-image", bing_project_connection_id="/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/sanitized-resource-group/providers/Microsoft.CognitiveServices/accounts/sanitized-account/projects/sanitized-project/connections/sanitized-bing-connection", From 2a124cf96ea807acd4212a0fd4be88322eec8482 Mon Sep 17 00:00:00 2001 From: Darren Cohen <39422044+dargilco@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:08:40 -0700 Subject: [PATCH 11/11] asserts --- sdk/ai/azure-ai-projects/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-projects/assets.json b/sdk/ai/azure-ai-projects/assets.json index f55c57223d1f..3eb2fbb88bb2 100644 --- a/sdk/ai/azure-ai-projects/assets.json +++ b/sdk/ai/azure-ai-projects/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/ai/azure-ai-projects", - "Tag": "python/ai/azure-ai-projects_256abfb29c" + "Tag": "python/ai/azure-ai-projects_cbf65e62f5" }