Skip to content

Commit dddd414

Browse files
[azure-ai-ml] Fix models.get(name, label="latest") dropping deployment-template references (Bug 5423568) (#47997)
* Fix models.get(name, label="latest") dropping deployment-template references models.get(name, label="latest") returned a Model whose default_deployment_template and allowed_deployment_templates references had asset_id=None, while models.get(name, version=<same>) populated them. Label resolution goes through the version list endpoint (top=1), whose items omit the deployment-template references; the explicit version path uses the get endpoint, which includes them. After resolving the label to a concrete version, the resolved version is now re-fetched through the get endpoint so the label path hydrates these references identically to the version path. Bug 5423568 * Scope models.get(label="latest") deployment-template re-fetch to registry The deployment-template re-fetch on the label path must only run for registry models. Deployment-template references are a registry-model concept and workspace models do not carry them, so the extra GET both wasted a call and broke the recorded workspace e2e sessions (test_models_get_latest_label / test_evaluators_get_latest_label), which 404 on the unrecorded request. Guarding the re-fetch with self._registry_name keeps the workspace path byte-identical to before while still hydrating references on the registry path. Added test_get_with_label_workspace_does_not_refetch to lock this in. Bug 5423568 * Removed the redundant comment on label re-fetch
1 parent 1fbcec1 commit dddd414

3 files changed

Lines changed: 108 additions & 1 deletion

File tree

sdk/ml/azure-ai-ml/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Fixed `DeploymentTemplate.creation_context` always being `None` when retrieved via `get()` or `list()`. The created/modified timestamps and identity returned by the service (as `createdTime` / `modifiedTime` / `createdBy`) are now populated on `creation_context`, making `DeploymentTemplate` consistent with `Model` and `Environment`.
1010
- Fixed `deployment_templates.list(name=...)` raising `AttributeError: 'str' object has no attribute 'request_timeout'`. In the list response, `requestSettings` / `livenessProbe` / `readinessProbe` arrive as stringified dicts nested under `properties`; these are now parsed before conversion, giving `list()` parity with `models.list()` / `environments.list()`.
1111
- Fixed `deployment_templates.get(name)` failing with a 404 (`DeploymentTemplate {name}:latest not found`) when no version was supplied, because the literal string `"latest"` was sent as the version. The latest version is now resolved client-side (the service exposes no `latest` label and no server-side ordering), and `get()` accepts a `label` keyword (`label="latest"` resolves to the latest version) mirroring `models.get()`. `delete(name)` resolves the latest version the same way.
12+
- Fixed `models.get(name, label="latest")` returning a `Model` whose `default_deployment_template` / `allowed_deployment_templates` references had `asset_id=None`. Label resolution goes through the version list endpoint (`top=1`), whose items omit the deployment-template references; for registry models the resolved version is now re-fetched through the get endpoint, so the label path hydrates these references identically to the explicit `version=` path.
1213

1314
### Other Changes
1415

sdk/ml/azure-ai-ml/azure/ai/ml/operations/_model_operations.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,10 @@ def get(self, name: str, version: Optional[str] = None, label: Optional[str] = N
378378
)
379379

380380
if label:
381-
return _resolve_label_to_asset(self, name, label)
381+
resolved_model = _resolve_label_to_asset(self, name, label)
382+
if self._registry_name and resolved_model.version is not None:
383+
return Model._from_rest_object(self._get(name, resolved_model.version))
384+
return resolved_model
382385

383386
if not version:
384387
msg = "Must provide either version or label"

sdk/ml/azure-ai-ml/tests/model/unittests/test_model_operations.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
ModelVersionProperties as ModelVersionDetails,
1313
)
1414
from azure.ai.ml._scope_dependent_operations import OperationConfig, OperationScope
15+
from azure.ai.ml._restclient.v2021_10_01_dataplanepreview.models import (
16+
ModelVersionData as RegistryModelVersionData,
17+
ModelVersionDetails as RegistryModelVersionDetails,
18+
)
1519
from azure.ai.ml.entities._assets import Model
1620
from azure.ai.ml.entities._assets._artifacts.artifact import ArtifactStorageInfo
1721
from azure.ai.ml.exceptions import ErrorTarget, ValidationException
@@ -64,6 +68,49 @@ def mock_model_operation_reg(
6468
)
6569

6670

