diff --git a/sagemaker-serve/src/sagemaker/serve/model_builder.py b/sagemaker-serve/src/sagemaker/serve/model_builder.py index 4e09c64abc..a065e5ff87 100644 --- a/sagemaker-serve/src/sagemaker/serve/model_builder.py +++ b/sagemaker-serve/src/sagemaker/serve/model_builder.py @@ -2373,6 +2373,13 @@ def _build_single_modelbuilder( "HostingArtifactUri not found in JumpStart hub metadata. " "Cannot deploy LORA adapter without base model artifacts." ) + accept_eula = getattr(self, "accept_eula", None) + if not accept_eula: + raise ValueError( + "accept_eula must be set to True to deploy this model. " + "Please set accept_eula=True on the ModelBuilder instance to confirm " + "you have read and accepted the end-user license agreement for this model." + ) container_def = ContainerDefinition( image=self.image_uri, environment=self.env_vars, @@ -2381,7 +2388,7 @@ def _build_single_modelbuilder( "s3_uri": hosting_artifact_uri, "s3_data_type": "S3Prefix", "compression_type": "None", - "model_access_config": {"accept_eula": True}, + "model_access_config": {"accept_eula": accept_eula}, } }, ) diff --git a/sagemaker-serve/tests/integ/test_model_customization_deployment.py b/sagemaker-serve/tests/integ/test_model_customization_deployment.py index 074d71e3a4..0eacafbaa2 100644 --- a/sagemaker-serve/tests/integ/test_model_customization_deployment.py +++ b/sagemaker-serve/tests/integ/test_model_customization_deployment.py @@ -119,6 +119,7 @@ def test_build_from_training_job(self, training_job_name): training_job = TrainingJob.get(training_job_name=training_job_name) model_builder = ModelBuilder(model=training_job) + model_builder.accept_eula = True model = model_builder.build(model_name=f"test-model-{int(time.time())}-{random.randint(100, 10000)}") assert model is not None @@ -139,6 +140,7 @@ def test_deploy_from_training_job(self, training_job_name, endpoint_name, cleanu training_job = TrainingJob.get(training_job_name=training_job_name) model_builder = ModelBuilder(model=training_job, instance_type="ml.g5.4xlarge") + model_builder.accept_eula = True model_builder.build(model_name=f"test-model-{int(time.time())}-{random.randint(100, 10000)}") peft_type = model_builder._fetch_peft() @@ -187,6 +189,7 @@ def test_build_from_model_package(self, model_package_arn): model_package = ModelPackage.get(model_package_name=model_package_arn) model_builder = ModelBuilder(model=model_package) + model_builder.accept_eula = True model = model_builder.build() assert model is not None @@ -201,6 +204,7 @@ def test_deploy_from_model_package(self, model_package_arn, cleanup_endpoints): model_package = ModelPackage.get(model_package_name=model_package_arn) endpoint_name = f"e2e-{int(time.time())}-{random.randint(100, 10000)}" model_builder = ModelBuilder(model=model_package) + model_builder.accept_eula = True model_builder.build() endpoint = model_builder.deploy(endpoint_name=endpoint_name) @@ -220,6 +224,7 @@ def test_instance_type_from_recipe(self, training_job_name): training_job = TrainingJob.get(training_job_name=training_job_name) model_builder = ModelBuilder(model=training_job) + model_builder.accept_eula = True model_builder.build() assert model_builder.instance_type is not None diff --git a/sagemaker-serve/tests/unit/test_model_builder.py b/sagemaker-serve/tests/unit/test_model_builder.py index e5900f5562..6c36e46e49 100644 --- a/sagemaker-serve/tests/unit/test_model_builder.py +++ b/sagemaker-serve/tests/unit/test_model_builder.py @@ -715,3 +715,96 @@ def test_deploy_passes_inference_config_to_model_customization(self): call_kwargs = mock_deploy_mc.call_args[1] self.assertEqual(call_kwargs['inference_config'], inference_config) self.assertEqual(result, mock_endpoint) + + +class TestLoraAcceptEula(unittest.TestCase): + """Tests for accept_eula handling in the LoRA deployment path.""" + + def _make_mb(self, accept_eula=None): + mb = ModelBuilder.__new__(ModelBuilder) + mb.accept_eula = accept_eula + mb.image_uri = "some-image-uri" + mb.env_vars = {} + mb.model_name = None + mb.model_path = "/tmp/fake-model-path" + mb.role_arn = "arn:aws:iam::123456789012:role/role" + mb.model = MagicMock() + mb._adapter_s3_uri = None + mb.shared_libs = [] + mb.dependencies = {"auto": True} + mb.image_config = None + mb.inference_spec = None + mb.schema_builder = None + mb.modelbuilder_list = None + mb.sagemaker_session = None + mb.s3_model_data_url = None + mb.source_code = None + mb.model_server = None + mb.model_metadata = None + mb.log_level = None + mb.content_type = None + mb.accept_type = None + mb.compute = None + mb.network = None + mb.instance_type = None + mb.mode = None + return mb + + def _patch_lora_deps(self, mb, hosting_uri="s3://bucket/hosting/"): + """Patch all dependencies needed to reach the LoRA ContainerDefinition block.""" + patches = [ + patch.object(mb, "_get_serve_setting", return_value=MagicMock()), + patch.object(mb, "_is_model_customization", return_value=True), + patch.object(mb, "_fetch_model_package", return_value=MagicMock()), + patch.object(mb, "_fetch_and_cache_recipe_config"), + patch.object(mb, "_is_nova_model", return_value=False), + patch.object(mb, "_fetch_peft", return_value="LORA"), + patch.object(mb, "_fetch_hub_document_for_custom_model", + return_value={"HostingArtifactUri": hosting_uri}), + ] + return patches + + def test_lora_build_raises_when_accept_eula_false(self): + mb = self._make_mb(accept_eula=False) + patches = self._patch_lora_deps(mb) + for p in patches: + p.start() + try: + with self.assertRaises(ValueError) as ctx: + mb._build_single_modelbuilder() + self.assertIn("accept_eula", str(ctx.exception)) + finally: + for p in patches: + p.stop() + + def test_lora_build_raises_when_accept_eula_not_set(self): + mb = self._make_mb(accept_eula=None) + patches = self._patch_lora_deps(mb) + for p in patches: + p.start() + try: + with self.assertRaises(ValueError) as ctx: + mb._build_single_modelbuilder() + self.assertIn("accept_eula", str(ctx.exception)) + finally: + for p in patches: + p.stop() + + @patch("sagemaker.serve.model_builder.ContainerDefinition") + @patch("sagemaker.serve.model_builder.Model") + def test_lora_build_passes_accept_eula_true(self, mock_model, mock_container_def): + mb = self._make_mb(accept_eula=True) + mock_model.create.return_value = MagicMock() + patches = self._patch_lora_deps(mb) + for p in patches: + p.start() + try: + mb._build_single_modelbuilder() + call_kwargs = mock_container_def.call_args[1] + eula_val = ( + call_kwargs["model_data_source"]["s3_data_source"]["model_access_config"]["accept_eula"] + ) + self.assertTrue(eula_val) + finally: + for p in patches: + p.stop()