Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b0efb2c
SDK operations for Models, Unit tests and Sample notebook
kshitij-microsoft May 12, 2026
bce90ad
modifying sample to .py instead of .ipynb and update changelog
kshitij-microsoft May 12, 2026
a8d028b
post emitter fixes and resolving review comments
kshitij-microsoft May 13, 2026
5aec13c
fix cpell - azcopy
kshitij-microsoft May 13, 2026
c2a550a
re emit from typespec for PendingUploadType changes
kshitij-microsoft May 14, 2026
bbecfd6
merge conflicts resolve
kshitij-microsoft May 14, 2026
f5274d0
reverting pyproject.toml
kshitij-microsoft May 14, 2026
756361a
Merge branch 'feature/azure-ai-projects/2.2.0' of https://github.com/…
kshitij-microsoft May 21, 2026
b9f1fc2
pulling base branch of foundry sdk release for build
kshitij-microsoft May 21, 2026
a9e9412
Revert aio _patch.py to base; minimize sync _patch.py diff for BetaMo…
kshitij-microsoft May 21, 2026
aa5362f
Merge remote-tracking branch 'origin/feature/azure-ai-projects/2.2.0'…
kshitij-microsoft May 22, 2026
64e9968
Address PR #46842 review comments
kshitij-microsoft May 22, 2026
3606235
Rename .beta.models patched helper create_version -> create
kshitij-microsoft May 22, 2026
5a2d9f8
Add sample recordings for .beta.models and fix generated arg names
kshitij-microsoft May 22, 2026
bbb13c9
Exclude .beta.models.create from foundry-features header test
kshitij-microsoft May 25, 2026
48dee87
Merge branch 'feature/azure-ai-projects/2.2.0' of https://github.com/…
kshitij-microsoft May 25, 2026
a9a8b1e
Add cspell entries: recsmplmdl, simpleqna, skoid
kshitij-microsoft May 25, 2026
aef129a
Fix pyright and pylint issues in BetaModelsOperations patches
kshitij-microsoft May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion sdk/ai/azure-ai-projects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* New optional `force` parameter on `agents.delete` and `agents.delete_version` methods.
* New optional `blueprint_reference` parameters on `agents.create_version` method.
* New sample `sample_dataset_generation_job_simpleqna_with_prompt_source.py` showing an end-to-end flow that generates a QnA dataset via `.beta.datasets.create_generation_job` and runs an OpenAI evaluation.

* New convenience method `.beta.models.create()` that wraps the spec's three-step upload-first sequence (`pending_upload` → `azcopy copy` → `pending_create_version`) and polls `get()` until the new `ModelVersion` is observable.

Comment thread
kshitij-microsoft marked this conversation as resolved.
### Breaking Changes

Expand Down Expand Up @@ -52,6 +52,10 @@ Breaking changes in beta classes:
* The Hosted Agent creation sample also demonstrates assigning the hosted agent managed identity the Azure AI User RBAC role on the backing Azure AI account.
* Updated the other Hosted Agent samples to reuse an existing Hosted Agent as a prerequisite, instead of creating a new hosted agent version in each sample.
* Added Toolbox tool-search sample `sample_toolboxes_with_search_preview.py` and `sample_toolboxes_with_search_preview_async.py`, demonstrating creating a Toolbox version with `ToolboxSearchPreviewTool` and invoking `MCPTool`.
* Added `.beta.models` samples under `samples/models/`:
* `sample_models_basic.py` — synchronous end-to-end registration via the `create` helper (uses `azcopy`), followed by `get`, `list_versions`, `list`, `get_credentials`, `update`, and `delete`.
* `sample_models_without_patch.py` — alternative synchronous registration that hand-rolls the spec's three-step flow (`pending_upload` → upload via `azure-storage-blob` → `pending_create_version` + poll), without taking a dependency on `azcopy`.
* `sample_models_basic_async.py` — asynchronous version of the same three-step flow using `azure.ai.projects.aio.AIProjectClient` and `azure.storage.blob.aio.ContainerClient`.