71+
def _make_registry_model_rest_object(
72+
version: str,
73+
default_dt_asset_id: Optional[str] = None,
74+
allowed_dt_asset_ids: Optional[list] = None,
75+
) -> Mock:
76+
"""Build a registry ModelVersionData mock mirroring the real GET/LIST response shapes.
77+
78+
The GET response carries default_deployment_template / allowed_deployment_templates; the
79+
LIST (top=1) response used for label resolution omits them, which is what drops the
80+
references on the label path (bug 5423568).
81+
"""
82+
rest_properties = Mock(spec=RegistryModelVersionDetails)
83+
rest_properties.description = "Test model"
84+
rest_properties.tags = {}
85+
rest_properties.properties = {}
86+
rest_properties.flavors = {}
87+
rest_properties.model_uri = "azureml://locations/test/artifacts/model"
88+
rest_properties.model_type = "custom_model"
89+
rest_properties.stage = None
90+
rest_properties.job_name = None
91+
rest_properties.intellectual_property = None
92+
rest_properties.system_metadata = None
93+
rest_properties.default_deployment_template = {"asset_id": default_dt_asset_id} if default_dt_asset_id else None
94+
rest_properties.allowed_deployment_templates = (
95+
[{"asset_id": asset_id} for asset_id in allowed_dt_asset_ids] if allowed_dt_asset_ids else None
96+
)
97+
98+
rest_object = Mock(spec=RegistryModelVersionData)
99+
rest_object.id = (
100+
"/subscriptions/sub/resourceGroups/rg/providers/Microsoft.MachineLearningServices"
101+
f"/workspaces/ws/models/test-model/versions/{version}"
102+
)
103+
rest_object.properties = rest_properties
104+
105+
system_data = Mock()
106+
system_data.created_by = "test_user"
107+
system_data.created_at = None
108+
system_data.last_modified_by = None
109+
system_data.last_modified_at = None
110+
rest_object.system_data = system_data
111+
return rest_object
112+
113+
67114
@pytest.mark.unittest
68115
@pytest.mark.production_experiences_test
69116
class TestModelOperations:
@@ -172,6 +219,62 @@ def test_get_no_version(self, mock_model_operation: ModelOperations) -> None:
172219
with pytest.raises(Exception):
173220
mock_model_operation.get(name=name)
174221

222+
def test_get_with_label_rehydrates_deployment_template_references(
223+
self, mock_model_operation_reg: ModelOperations
224+
) -> None:
225+
"""Bug 5423568: models.get(name, label='latest') must hydrate deployment-template
226+
references identically to the explicit version= path.
227+
228+
Label resolution goes through the version LIST endpoint (top=1), whose items omit
229+
default_deployment_template / allowed_deployment_templates. The fix re-fetches the
230+
resolved version through the GET endpoint, which carries those references.
231+
"""
232+
default_dt = "azureml://registries/azure-huggingface/deploymenttemplates/dt/versions/5"
233+
allowed_dt = "azureml://registries/azure-huggingface/deploymenttemplates/dt/labels/latest"
234+
235+
# LIST shape (used by label resolution) -> deployment-template references dropped.
236+
list_pager = Mock()
237+
list_pager.next.return_value = _make_registry_model_rest_object(version="5")
238+
mock_model_operation_reg._model_versions_operation.list.return_value = list_pager
239+
240+
# GET shape (used by the re-fetch) -> deployment-template references populated.
241+
mock_model_operation_reg._model_versions_operation.get.return_value = _make_registry_model_rest_object(
242+
version="5", default_dt_asset_id=default_dt, allowed_dt_asset_ids=[allowed_dt]
243+
)
244+
245+
result = mock_model_operation_reg.get(name="test-model", label="latest")
246+
247+
# The resolved version was re-fetched through GET (not returned straight from the LIST shape).
248+
mock_model_operation_reg._model_versions_operation.get.assert_called_once_with(
249+
name="test-model",
250+
version="5",
251+
registry_name=mock_model_operation_reg._registry_name,
252+
**mock_model_operation_reg._scope_kwargs,
253+
)
254+
# Deployment-template references are hydrated, matching the explicit version= path.
255+
assert result.version == "5"
256+
assert result.default_deployment_template is not None
257+
assert result.default_deployment_template.asset_id == default_dt
258+
assert result.allowed_deployment_templates is not None
259+
assert len(result.allowed_deployment_templates) == 1
260+
assert result.allowed_deployment_templates[0].asset_id == allowed_dt
261+
262+
def test_get_with_label_workspace_does_not_refetch(self, mock_model_operation: ModelOperations) -> None:
263+
"""Bug 5423568: the deployment-template re-fetch is scoped to the registry.
264+
265+
Workspace models carry no deployment-template references, so the label path must not issue
266+
the extra GET; this keeps workspace behaviour (and its recorded e2e sessions) unchanged.
267+
"""
268+
resolved = Model(name="test-model", version="4")
269+
with patch(
270+
"azure.ai.ml.operations._model_operations._resolve_label_to_asset",
271+
return_value=resolved,
272+
):
273+
result = mock_model_operation.get(name="test-model", label="latest")
274+
275+
assert result is resolved
276+
assert mock_model_operation._model_versions_operation.get.call_count == 0
277+
175278
@patch.object(Model, "_from_rest_object", new=Mock())
176279
@patch.object(Model, "_from_container_rest_object", new=Mock())
177280
def test_list(self, mock_model_operation: ModelOperations) -> None:

0 commit comments

Comments
 (0)