Skip to content

Commit 4e97ddf

Browse files
author
Aniruddha Kappagantu
committed
fix: accept CUSTOMIZATION capability in JumpStart hub documents
CapabilityEnum was missing the CUSTOMIZATION member, so pydantic rejected the entire HubContentDocument for any JumpStart model whose Capabilities include CUSTOMIZATION. This broke every v3 training path that loads the document (ModelTrainer.from_jumpstart_config and the JumpStartTrainDefaults getters) for 42 of 754 public models in us-west-2, including three with TrainingSupported=true. Add CUSTOMIZATION to CapabilityEnum, and make Capabilities parsing forward-compatible: unknown values are kept as plain strings with a warning instead of failing validation, so a future service-side capability cannot break a pinned SDK version again. Applied to both the HubContentDocument field and the HostingComponentsModel base-class field. --- X-AI-Prompt: Address t.corp.amazon.com/P535211426 by fixing the SageMaker Python SDK v3 CapabilityEnum so JumpStart hub documents with the CUSTOMIZATION capability parse, and make capability parsing forward-compatible. X-AI-Tool: MeshClaw/Kiro
1 parent 94ec873 commit 4e97ddf

2 files changed

Lines changed: 100 additions & 4 deletions

File tree

‎sagemaker-core/src/sagemaker/core/jumpstart/models.py‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414

1515
from __future__ import absolute_import, annotations
1616

17+
import logging
1718
from enum import Enum
1819
from typing import Any, Dict, List, Optional, Union
19-
from pydantic import Field, ConfigDict
20+
from pydantic import Field, ConfigDict, field_validator
2021

2122
from sagemaker.core.jumpstart.configs import BaseConfig
2223

24+
logger = logging.getLogger(__name__)
25+
2326

2427
class StrEnum(str, Enum):
2528
"""A string-valued enumeration."""
@@ -406,6 +409,31 @@ class CapabilityEnum(StrEnum):
406409
FINE_TUNING = "FINE_TUNING"
407410
VALIDATION = "VALIDATION"
408411
INCREMENTAL_TRAINING = "INCREMENTAL_TRAINING"
412+
CUSTOMIZATION = "CUSTOMIZATION"
413+
414+
415+
def _coerce_capabilities(value: Any) -> Any:
416+
"""Coerce ``Capabilities`` values, tolerating unknown entries.
417+
418+
Known values are converted to ``CapabilityEnum`` members. Unknown values
419+
are kept as plain strings with a warning, so a new service-side capability
420+
cannot fail validation of the entire ``HubContentDocument`` for a pinned
421+
SDK version.
422+
"""
423+
if not isinstance(value, list):
424+
return value
425+
coerced = []
426+
for item in value:
427+
try:
428+
coerced.append(CapabilityEnum(item))
429+
except ValueError:
430+
logger.warning(
431+
"Unknown capability %r is not a member of CapabilityEnum; "
432+
"keeping it as a string.",
433+
item,
434+
)
435+
coerced.append(item)
436+
return coerced
409437

410438

411439
class DemoNotebookModel(BaseConfig):
@@ -444,7 +472,14 @@ class HostingComponentsModel(BaseConfig):
444472
DynamicContainerDeploymentSupported: Optional[bool] = None
445473
HostingAdditionalDataSources: Optional[Dict[str, List[HostingAdditionalDataSourceModel]]] = None
446474
BedrockIOMappingId: Optional[str] = None
447-
Capabilities: Optional[List[CapabilityEnum]] = None
475+
Capabilities: Optional[List[Union[CapabilityEnum, str]]] = None
476+
477+
@field_validator("Capabilities", mode="before")
478+
@classmethod
479+
def _validate_capabilities(cls, value: Any) -> Any:
480+
"""Keep unknown capability values as strings instead of failing."""
481+
return _coerce_capabilities(value)
482+
448483
HostingEcrUri: Optional[str] = None
449484
HostingArtifactS3DataType: Optional[HostingArtifactS3DataTypeEnum] = None
450485
HostingArtifactCompressionType: Optional[HostingArtifactCompressionTypeEnum] = None
@@ -506,4 +541,10 @@ class HubContentDocument(HostingComponentsModel, TrainingComponentsModel):
506541
TrainingConfigRankings: Optional[Dict[str, RankingSchemaModel]] = None
507542
Resources: Optional[List[ResourceModel]] = None
508543
Highlights: Optional[List[str]] = None
509-
Capabilities: List[CapabilityEnum] = None
544+
Capabilities: List[Union[CapabilityEnum, str]] = None
545+
546+
@field_validator("Capabilities", mode="before")
547+
@classmethod
548+
def _validate_capabilities(cls, value: Any) -> Any:
549+
"""Keep unknown capability values as strings instead of failing."""
550+
return _coerce_capabilities(value)

