@aws/ml-container-creator 0.2.0

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.
Files changed (143) hide show
  1. package/LICENSE +202 -0
  2. package/LICENSE-THIRD-PARTY +68620 -0
  3. package/NOTICE +2 -0
  4. package/README.md +106 -0
  5. package/bin/cli.js +365 -0
  6. package/config/defaults.json +32 -0
  7. package/config/presets/transformers-djl.json +26 -0
  8. package/config/presets/transformers-gpu.json +24 -0
  9. package/config/presets/transformers-lmi.json +27 -0
  10. package/package.json +129 -0
  11. package/servers/README.md +419 -0
  12. package/servers/base-image-picker/catalogs/model-servers.json +1191 -0
  13. package/servers/base-image-picker/catalogs/python-slim.json +38 -0
  14. package/servers/base-image-picker/catalogs/triton-backends.json +51 -0
  15. package/servers/base-image-picker/catalogs/triton.json +38 -0
  16. package/servers/base-image-picker/index.js +495 -0
  17. package/servers/base-image-picker/manifest.json +17 -0
  18. package/servers/base-image-picker/package.json +15 -0
  19. package/servers/hyperpod-cluster-picker/LICENSE +202 -0
  20. package/servers/hyperpod-cluster-picker/index.js +424 -0
  21. package/servers/hyperpod-cluster-picker/manifest.json +14 -0
  22. package/servers/hyperpod-cluster-picker/package.json +17 -0
  23. package/servers/instance-recommender/LICENSE +202 -0
  24. package/servers/instance-recommender/catalogs/instances.json +852 -0
  25. package/servers/instance-recommender/index.js +284 -0
  26. package/servers/instance-recommender/manifest.json +16 -0
  27. package/servers/instance-recommender/package.json +15 -0
  28. package/servers/lib/LICENSE +202 -0
  29. package/servers/lib/bedrock-client.js +160 -0
  30. package/servers/lib/custom-validators.js +46 -0
  31. package/servers/lib/dynamic-resolver.js +36 -0
  32. package/servers/lib/package.json +11 -0
  33. package/servers/lib/schemas/image-catalog.schema.json +185 -0
  34. package/servers/lib/schemas/instances.schema.json +124 -0
  35. package/servers/lib/schemas/manifest.schema.json +64 -0
  36. package/servers/lib/schemas/model-catalog.schema.json +91 -0
  37. package/servers/lib/schemas/regions.schema.json +26 -0
  38. package/servers/lib/schemas/triton-backends.schema.json +51 -0
  39. package/servers/model-picker/catalogs/jumpstart-public.json +66 -0
  40. package/servers/model-picker/catalogs/popular-diffusors.json +88 -0
  41. package/servers/model-picker/catalogs/popular-transformers.json +226 -0
  42. package/servers/model-picker/index.js +1693 -0
  43. package/servers/model-picker/manifest.json +18 -0
  44. package/servers/model-picker/package.json +20 -0
  45. package/servers/region-picker/LICENSE +202 -0
  46. package/servers/region-picker/catalogs/regions.json +263 -0
  47. package/servers/region-picker/index.js +230 -0
  48. package/servers/region-picker/manifest.json +16 -0
  49. package/servers/region-picker/package.json +15 -0
  50. package/src/app.js +1007 -0
  51. package/src/copy-tpl.js +77 -0
  52. package/src/lib/accelerator-validator.js +39 -0
  53. package/src/lib/asset-manager.js +385 -0
  54. package/src/lib/aws-profile-parser.js +181 -0
  55. package/src/lib/bootstrap-command-handler.js +1647 -0
  56. package/src/lib/bootstrap-config.js +238 -0
  57. package/src/lib/ci-register-helpers.js +124 -0
  58. package/src/lib/ci-report-helpers.js +158 -0
  59. package/src/lib/ci-stage-helpers.js +268 -0
  60. package/src/lib/cli-handler.js +529 -0
  61. package/src/lib/comment-generator.js +544 -0
  62. package/src/lib/community-reports-validator.js +91 -0
  63. package/src/lib/config-manager.js +2106 -0
  64. package/src/lib/configuration-exporter.js +204 -0
  65. package/src/lib/configuration-manager.js +695 -0
  66. package/src/lib/configuration-matcher.js +221 -0
  67. package/src/lib/cpu-validator.js +36 -0
  68. package/src/lib/cuda-validator.js +57 -0
  69. package/src/lib/deployment-config-resolver.js +103 -0
  70. package/src/lib/deployment-entry-schema.js +125 -0
  71. package/src/lib/deployment-registry.js +598 -0
  72. package/src/lib/docker-introspection-validator.js +51 -0
  73. package/src/lib/engine-prefix-resolver.js +60 -0
  74. package/src/lib/huggingface-client.js +172 -0
  75. package/src/lib/key-value-parser.js +37 -0
  76. package/src/lib/known-flags-validator.js +200 -0
  77. package/src/lib/manifest-cli.js +280 -0
  78. package/src/lib/mcp-client.js +303 -0
  79. package/src/lib/mcp-command-handler.js +532 -0
  80. package/src/lib/neuron-validator.js +80 -0
  81. package/src/lib/parameter-schema-validator.js +284 -0
  82. package/src/lib/prompt-runner.js +1349 -0
  83. package/src/lib/prompts.js +1138 -0
  84. package/src/lib/registry-command-handler.js +519 -0
  85. package/src/lib/registry-loader.js +198 -0
  86. package/src/lib/rocm-validator.js +80 -0
  87. package/src/lib/schema-validator.js +157 -0
  88. package/src/lib/sensitive-redactor.js +59 -0
  89. package/src/lib/template-engine.js +156 -0
  90. package/src/lib/template-manager.js +341 -0
  91. package/src/lib/validation-engine.js +314 -0
  92. package/src/prompt-adapter.js +63 -0
  93. package/templates/Dockerfile +300 -0
  94. package/templates/IAM_PERMISSIONS.md +84 -0
  95. package/templates/MIGRATION.md +488 -0
  96. package/templates/PROJECT_README.md +439 -0
  97. package/templates/TEMPLATE_SYSTEM.md +243 -0
  98. package/templates/buildspec.yml +64 -0
  99. package/templates/code/chat_template.jinja +1 -0
  100. package/templates/code/flask/gunicorn_config.py +35 -0
  101. package/templates/code/flask/wsgi.py +10 -0
  102. package/templates/code/model_handler.py +387 -0
  103. package/templates/code/serve +300 -0
  104. package/templates/code/serve.py +175 -0
  105. package/templates/code/serving.properties +105 -0
  106. package/templates/code/start_server.py +39 -0
  107. package/templates/code/start_server.sh +39 -0
  108. package/templates/diffusors/Dockerfile +72 -0
  109. package/templates/diffusors/patch_image_api.py +35 -0
  110. package/templates/diffusors/serve +115 -0
  111. package/templates/diffusors/start_server.sh +114 -0
  112. package/templates/do/.gitkeep +1 -0
  113. package/templates/do/README.md +541 -0
  114. package/templates/do/build +83 -0
  115. package/templates/do/ci +681 -0
  116. package/templates/do/clean +811 -0
  117. package/templates/do/config +260 -0
  118. package/templates/do/deploy +1560 -0
  119. package/templates/do/export +306 -0
  120. package/templates/do/logs +319 -0
  121. package/templates/do/manifest +12 -0
  122. package/templates/do/push +119 -0
  123. package/templates/do/register +580 -0
  124. package/templates/do/run +113 -0
  125. package/templates/do/submit +417 -0
  126. package/templates/do/test +1147 -0
  127. package/templates/hyperpod/configmap.yaml +24 -0
  128. package/templates/hyperpod/deployment.yaml +71 -0
  129. package/templates/hyperpod/pvc.yaml +42 -0
  130. package/templates/hyperpod/service.yaml +17 -0
  131. package/templates/nginx-diffusors.conf +74 -0
  132. package/templates/nginx-predictors.conf +47 -0
  133. package/templates/nginx-tensorrt.conf +74 -0
  134. package/templates/requirements.txt +61 -0
  135. package/templates/sample_model/test_inference.py +123 -0
  136. package/templates/sample_model/train_abalone.py +252 -0
  137. package/templates/test/test_endpoint.sh +79 -0
  138. package/templates/test/test_local_image.sh +80 -0
  139. package/templates/test/test_model_handler.py +180 -0
  140. package/templates/triton/Dockerfile +128 -0
  141. package/templates/triton/config.pbtxt +163 -0
  142. package/templates/triton/model.py +130 -0
  143. package/templates/triton/requirements.txt +11 -0
