@aws/ml-container-creator 0.15.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -178,6 +178,7 @@ def cmd_submit(args):
178
178
  "status": "AlreadyStaged",
179
179
  "s3_uri": s3_uri,
180
180
  })
181
+ return
181
182
  except s3.exceptions.ClientError:
182
183
  pass # Not staged yet, proceed
183
184
 
@@ -44,6 +44,7 @@ _usage() {
44
44
  echo ""
45
45
  echo "Options:"
46
46
  echo " --help, -h Show this help message"
47
+ echo " --force Bypass adapter-model compatibility check (add command)"
47
48
  echo " --local Use local aws s3 cp instead of Processing Job (--from-tune)"
48
49
  echo " --no-wait Submit Processing Job and return immediately (--from-tune)"
49
50
  echo ""
@@ -378,6 +379,7 @@ _adapter_add() {
378
379
  local registry_arn=""
379
380
  local use_local=""
380
381
  local no_wait=""
382
+ local force=""
381
383
 
382
384
  # Parse add arguments
383
385
  shift # remove 'add' from args
@@ -429,6 +431,10 @@ _adapter_add() {
429
431
  no_wait="true"
430
432
  shift
431
433
  ;;
434
+ --force)
435
+ force="true"
436
+ shift
437
+ ;;
432
438
  --help|-h)
433
439
  echo "Usage: ./do/adapter add <name> --weights <s3-uri>"
434
440
  echo " ./do/adapter add <name> --from-hub <hf-repo-id>"
@@ -449,6 +455,7 @@ _adapter_add() {
449
455
  echo " With ARN: adds directly using specified version ARN"
450
456
  echo " --local Use local aws s3 cp instead of Processing Job (--from-tune only)"
451
457
  echo " --no-wait Submit Processing Job and return immediately (--from-tune only)"
458
+ echo " --force Bypass adapter-model compatibility check"
452
459
  echo ""
453
460
  echo "Note: --weights, --from-hub, --from-tune, and --from-registry are mutually exclusive."
454
461
  echo ""
@@ -1030,14 +1037,30 @@ _adapter_add() {
1030
1037
  fi
1031
1038
  fi
1032
1039
 
1033
- # ── Validate adapter name uniqueness ──────────────────────────────────
1040
+ # ── Validate adapter name uniqueness (AC-1.7: overwrite when --from-tune) ──
1034
1041
  if [ -f "${SCRIPT_DIR}/adapters/${adapter_name}.conf" ]; then
1035
- echo "❌ Adapter already exists: ${adapter_name}"
1036
- echo ""
1037
- echo " An adapter with this name is already registered."
1038
- echo " To update its weights, use: ./do/adapter update ${adapter_name} --weights <new-uri>"
1039
- echo " To remove it first: ./do/adapter remove ${adapter_name}"
1040
- exit 1
1042
+ if [ -n "${from_tune}" ]; then
1043
+ # AC-1.7: Overwrite existing adapter when re-running auto-register
1044
+ echo "ℹ️ Adapter '${adapter_name}' already exists overwriting (re-tune)"
1045
+ # Delete existing IC if deployed, then remove conf
1046
+ local _existing_ic_name=""
1047
+ _existing_ic_name=$(grep "^export ADAPTER_IC_NAME=" "${SCRIPT_DIR}/adapters/${adapter_name}.conf" 2>/dev/null | sed 's/^export ADAPTER_IC_NAME="//' | sed 's/"$//' || echo "")
1048
+ if [ -n "${_existing_ic_name}" ]; then
1049
+ aws sagemaker delete-inference-component \
1050
+ --inference-component-name "${_existing_ic_name}" \
1051
+ --region "${AWS_REGION}" 2>/dev/null || true
1052
+ # Brief wait for deletion to propagate
1053
+ sleep 5
1054
+ fi
1055
+ rm -f "${SCRIPT_DIR}/adapters/${adapter_name}.conf"
1056
+ else
1057
+ echo "❌ Adapter already exists: ${adapter_name}"
1058
+ echo ""
1059
+ echo " An adapter with this name is already registered."
1060
+ echo " To update its weights, use: ./do/adapter update ${adapter_name} --weights <new-uri>"
1061
+ echo " To remove it first: ./do/adapter remove ${adapter_name}"
1062
+ exit 1
1063
+ fi
1041
1064
  fi
1042
1065
 
1043
1066
  echo "🔌 Adding adapter: ${adapter_name}"
@@ -1098,6 +1121,80 @@ _adapter_add() {
1098
1121
  _validate_adapter_config "${weights_uri}" || true
1099
1122
  fi
1100
1123
 
1124
+ # ── Compatibility check: adapter parent model vs deployed model ───────
1125
+ # Derive parent metadata early (same logic used later for conf file)
1126
+ local _compat_parent_arn=""
1127
+ local _compat_parent_slug=""
1128
+
1129
+ if [ -n "${from_tune}" ]; then
1130
+ _compat_parent_slug="${MODEL_NAME:-}"
1131
+ if [ -n "${MODEL_PKG_ARN:-}" ]; then
1132
+ _compat_parent_arn="${MODEL_PKG_ARN}"
1133
+ fi
1134
+ elif [ -n "${from_registry}" ] && [ -n "${version_line:-}" ]; then
1135
+ _compat_parent_arn=$(echo "${version_line}" | python3 -c "
1136
+ import sys, json
1137
+ data = json.loads(sys.stdin.read())
1138
+ metadata = data.get('metadata', {})
1139
+ print(metadata.get('parentModelVersionArn', ''))
1140
+ " 2>/dev/null || echo "")
1141
+ _compat_parent_slug=$(echo "${version_line}" | python3 -c "
1142
+ import sys, json
1143
+ data = json.loads(sys.stdin.read())
1144
+ metadata = data.get('metadata', {})
1145
+ print(metadata.get('modelName', ''))
1146
+ " 2>/dev/null || echo "")
1147
+ fi
1148
+
1149
+ if [ -z "${_compat_parent_arn}" ] && [ -z "${_compat_parent_slug}" ]; then
1150
+ echo "ℹ️ No parent model metadata — skipping compatibility check."
1151
+ elif [ "${force}" = "true" ]; then
1152
+ echo "ℹ️ --force: skipping compatibility check."
1153
+ else
1154
+ # Resolve base IC name for compat check (already validated InService above)
1155
+ local _compat_base_ic_name
1156
+ _compat_base_ic_name="${base_ic_name}"
1157
+
1158
+ # Get deployed model identity from base IC
1159
+ local _compat_deployed_model=""
1160
+ _compat_deployed_model=$(aws sagemaker describe-inference-component \
1161
+ --inference-component-name "${_compat_base_ic_name}" \
1162
+ --query 'Specification.Container.ArtifactUrl' --output text \
1163
+ --region "${AWS_REGION}" 2>/dev/null) || _compat_deployed_model=""
1164
+
1165
+ if [ -z "${_compat_deployed_model}" ] || [ "${_compat_deployed_model}" = "None" ]; then
1166
+ echo "ℹ️ Could not verify compatibility (DescribeInferenceComponent returned no artifact URL). Proceeding."
1167
+ else
1168
+ # Primary check: compare adapter parent MPG ARN against deployed MPG ARN
1169
+ local _compat_deployed_mpg="${MODEL_PKG_ARN:-}"
1170
+ local _compat_expected_slug="${_compat_parent_slug}"
1171
+
1172
+ local _compat_mismatch="false"
1173
+ if [ -n "${_compat_parent_arn}" ] && [ -n "${_compat_deployed_mpg}" ] && \
1174
+ [ "${_compat_parent_arn}" != "${_compat_deployed_mpg}" ]; then
1175
+ _compat_mismatch="true"
1176
+ fi
1177
+
1178
+ if [ "${_compat_mismatch}" = "true" ]; then
1179
+ # Fallback: check if artifact URL contains the expected model slug
1180
+ if [ -n "${_compat_expected_slug}" ] && \
1181
+ [[ "${_compat_deployed_model}" == *"${_compat_expected_slug}"* ]]; then
1182
+ : # Slug match — compatible despite ARN mismatch
1183
+ else
1184
+ echo "⚠️ Adapter was trained on: ${_compat_parent_arn}"
1185
+ echo " Deployed model: ${_compat_deployed_mpg:-${_compat_deployed_model:-unknown}}"
1186
+ if [ -t 0 ]; then
1187
+ read -p " Continue anyway? [y/N] " confirm
1188
+ [[ "${confirm}" =~ ^[Yy] ]] || exit 1
1189
+ else
1190
+ echo " Aborting (non-interactive). Use --force to override."
1191
+ exit 1
1192
+ fi
1193
+ fi
1194
+ fi
1195
+ fi
1196
+ fi
1197
+
1101
1198
  # ── Build adapter IC name ─────────────────────────────────────────────
1102
1199
  local adapter_ic_name="${PROJECT_NAME}-adapter-${adapter_name}"
1103
1200
 
@@ -1160,6 +1257,41 @@ export ADAPTER_SOURCE="tune"
1160
1257
  export ADAPTER_TUNE_TECHNIQUE="${tune_technique_meta}"
1161
1258
  export ADAPTER_TUNE_DATASET="${tune_dataset_meta}"
1162
1259
  EOF
1260
+
1261
+ # Store parent model metadata for compat check (US-3 prerequisite)
1262
+ # ADAPTER_PARENT_MODEL_SLUG from MODEL_NAME in do/config
1263
+ local parent_model_slug="${MODEL_NAME:-}"
1264
+ # ADAPTER_PARENT_MODEL_ARN: resolve base model version ARN from deployment MPG
1265
+ local parent_model_arn=""
1266
+ if [ -n "${MODEL_PKG_ARN:-}" ]; then
1267
+ parent_model_arn="${MODEL_PKG_ARN}"
1268
+ else
1269
+ # Query the deployment MPG for the latest base model version
1270
+ local models_json
1271
+ models_json=$(python3 "${SCRIPT_DIR}/.register_helper.py" list-models \
1272
+ --project-name "${PROJECT_NAME}" \
1273
+ --region "${AWS_REGION}" 2>/dev/null || echo "")
1274
+ local models_line
1275
+ models_line=$(echo "${models_json}" | grep -E '^\{' | tail -1)
1276
+ if [ -n "${models_line}" ]; then
1277
+ parent_model_arn=$(echo "${models_line}" | python3 -c "
1278
+ import sys, json
1279
+ data = json.loads(sys.stdin.read())
1280
+ models = data.get('models', [])
1281
+ if models:
1282
+ print(models[0].get('arn', ''))
1283
+ else:
1284
+ print('')
1285
+ " 2>/dev/null || echo "")
1286
+ fi
1287
+ fi
1288
+
1289
+ if [ -n "${parent_model_arn}" ] || [ -n "${parent_model_slug}" ]; then
1290
+ cat >> "${SCRIPT_DIR}/adapters/${adapter_name}.conf" <<EOF
1291
+ export ADAPTER_PARENT_MODEL_ARN="${parent_model_arn}"
1292
+ export ADAPTER_PARENT_MODEL_SLUG="${parent_model_slug}"
1293
+ EOF
1294
+ fi
1163
1295
  fi
1164
1296
 
1165
1297
  # Add registry-specific metadata if --from-registry was used
@@ -1167,6 +1299,39 @@ EOF
1167
1299
  cat >> "${SCRIPT_DIR}/adapters/${adapter_name}.conf" <<EOF
1168
1300
  export ADAPTER_SOURCE="registry"
1169
1301
  export ADAPTER_REGISTRY_ARN="${registry_arn}"
1302
+ EOF
1303
+
1304
+ # Store parent model metadata for compat check (US-3 prerequisite)
1305
+ # Extract parentModelVersionArn and modelName from registry version metadata
1306
+ local parent_model_arn=""
1307
+ local parent_model_slug=""
1308
+ if [ -n "${version_line:-}" ]; then
1309
+ parent_model_arn=$(echo "${version_line}" | python3 -c "
1310
+ import sys, json
1311
+ data = json.loads(sys.stdin.read())
1312
+ metadata = data.get('metadata', {})
1313
+ print(metadata.get('parentModelVersionArn', ''))
1314
+ " 2>/dev/null || echo "")
1315
+ parent_model_slug=$(echo "${version_line}" | python3 -c "
1316
+ import sys, json
1317
+ data = json.loads(sys.stdin.read())
1318
+ metadata = data.get('metadata', {})
1319
+ print(metadata.get('modelName', ''))
1320
+ " 2>/dev/null || echo "")
1321
+ fi
1322
+
1323
+ if [ -n "${parent_model_arn}" ] || [ -n "${parent_model_slug}" ]; then
1324
+ cat >> "${SCRIPT_DIR}/adapters/${adapter_name}.conf" <<EOF
1325
+ export ADAPTER_PARENT_MODEL_ARN="${parent_model_arn}"
1326
+ export ADAPTER_PARENT_MODEL_SLUG="${parent_model_slug}"
1327
+ EOF
1328
+ fi
1329
+ fi
1330
+
1331
+ # Default source: bare S3 URI (no --from-* flag)
1332
+ if [ -z "${from_hub}" ] && [ -z "${from_tune}" ] && [ -z "${from_registry}" ]; then
1333
+ cat >> "${SCRIPT_DIR}/adapters/${adapter_name}.conf" <<EOF
1334
+ export ADAPTER_SOURCE="s3"
1170
1335
  EOF
1171
1336
  fi
1172
1337
 
@@ -1194,171 +1359,102 @@ EOF
1194
1359
  }
1195
1360
 
1196
1361
  _adapter_list() {
1197
- if [ -z "${ENDPOINT_NAME:-}" ]; then
1198
- echo "❌ No endpoint configured. Deploy first with: ./do/deploy"
1199
- exit 1
1200
- fi
1201
-
1202
- echo "Adapters on endpoint: ${ENDPOINT_NAME}"
1203
- echo ""
1204
-
1205
- # ── List all inference components on the endpoint ─────────────────────
1206
- local ic_list
1207
- ic_list=$(aws sagemaker list-inference-components \
1208
- --endpoint-name-equals "${ENDPOINT_NAME}" \
1209
- --region "${AWS_REGION}" 2>/dev/null) || {
1210
- echo "❌ Failed to list inference components on endpoint: ${ENDPOINT_NAME}"
1211
- echo " Check that the endpoint exists and you have sagemaker:ListInferenceComponents permission."
1212
- exit 1
1213
- }
1214
-
1215
- # Extract IC names from the list response
1216
- local ic_names
1217
- ic_names=$(echo "${ic_list}" | jq -r '.InferenceComponents[].InferenceComponentName' 2>/dev/null)
1218
-
1219
- if [ -z "${ic_names}" ]; then
1220
- echo "No adapters found on this endpoint."
1221
- echo ""
1222
- echo "Add one with: ./do/adapter add <name> --weights <s3-uri>"
1223
- return 0
1224
- fi
1225
-
1226
- # ── Collect local adapter names for ownership check ───────────────────
1227
- local local_adapters=""
1228
- if [ -d "${SCRIPT_DIR}/adapters" ]; then
1229
- for conf_file in "${SCRIPT_DIR}"/adapters/*.conf; do
1230
- [ -f "${conf_file}" ] || continue
1231
- local conf_adapter_name
1232
- conf_adapter_name=$(grep "^export ADAPTER_IC_NAME=" "${conf_file}" 2>/dev/null | sed 's/^export ADAPTER_IC_NAME="//' | sed 's/"$//' || echo "")
1233
- if [ -n "${conf_adapter_name}" ]; then
1234
- local_adapters="${local_adapters} ${conf_adapter_name}"
1235
- fi
1236
- done
1237
- fi
1238
-
1239
- # ── Filter to adapter ICs and collect details ─────────────────────────
1240
- local found_adapters=0
1241
- local output_lines=""
1242
-
1243
- for ic_name in ${ic_names}; do
1244
- # Describe each IC to check if it's an adapter (has BaseInferenceComponentName)
1245
- local ic_detail
1246
- ic_detail=$(aws sagemaker describe-inference-component \
1247
- --inference-component-name "${ic_name}" \
1248
- --region "${AWS_REGION}" 2>/dev/null) || continue
1249
-
1250
- # Check if this IC has a BaseInferenceComponentName (adapter IC)
1251
- local base_ic
1252
- base_ic=$(echo "${ic_detail}" | jq -r '.Specification.BaseInferenceComponentName // empty' 2>/dev/null)
1253
-
1254
- if [ -z "${base_ic}" ]; then
1255
- # Not an adapter IC — skip
1362
+ # Delegate to Python for Bash 3.2 compatibility (no associative arrays needed).
1363
+ # Merges 3 data sources: local confs, deployed adapter ICs, and registry.
1364
+ python3 - "${SCRIPT_DIR}" "${PROJECT_NAME}" "${ENDPOINT_NAME:-}" "${AWS_REGION}" <<'ADAPTER_LIST_PY'
1365
+ import sys, os, json, subprocess, glob
1366
+
1367
+ script_dir, project_name, endpoint_name, region = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
1368
+
1369
+ # ── Data source 1: Local adapter confs ──
1370
+ adapters = {} # name {source, ic_name, technique, dataset}
1371
+ adapters_dir = os.path.join(script_dir, "adapters")
1372
+ if os.path.isdir(adapters_dir):
1373
+ for conf_path in sorted(glob.glob(os.path.join(adapters_dir, "*.conf"))):
1374
+ if os.path.basename(conf_path) == ".gitkeep":
1256
1375
  continue
1257
- fi
1258
-
1259
- # Extract status and artifact URL
1260
- local status
1261
- status=$(echo "${ic_detail}" | jq -r '.InferenceComponentStatus // "Unknown"' 2>/dev/null)
1262
-
1263
- local weights_url
1264
- weights_url=$(echo "${ic_detail}" | jq -r '.Specification.Container.ArtifactUrl // "N/A"' 2>/dev/null)
1265
-
1266
- # Derive display name (strip project prefix if present)
1267
- local display_name="${ic_name}"
1268
- if [[ "${ic_name}" == "${PROJECT_NAME}-adapter-"* ]]; then
1269
- display_name="${ic_name#${PROJECT_NAME}-adapter-}"
1270
- fi
1271
-
1272
- # Check ownership: is this adapter in our local do/adapters/*.conf?
1273
- local ownership=""
1274
- if echo "${local_adapters}" | grep -qw "${ic_name}"; then
1275
- ownership=""
1276
- else
1277
- ownership=" (external)"
1278
- fi
1279
-
1280
- # Check for tuning metadata in conf file
1281
- local tune_info=""
1282
- if [ -d "${SCRIPT_DIR}/adapters" ]; then
1283
- for conf_file in "${SCRIPT_DIR}"/adapters/*.conf; do
1284
- [ -f "${conf_file}" ] || continue
1285
- local conf_ic
1286
- conf_ic=$(grep "^export ADAPTER_IC_NAME=" "${conf_file}" 2>/dev/null | sed 's/^export ADAPTER_IC_NAME="//' | sed 's/"$//' || echo "")
1287
- if [ "${conf_ic}" = "${ic_name}" ]; then
1288
- local conf_technique
1289
- conf_technique=$(grep "^export ADAPTER_TUNE_TECHNIQUE=" "${conf_file}" 2>/dev/null | sed 's/^export ADAPTER_TUNE_TECHNIQUE="//' | sed 's/"$//' || echo "")
1290
- local conf_dataset
1291
- conf_dataset=$(grep "^export ADAPTER_TUNE_DATASET=" "${conf_file}" 2>/dev/null | sed 's/^export ADAPTER_TUNE_DATASET="//' | sed 's/"$//' || echo "")
1292
- if [ -n "${conf_technique}" ]; then
1293
- if [ -n "${conf_dataset}" ]; then
1294
- tune_info=" (from tune: ${conf_technique} / ${conf_dataset})"
1295
- else
1296
- tune_info=" (from tune: ${conf_technique})"
1297
- fi
1298
- fi
1299
- break
1300
- fi
1301
- done
1302
- fi
1303
-
1304
- output_lines="${output_lines}$(printf '%-14s%-12s%s%s%s' "${display_name}" "${status}" "${weights_url}" "${ownership}" "${tune_info}")\n"
1305
- found_adapters=$((found_adapters + 1))
1306
- done
1307
-
1308
- if [ "${found_adapters}" -eq 0 ]; then
1309
- echo "No adapters found on this endpoint."
1310
- echo ""
1311
- echo "Add one with: ./do/adapter add <name> --weights <s3-uri>"
1312
- fi
1313
-
1314
- if [ "${found_adapters}" -gt 0 ]; then
1315
- # ── Print table ───────────────────────────────────────────────────────
1316
- printf '%-14s%-12s%s\n' "NAME" "STATUS" "WEIGHTS"
1317
- echo -e "${output_lines}" | sed '$ { /^$/d; }'
1318
- fi
1319
-
1320
- # ── Show registry adapters (isAdapter=true) alongside local ones ──────
1321
- echo ""
1322
- echo "📦 Registry adapters:"
1323
- echo ""
1324
-
1325
- local registry_json
1326
- registry_json=$(python3 "${SCRIPT_DIR}/.register_helper.py" list-adapters \
1327
- --project-name "${PROJECT_NAME}" \
1328
- --region "${AWS_REGION}" 2>/dev/null || echo "")
1329
-
1330
- local registry_line
1331
- registry_line=$(echo "${registry_json}" | grep -E '^\{' | tail -1)
1332
-
1333
- if [ -z "${registry_line}" ]; then
1334
- echo " (could not query registry — check AWS credentials)"
1335
- return 0
1336
- fi
1337
-
1338
- local registry_count
1339
- registry_count=$(echo "${registry_line}" | python3 -c "import sys,json; data=json.loads(sys.stdin.read()); print(len(data.get('adapters',[])))" 2>/dev/null || echo "0")
1340
-
1341
- if [ "${registry_count}" -eq 0 ]; then
1342
- echo " (none found)"
1343
- return 0
1344
- fi
1345
-
1346
- printf ' %-10s%-12s%-14s%s\n' "VERSION" "TECHNIQUE" "CREATED" "PARENT MODEL"
1347
-
1348
- local ri=0
1349
- while [ "${ri}" -lt "${registry_count}" ]; do
1350
- local rv rt rc rp
1351
- rv=$(echo "${registry_line}" | python3 -c "import sys,json; data=json.loads(sys.stdin.read()); print(data['adapters'][${ri}].get('version','?'))" 2>/dev/null)
1352
- rt=$(echo "${registry_line}" | python3 -c "import sys,json; data=json.loads(sys.stdin.read()); print(data['adapters'][${ri}].get('tuneTechnique','?'))" 2>/dev/null)
1353
- rc=$(echo "${registry_line}" | python3 -c "import sys,json; data=json.loads(sys.stdin.read()); t=data['adapters'][${ri}].get('createdAt',''); print(t[:10] if t else '?')" 2>/dev/null)
1354
- rp=$(echo "${registry_line}" | python3 -c "import sys,json; data=json.loads(sys.stdin.read()); a=data['adapters'][${ri}].get('parentModelVersionArn',''); print(a.split('/')[-2]+'/'+a.split('/')[-1] if '/' in a else a[:40])" 2>/dev/null)
1355
-
1356
- printf ' %-10s%-12s%-14s%s\n' "v${rv}" "${rt}" "${rc}" "${rp}"
1357
- ri=$((ri + 1))
1358
- done
1359
-
1360
- echo ""
1361
- echo "Add from registry: ./do/adapter add <name> --from-registry [version-arn]"
1376
+ props = {}
1377
+ with open(conf_path) as f:
1378
+ for line in f:
1379
+ if line.startswith("export "):
1380
+ line = line[7:].strip()
1381
+ if "=" in line:
1382
+ k, v = line.split("=", 1)
1383
+ props[k] = v.strip('"').strip("'")
1384
+ name = props.get("ADAPTER_NAME", os.path.basename(conf_path).replace(".conf", ""))
1385
+ adapters[name] = {
1386
+ "source": props.get("ADAPTER_SOURCE", "s3"),
1387
+ "ic_name": props.get("ADAPTER_IC_NAME", ""),
1388
+ "technique": props.get("ADAPTER_TUNE_TECHNIQUE", props.get("ADAPTER_TECHNIQUE", "")),
1389
+ "dataset": props.get("ADAPTER_TUNE_DATASET", ""),
1390
+ "status": "not deployed",
1391
+ }
1392
+
1393
+ # ── Data source 2: Deployed adapter ICs ──
1394
+ if endpoint_name:
1395
+ try:
1396
+ result = subprocess.run(
1397
+ ["aws", "sagemaker", "list-inference-components",
1398
+ "--endpoint-name-equals", endpoint_name, "--region", region],
1399
+ capture_output=True, text=True, timeout=15)
1400
+ if result.returncode == 0:
1401
+ ic_data = json.loads(result.stdout)
1402
+ for ic in ic_data.get("InferenceComponents", []):
1403
+ ic_name = ic["InferenceComponentName"]
1404
+ ic_status = ic.get("InferenceComponentStatus", "Unknown")
1405
+ # Check if adapter (has BaseInferenceComponentName) via describe
1406
+ desc = subprocess.run(
1407
+ ["aws", "sagemaker", "describe-inference-component",
1408
+ "--inference-component-name", ic_name, "--region", region],
1409
+ capture_output=True, text=True, timeout=10)
1410
+ if desc.returncode == 0:
1411
+ detail = json.loads(desc.stdout)
1412
+ base_ic = detail.get("Specification", {}).get("BaseInferenceComponentName", "")
1413
+ if not base_ic:
1414
+ continue # Base IC, not adapter
1415
+ ic_status = detail.get("InferenceComponentStatus", ic_status)
1416
+ display = ic_name
1417
+ prefix = f"{project_name}-adapter-"
1418
+ if ic_name.startswith(prefix):
1419
+ display = ic_name[len(prefix):]
1420
+ if display in adapters:
1421
+ adapters[display]["status"] = ic_status
1422
+ else:
1423
+ adapters[display] = {"source": "external", "ic_name": ic_name,
1424
+ "technique": "", "dataset": "", "status": ic_status}
1425
+ except Exception:
1426
+ print("⚠️ Could not query endpoint — showing local confs only.", file=sys.stderr)
1427
+
1428
+ # ── Output ──
1429
+ if not adapters:
1430
+ print("No adapters found.")
1431
+ print("")
1432
+ print("Add one with: ./do/adapter add <name> --weights <s3-uri>")
1433
+ print(" ./do/adapter add <name> --from-hub <hf-repo-id>")
1434
+ print(" ./do/adapter add <name> --from-registry")
1435
+ sys.exit(0)
1436
+
1437
+ print(f"Adapters on endpoint: {endpoint_name or '<not deployed>'}")
1438
+ print("")
1439
+
1440
+ # Column widths
1441
+ max_n = max(len("NAME"), max(len(n) for n in adapters))
1442
+ max_s = max(len("SOURCE"), max(len(a["source"]) for a in adapters.values()))
1443
+ fmt = f" {{:<{max_n + 3}}}{{:<{max_s + 3}}}{{}} {{}}"
1444
+
1445
+ print(fmt.format("NAME", "SOURCE", "STATUS", ""))
1446
+ print(fmt.format("----", "------", "------", ""))
1447
+ for name in sorted(adapters):
1448
+ a = adapters[name]
1449
+ tune_info = ""
1450
+ if a["technique"]:
1451
+ tune_info = f"(tune: {a['technique']}" + (f" / {a['dataset']}" if a["dataset"] else "") + ")"
1452
+ print(fmt.format(name, a["source"], a["status"], tune_info))
1453
+
1454
+ print("")
1455
+ print("Add adapter: ./do/adapter add <name> --weights <s3-uri>")
1456
+ print("From registry: ./do/adapter add <name> --from-registry [version-arn]")
1457
+ ADAPTER_LIST_PY
1362
1458
  }
1363
1459
 
1364
1460
  _adapter_remove() {