‎sagemaker-core/tests/unit/jumpstart/test_models.py‎

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,21 @@
1616

1717
import json
1818
import os
19-
from sagemaker.core.jumpstart.models import HubContentDocument
19+
from sagemaker.core.jumpstart.models import (
20+
CapabilityEnum,
21+
HostingComponentsModel,
22+
HubContentDocument,
23+
)
2024

2125
TEST_HUB_CONTENT_DOCUMENT = "hub_content_document.json"
2226

27+
MINIMAL_DOCUMENT = {
28+
"ModelTypes": ["OPEN_WEIGHTS"],
29+
"Url": "https://example.com",
30+
"TrainingSupported": True,
31+
"IncrementalTrainingSupported": False,
32+
}
33+
2334

2435
def test_init_hub_content_document():
2536
"""Test HubContentDocument initialization."""
@@ -28,3 +39,47 @@ def test_init_hub_content_document():
2839
hub_content_document = json.load(f)
2940
hub_content_document_instance = HubContentDocument(**hub_content_document)
3041
assert isinstance(hub_content_document_instance, HubContentDocument)
42+
43+
44+
def test_capability_enum_has_customization():
45+
"""CUSTOMIZATION is a valid, documented capability value."""
46+
47+
assert CapabilityEnum.CUSTOMIZATION.value == "CUSTOMIZATION"
48+
49+
50+
def test_hub_content_document_parses_customization_capability():
51+
"""A document whose Capabilities include CUSTOMIZATION parses successfully."""
52+
53+
document = HubContentDocument(**MINIMAL_DOCUMENT, Capabilities=["TRAINING", "CUSTOMIZATION"])
54+
assert CapabilityEnum.CUSTOMIZATION in document.Capabilities
55+
56+
57+
def test_hub_content_document_parses_all_known_capabilities():
58+
"""Every documented CapabilityEnum value parses without error."""
59+
60+
known = [capability.value for capability in CapabilityEnum]
61+
document = HubContentDocument(**MINIMAL_DOCUMENT, Capabilities=known)
62+
assert all(isinstance(capability, CapabilityEnum) for capability in document.Capabilities)
63+
assert len(document.Capabilities) == len(known)
64+
65+
66+
def test_hub_content_document_tolerates_unknown_capability(caplog):
67+
"""An unknown capability is kept as a string with a warning, not rejected."""
68+
69+
with caplog.at_level("WARNING", logger="sagemaker.core.jumpstart.models"):
70+
document = HubContentDocument(
71+
**MINIMAL_DOCUMENT, Capabilities=["TRAINING", "SOME_FUTURE_CAPABILITY"]
72+
)
73+
74+
assert "SOME_FUTURE_CAPABILITY" in document.Capabilities
75+
assert isinstance(document.Capabilities[-1], str)
76+
assert CapabilityEnum.TRAINING in document.Capabilities
77+
assert "SOME_FUTURE_CAPABILITY" in caplog.text
78+
79+
80+
def test_hosting_components_model_tolerates_unknown_capability():
81+
"""The base-class Capabilities field also tolerates unknown values."""
82+
83+
component = HostingComponentsModel(Capabilities=["CUSTOMIZATION", "SOME_FUTURE_CAPABILITY"])
84+
assert CapabilityEnum.CUSTOMIZATION in component.Capabilities
85+
assert "SOME_FUTURE_CAPABILITY" in component.Capabilities

0 commit comments

Comments
 (0)