@@ -0,0 +1,598 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Deployment Registry
6
+ *
7
+ * Core data-access module for the deployment registry system.
8
+ * Handles CRUD, search, import/export, and schema validation
9
+ * for deployment entries stored as JSON files.
10
+ *
11
+ * Registry file format:
12
+ * {
13
+ * "schemaVersion": "2026-03-20",
14
+ * "entries": [ ...Deployment_Entry objects ]
15
+ * }
16
+ */
17
+
18
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
19
+ import { dirname } from 'node:path'
20
+ import { createHash } from 'node:crypto'
21
+ import Ajv from 'ajv'
22
+ import { minimatch } from 'minimatch'
23
+ import deploymentEntrySchema from './deployment-entry-schema.js'
24
+
25
+ const CURRENT_SCHEMA_VERSION = '2026-03-20'
26
+
27
+ export default class DeploymentRegistry {
28
+ /**
29
+ * @param {string} registryPath - Absolute path to the registry JSON file
30
+ */
31
+ constructor(registryPath) {
32
+ this.registryPath = registryPath
33
+ this._ajv = new Ajv({ allErrors: true, strict: false })
34
+ this._validate = this._ajv.compile(deploymentEntrySchema)
35
+ }
36
+
37
+ /**
38
+ * Read the registry file and return the entries array.
39
+ *
40
+ * Handles:
41
+ * - Missing file → return []
42
+ * - Invalid JSON → throw with descriptive message
43
+ * - Missing/unrecognized schemaVersion → console.warn + best-effort
44
+ *
45
+ * @returns {Array<Object>} entries array
46
+ */
47
+ _readRegistry() {
48
+ if (!existsSync(this.registryPath)) {
49
+ return []
50
+ }
51
+
52
+ const raw = readFileSync(this.registryPath, 'utf8')
53
+
54
+ let data
55
+ try {
56
+ data = JSON.parse(raw)
57
+ } catch (err) {
58
+ throw new Error(`Invalid JSON in registry file ${this.registryPath}: ${err.message}`)
59
+ }
60
+
61
+ const migrated = this._migrateIfNeeded(data)
62
+
63
+ if (!migrated.schemaVersion) {
64
+ console.warn(`Warning: Registry file ${this.registryPath} has no schemaVersion. Attempting best-effort read.`)
65
+ } else if (migrated.schemaVersion !== CURRENT_SCHEMA_VERSION) {
66
+ console.warn(`Warning: Registry file ${this.registryPath} has unrecognized schemaVersion "${migrated.schemaVersion}". Attempting best-effort read.`)
67
+ }
68
+
69
+ return Array.isArray(migrated.entries) ? migrated.entries : []
70
+ }
71
+
72
+ /**
73
+ * Write entries to the registry file wrapped in a versioned envelope.
74
+ *
75
+ * Creates parent directories if they don't exist.
76
+ * Uses 2-space indentation and a trailing newline.
77
+ *
78
+ * @param {Array<Object>} entries - The entries array to write
79
+ */
80
+ _writeRegistry(entries) {
81
+ const dir = dirname(this.registryPath)
82
+ if (!existsSync(dir)) {
83
+ mkdirSync(dir, { recursive: true })
84
+ }
85
+
86
+ const envelope = {
87
+ schemaVersion: CURRENT_SCHEMA_VERSION,
88
+ entries
89
+ }
90
+
91
+ writeFileSync(this.registryPath, JSON.stringify(envelope, null, 2) + '\n')
92
+ }
93
+
94
+ /**
95
+ * Generate an 8-character hex ID from a hash of the entry's timestamp
96
+ * and deploymentConfig. Retries with random entropy on collision.
97
+ *
98
+ * @param {Object} entry - The deployment entry
99
+ * @param {Array<Object>} [existingEntries] - Existing entries to check for collisions
100
+ * @returns {string} 8-character hex string
101
+ */
102
+ _generateId(entry, existingEntries = []) {
103
+ const baseInput = `${entry.timestamp}:${entry.deployment.deploymentConfig}`
104
+ let id = createHash('sha256').update(baseInput).digest('hex').slice(0, 8)
105
+
106
+ const existingIds = new Set(existingEntries.map(e => e.id))
107
+
108
+ while (existingIds.has(id)) {
109
+ const entropy = Math.random().toString(36).slice(2)
110
+ id = createHash('sha256').update(baseInput + entropy).digest('hex').slice(0, 8)
111
+ }
112
+
113
+ return id
114
+ }
115
+
116
+ /**
117
+ * Validate an entry against the deployment entry schema using ajv.
118
+ *
119
+ * @param {Object} entry - The entry to validate
120
+ * @returns {{ valid: boolean, errors: Array|null }} Validation result
121
+ */
122
+ _validateEntry(entry) {
123
+ const valid = this._validate(entry)
124
+ return {
125
+ valid: !!valid,
126
+ errors: valid ? null : this._validate.errors
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Migrate old registry formats to the current versioned envelope.
132
+ *
133
+ * Handles:
134
+ * - Plain arrays (legacy format) → wrap in envelope
135
+ * - Already-enveloped data → return as-is
136
+ *
137
+ * @param {*} data - Parsed JSON data from the registry file
138
+ * @returns {{ schemaVersion: string|undefined, entries: Array }}
139
+ */
140
+ _migrateIfNeeded(data) {
141
+ // Legacy format: plain array of entries
142
+ if (Array.isArray(data)) {
143
+ return {
144
+ schemaVersion: CURRENT_SCHEMA_VERSION,
145
+ entries: data
146
+ }
147
+ }
148
+
149
+ // Already an envelope with schemaVersion and entries
150
+ if (data && typeof data === 'object' && 'entries' in data) {
151
+ return data
152
+ }
153
+
154
+ // Unknown format — wrap in envelope with empty entries
155
+ return {
156
+ schemaVersion: undefined,
157
+ entries: []
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Add a new deployment entry to the registry.
163
+ *
164
+ * Validates the entry against the schema, generates a unique ID,
165
+ * reads existing entries, appends the new entry, and writes back.
166
+ *
167
+ * @param {Object} entry - The deployment entry (without id)
168
+ * @returns {string} The generated 8-character hex ID
169
+ * @throws {Error} If the entry fails schema validation
170
+ */
171
+ add(entry) {
172
+ const existingEntries = this._readRegistry()
173
+ const id = this._generateId(entry, existingEntries)
174
+
175
+ const fullEntry = { ...entry, id }
176
+
177
+ const { valid, errors } = this._validateEntry(fullEntry)
178
+ if (!valid) {
179
+ const details = errors.map(e => `${e.instancePath || '/'} ${e.message}`).join(', ')
180
+ throw new Error(`Validation failed: ${details}`)
181
+ }
182
+
183
+ existingEntries.push(fullEntry)
184
+ this._writeRegistry(existingEntries)
185
+
186
+ return id
187
+ }
188
+
189
+ /**
190
+ * Get a deployment entry by its ID.
191
+ *
192
+ * @param {string} id - The entry ID to look up
193
+ * @returns {Object|null} The matching entry, or null if not found
194
+ */
195
+ get(id) {
196
+ const entries = this._readRegistry()
197
+ return entries.find(e => e.id === id) || null
198
+ }
199
+
200
+ /**
201
+ * Remove a deployment entry by its ID.
202
+ *
203
+ * @param {string} id - The entry ID to remove
204
+ * @returns {boolean} true if an entry was removed, false if not found
205
+ */
206
+ remove(id) {
207
+ const entries = this._readRegistry()
208
+ const filtered = entries.filter(e => e.id !== id)
209
+
210
+ if (filtered.length === entries.length) {
211
+ return false
212
+ }
213
+
214
+ this._writeRegistry(filtered)
215
+ return true
216
+ }
217
+
218
+ /**
219
+ * Check whether an entry matches all provided filters (AND logic).
220
+ *
221
+ * Supported filter keys:
222
+ * - backend: exact match on deployment.backend
223
+ * - architecture: exact match on deployment.architecture
224
+ * - model: substring match on model.modelName (case-insensitive)
225
+ * - 'instance-type': exact match on infrastructure.instanceType
226
+ * - status: exact match on status
227
+ *
228
+ * @param {Object} entry - The deployment entry to test
229
+ * @param {Object} filters - Key-value pairs of filter criteria
230
+ * @returns {boolean} true if the entry matches all filters
231
+ */
232
+ _matchesFilters(entry, filters) {
233
+ if (!filters || typeof filters !== 'object') {
234
+ return true
235
+ }
236
+
237
+ for (const [key, value] of Object.entries(filters)) {
238
+ if (value === undefined || value === null) {
239
+ continue
240
+ }
241
+
242
+ switch (key) {
243
+ case 'backend':
244
+ if (entry.deployment?.backend !== value) return false
245
+ break
246
+ case 'architecture':
247
+ if (entry.deployment?.architecture !== value) return false
248
+ break
249
+ case 'model':
250
+ if (!entry.model?.modelName?.toLowerCase().includes(value.toLowerCase())) return false
251
+ break
252
+ case 'instance-type':
253
+ if (entry.infrastructure?.instanceType !== value) return false
254
+ break
255
+ case 'status':
256
+ if (entry.status !== value) return false
257
+ break
258
+ default:
259
+ break
260
+ }
261
+ }
262
+
263
+ return true
264
+ }
265
+
266
+ /**
267
+ * List entries from the registry, optionally filtered.
268
+ *
269
+ * Reads all entries and returns those matching every provided filter
270
+ * using AND logic.
271
+ *
272
+ * @param {Object} [filters] - Optional filter criteria
273
+ * @returns {Array<Object>} Matching entries
274
+ */
275
+ list(filters) {
276
+ const entries = this._readRegistry()
277
+
278
+ if (!filters || Object.keys(filters).length === 0) {
279
+ return entries
280
+ }
281
+
282
+ return entries.filter(entry => this._matchesFilters(entry, filters))
283
+ }
284
+
285
+ /**
286
+ * Search entries using glob-based model matching and standard filters.
287
+ *
288
+ * Similar to list(), but the `model` filter uses glob pattern matching
289
+ * (via minimatch) instead of substring matching.
290
+ *
291
+ * @param {Object} [query] - Search criteria; `model` supports glob patterns
292
+ * @returns {Array<Object>} Matching entries
293
+ */
294
+ search(query) {
295
+ const entries = this._readRegistry()
296
+
297
+ if (!query || Object.keys(query).length === 0) {
298
+ return entries
299
+ }
300
+
301
+ return entries.filter(entry => {
302
+ for (const [key, value] of Object.entries(query)) {
303
+ if (value === undefined || value === null) {
304
+ continue
305
+ }
306
+
307
+ switch (key) {
308
+ case 'model':
309
+ if (!entry.model?.modelName || !minimatch(entry.model.modelName, value)) return false
310
+ break
311
+ case 'backend':
312
+ if (entry.deployment?.backend !== value) return false
313
+ break
314
+ case 'architecture':
315
+ if (entry.deployment?.architecture !== value) return false
316
+ break
317
+ case 'instance-type':
318
+ if (entry.infrastructure?.instanceType !== value) return false
319
+ break
320
+ case 'status':
321
+ if (entry.status !== value) return false
322
+ break
323
+ default:
324
+ break
325
+ }
326
+ }
327
+
328
+ return true
329
+ })
330
+ }
331
+
332
+ /**
333
+ * Strip sensitive fields from a deployment entry.
334
+ *
335
+ * Returns a deep-cloned copy with the following fields removed:
336
+ * - infrastructure.roleArn
337
+ * - infrastructure.region
338
+ * - configuration.parameters.HF_TOKEN
339
+ * - configuration.parameters.NGC_API_KEY
340
+ *
341
+ * The original entry is not mutated.
342
+ *
343
+ * @param {Object} entry - The deployment entry to sanitize
344
+ * @returns {Object} A sanitized deep copy of the entry
345
+ */
346
+ _stripSensitiveFields(entry) {
347
+ const copy = JSON.parse(JSON.stringify(entry))
348
+
349
+ if (copy.infrastructure) {
350
+ delete copy.infrastructure.roleArn
351
+ delete copy.infrastructure.region
352
+ }
353
+
354
+ if (copy.configuration?.parameters) {
355
+ delete copy.configuration.parameters.HF_TOKEN
356
+ delete copy.configuration.parameters.NGC_API_KEY
357
+ }
358
+
359
+ return copy
360
+ }
361
+
362
+ /**
363
+ * Export deployment entries in the standard Export_Format.
364
+ *
365
+ * If an id is provided, exports only that single entry.
366
+ * Otherwise exports all entries matching the status filter.
367
+ * By default, only entries with status "success" are exported.
368
+ * All exported entries have sensitive fields stripped.
369
+ *
370
+ * @param {string|null} [id] - Optional entry ID to export a single entry
371
+ * @param {Object} [options] - Export options
372
+ * @param {string} [options.status] - Status filter override (default: 'success')
373
+ * @returns {Object} Export_Format object with version, exportedAt, exportedBy, entries
374
+ */
375
+ exportEntries(id, options = {}) {
376
+ const entries = this._readRegistry()
377
+ const statusFilter = options.status || 'success'
378
+
379
+ let filtered
380
+ if (id) {
381
+ const entry = entries.find(e => e.id === id)
382
+ filtered = entry ? [entry] : []
383
+ } else {
384
+ filtered = entries.filter(e => e.status === statusFilter)
385
+ }
386
+
387
+ const sanitized = filtered.map(e => this._stripSensitiveFields(e))
388
+
389
+ return {
390
+ version: '1.0',
391
+ exportedAt: new Date().toISOString(),
392
+ exportedBy: 'anonymous',
393
+ entries: sanitized
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Reconstruct CLI flags from a stored deployment entry.
399
+ * Delegates to the standalone reconstructReplayFlags function.
400
+ *
401
+ * @param {Object} entry - The deployment entry
402
+ * @param {Object} [overrides] - User-provided CLI overrides
403
+ * @returns {Object} CLI flag key-value pairs
404
+ */
405
+ reconstructReplayFlags(entry, overrides = {}) {
406
+ return reconstructReplayFlags(entry, overrides)
407
+ }
408
+
409
+ /**
410
+ * Import deployment entries from an Export_Format JSON object.
411
+ *
412
+ * Validates the import format, sets metadata on each entry,
413
+ * detects conflicts, and applies the specified resolution strategy.
414
+ *
415
+ * Conflict detection matches on: modelName, backend, instanceType,
416
+ * and parameters (deep equality via JSON.stringify).
417
+ *
418
+ * Strategies:
419
+ * - 'skip' (default): skip conflicting entries
420
+ * - 'merge': keep both existing and imported entries
421
+ * - 'replace': overwrite existing entries with imported ones
422
+ *
423
+ * @param {Object} json - Parsed Export_Format JSON object
424
+ * @param {string} [strategy='skip'] - Conflict resolution strategy
425
+ * @param {string} [filename='unknown'] - Source filename for metadata
426
+ * @returns {{ added: number, skipped: number, conflicts: number }}
427
+ * @throws {Error} If the import format is invalid
428
+ */
429
+ importEntries(json, strategy = 'skip', filename = 'unknown') {
430
+ if (!json || typeof json !== 'object' || !json.version || !Array.isArray(json.entries)) {
431
+ throw new Error('Invalid export format: missing required "version" or "entries" fields')
432
+ }
433
+
434
+ const existingEntries = this._readRegistry()
435
+ let added = 0
436
+ let skipped = 0
437
+ let conflicts = 0
438
+
439
+ for (const importedEntry of json.entries) {
440
+ const entry = JSON.parse(JSON.stringify(importedEntry))
441
+
442
+ if (!entry.metadata) {
443
+ entry.metadata = {}
444
+ }
445
+ entry.metadata.source = 'imported'
446
+ entry.metadata.importedFrom = filename
447
+
448
+ const isConflict = existingEntries.some(existing =>
449
+ existing.model?.modelName === entry.model?.modelName &&
450
+ existing.deployment?.backend === entry.deployment?.backend &&
451
+ existing.infrastructure?.instanceType === entry.infrastructure?.instanceType &&
452
+ JSON.stringify(existing.configuration?.parameters) === JSON.stringify(entry.configuration?.parameters)
453
+ )
454
+
455
+ if (isConflict) {
456
+ conflicts++
457
+ if (strategy === 'merge') {
458
+ const id = this._generateId(entry, existingEntries)
459
+ entry.id = id
460
+ existingEntries.push(entry)
461
+ } else if (strategy === 'replace') {
462
+ const conflictIndex = existingEntries.findIndex(existing =>
463
+ existing.model?.modelName === entry.model?.modelName &&
464
+ existing.deployment?.backend === entry.deployment?.backend &&
465
+ existing.infrastructure?.instanceType === entry.infrastructure?.instanceType &&
466
+ JSON.stringify(existing.configuration?.parameters) === JSON.stringify(entry.configuration?.parameters)
467
+ )
468
+ if (conflictIndex !== -1) {
469
+ entry.id = existingEntries[conflictIndex].id
470
+ existingEntries[conflictIndex] = entry
471
+ }
472
+ } else {
473
+ skipped++
474
+ }
475
+ } else {
476
+ const id = this._generateId(entry, existingEntries)
477
+ entry.id = id
478
+ existingEntries.push(entry)
479
+ added++
480
+ }
481
+ }
482
+
483
+ this._writeRegistry(existingEntries)
484
+
485
+ return { added, skipped, conflicts }
486
+ }
487
+
488
+ }
489
+
490
+ /**
491
+ * Reconstruct CLI flags from a stored deployment entry.
492
+ *
493
+ * Maps entry fields to their corresponding CLI flags:
494
+ * - deployment.deploymentConfig → --deployment-config
495
+ * - model.modelName → --model-name
496
+ * - infrastructure.instanceType → --instance-type
497
+ * - infrastructure.region → --region
498
+ * - model.modelFormat → --model-format (omitted for transformers architecture)
499
+ *
500
+ * Null/undefined fields are omitted so the generator prompts for them.
501
+ * User overrides take precedence over stored entry values.
502
+ *
503
+ * @param {Object} entry - The deployment entry
504
+ * @param {Object} [overrides={}] - User-provided CLI overrides (keyed by flag name, e.g. '--model-name')
505
+ * @returns {Object} CLI flag key-value pairs
506
+ */
507
+ export function reconstructReplayFlags(entry, overrides = {}) {
508
+ const flags = {}
509
+
510
+ const mappings = [
511
+ { field: entry?.deployment?.deploymentConfig, flag: '--deployment-config' },
512
+ { field: entry?.model?.modelName, flag: '--model-name' },
513
+ { field: entry?.infrastructure?.instanceType, flag: '--instance-type' },
514
+ { field: entry?.infrastructure?.region, flag: '--region' },
515
+ ]
516
+
517
+ for (const { field, flag } of mappings) {
518
+ if (field != null) {
519
+ flags[flag] = field
520
+ }
521
+ }
522
+
523
+ // Omit --model-format for transformers architecture
524
+ const isTransformers = entry?.deployment?.architecture === 'transformers'
525
+ if (!isTransformers && entry?.model?.modelFormat != null) {
526
+ flags['--model-format'] = entry.model.modelFormat
527
+ }
528
+
529
+ // Apply user overrides with higher precedence
530
+ for (const [key, value] of Object.entries(overrides)) {
531
+ if (value != null) {
532
+ flags[key] = value
533
+ }
534
+ }
535
+
536
+ return flags
537
+ }
538
+
539
+ /**
540
+ * System environment variables excluded from http architecture deployments.
541
+ * These are standard system/Python vars that are not relevant to the
542
+ * deployment configuration.
543
+ */
544
+ const HTTP_SYSTEM_VARS = new Set([
545
+ 'PATH',
546
+ 'PYTHONPATH',
547
+ 'SAGEMAKER_BIND_TO_PORT',
548
+ 'LANG',
549
+ 'GPG_KEY',
550
+ 'PYTHON_VERSION',
551
+ 'PYTHON_PIP_VERSION',
552
+ 'PYTHON_SETUPTOOLS_VERSION',
553
+ 'PYTHON_GET_PIP_URL',
554
+ 'PYTHON_GET_PIP_SHA256',
555
+ ])
556
+
557
+ /**
558
+ * Filter environment variables for transformer architecture deployments.
559
+ *
560
+ * Keeps only vars whose key starts with the given engine prefix
561
+ * (e.g. 'VLLM_', 'SGLANG_'), plus HF_TOKEN and HF_MODEL_ID if present.
562
+ *
563
+ * @param {Object} envVars - Key-value pairs of environment variables
564
+ * @param {string} enginePrefix - Engine prefix string (e.g. 'VLLM_')
565
+ * @returns {Object} Filtered key-value pairs
566
+ */
567
+ export function filterTransformerEnvVars(envVars, enginePrefix) {
568
+ const result = {}
569
+
570
+ for (const [key, value] of Object.entries(envVars)) {
571
+ if (key.startsWith(enginePrefix) || key === 'HF_TOKEN' || key === 'HF_MODEL_ID') {
572
+ result[key] = value
573
+ }
574
+ }
575
+
576
+ return result
577
+ }
578
+
579
+ /**
580
+ * Filter environment variables for http architecture deployments.
581
+ *
582
+ * Excludes known system variables (PATH, PYTHONPATH, etc.) and
583
+ * returns everything else.
584
+ *
585
+ * @param {Object} envVars - Key-value pairs of environment variables
586
+ * @returns {Object} Filtered key-value pairs with system vars removed
587
+ */
588
+ export function filterHttpEnvVars(envVars) {
589
+ const result = {}
590
+
591
+ for (const [key, value] of Object.entries(envVars)) {
592
+ if (!HTTP_SYSTEM_VARS.has(key)) {
593
+ result[key] = value
594
+ }
595
+ }
596
+
597
+ return result
598
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Docker Introspection Validator Strategy (Opt-in)
3
+ *
4
+ * Validates environment variables by introspecting Docker images.
5
+ * This is an experimental strategy that requires Docker to be available.
6
+ *
7
+ * Requirements: 13.11, 13.18
8
+ */
9
+ export default class DockerIntrospectionValidator {
10
+ /**
11
+ * Create a new DockerIntrospectionValidator.
12
+ */
13
+ constructor() {
14
+ this.name = 'docker-introspection';
15
+ }
16
+
17
+ /**
18
+ * Validate environment variables using Docker introspection.
19
+ *
20
+ * Note: This is an experimental feature and not tested in CI/CD.
21
+ * It requires Docker to be available and the framework image to be pullable.
22
+ *
23
+ * @param {string} framework - Framework name
24
+ * @param {string} version - Framework version
25
+ * @param {Object} envVars - Environment variables to validate
26
+ * @returns {Object} ValidationResult
27
+ * @returns {Array<Object>} ValidationResult.warnings - Warning messages
28
+ * @returns {Array<Object>} ValidationResult.errors - Error messages
29
+ */
30
+ async validate(_framework, _version, _envVars) {
31
+ const warnings = [];
32
+ const errors = [];
33
+
34
+ // Add experimental warning
35
+ warnings.push({
36
+ key: null,
37
+ message: 'Docker introspection validation is experimental and not tested in CI/CD'
38
+ });
39
+
40
+ // Docker introspection implementation would go here
41
+ // This is a placeholder for the opt-in experimental feature
42
+ // Actual implementation would:
43
+ // 1. Pull the framework Docker image
44
+ // 2. Run a container with the env vars
45
+ // 3. Check if the container starts successfully
46
+ // 4. Parse any error messages from the container logs
47
+
48
+ // For now, just return the experimental warning
49
+ return { warnings, errors };
50
+ }
51
+ }
@@ -0,0 +1,60 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Engine Prefix Resolver
6
+ *
7
+ * Maps model server names to their engine-specific environment variable
8
+ * prefixes. When propagating --server-env values to the Dockerfile and
9
+ * do/config templates, the resolver prepends the appropriate prefix so
10
+ * users don't need to know internal prefix conventions.
11
+ *
12
+ * Requirements: 4.6
13
+ */
14
+
15
+ /**
16
+ * Engine-to-prefix mapping for server environment variables.
17
+ * Engines not listed here (flask, fastapi) pass keys through unchanged.
18
+ */
19
+ export const ENGINE_PREFIX_MAP = {
20
+ 'vllm': 'VLLM_',
21
+ 'vllm-omni': 'VLLM_OMNI_',
22
+ 'sglang': 'SGLANG_',
23
+ 'tensorrt-llm': 'TRTLLM_',
24
+ 'lmi': 'LMI_',
25
+ 'djl': 'DJL_'
26
+ }
27
+
28
+ /**
29
+ * Resolve the prefixed key for a given engine and user-provided key.
30
+ * If the engine has a defined prefix, prepends it to the key.
31
+ * If the engine has no prefix (flask, fastapi, or unknown), returns the key unchanged.
32
+ *
33
+ * @param {string} engine - The model server engine name (e.g., 'vllm', 'flask')
34
+ * @param {string} key - The user-provided environment variable key
35
+ * @returns {string} The resolved key with engine prefix applied (or unchanged)
36
+ */
37
+ export function resolvePrefix(engine, key) {
38
+ const prefix = ENGINE_PREFIX_MAP[engine]
39
+ if (prefix) {
40
+ return `${prefix}${key}`
41
+ }
42
+ return key
43
+ }
44
+
45
+ /**
46
+ * Resolve prefixed keys for a batch of server environment variables.
47
+ * Returns a new object with all keys prefixed according to the engine mapping.
48
+ *
49
+ * @param {string} engine - The model server engine name
50
+ * @param {Object<string, string>} serverEnvVars - Map of user-provided key-value pairs
51
+ * @returns {Object<string, string>} New object with prefixed keys and original values
52
+ */
53
+ export function resolvePrefixedEnvVars(engine, serverEnvVars) {
54
+ const result = {}
55
+ for (const [key, value] of Object.entries(serverEnvVars)) {
56
+ const prefixedKey = resolvePrefix(engine, key)
57
+ result[prefixedKey] = value
58
+ }
59
+ return result
60
+ }