Repository navigation
Conversation
…m ModelTrainer.train() `ModelTrainer.train()` returned `None` and kept the created job on the private `_latest_training_job`. A caller that needed the job name or ARN after `train(wait=False)` had to read the private attribute or page `ListTrainingJobs` by name prefix. The SDK's own docs, seven example notebooks, and `ModelBuilder` read the private attribute too, and `DPOTrainer` stored its job on a different attribute than the other serverless trainers, so `show_metrics()` and `stream_logs()` could not find a DPO job. Add a public `latest_training_job` property with a typed setter on `ModelTrainer` and `BaseTrainer`, return the created `TrainingJob` from `ModelTrainer.train()` for a SageMaker training job (`None` for a dry run, a pipeline capture, and local mode), store the DPO job on the shared attribute, and point the docs and error messages at the public name. --- X-AI-Prompt: How do I get the training job that ModelTrainer.train() created without reading a private attribute? X-AI-Tool: claude-code
…s every deploy path
`deploy()` took `tags` only through `**kwargs`, so `inspect.signature` could
not validate it, and two deploy paths dropped it: the fine-tuned model package
path read only `self._tags`, and the Nova path did not read the kwarg at all.
The accumulated tags also had two shapes in one SDK. `add_tags` compared
`tag["Key"]` and raised `KeyError: 'Key'` for the `{"key": ..., "value": ...}`
form that the `sagemaker.core.resources` classes take, and raised `TypeError`
for a `sagemaker.core.shapes.Tag`, while the fine-tuned path converted the
stored tags back to lowercase by hand.
Add `tags` as a named `deploy()` parameter. Normalize every accepted form (a
`{key: value}` mapping, boto-style dicts, core-style dicts, `Tag` objects) once
in `format_tags`, and make `add_tags`, `tag_exists`, `_validate_new_tags`, and
`remove_tag_with_key` go through it. Merge the caller's tags into the builder
before dispatch so the built-model, inference-component, recommendation,
fine-tuned, restricted-package, and Nova paths all tag the endpoint, and
replace the two hand-written case conversions with one helper.
---
X-AI-Prompt: Why does ModelBuilder.deploy drop my tags on a fine-tuned model, and why does add_tags raise KeyError: 'Key'?
X-AI-Tool: claude-code
…isted hub content `HubContent` had no `get_all`. The resource generator skips a List API when the list summary lacks a member the Describe input requires, and `HubContentInfo` omits `HubName`, the name the list call was made with. So every caller paged `list_hub_contents` on the boto3 client by hand: six places in the SDK and three scripts in the SageMaker agent skills repeat the same loop. `HubContentInfo` also carries `OriginalCreationTime`, the model-level catalog date, which `DescribeHubContent` does not return, and the `HubContent` class had no field for it, so `ResourceIterator` dropped it. The same gap made `HubContent.get_all_versions()` build each version without `hub_name`, so the first `refresh()` failed with `Missing required parameter in input: "HubName"`. Teach the generator two per-resource tables: list-input members to copy onto every listed resource (`HubName` -> `hub_name`), and summary-only members to keep as optional attributes (`OriginalCreationTime`). `ResourceIterator` takes the copied members as `extra_init_data`. `resources.py` is the regenerated output: `HubContent.get_all(hub_name, hub_content_type, name_contains, max_schema_version, creation_time_before, creation_time_after, sort_by, sort_order, session, region)`, the `original_creation_time` attribute, and the `hub_name` binding on `get_all_versions()`. --- X-AI-Prompt: Why does sagemaker-core have no HubContent.get_all, and why does get_all_versions() fail on refresh with a missing HubName? X-AI-Tool: claude-code
… and document
A hub model's task, license, provider, size, context window, languages,
modalities, model type, capabilities, and recipes live in
`HubContentSearchKeywords` as prefixed strings (`@task:`, `@license:`,
`@recipe:finetuning_sft_lora`, ...) and in the `HubContentDocument` JSON. The
`HubContent` resource exposed the two raw attributes and nothing else, so
`jumpstart/search.py`, `recipe_utils.py`, and `training_session/_utils.py` each
split the keywords for the one field they use, and the SageMaker agent skills
repeat the full parse in four scripts. The hub writes at most 50 keywords per
model, so a model with many languages loses its `@model-size:` and
`@context-window:` keywords while the document still carries the values. No
consumer handled that.
Add `HubContentMixin` in `sagemaker.core.hub_content` and teach the resource
generator a `RESOURCE_MIXINS` table so `resources.py` emits
`class HubContent(HubContentMixin, Base)`. The mixin adds no model field and
no state. Its properties read `hub_content_search_keywords` and
`hub_content_document` at call time: a keyword is authoritative and the
document fills what the cap dropped, so the same resource answers before and
after `refresh()`. `HubContentRecipe` parses `@recipe:` keywords and
`RecipeCollection` entries into type, technique, and strategy, and the resource
answers `supports_technique("sft")` and `fine_tuning_techniques()` directly.
---
X-AI-Prompt: How do I read a hub model's license, size, and supported fine-tuning techniques from HubContent without parsing the search keywords myself?
X-AI-Tool: claude-code
A caller that wants the job name or ARN of a long run has two choices today. `train(wait=True)` returns after the job ends, so the handle arrives hours late. `train(wait=False)` returns the job at once, but the caller then has to wait on it alone, and the shared fine-tuning wait lives in the private `sagemaker.train.common_utils.trainer_wait.wait`. Five SageMaker agent skill templates import that private function for this one reason. Add `on_submit: Optional[Callable[[TrainingJob], None]]` to `BaseTrainer.train`, `ModelTrainer.train`, and every trainer `train()` override. The trainer calls it once with the created job as soon as the service accepts the job and before any wait. A `dry_run` and a `PipelineSession` capture submit nothing and call nothing. The serverful path submits through `ModelTrainer` first and waits on the returned job afterwards, so the callback sees the job before the wait on that path too. `_record_submitted_job` stores `latest_training_job` and fires the callback in one place, so the serverless, serverful, and HyperPod paths cannot drift. --- X-AI-Prompt: How do I get the training job name or ARN right after submit when I call train(wait=True)? X-AI-Tool: claude-code
There was a problem hiding this comment.
🤖 AI code review
This PR adds four public handles the SDK previously lacked: HubContent.get_all() plus a HubContentMixin with typed catalog accessors, a code-generator mechanism (RESOURCE_MIXINS, LIST_SUMMARY_EXTRA_ATTRIBUTES, GET_ALL_INPUT_ATTRIBUTES) that produces them, a public latest_training_job property/setter and a return value from ModelTrainer.train(), and a ModelBuilder.deploy(tags=...) parameter backed by unified tag normalization (_normalize_tag/format_tags). The implementation is careful and well tested, with new regression tests for every change, and I found no correctness or compatibility regressions — the format_tags rewrite still satisfies the existing test_common_utils.py cases. Only a few minor doc/annotation nits below.
Reviewed commit d352fe5. Automated review; verify before acting.
|
|
||
| Raises: | ||
| botocore.exceptions.ClientError: This exception is raised for AWS service related errors. | ||
| The error message and error code can be parsed from the exception as follows: |
There was a problem hiding this comment.
🟡 Low: The get_all docstring documents max_results and next_token parameters, but neither exists in the method signature (ResourceIterator handles pagination internally). These stray entries will confuse readers; drop them from the docstring.
There was a problem hiding this comment.
Dropped. The generator now excludes the paging members from every get_all docstring as it excludes them from the signature (ResourceIterator pages for the caller), which removes the two stray lines here and 161 more across resources.py. Commit a20a1e6.
| } | ||
|
|
||
|
|
||
| def _normalize_tag(tag: Any) -> TagsDict: |
There was a problem hiding this comment.
🟡 Low: format_tags is annotated -> List[TagsDict] but now returns None unchanged when tags is None. Consider Optional[List[TagsDict]] to match the actual contract (several callers like _deploy_recommendation rely on the None passthrough).
There was a problem hiding this comment.
Changed to Optional[List[TagsDict]], with Optional[Tags] on the parameter. Commit a20a1e6.
| tags = kwargs.get("tags", getattr(self, "_tags", None)) | ||
| if "tags" in kwargs: | ||
| self._tags = tags | ||
| if "tags" in kwargs and tags is not None: |
There was a problem hiding this comment.
🟡 Low: _deploy_core_endpoint now calls self.add_tags(tags) here, and then the pre-existing block at lines 4001-4005 calls self.add_tags(tags) again on the same (already-merged) tags before re-reading self._tags. It's harmless because _validate_new_tags de-dups by key, but the redundant merge is dead work; the earlier block could just set tags without re-adding.
There was a problem hiding this comment.
Removed the second merge. _deploy_core_endpoint now merges the kwarg tags once and reads format_tags(self._tags) for every request. Commit a20a1e6.
… an unknown kwarg `deploy()` took `data_capture_config`, `endpoint_type`, and `inference_component_name` through `**kwargs`, and only the core endpoint path read them, so a fine-tuned or Nova deploy dropped `data_capture_config` without a word. `fallback_instance_types` and `tracing_config` were read nowhere, and a misspelled option deployed an endpoint without it. The `update_endpoint` path passed `create_endpoint_config` eleven keyword arguments the method does not take. Make the five options named parameters, validated once against `inference_config` in `_EndpointOptions`, and applied on every path: - `data_capture_config` reaches the endpoint configuration of a single-model, serverless, async, fine-tuned, restricted, and Nova deploy. An inference-component path raises `ValueError`, because the service rejects data capture there. - `fallback_instance_types` builds `InstancePools` with `instance_type` at priority 1 and up to 4 fallbacks after it. `session_helper.production_variant` takes `instance_pools` and emits `InstancePools` in place of `InstanceType`, as the service contract requires. - `tracing_config` reaches `CreateEndpointConfig` through the session, which raises `ValueError` when the installed botocore service model has no `TracingConfig` member instead of letting botocore drop it. The generated `EndpointConfig.create` gains the member once sagemaker-core is regenerated from a service model that has it. - `endpoint_type` and `inference_component_name` keep their meaning and now raise on a path that cannot apply them. - A kwarg outside `DEPLOY_KWARGS` raises `TypeError` before any service call. - `update_endpoint` creates the new endpoint configuration from the built production variant with the arguments `create_endpoint_config` takes. --- X-AI-Prompt: Why did ModelBuilder.deploy(data_capture_config=...) deploy my fine-tuned model without data capture, and how do I set fallback instance types? X-AI-Tool: claude-code
The V3 structured-configuration example passes `use_spot_instances=True` and `max_wait_time_in_seconds=7200` to `Compute`. `Compute` forbids extra fields, so the example fails on 3.27.0 with a pydantic `extra_forbidden` error. The spot flag is `Compute(enable_managed_spot_training=True)` and the wait limit lives on `StoppingCondition(max_wait_time_in_seconds=7200)`, next to `max_runtime_in_seconds`. --- X-AI-Prompt: Why does the migration guide's Compute(use_spot_instances=True) example fail with extra_forbidden? X-AI-Tool: claude-code
… API
The service indexes hub content for `Search(Resource="HubContent")` and
botocore 1.43.100 added the typed `SearchRecord.HubContent` record, but the
SDK never read it. A caller who wanted the public hub narrowed by license,
provider, task, or model size either walked every model with
`ListHubContents` or wrote the `Search` request, the `Metadata.<Field>` names,
the 20-filter and 1024-character limits, the paging, and the result parsing by
hand. The SageMaker agent skills carry a 955-line adapter for exactly this.
Add `HubContent.search(hub_name, filters, hub_content_type, session, region)`
to `HubContentMixin`. It builds the `And` expression of `HubName`,
`HubContentType`, and the caller's `HubContentFilter` entries, sorts by
`HubContentName` so the pages are stable, pages until the service returns no
`NextToken` or repeats one, and yields each record as a `HubContent` bound to
the session and region, with its keywords and document. `HubContentFilter`
holds one service filter. `HubContentFilter.metadata("license", "apache-2.0")`
maps a `HubContent` attribute name to its `Metadata.<Field>` name, and a
sequence of values selects the `In` operator. The filter checks the operator,
the value length, and the filter count against the service limits before the
request.
---
X-AI-Prompt: How do I search the SageMaker public hub for Apache-2.0 models from Meta without listing every model?
X-AI-Tool: claude-code
Commit 6e9df41 made `format_tags` normalize every tag through `_normalize_tag`, which raised `TypeError` for a `(key, value)` tuple. `Processor(tags=[("Key", "Value")])`, `ModelBiasMonitor(tags=...)`, and the feature processor `to_pipeline(tags=[(key, value)])` callers pass pairs, and three unit tests in `test_processing.py` and `test_clarify_model_monitoring.py` failed with it. Accept a two-item tuple or list as one tag. --- X-AI-Prompt: Why does Processor(tags=[("Key", "Value")]) raise TypeError after the deploy(tags=) change? X-AI-Tool: claude-code
Callers that want training-loss values, not a PNG, set the MLflow tracking URI from `training_job.mlflow_config.mlflow_resource_arn` and call `MlflowClient().get_metric_history` with `training_job.mlflow_details.mlflow_run_id` themselves. The SDK repeated those lines in `plot_training_metrics` and `get_available_metrics`, and each caller copied the recipe-specific metric names (`total_loss` for LLMFT recipes, `train/loss` for Verl recipes). `get_metric_history` returns `Dict[str, List[MetricPoint]]` sorted by step. It accepts a job object or a job name, defaults to every logged metric, and resolves caller names through `DEFAULT_METRIC_ALIASES` (`loss`, `val_loss`), first logged candidate wins. An unknown metric raises `ValueError` that lists the metrics the run has. The tracking-URI and run-id resolution now lives in one private helper that the three public functions share. The lazy `__getattr__` in `sagemaker.train` exports `get_metric_history` and `MetricPoint`, and loses its duplicate `plot_training_metrics` through `get_mlflow_url` branches. --- X-AI-Prompt: How do I read the training loss history of a SageMaker training job as data instead of a plot? X-AI-Tool: claude-code
…time `verify_reward_function` accepted any dict as a sample, sent every sample in one invocation, and selected the response format only through `is_nova`. The evaluation custom-scorer contract differs: one invocation per sample, with `id`, `model_response`, `query`, `response`, and `reference_answer` fields, and the `statusCode` and `body` envelope for Nova too. The function and `RewardFunctionOutput` were importable only from `sagemaker.train.common_utils.rlvr_reward_verifier`. `Evaluator.create` deployed a reward Lambda from a file with `python3.9` (`EVALUATOR_DEFAULT_RUNTIME`) while `CustomAgentLambda.create` carried its own `python3.12` literal. `RewardSample` and `EvalRewardSample` are the typed inputs. The `envelope` option (`oss`, `nova`, `eval_oss`, `eval_nova`) selects the request shape, the response shape, and the sample model, and `None` keeps the `is_nova` behavior. The options live in `RewardVerificationOptions`, which the function takes as an object or as keyword arguments, so `verify_reward_function(reward_function, sample_data, is_nova=False)` still works. `sagemaker.train` exports `verify_reward_function`, `RewardVerificationOptions`, `RewardFunctionOutput`, `RewardSample`, and `EvalRewardSample`. `EVALUATOR_DEFAULT_RUNTIME` is `python3.12`, `Evaluator.create(runtime=...)` reaches the Lambda create call, and `CustomAgentLambda.create` defaults to the same constant. --- X-AI-Prompt: How do I test my evaluation custom-scorer Lambda locally with the exact payload the evaluation sends, and why does Evaluator.create deploy python3.9? X-AI-Tool: claude-code
`LLMAsJudgeEvaluator._validate_custom_metrics_json` only parsed the JSON. A metric whose
`instructions` had no `{{prompt}}` or `{{prediction}}`, an array of 11 metrics, a `ratingScale`
`definition` of 101 characters, or a `value` with neither or the two of `floatValue` and
`stringValue` reached the Bedrock evaluation job and failed there, after the pipeline started.
`LLMAsJudgeEvaluator.validate_custom_metrics(custom_metrics)` is a static method that returns
every violation as a message, an empty list means valid. It accepts the list or its JSON
string. The rules: 1 to 10 metrics, each with a `customMetricDefinition` that has a string
`name`, `instructions` of at most 5000 characters that contain `{{prompt}}` or `{{prediction}}`,
and an optional `ratingScale` whose entries have a `definition` of at most 100 characters and
a `value` with exactly one of `floatValue` or `stringValue`, one value type across the scale.
`_validate_custom_metrics_json` calls it and raises `ValueError` with the joined messages, so
`evaluate()` stops before any upload. Three existing tests passed `[{"name": "test"}]` as a
valid payload and now use a valid metric.
---
X-AI-Prompt: Can the SDK check my LLM-as-Judge custom metrics JSON before I start the evaluation job?
X-AI-Tool: claude-code
…ad metadata `S3Downloader.read_bytes` downloaded the whole object through `s3_resource.Bucket(bucket).download_fileobj`, so a caller that wanted the first megabyte of a dataset, one line at a time, or the object size had to drop to boto3. Fifteen SageMaker agent skill scripts split `s3://` URIs by hand, eight call `get_object(Range=...)`, and four call `head_object`, each without the `ExpectedBucketOwner` spot check that the SDK applies on the default bucket. Add `byte_range=(start, end)` to `read_bytes`, inclusive like an HTTP `Range` header, served through `get_object`. Add `iter_lines`, which decodes `Body.iter_lines()` and never holds the whole object, and `head`, which returns a frozen `S3ObjectHead(content_length, last_modified, etag, content_type)` from `head_object`. The three methods share `parse_s3_url`, the session's `s3_client` with the same fallback as `Session.read_s3_file`, and the `_get_account_id_if_default_bucket` spot check. A negative start or an end before the start raises `ValueError` before any request. --- X-AI-Prompt: How do I read the first megabyte of an S3 object or stream its lines with the SageMaker SDK instead of boto3? X-AI-Tool: claude-code
`DatasetFormatDetector.validate_dataset` answered only `True` or `False` for a local path. It stopped at the first line, named no format, read no `s3://` source, and knew none of the Nova RLVR, PPO, and CPT shapes. Two SageMaker agent skill scripts filled the gap with their own detectors (11 and 9 format names, their own `s3://` parsing, their own `get_object(Range=)` call), and the two disagree on names for the same shape. Add `detect_dataset_format(source, options)` in `dataset_format_detector.py`. It reads at most `sample_size_bytes` (1 MiB default) through `S3Downloader.read_bytes(byte_range=)` for `s3://` or from the local file, drops the last partial line, and matches every complete line against `FORMAT_MATCHERS`: the 7 SDK JSON schemas through `jsonschema`, plus `nova_rlvr`, `nova_ppo`, and `nova_cpt` as small pure functions. The frozen `FormatDetectionResult` carries the format, the matched-line ratio, the sampled line count, `"<line>: <reason>"` entries for the lines that failed, and every candidate with its ratio. `strict=True` demands a full match. `strict=False` picks the best candidate above a 0.5 ratio. `.parquet`, `.csv`, and `.json` keep the detector's whole-file checks. `SKILL_FORMAT_ALIASES` maps the 19 skill names onto the matcher names. Export the function, the options, and the result from `sagemaker.ai_registry`. --- X-AI-Prompt: How do I find out which fine-tuning format a JSONL dataset in S3 uses before I create a DataSet from it? X-AI-Tool: claude-code
…ndle Problem: the skill templates call bedrock and bedrock-runtime by hand around BedrockModelBuilder. get_bedrock_candidates.py pages list_foundation_models and list_inference_profiles and joins the two on models[].modelArn. deploy-oss-bedrock.py loops invoke_model for 12 minutes on ModelNotReadyException after the import job completes. deploy-nova-bedrock.py calls converse with the deployment ARN. The SDK opens three Bedrock clients and lists nothing and invokes nothing. deploy() returns a response dict, so the caller picks the ARNs out of it by hand. Solution: add sagemaker.serve.bedrock_deployment. list_invocable_foundation_models() pages ListFoundationModels filtered by output modality and ListInferenceProfiles filtered to SYSTEM_DEFINED, then joins profile ids to model ids. It keeps a model when inferenceTypesSupported holds ON_DEMAND or an active cross-region profile routes to it. BedrockDeployment.wait_until_ready() probes Converse for a custom model deployment and InvokeModel for an imported model. It retries only ModelNotReadyException and raises a TimeoutError that names the ARN. invoke() does a JSON round trip, and converse() passes inferenceConfig and system through. BedrockModelBuilder.deploy() keeps its return value and sets deployment on every successful path (importedModelArn on the OSS path, modelArn plus customModelDeploymentArn on the Nova path). latest_deployment() raises ValueError before the first deploy(). Field names come from the botocore 1.43.118 bedrock and bedrock-runtime service models. Tests: sagemaker-serve/tests/unit/test_bedrock_deployment.py fails on master with ModuleNotFoundError: No module named 'sagemaker.serve.bedrock_deployment'. On this branch test_bedrock_model_builder.py and test_bedrock_deployment.py report 124 passed. --- X-AI-Prompt: Can BedrockModelBuilder list the Bedrock models I can invoke, wait until an import is ready, and invoke it without raw boto3 calls? X-AI-Tool: claude-code
ModelBuilder held two private copies of the same EC2 lookup. `_get_instance_resources` and `_infer_accelerator_count_from_instance_type` each created an EC2 client, stripped the `ml.` prefix with `str.replace`, called `describe_instance_types` for one name, and read a different slice of the response. No public SDK function listed the SageMaker instance type names or described their hardware, so every caller outside ModelBuilder wrote its own EC2 call. `sagemaker.core.instance_types` gains `InstanceSpec`, `list_instance_types`, `describe_instance_type`, and `describe_instance_types`. `list_instance_types` reads the `ProductionVariantInstanceType` enum of the loaded SageMaker service model and makes no request. `describe_instance_types` strips the `ml.` prefix, sends at most 100 names per EC2 request (the `RequestInstanceTypeList` limit), reads every page, and maps vCPUs, memory, GPU count, GPU memory, and GPU name. EC2 rejects a whole request with `InvalidInstanceType` when one name does not exist, so the function then asks for each name alone. An unknown type gets `gpu_count=0`, `None` elsewhere, and a warning. Every other `ClientError` propagates. The two ModelBuilder helpers call `describe_instance_type` with the session's `boto_session` and keep their signatures and fallbacks: `(None, None)` and `None` on an unknown type or a failed call. The existing `test_get_instance_resources` stub now carries `InstanceType`, which the real EC2 response always holds and the mapper keys on. The module joins the `sagemaker-core` API reference. --- X-AI-Prompt: Can sagemaker-core list and describe SageMaker instance types, so ModelBuilder drops its two private EC2 lookups? X-AI-Tool: claude-code
`DescribeModelPackageGroup` reports a missing group as a `ValidationException`
whose message ends in `does not exist`, not as a `ResourceNotFound` error, and
`ModelPackageGroup` offered only the generated `get` and `create`. So every
"use the group, or create it" flow parsed `ClientError` codes by hand. Ten
fine-tuning templates in the SageMaker agent skills wrap `create` in a `try`
block and match `e.response["Error"]["Code"]` against
`("ResourceInUse", "ValidationException")` to fall back to `get`, which also
treats an unrelated validation error as "the group exists".
Add `sagemaker.core.model_package_group.ModelPackageGroupMixin` and attach it
to the generated class through the `RESOURCE_MIXINS` table, so `resources.py`
emits `class ModelPackageGroup(ModelPackageGroupMixin, Base)`.
`get_or_create(name, spec=None, session=None, region=None)` calls the
generated `get`, and on a `ValidationException` whose message says the group
is absent, or on a code that starts with `ResourceNotFound`, calls the
generated `create` with the set fields of a `ModelPackageGroupSpec`
(description, tags, managed configuration). Every other error propagates. The
mixin adds no model field and no state, and the returned group keeps the
session and region it was loaded with.
---
X-AI-Prompt: Can ModelPackageGroup get or create a group in one call instead of parsing ClientError codes?
X-AI-Tool: claude-code
…cessor._start_new `Processor._start_new` built `ProcessingJob(**transformed)` from the create request. The job carried no `processing_job_arn`, no `processing_job_status`, and no client context, so `processor.latest_job.refresh()` built its client from the process default session and polled the default region (aws#5796). Four processing templates in the SageMaker agent skills call `describe_processing_job` on a boto3 client after `run()` to read the ARN. After the submit through the configured session, bind that session and its region to the job with `_set_client_context` and describe the job once with `refresh()`, so the returned job has its ARN and status and every later `refresh()`, `wait()`, and `stop()` talks to the account and region the job was created in. The `PipelineSession` path still returns the captured step arguments with no describe call. A `LocalSession` job exists only in the session's `LocalSagemakerClient`, so that path returns the request-built job as before and `run()` keeps polling it through the local client. --- X-AI-Prompt: Why does processor.latest_job have no ARN and refresh() poll the wrong region after run()? X-AI-Tool: claude-code
A generated `wait()` raised `FailedStatusError` only for a status that contains `failed`. `Stopped` is a terminal state too, so `TrainingJob.wait()` returned on a stopped job as if the job completed, and the caller had to read `training_job_status` after the wait to tell the two apart. Three training templates in the SageMaker agent skills do that and raise on `Stopped` themselves. Add `raise_on_stopped: bool = False` after `timeout` to `WAIT_METHOD_TEMPLATE` and a `STOPPED_STATUS_ERROR_TEMPLATE` block that the generator formats with the same failure-reason reference as the failed block. With `wait(raise_on_stopped=True)` a status that contains `stopped` raises `FailedStatusError` with the resource type, the status, and the failure reason. The default keeps the old return. Regenerated `resources.py`: all 15 `wait()` methods take the parameter. --- X-AI-Prompt: Can TrainingJob.wait() raise when the job is stopped instead of returning like it completed? X-AI-Tool: claude-code
When an endpoint misbehaves, an operator reads the endpoint status and failure reason, the per-variant instance type and counts, the 11 CloudWatch metrics of the endpoint dashboard, and the latest lines of the `/aws/sagemaker/Endpoints/<name>` log group. `Endpoint` exposed none of that beyond `refresh()`, so the SageMaker agent skill `collect_diagnostics.py` rebuilt the collection with raw boto3 clients from a region string. That script reads `InstanceType` from `ProductionVariantSummary`, where `DescribeEndpoint` never returns it, so the instance type is always `None`. It fetches one unpaged page of 100 log events, and it loses the session a caller bound the resource with. Add `EndpointMixin` in `sagemaker.core.endpoint` and register it in the generator's `RESOURCE_MIXINS` table, so `resources.py` emits `class Endpoint(EndpointMixin, Base)`. `diagnostics()` refreshes the resource, reads the instance type per variant from `EndpointConfig.get(endpoint_config_name)` matched by variant name, sends the 11 dashboard queries in one `GetMetricData` call keyed `"<MetricName>/<Stat>"`, and collects the log lines. A CloudWatch, Logs, or endpoint config failure lands in `errors` instead of raising. Only `DescribeEndpoint` raises. `recent_logs()` pages `FilterLogEvents` until the next token is absent, the page is empty, or the limit is reached, and formats `"<ISO timestamp> <log stream> <message>"` lines. The CloudWatch and Logs clients come from the session, region, and config the resource is bound to, so `Endpoint.get(name, session=..., region=...)` reads the metrics and logs of that account and region. The mixin adds no model field and no state. --- X-AI-Prompt: How do I get the status, metrics, and recent logs of a SageMaker endpoint from the Endpoint resource without writing my own boto3 calls? X-AI-Tool: claude-code
HyperparameterTuner builds a HyperParameterTrainingJobDefinition, which has no serverless_job_config, so AMT cannot run SFTTrainer, DPOTrainer, RLVRTrainer, or RLAIFTrainer trials. Users wrote their own sweep loops with a JSON manifest, a thread pool, and MLflow polling. FineTuningSweep runs a trainer whose train(wait=False) returns a TrainingJob or sets latest_training_job. SweepConfig selects a grid, random, or ASHA schedule with a bounded number of parallel jobs. ASHA reads the rung metric from the job's MLflow run and stops the bottom 1 - 1/reduction_factor share of each batch. With manifest_s3_uri the sweep writes its state to S3 after every trial change, and resume() skips terminal trials and attaches to running jobs by name. Trials poll training_job_status instead of TrainingJob.wait(), because rich 13 permits one live display per console and parallel waits raise LiveError. --- X-AI-Prompt: How can I run a hyperparameter sweep for SFTTrainer when HyperparameterTuner only supports ModelTrainer? X-AI-Tool: claude-code
…g, and the tag merge The generated `get_all` docstrings documented `max_results` and `next_token`, which the methods do not take because `ResourceIterator` pages for the caller. The generator now excludes the two paging members from the docstring as it excludes them from the signature, which drops 163 stale lines from `resources.py`. `format_tags` returns `None` for `None`, so its annotation is `Optional[List[TagsDict]]`. `_deploy_core_endpoint` merged the caller's tags into the builder twice. It merges once and reads the merged list. --- X-AI-Prompt: Can you address the review comments on the hub content get_all docstring and the tag merge? X-AI-Tool: claude-code
There was a problem hiding this comment.
🤖 AI code review
This is a very large, multi-feature PR (sixteen distinct changes, each with its own regression tests) that adds public SDK surfaces: HubContent metadata/search, trainer job handles (latest_training_job, train() return value, on_submit), typed ModelBuilder.deploy options, Endpoint.diagnostics()/recent_logs(), Bedrock helpers, S3 helpers, instance-type lookups, raise_on_stopped on generated wait(), and tag normalization. The new modules are well-structured and carry thorough unit tests. Overall the change looks sound; I found only a few minor correctness/maintenance points worth the author's attention, mostly around behavioral changes in shared tag helpers and deploy(). Given the diff size (~13k lines) I reviewed the hand-written source modules in full and spot-checked the generated resources.py and test files.
Reviewed commit a20a1e6. Automated review; verify before acting.
| ``{"Key": ..., "Value": ...}`` dicts: | ||
|
|
||
| * a ``{key: value}`` mapping, one tag per entry; | ||
| * a single tag dict or ``Tag`` object; |
There was a problem hiding this comment.
🟡 Low: format_tags is now the single normalizer for every tag path (tag_exists, _validate_new_tags, remove_tag_with_key, and the serve layer), but it stringifies values only for the {key: value} mapping branch (str(k), str(v)), while _normalize_tag preserves the raw value for lists, (key, value) pairs, boto/core dicts, and Tag objects. The result is that format_tags({"n": 1}) yields {"Value": "1"} but format_tags([("n", 1)]) yields {"Value": 1}. Consider normalizing to a string consistently (or not at all) so the output type does not depend on the input representation.
There was a problem hiding this comment.
Every representation now comes out with str keys and values (format_tags({"n": 1}), format_tags([("n", 1)]), the core dict form, and Tag objects all give {"Key": "n", "Value": "1"}). A PipelineVariable stays intact for the request serializer. Commit fa28a67.
| if curr_tags is None: | ||
| return new_tags | ||
| if new_tags is None: | ||
| return format_tags(curr_tags) if curr_tags is not None else None |
There was a problem hiding this comment.
🟡 Low: _validate_new_tags previously returned new_tags / curr_tags largely unchanged (a dict stayed a dict). It now always returns a normalized List[TagsDict] (or None). This is a reasonable cleanup, but it is a behavioral change for any caller that relied on getting back a single-dict form; worth confirming no downstream consumer in the SDK depends on the old passthrough shape.
There was a problem hiding this comment.
Confirmed: ModelBuilder.add_tags is the one caller in the SDK, and it stores the result, so one shape there is the point. The docstring now states the contract (merged list in boto form, None only when the two inputs are None). The three existing test_common_utils.py cases still pass. Commit fa28a67.
| ) | ||
| endpoint_options.validate(inference_config) | ||
|
|
||
| # Normalize the caller's tags once, in boto form, and merge them into the |
There was a problem hiding this comment.
🟡 Low: deploy(tags=...) calls self.add_tags(tags) near the top, permanently merging the tags into self._tags. If deploy() is invoked more than once on the same builder (e.g. a retry or a second endpoint), tags from the earlier call persist and are reapplied. If that accumulation is not intended, consider merging into a local copy for the duration of the call rather than mutating builder state.
There was a problem hiding this comment.
Changed: the merge now lasts for the call. deploy() saves the builder tags, merges the call tags, runs the path selection in _deploy_with_options, and restores the saved tags on return and on raise. Two tests cover it (test_deploy_tags_do_not_outlive_the_call, test_deploy_tags_are_restored_when_the_path_raises). Commit fa28a67.
Brings in the serve test extra pin `pandas>=2.3.0,<3.1.0` (aws#6524), which is what the `unit-tests (sagemaker-serve)` hook failed on: pandas 3.1.0 imports with `ImportError: C extension: None not built` on py311 and py312. --- X-AI-Prompt: Can you fix the failing PR hooks on my sagemaker-python-sdk pull request? X-AI-Tool: claude-code
The `codestyle-doc-tests (sagemaker-core)` hook fails on this branch and on `master`. `black --check` rejects `resources.py` because the import block ends with two newlines and `LOGGER_STRING` opened with a third, so the generator emitted two blank lines before `logger = ...` where black allows one. `pydocstyle` reports `D100: Missing docstring in public module` on the two generated data modules, `config_schema.py` and `code_injection/shape_dag.py`. Drop the leading newline from `LOGGER_STRING`, and make the generator write a module docstring at the top of the two data modules. Regenerate the three files, so the next botocore sync keeps them clean. --- X-AI-Prompt: Why does the codestyle-doc-tests hook fail with black-check and D100 on generated files, and how do I fix it in the generator? X-AI-Tool: claude-code
…tation
Three review points on the shared tag helpers and `deploy()`:
- `format_tags({"n": 1})` gave `{"Value": "1"}` while `format_tags([("n", 1)])`
gave `{"Value": 1}`, so the output type depended on the input shape. Every
representation now comes out with `str` keys and values. A `PipelineVariable`
stays intact for the request serializer.
- `deploy(tags=...)` merged the call's tags into the builder for good, so a
second `deploy()` on the same builder (a retry, another endpoint) carried the
first call's tags. The merge now lasts for the call. `deploy()` validates,
merges, runs the path selection in `_deploy_with_options`, and restores the
builder's tags on return and on raise.
- `_validate_new_tags` returns the merged list in boto form on purpose, so
`ModelBuilder.add_tags`, its one caller, stores one shape whatever shape it
received. The docstring now says so.
---
X-AI-Prompt: Can you address the review comments about tag value types and deploy() tags persisting across calls?
X-AI-Tool: claude-code
There was a problem hiding this comment.
🤖 AI code review
This PR bundles sixteen additive features across sagemaker-core, sagemaker-train, and sagemaker-serve: hub-content metadata/search mixins, endpoint diagnostics, instance-type helpers, ModelPackageGroup.get_or_create, typed ModelBuilder.deploy options with InstancePools, a rewritten format_tags, a public latest_training_job property, get_metric_history, an extended verify_reward_function, plus a large regenerated resources.py (new raise_on_stopped, HubContent.get_all, pruned pagination docstrings). The hand-written logic is carefully structured and backed by thorough unit tests; I found no outright bugs. A couple of public-signature changes carry minor backward-compatibility risk worth a note, called out below.
Reviewed commit fa28a67. Automated review; verify before acting.
| sample_model=RewardSample, per_sample=False, nova_model=False, bare_list_response=False | ||
| ), | ||
| "nova": _EnvelopeSpec( | ||
| sample_model=RewardSample, per_sample=False, nova_model=True, bare_list_response=True |
There was a problem hiding this comment.
🟡 Low: verify_reward_function changed its signature from (reward_function, sample_data, validate_format=True, compute=None, is_nova=True) to (reward_function, sample_data, options=None, **option_fields). Callers that passed validate_format/compute/is_nova positionally (e.g. verify_reward_function(fn, data, True)) now bind that third positional to options and break. Keyword callers (the internal rlvr_trainer._verify_reward_function) are unaffected. This is a public, exported helper, so consider keeping the old keyword names as explicit parameters alongside options, or at least documenting the positional break in the changelog.
| return value | ||
| return str(value) | ||
|
|
||
|
|
There was a problem hiding this comment.
🟡 Low: In _tag_dict, a value of None is passed through unchanged by _tag_text, so a tag dict missing Value (e.g. {"Key": "x"}) now normalizes to {"Key": "x", "Value": None}. The previous {key: value} path stringified everything (str(v)), so this is a behavior change that can emit "Value": None into a request. Worth confirming the service/serializer tolerates a null Value, or defaulting the missing value to "".
Sixteen changes to the surfaces an agent uses to pick a hub model, train it, evaluate it, and deploy it. Each closes a gap where the SageMaker agent skills (
aws-ai-ml) drop to rawboto3or a private attribute because the SDK has no public handle. One commit per change, each with a regression test that fails onmasterand passes here. Nothing regeneratessagemaker-corefrom a newer botocore. Every "Live run" line below is from this branch inus-west-2on 2026-10-25. The hub runs targetSageMakerPublicHub.Hub Service: list, read, and search models
Before this PR, the SDK had no
HubContent.get_all, no typed reader forHubContentSearchKeywords, and no binding forSearch(Resource="HubContent"). The agent skills pagedlist_hub_contentsby hand in three scripts, parsed@license:and@model-size:prefixes by hand, and built theSearchexpression by hand.List the catalog (
c2db676d)Live run: 766 bound
HubContentresources in 95.7 s (oneDescribeHubContentrefresh per item).original_creation_timeis the catalog date. On the first item it is2025-03-20, andcreation_time(the latest version) is2026-03-16. The three newest:huggingface-vlm-nvidia-nemotron-3-5-bolt-74b-a9b-bf16,meta-vlm-muse-spark-open-816b-a42b-nvfp4,meta-vlm-muse-spark-open-816b-a42b.name_contains="qwen3"returns 36.get_all_versions()on a listed item returns 4 versions ofhuggingface-vlm-qwen3-8-27b. Onmaster,get_all_versions()fails on the firstrefresh()withMissing required parameter in input: "HubName".Read the metadata of a model (
d352fe5d)Live run:
license,provider,model_size, andcontext_windowareapache-2.0,qwen,1b-10b, and8k-32k.tasksis['text generation'],languagesis['english'],capabilitiesis['bedrock_console', 'customization'], andfine_tuning_techniques()is['dpo', 'rlaif', 'rlvr', 'sft']. The two*_supportedflags andsupports_technique("dpo")areTrue.small_sftover the 766 listed models holds 12 names,huggingface-reasoning-qwen3-8bfirst. A keyword, when present, is authoritative. The document fills what the 50-keyword cap dropped, in the casing of the document. Across the 766 models, 79 are at the cap. The document alone supplies the license of 30 models, the model size of 13, and extra languages of 20 (huggingface-asr-whisper-base: 42 keyword languages, 50 merged). The mixin adds no model field:model_dump()is unchanged.Search server side (
93c0a67c)Live run:
fine_tunable_smallreturns 9 in 0.8 s (deepseek-llm-r1-distill-llama-8b,deepseek-llm-r1-distill-qwen-1-5b,deepseek-llm-r1-distill-qwen-7b,huggingface-llm-qwen2-5-7b-instruct,huggingface-vlm-gemma-4-e4b-it,huggingface-vlm-qwen3-5-4b,meta-textgeneration-llama-3-1-8b-instruct,meta-textgeneration-llama-3-2-1b-instruct,meta-textgeneration-llama-3-2-3b-instruct).apache_on_bedrockreturns 45 in 1.0 s.llama3returns 4 in 0.3 s. An unfiltered search returns all 766 in 8.5 s, against 95.7 s forget_all. Each result is a fullHubContentwith keywords and document, som.licenseandm.fine_tuning_techniques()make no second call.HubContentFilter.metadatamaps theHubContentattribute to the indexed field (licensetoMetadata.License,model_sizetoMetadata.ModelSize,capabilitiestoMetadata.Capabilities) and rejects an unknown attribute. A sequence selectsIn. The 20-filter and 1024-character service limits raiseValueErrorbefore the call.Metadata.<Field>matches the document spelling exactly:Apache-2.0matches 113 models,apache-2.0matches 0,1B-10Bmatches 91,1b-10bmatches 0.HubContentFilter.keywordmatches the stored keyword:@license:apache-2.0matches 122. Search records carry noOriginalCreationTime, soget_allstays the call for catalog dates.Training jobs
ModelTrainer.train()returns the createdTrainingJobandlatest_training_jobis public on every trainer (450c866d).on_submitruns as soon as the service accepts the job, before any wait, on the serverless, serverful, and HyperPod paths (8c8dd9ad).get_metric_historyreturns MLflow points as data, withlossresolving tototal_lossortrain/lossby recipe (fb7c6167).FineTuningSweepruns grid, random, or ASHA sweeps over the serverless trainers with a resumable S3 manifest, which AMT cannot do (HyperParameterTrainingJobDefinitionhas noserverless_job_config) (4b89520e).Evaluation
verify_reward_functionis exported with typedRewardSampleandEvalRewardSampleand anenvelopeofoss,nova,eval_oss, oreval_nova.Evaluator.createandCustomAgentLambdashare one Lambda runtime,python3.12(a4c27861).validate_custom_metricsreports every violation before the job starts, for example['Metric [0]: instructions must contain {{prompt}} or {{prediction}}.'](5a134b93).detect_dataset_formatreads a local file or one S3 byte range and names the format from the 7 SDK schemas plus the Nova names of the skill. Four committed fixtures returnhf_prompt_completion,dpo,verl, andgenqaat confidence 1.0 (cd594911).Deployment
ModelBuilder.deploynamestags,data_capture_config,tracing_config,fallback_instance_types,endpoint_type, andinference_component_name, and applies each on every deploy path or raises. An unknown kwarg raisesTypeErrorinstead of deploying without it.fallback_instance_typesbecomesInstancePoolswith the primary type at priority 1 (6e9df413,b04f4c93,097192a5).Endpoint.diagnostics()returns the status, failure reason, per-variant instance type from the endpoint config, 11 CloudWatch metrics, and recent logs. Live run on aFailedendpoint:failure_reasonstartsUnable to provision requested ML compute capacity due to InsufficientInstanceCapacity error,metricshas the 11 keys, and the missing log group lands inerrorsasFilterLogEvents failed: An error occurred (ResourceNotFoundException) when calling the FilterLogEvents operation: The specified log group does not exist.instead of raising (4c9bedb0).list_invocable_foundation_models()joinsListFoundationModelswith inference profiles: 89 of 92TEXTmodels inus-west-2, 43 through a cross-region profile.BedrockDeploymentwaits throughModelNotReadyException, theninvokeandconverse.BedrockModelBuilder.deploy()records it aslatest_deployment()(41aae122).Core resources
S3Downloader.read_bytes(byte_range=),iter_lines, andhead(2c09cf5f).ModelPackageGroup.get_or_create(6e820f61).Processor._start_newreturns a session-boundProcessingJobwith its ARN, solatest_job.refresh()no longer hits the global default region (#5796) (0727c476). Every generatedwait()takesraise_on_stopped(c6294e14).list_instance_types()returns theProductionVariantInstanceTypeenum of the loaded service model (277 names in botocore 1.43.118) with no request.describe_instance_type("ml.g5.xlarge")returnsInstanceSpec(name='ml.g5.xlarge', vcpus=4, memory_mib=16384, gpu_count=1, gpu_memory_mib=22888, gpu_name='A10G'). The two replace two private EC2 lookups inmodel_builder.py(d9069112).migration.mdnamesCompute(enable_managed_spot_training=True)andStoppingCondition(max_wait_time_in_seconds=)in place of theCompute(use_spot_instances=...)example that fails withextra_forbidden(b6959d83).Hand-written behavior for a generated class lives in its own module (
hub_content.py,endpoint.py,model_package_group.py). ARESOURCE_MIXINStable in the generator mixes it in, soresources.pyemitsclass HubContent(HubContentMixin, Base)and the two others.python src/sagemaker/core/tools/codegen.pyregeneratesresources.pyto the committed content.Tests
Twenty new test files, each failing on
origin/masterat collection or with the old behavior:sagemaker-core/tests/unit/test_hub_content_get_all.py,test_hub_content_mixin.py,test_hub_content_search.py,test_endpoint_diagnostics.py,test_model_package_group_get_or_create.py,test_processing_start_new_arn.py,test_wait_raise_on_stopped.py,test_instance_types.py,s3/test_client_read_primitives.py;sagemaker-train/tests/unit/train/test_latest_training_job.py,test_fine_tuning_sweep.py,common_utils/test_get_metric_history.py,common_utils/test_reward_function_contract.py,evaluate/test_validate_custom_metrics.py,ai_registry/test_evaluator_runtime.py,ai_registry/test_detect_dataset_format.py;sagemaker-serve/tests/unit/test_model_builder_deploy_tags.py,test_model_builder_deploy_options.py,test_bedrock_deployment.py,test_model_builder_instance_types.py.Full unit suites on this branch after the merge of
master(6a5d9436),pytest -n 8:sagemaker-core3953 passed, 23 skipped, 5 failed,sagemaker-train3632 passed, 19 skipped, 1 failed,sagemaker-serve2227 passed, 25 skipped, 59 failed. Every failure fails identically onorigin/masteron the same host. The 5test_common_utils.pypath tests fail withDID NOT RAISE ValueError.sm_recipes/test_utils.py::test_load_base_recipe_types[test_case2]fails withURLError: no host given. The 59sagemaker-servetests importtorch,tensorflow, ortritonclient, or read JumpStart specs from S3 with expired host credentials (ExpiredToken). Thecodestyle-doc-tests (sagemaker-core)hook,tox -e flake8,pylint,docstyle,black-check, is green on this branch:black --check ./leaves 518 files unchanged andpydocstyle src/sagemakeris clean. The hook failed onmasterand on every open PR: the generator emitted two blank lines beforeloggerand no module docstring inconfig_schema.pyandshape_dag.py. The fix is in the generator, so the next botocore sync keeps the files clean. Theunit-tests (sagemaker-serve)hook failed on pandas 3.1.0 (ImportError: C extension: None not built). The merge ofmasterbrings thepandas<3.1.0test pin from #6524.Merge Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.