## 2.1.0 (2026-04-20)

Expand Down
2 changes: 2 additions & 0 deletions sdk/ai/azure-ai-projects/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ resources in your Microsoft Foundry Project. Use it to:
* **Enumerate AI Models** deployed to your Foundry Project using `.deployments` operations.
* **Enumerate connected Azure resources** in your Foundry project using `.connections` operations.
* **Upload documents and create Datasets** to reference them using `.datasets` operations.
* **Register and manage local model weights** as Foundry `ModelVersion` resources using `.beta.models` operations, including the `create` end-to-end helper.
* **Create and enumerate Search Indexes** using `.indexes` operations.

The client library uses version `v1` of the Microsoft Foundry [data plane REST APIs](https://aka.ms/azsdk/azure-ai-projects-v2/api-reference-v1).
Expand Down Expand Up @@ -166,6 +167,7 @@ Full descriptions and working code for all of the above are available in:
| Deployments | [Deployment types](https://learn.microsoft.com/azure/foundry/foundry-models/concepts/deployment-types) | `samples/deployments/` |
| Connections | [Connections operations](https://learn.microsoft.com/python/api/overview/azure/ai-projects-readme?view=azure-python#connections-operations) | `samples/connections/` |
| Datasets | [Dataset operations](https://learn.microsoft.com/python/api/overview/azure/ai-projects-readme?view=azure-python#dataset-operations) | `samples/datasets/` |
| Models (preview) | Register local model weights as Foundry `ModelVersion` resources via `.beta.models` (`create`, `list`, `list_versions`, `get`, `update`, `delete`, `pending_upload`, `pending_create_version`, `get_credentials`). | `samples/models/` |
| Indexes | [Azure AI Search](https://learn.microsoft.com/azure/search/search-what-is-azure-search) | `samples/indexes/` |
| Files (upload, retrieve, list, delete) | [OpenAI Files API](https://platform.openai.com/docs/api-reference/files) | `samples/files/` |
| Fine-tuning | [Fine-Tuning in AI Foundry](https://github.com/microsoft-foundry/fine-tuning) | `samples/finetuning/` |
Expand Down
2 changes: 1 addition & 1 deletion sdk/ai/azure-ai-projects/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "python",
"TagPrefix": "python/ai/azure-ai-projects",
"Tag": "python/ai/azure-ai-projects_b13a910d61"
"Tag": "python/ai/azure-ai-projects_f04966e97c"
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
from ._patch_telemetry_async import TelemetryOperations
from ._patch_connections_async import ConnectionsOperations
from ._patch_memories_async import BetaMemoryStoresOperations
from ._patch_models_async import BetaModelsOperations
from ._patch_sessions_async import BetaAgentsOperations
from ...operations._patch import _BETA_OPERATION_FEATURE_HEADERS, _OperationMethodHeaderProxy
from ._operations import (
BetaDatasetsOperations,
BetaEvaluationTaxonomiesOperations,
BetaEvaluatorsOperations,
BetaInsightsOperations,
BetaModelsOperations,
BetaOperations as GeneratedBetaOperations,
BetaRedTeamsOperations,
BetaRoutinesOperations,
Expand Down Expand Up @@ -75,6 +75,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self.agents = BetaAgentsOperations(self._client, self._config, self._serialize, self._deserialize)
# Replace with patched class that includes begin_update_memories
self.memory_stores = BetaMemoryStoresOperations(self._client, self._config, self._serialize, self._deserialize)
# Replace with patched class that includes create (3-step upload helper)
self.models = BetaModelsOperations(self._client, self._config, self._serialize, self._deserialize)

for property_name, foundry_features_value in _BETA_OPERATION_FEATURE_HEADERS.items():
setattr(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
# pylint: disable=line-too-long,useless-suppression
# ------------------------------------
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
"""Customize generated code here.

Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize
"""

import asyncio # pylint: disable=do-not-import-asyncio
import logging
import os
from pathlib import Path
from typing import Any, Optional, Union

from azure.core.exceptions import ResourceNotFoundError
from azure.core.tracing.decorator_async import distributed_trace_async

from ._operations import BetaModelsOperations as BetaModelsOperationsGenerated
from ...models._models import (
ModelPendingUploadRequest,
ModelPendingUploadResponse,
ModelVersion,
PendingUploadType,
)

logger = logging.getLogger(__name__)


class BetaModelsOperations(BetaModelsOperationsGenerated):
"""
.. warning::
**DO NOT** instantiate this class directly.

Instead, you should access the following operations through
:class:`~azure.ai.projects.aio.AIProjectClient`'s
:attr:`beta.models <azure.ai.projects.aio.operations.BetaOperations.models>` attribute.
"""

@staticmethod
def _extract_pending_upload_targets(
response: Union[ModelPendingUploadResponse, dict],
) -> "tuple[str, str, Optional[str]]":
"""Return ``(sas_uri, container_blob_uri, pending_upload_id)`` from a pending-upload response.

The service currently returns the raw datastore-style payload
(``blobReferenceForConsumption`` / ``temporaryDataReferenceId``) for some
Foundry deployments rather than the SDK-modeled ``ModelPendingUploadResponse``
shape (``blobReference`` / ``pendingUploadId``). Tolerate both wire
shapes so callers don't have to.

:param response: The pending-upload response from the service.
:type response: ~azure.ai.projects.models.ModelPendingUploadResponse or dict
:return: A tuple of ``(sas_uri, container_blob_uri, pending_upload_id)``.
:rtype: tuple[str, str, str or None]
"""
payload = dict(response) if isinstance(response, dict) else response.as_dict()

blob_ref = payload.get("blobReferenceForConsumption") or payload.get("blobReference") or {}
sas_uri = (blob_ref.get("credential") or {}).get("sasUri")
container_blob_uri = blob_ref.get("blobUri")
pending_upload_id = payload.get("temporaryDataReferenceId") or payload.get("pendingUploadId")

if not sas_uri or not container_blob_uri:
raise ValueError("Could not locate SAS URI / blob URI in pending_upload response: " f"{payload!r}")
return sas_uri, container_blob_uri, pending_upload_id

@staticmethod
def _validate_create_inputs(
*,
name: str,
version: str,
source: Union[str, "os.PathLike[str]"],
wait_for_commit: bool,
polling_timeout: float,
polling_interval: float,
) -> Path:
"""Validate ``create`` inputs up-front, before any service call.

Returns the resolved ``Path`` for ``source``. Raises ``ValueError`` for
bad inputs.

:keyword name: Name of the model to register.
:paramtype name: str
:keyword version: Version identifier for the model.
:paramtype version: str
:keyword source: Local file or directory containing the model weights.
:paramtype source: str or os.PathLike[str]
:keyword wait_for_commit: Whether to poll for commit completion.
:paramtype wait_for_commit: bool
:keyword polling_timeout: Total seconds to poll for commit completion.
:paramtype polling_timeout: float
:keyword polling_interval: Seconds between poll attempts.
:paramtype polling_interval: float
:return: The resolved ``Path`` for ``source``.
:rtype: pathlib.Path
"""
if not isinstance(name, str) or not name.strip():
raise ValueError("`name` must be a non-empty string.")
if not isinstance(version, str) or not version.strip():
raise ValueError("`version` must be a non-empty string.")

source_path = Path(os.fspath(source))
if not source_path.exists():
raise ValueError(f"Upload source does not exist: {source_path}")
if source_path.is_dir() and not any(p.is_file() for p in source_path.rglob("*")):
raise ValueError(f"Upload source directory is empty: {source_path}")
if source_path.is_file() and source_path.stat().st_size == 0:
raise ValueError(f"Upload source file is empty: {source_path}")

if wait_for_commit:
if polling_timeout <= 0:
raise ValueError("`polling_timeout` must be > 0 when `wait_for_commit` is True.")
if polling_interval <= 0:
raise ValueError("`polling_interval` must be > 0 when `wait_for_commit` is True.")

return source_path

@staticmethod
async def _upload_with_container_client(source: Path, sas_uri: str) -> None:
"""Upload ``source`` to the SAS container using ``azure.storage.blob.aio.ContainerClient``.

:param source: Local file or directory to upload.
:type source: pathlib.Path
:param sas_uri: SAS URI for the destination container.
:type sas_uri: str
:raises RuntimeError: If ``azure-storage-blob`` is not installed.
"""
try:
from azure.storage.blob.aio import ContainerClient # pylint: disable=import-outside-toplevel
except ImportError as ex:
raise RuntimeError(
"`azure-storage-blob` is required for the async `create` helper. "
"Install it with `pip install azure-storage-blob aiohttp`."
) from ex

if source.is_dir():
files = [p for p in source.rglob("*") if p.is_file()]
if not files:
raise ValueError(f"Upload source directory is empty: {source}")
elif source.is_file():
files = [source]
else:
raise ValueError(f"Upload source does not exist: {source}")

# Don't log the SAS query string — it's a credential.
redacted = sas_uri.split("?", 1)[0] + "?<sas-redacted>"
logger.info("[create] uploading %d file(s) to %s", len(files), redacted)

async with ContainerClient.from_container_url(sas_uri) as container_client:
for f in files:
rel = f.relative_to(source).as_posix() if source.is_dir() else f.name
with f.open("rb") as fp:
await container_client.upload_blob(name=rel, data=fp, overwrite=True)
logger.debug("[create] uploaded %s (%d bytes)", rel, f.stat().st_size)

@distributed_trace_async
async def create(
self,
*,
name: str,
version: str,
source: Union[str, "os.PathLike[str]"],
weight_type: Optional[str] = None,
base_model: Optional[str] = None,
description: Optional[str] = None,
tags: Optional["dict[str, str]"] = None,
wait_for_commit: bool = True,
polling_timeout: float = 300.0,
polling_interval: float = 2.0,
**kwargs: Any,
) -> Optional[ModelVersion]:
"""Register a local model by running the full upload-first sequence (async).

This wraps the three mandatory steps of the model-registration spec
into a single call:

1. :meth:`pending_upload` — provision a project-managed blob container
and obtain a SAS URI.
2. Upload the local weight files to the SAS container using
:class:`azure.storage.blob.aio.ContainerClient`.
3. :meth:`pending_create_version` — finalize registration with the
``ModelVersion`` body (``blob_uri``, ``weight_type``, ``base_model``,
``description``, ``tags``).

Requires the ``azure-storage-blob`` package (with ``aiohttp``) for the
upload step.

:keyword name: Name of the model to register. Required.
:paramtype name: str
:keyword version: Version identifier for the model. Required.
:paramtype version: str
:keyword source: Local file or directory containing the model weights.
If a directory, its contents are uploaded recursively to the SAS
container root. Required.
:paramtype source: str or os.PathLike[str]
:keyword weight_type: Optional weight type (e.g. ``"FullWeight"``,
``"LoRA"``, ``"DraftModel"``).
:paramtype weight_type: str
:keyword base_model: Optional base model asset ID.
:paramtype base_model: str
:keyword description: Optional asset description.
:paramtype description: str
:keyword tags: Optional asset tags.
:paramtype tags: dict[str, str]
:keyword wait_for_commit: When True (default) poll :meth:`get` until
the committed ``ModelVersion`` is observable, and return it.
When False, return ``None`` after the async commit is accepted.
:paramtype wait_for_commit: bool
:keyword polling_timeout: Total seconds to poll for commit completion.
:paramtype polling_timeout: float
:keyword polling_interval: Seconds between poll attempts.
:paramtype polling_interval: float
:return: The committed :class:`~azure.ai.projects.models.ModelVersion`
when ``wait_for_commit`` is True, otherwise ``None``.
:rtype: ~azure.ai.projects.models.ModelVersion or None
:raises ValueError: If ``name``/``version`` are empty, ``source`` does
not exist or is empty, polling parameters are non-positive, or the
pending-upload response is missing the SAS / blob URI.
:raises RuntimeError: If ``azure-storage-blob`` is not installed or
the registration does not commit before ``polling_timeout`` elapses.
"""
# --- Step 0: validate inputs up-front --------------------------------
source_path = self._validate_create_inputs(
name=name,
version=version,
source=source,
wait_for_commit=wait_for_commit,
polling_timeout=polling_timeout,
polling_interval=polling_interval,
)

# --- Step 1: StartPendingUpload --------------------------------------
logger.info(
"[create] step 1/3 pending_upload(name=%r, version=%r)",
name,
version,
)
pending = await self.pending_upload(
name=name,
version=version,
pending_upload_request=ModelPendingUploadRequest(
pending_upload_type=PendingUploadType.TEMPORARY_BLOB_REFERENCE,
),
**kwargs,
)
sas_uri, container_blob_uri, pending_upload_id = self._extract_pending_upload_targets(pending)
logger.info(
"[create] pending_upload_id=%s blob_uri=%s",
pending_upload_id,
container_blob_uri,
)

# --- Step 2: Upload via async ContainerClient ------------------------
logger.info("[create] step 2/3 async upload from %s", source_path)
await self._upload_with_container_client(source_path, sas_uri)

# --- Step 3: Commit registration -------------------------------------
model_version_body = ModelVersion(
blob_uri=container_blob_uri,
weight_type=weight_type,
base_model=base_model,
description=description,
tags=tags or {},
)
logger.info(
"[create] step 3/3 pending_create_version(name=%r, version=%r)",
name,
version,
)
await self.pending_create_version(name=name, version=version, model_version=model_version_body, **kwargs)

if not wait_for_commit:
return None

# The async op returns 202; the service materializes the ModelVersion
# asynchronously. Poll get() until it appears or we time out.
import time # pylint: disable=import-outside-toplevel

deadline = time.monotonic() + polling_timeout
last_exc: Optional[BaseException] = None
while True:
try:
return await self.get(name=name, version=version, **kwargs)
except ResourceNotFoundError as ex:
last_exc = ex
if time.monotonic() >= deadline:
raise RuntimeError(
f"Model {name!r}@{version!r} did not appear within " f"{polling_timeout}s after pending_create_version."
) from last_exc
await asyncio.sleep(polling_interval)


__all__ = ["BetaModelsOperations"]
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
from ._patch_telemetry import TelemetryOperations
from ._patch_connections import ConnectionsOperations
Comment thread
kshitij-microsoft marked this conversation as resolved.
from ._patch_memories import BetaMemoryStoresOperations
from ._patch_models import BetaModelsOperations
from ._patch_sessions import BetaAgentsOperations
from ._operations import (
BetaDatasetsOperations,
BetaEvaluationTaxonomiesOperations,
BetaEvaluatorsOperations,
BetaInsightsOperations,
BetaModelsOperations,
BetaOperations as GeneratedBetaOperations,
BetaRedTeamsOperations,
BetaRoutinesOperations,
Expand Down Expand Up @@ -130,6 +130,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self.agents = BetaAgentsOperations(self._client, self._config, self._serialize, self._deserialize)
# Replace with patched class that includes begin_update_memories
self.memory_stores = BetaMemoryStoresOperations(self._client, self._config, self._serialize, self._deserialize)
# Replace with patched class that includes create (3-step upload helper)
self.models = BetaModelsOperations(self._client, self._config, self._serialize, self._deserialize)

for property_name, foundry_features_value in _BETA_OPERATION_FEATURE_HEADERS.items():
setattr(
Expand Down
Loading
Loading