Repository navigation
fix: accept core shapes and omitted content types in ModelBuilder.register - #6489
Conversation
…ister ModelBuilder.register() had two independent AttributeError defects on the same registry/deploy-from-package workflow. 1. register() rejected the ModelMetrics type its own signature declares. The signature types model_metrics (and metadata_properties, drift_check_baselines, model_life_cycle) as sagemaker.core.shapes classes, but get_model_package_args() converted them by calling value._to_request_dict(). The core shapes are pydantic models that do not implement that method, so the documented type raised "AttributeError: 'ModelMetrics' object has no attribute '_to_request_dict'". Only the older sagemaker.core.model_metrics compatibility class, which is not the declared type, has the method. get_model_package_args() now routes these four arguments through a small _to_request_dict() helper that keeps using _to_request_dict() when the value implements it and otherwise falls back to the core serializer, sagemaker.core.utils.utils.serialize(). The compatibility classes are unaffected, so they stay supported for the rest of their deprecation period, and dicts already in request form pass through. Replacing the call with serialize() unconditionally, as suggested on the issue, was rejected: the compatibility classes carry flat fields (model_statistics, model_constraints, ...) that serialize() renders as ModelStatistics/ModelConstraints instead of the nested ModelQuality.Statistics block the API expects, which would have broken the path that works today. 2. register() failed when content_types and response_types were omitted. Both are declared optional with None defaults, but the method only assigned self.content_types / self.response_types when the arguments were not None and then read those attributes unconditionally, raising "AttributeError: 'ModelBuilder' object has no attribute 'content_types'" on a first registration that omits them. The read now uses getattr(self, ..., None). This keeps the existing behaviour of reusing values supplied to an earlier register() call, and avoids adding new ModelBuilder dataclass fields next to the unrelated, already-public content_type field. Fixes aws#6479 Fixes aws#6480
There was a problem hiding this comment.
🤖 AI code review
The PR fixes two AttributeError defects in ModelBuilder.register(): (1) get_model_package_args() now dispatches via a new module-private _to_request_dict() helper that calls value._to_request_dict() for the legacy compatibility classes and falls back to the core serializer for sagemaker.core.shapes pydantic types (and plain dicts); (2) the read of content_types/response_types now uses getattr(..., None) so a first registration that omits them no longer raises. I verified the serializer produces the expected PascalCase request shape, the compatibility classes implement _to_request_dict() so hasattr dispatch is correct, and the attribute-assignment behavior at the register site. The implementation is correct and well-tested; only minor coverage gaps noted.
Findings not attached to a changed line:
sagemaker-core/tests/unit/test_model_registry.py:83🟡 Low: The PR extends the_to_request_dictdispatch to four arguments (model_metrics,drift_check_baselines,metadata_properties,model_life_cycle), but onlymodel_metricsanddrift_check_baselinesget a core-shape test here.metadata_propertiesandmodel_life_cyclecore-shape serialization are untested. Since they share the helper the risk is low, but a shape likeModelLifeCyclemay serialize to a structure worth asserting (e.g. to confirm the serializer output matches what CreateModelPackage expects, as noted in the PR body for ModelMetrics). Consider adding at least one assertion for each.
Reviewed commit 8eddfc4. Automated review; verify before acting.
Issue #, if available: Fixes #6479, Fixes #6480
Description
Two independent
AttributeErrordefects inModelBuilder.register(). They sit on the same method and the same customer workflow (register / deploy-from-package), so they are fixed together.1.
register()rejected theModelMetricstype its own signature declares (#6479)ModelBuilder.register()typesmodel_metricsassagemaker.core.shapes.ModelMetrics, butget_model_package_args()converted it withmodel_metrics._to_request_dict(). Thesagemaker.core.shapesclasses are pydantic models and do not implement that method, so passing the documented type raised:Only the older
sagemaker.core.model_metrics.ModelMetricscompatibility class implements_to_request_dict(), and that class is not the type in the public signature.The same mismatch applies to the three sibling arguments converted the same way in the same function —
metadata_properties,drift_check_baselinesandmodel_life_cycleare all declared inregister()assagemaker.core.shapestypes, and none of those shapes implements_to_request_dict()either. Fixing onlymodel_metricswould have left the customer hitting the identical error on the next argument, so all four now go through one helper.get_model_package_args()gained a module-private_to_request_dict(value)that:value._to_request_dict()when the value implements it — the compatibility classes keep their existing behaviour unchanged, so they remain supported for the rest of their deprecation period;sagemaker.core.utils.utils.serialize(), which is what handles thesagemaker.core.shapestypes;Why not the patch suggested on the issue. The issue proposes replacing the call with
serialize(model_metrics)unconditionally. I verified that this fixes the broken path but regresses the one that works today. The compatibility class carries flat fields (model_statistics,model_constraints, …) andserialize()renders them literally:That is not a valid
CreateModelPackageModelMetricsshape — the API expects the nestedModelQuality.Statisticsblock that the compatibility class's own_to_request_dict()builds. Soserialize()is the right direction for the core shapes only, and the dispatch above is required rather than a straight substitution.2.
register()failed when content and response types were omitted (#6480)content_typesandresponse_typesare declared optional withNonedefaults, butregister()only assignedself.content_types/self.response_typeswhen the argument was notNone, and then read those attributes unconditionally:A first registration that omits them raised:
The read now uses
getattr(self, "content_types", None). This is deliberately a two-line change at the read site rather than the alternative suggested on the issue (initialising both inModelBuilder.__init__):register()that omits the types reuses values supplied to an earlier call, which the issue explicitly asks for;ModelBuilderis a dataclass, so initialising in__init__would add two new public constructor fields next to the unrelated and already-publiccontent_typefield — Python's own suggestion on the traceback isDid you mean: 'content_type'?, so the near-duplicate names would be a lasting source of confusion;register().When the types are absent they are now simply left out of the request, as the issue expects.
Testing
All validation was done offline. There were no usable AWS credentials in this environment, so there is no live
CreateModelPackageround-trip in this evidence — request correctness is established against the service contract, not against the live service.Reproduction. Both defects reproduce against released
sagemaker==3.25.0with the exact errors quoted in the issues (model_registry.py:79for #6479,model_builder.py:6989for #6480), and both are present onmasterat 8e07c37.Request correctness.
ModelBuilder.register()was driven with a mocked SageMaker client, and the resulting wire request read out ofcreate_model_package.call_args.kwargswas validated withbotocore.validate.ParamValidatoragainstbotocore.session.get_session().get_service_model("sagemaker").operation_model("CreateModelPackage").input_shape. With the fix, the declared core shape produces exactly the structure the issue asks for, and the request validates against the contract:The compatibility-class request and the content-types-supplied request are byte-identical before and after the change, confirming the working paths are untouched. Reuse of types across repeated
register()calls was checked explicitly and still works, andregister()no longer leavescontent_types/response_typesattributes behind when they were never supplied.New tests — 13 total.
sagemaker-core/tests/unit/test_model_registry.py— 4 added: coreshapes.ModelMetrics, themodel_metricscompatibility class, coreshapes.DriftCheckBaselines, and a request-form dict.sagemaker-serve/tests/unit/test_model_builder_register.py— 9 added:register()with the types omitted, with them supplied, reusing them from an earlier call, not inventing attributes, with bothModelMetricstypes, and the serialized request shape for both.Negative check. Each fix was reverted independently to confirm the new tests actually guard the behaviour:
test_model_builder_register.pytest_model_registry.pyRegression runs. Full
tests/unitof both touched packages, with the fixes applied and again with them reverted:sagemaker-core: 2 failed, 3963 passed, 21 skipped. Reverted: 5 failed, 3960 passed — the same 2 failures plus exactly the 3 new tests that guardModelBuilder.register()rejects its declared coreModelMetrics#6479. No failure appears with the fixes that was not there without them.sagemaker-serve: 70 failed, 2079 passed, 25 skipped — the same 70 failures, identical set, with the fixes reverted (2070 passed; the 9-test delta is exactly the new file).Those pre-existing failures are environmental in this sandbox, not caused by this change: 65 of the 70
sagemaker-servefailures arebotocore.exceptions.ClientError: ExpiredTokenfromGetCallerIdentity(the tests constructModelBuilder, which resolves a serving role via STS), and the other 5 areModuleNotFoundError: No module named 'tensorflow'. The 2sagemaker-corefailures come from running repo tests against the installed 3.25.0 wheel (adjl-neuronxregion added by the preceding commit, and a removed-module guidance message not present in the wheel).Lint. Run as CI does, from inside each package:
black --line-length 100 --checkclean,flake8 --max-line-length 120clean,pydocstyleclean on both changedsrcfiles.pylint --rcfile=../.pylintrcintroduced no new findings —model_registry.py9.71 → 9.72,model_builder.py9.84 → 9.84; the only message text change is the pre-existingwrong-import-orderfinding now also enumerating the new import.Merge Checklist
Put an
xin the boxes that apply.General
Tests
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.