@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.
- package/LICENSE +202 -0
- package/LICENSE-THIRD-PARTY +68620 -0
- package/NOTICE +2 -0
- package/README.md +106 -0
- package/bin/cli.js +365 -0
- package/config/defaults.json +32 -0
- package/config/presets/transformers-djl.json +26 -0
- package/config/presets/transformers-gpu.json +24 -0
- package/config/presets/transformers-lmi.json +27 -0
- package/package.json +129 -0
- package/servers/README.md +419 -0
- package/servers/base-image-picker/catalogs/model-servers.json +1191 -0
- package/servers/base-image-picker/catalogs/python-slim.json +38 -0
- package/servers/base-image-picker/catalogs/triton-backends.json +51 -0
- package/servers/base-image-picker/catalogs/triton.json +38 -0
- package/servers/base-image-picker/index.js +495 -0
- package/servers/base-image-picker/manifest.json +17 -0
- package/servers/base-image-picker/package.json +15 -0
- package/servers/hyperpod-cluster-picker/LICENSE +202 -0
- package/servers/hyperpod-cluster-picker/index.js +424 -0
- package/servers/hyperpod-cluster-picker/manifest.json +14 -0
- package/servers/hyperpod-cluster-picker/package.json +17 -0
- package/servers/instance-recommender/LICENSE +202 -0
- package/servers/instance-recommender/catalogs/instances.json +852 -0
- package/servers/instance-recommender/index.js +284 -0
- package/servers/instance-recommender/manifest.json +16 -0
- package/servers/instance-recommender/package.json +15 -0
- package/servers/lib/LICENSE +202 -0
- package/servers/lib/bedrock-client.js +160 -0
- package/servers/lib/custom-validators.js +46 -0
- package/servers/lib/dynamic-resolver.js +36 -0
- package/servers/lib/package.json +11 -0
- package/servers/lib/schemas/image-catalog.schema.json +185 -0
- package/servers/lib/schemas/instances.schema.json +124 -0
- package/servers/lib/schemas/manifest.schema.json +64 -0
- package/servers/lib/schemas/model-catalog.schema.json +91 -0
- package/servers/lib/schemas/regions.schema.json +26 -0
- package/servers/lib/schemas/triton-backends.schema.json +51 -0
- package/servers/model-picker/catalogs/jumpstart-public.json +66 -0
- package/servers/model-picker/catalogs/popular-diffusors.json +88 -0
- package/servers/model-picker/catalogs/popular-transformers.json +226 -0
- package/servers/model-picker/index.js +1693 -0
- package/servers/model-picker/manifest.json +18 -0
- package/servers/model-picker/package.json +20 -0
- package/servers/region-picker/LICENSE +202 -0
- package/servers/region-picker/catalogs/regions.json +263 -0
- package/servers/region-picker/index.js +230 -0
- package/servers/region-picker/manifest.json +16 -0
- package/servers/region-picker/package.json +15 -0
- package/src/app.js +1007 -0
- package/src/copy-tpl.js +77 -0
- package/src/lib/accelerator-validator.js +39 -0
- package/src/lib/asset-manager.js +385 -0
- package/src/lib/aws-profile-parser.js +181 -0
- package/src/lib/bootstrap-command-handler.js +1647 -0
- package/src/lib/bootstrap-config.js +238 -0
- package/src/lib/ci-register-helpers.js +124 -0
- package/src/lib/ci-report-helpers.js +158 -0
- package/src/lib/ci-stage-helpers.js +268 -0
- package/src/lib/cli-handler.js +529 -0
- package/src/lib/comment-generator.js +544 -0
- package/src/lib/community-reports-validator.js +91 -0
- package/src/lib/config-manager.js +2106 -0
- package/src/lib/configuration-exporter.js +204 -0
- package/src/lib/configuration-manager.js +695 -0
- package/src/lib/configuration-matcher.js +221 -0
- package/src/lib/cpu-validator.js +36 -0
- package/src/lib/cuda-validator.js +57 -0
- package/src/lib/deployment-config-resolver.js +103 -0
- package/src/lib/deployment-entry-schema.js +125 -0
- package/src/lib/deployment-registry.js +598 -0
- package/src/lib/docker-introspection-validator.js +51 -0
- package/src/lib/engine-prefix-resolver.js +60 -0
- package/src/lib/huggingface-client.js +172 -0
- package/src/lib/key-value-parser.js +37 -0
- package/src/lib/known-flags-validator.js +200 -0
- package/src/lib/manifest-cli.js +280 -0
- package/src/lib/mcp-client.js +303 -0
- package/src/lib/mcp-command-handler.js +532 -0
- package/src/lib/neuron-validator.js +80 -0
- package/src/lib/parameter-schema-validator.js +284 -0
- package/src/lib/prompt-runner.js +1349 -0
- package/src/lib/prompts.js +1138 -0
- package/src/lib/registry-command-handler.js +519 -0
- package/src/lib/registry-loader.js +198 -0
- package/src/lib/rocm-validator.js +80 -0
- package/src/lib/schema-validator.js +157 -0
- package/src/lib/sensitive-redactor.js +59 -0
- package/src/lib/template-engine.js +156 -0
- package/src/lib/template-manager.js +341 -0
- package/src/lib/validation-engine.js +314 -0
- package/src/prompt-adapter.js +63 -0
- package/templates/Dockerfile +300 -0
- package/templates/IAM_PERMISSIONS.md +84 -0
- package/templates/MIGRATION.md +488 -0
- package/templates/PROJECT_README.md +439 -0
- package/templates/TEMPLATE_SYSTEM.md +243 -0
- package/templates/buildspec.yml +64 -0
- package/templates/code/chat_template.jinja +1 -0
- package/templates/code/flask/gunicorn_config.py +35 -0
- package/templates/code/flask/wsgi.py +10 -0
- package/templates/code/model_handler.py +387 -0
- package/templates/code/serve +300 -0
- package/templates/code/serve.py +175 -0
- package/templates/code/serving.properties +105 -0
- package/templates/code/start_server.py +39 -0
- package/templates/code/start_server.sh +39 -0
- package/templates/diffusors/Dockerfile +72 -0
- package/templates/diffusors/patch_image_api.py +35 -0
- package/templates/diffusors/serve +115 -0
- package/templates/diffusors/start_server.sh +114 -0
- package/templates/do/.gitkeep +1 -0
- package/templates/do/README.md +541 -0
- package/templates/do/build +83 -0
- package/templates/do/ci +681 -0
- package/templates/do/clean +811 -0
- package/templates/do/config +260 -0
- package/templates/do/deploy +1560 -0
- package/templates/do/export +306 -0
- package/templates/do/logs +319 -0
- package/templates/do/manifest +12 -0
- package/templates/do/push +119 -0
- package/templates/do/register +580 -0
- package/templates/do/run +113 -0
- package/templates/do/submit +417 -0
- package/templates/do/test +1147 -0
- package/templates/hyperpod/configmap.yaml +24 -0
- package/templates/hyperpod/deployment.yaml +71 -0
- package/templates/hyperpod/pvc.yaml +42 -0
- package/templates/hyperpod/service.yaml +17 -0
- package/templates/nginx-diffusors.conf +74 -0
- package/templates/nginx-predictors.conf +47 -0
- package/templates/nginx-tensorrt.conf +74 -0
- package/templates/requirements.txt +61 -0
- package/templates/sample_model/test_inference.py +123 -0
- package/templates/sample_model/train_abalone.py +252 -0
- package/templates/test/test_endpoint.sh +79 -0
- package/templates/test/test_local_image.sh +80 -0
- package/templates/test/test_model_handler.py +180 -0
- package/templates/triton/Dockerfile +128 -0
- package/templates/triton/config.pbtxt +163 -0
- package/templates/triton/model.py +130 -0
- package/templates/triton/requirements.txt +11 -0
|
@@ -0,0 +1,2106 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration Manager - Handles configuration precedence and merging
|
|
6
|
+
*
|
|
7
|
+
* Implements the complete precedence order (Highest → Lowest Priority):
|
|
8
|
+
* 1. CLI Options (--framework=transformers)
|
|
9
|
+
* 2. CLI Arguments (yo generator projectName)
|
|
10
|
+
* 3. Environment Variables (AWS_REGION=us-east-1)
|
|
11
|
+
* 4. CLI Config File (--config=prod.json) / Inline JSON (--config-json='...')
|
|
12
|
+
* 5. Custom Config File (config/mcp.json)
|
|
13
|
+
* 6. Package.json Section ("ml-container-creator": {...})
|
|
14
|
+
* 7. Bootstrap Config (~/.ml-container-creator/config.json)
|
|
15
|
+
* 8. Generator Defaults
|
|
16
|
+
* 9. Prompting (fallback)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import { readFileSync } from 'node:fs';
|
|
22
|
+
import { resolve, dirname } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { McpClient } from './mcp-client.js';
|
|
25
|
+
import DeploymentConfigResolver from './deployment-config-resolver.js';
|
|
26
|
+
import BootstrapConfig from './bootstrap-config.js';
|
|
27
|
+
import { parseKeyValue } from './key-value-parser.js';
|
|
28
|
+
import ParameterSchemaValidator from './parameter-schema-validator.js';
|
|
29
|
+
|
|
30
|
+
const __configMgrFilename = fileURLToPath(import.meta.url);
|
|
31
|
+
const __configMgrDir = dirname(__configMgrFilename);
|
|
32
|
+
const tritonBackendsCatalogPath = resolve(__configMgrDir, '../../servers/base-image-picker/catalogs/triton-backends.json');
|
|
33
|
+
|
|
34
|
+
function loadTritonBackendsFromCatalog() {
|
|
35
|
+
try {
|
|
36
|
+
const raw = readFileSync(tritonBackendsCatalogPath, 'utf8');
|
|
37
|
+
return JSON.parse(raw);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.warn(`Failed to load triton backends catalog: ${error.message}`);
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const tritonBackends = loadTritonBackendsFromCatalog();
|
|
45
|
+
|
|
46
|
+
// Resolve the generator project root (two levels up from src/lib/)
|
|
47
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
48
|
+
const __dirname = path.dirname(__filename);
|
|
49
|
+
const GENERATOR_ROOT = path.resolve(__dirname, '..', '..');
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Configuration error for invalid configuration values
|
|
53
|
+
*/
|
|
54
|
+
export class ConfigurationError extends Error {
|
|
55
|
+
constructor(message, parameter, source) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = 'ConfigurationError';
|
|
58
|
+
this.parameter = parameter;
|
|
59
|
+
this.source = source;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validation error for invalid parameter values
|
|
65
|
+
*/
|
|
66
|
+
export class ValidationError extends Error {
|
|
67
|
+
constructor(message, parameter, value) {
|
|
68
|
+
super(message);
|
|
69
|
+
this.name = 'ValidationError';
|
|
70
|
+
this.parameter = parameter;
|
|
71
|
+
this.value = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default class ConfigManager {
|
|
76
|
+
constructor({ options, args }) {
|
|
77
|
+
this.options = options || {};
|
|
78
|
+
this.args = args || [];
|
|
79
|
+
this.config = {};
|
|
80
|
+
this.skipPrompts = false;
|
|
81
|
+
this.deploymentConfigResolver = new DeploymentConfigResolver();
|
|
82
|
+
this.parameterMatrix = this._getParameterMatrix();
|
|
83
|
+
this.schemaValidator = new ParameterSchemaValidator();
|
|
84
|
+
this.mcpSources = {};
|
|
85
|
+
this.mcpChoices = {};
|
|
86
|
+
this._sourceManifest = [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Loads configuration from all sources according to precedence
|
|
91
|
+
* @returns {Object} Merged configuration object
|
|
92
|
+
*/
|
|
93
|
+
async loadConfiguration() {
|
|
94
|
+
// Start with generator defaults
|
|
95
|
+
this.config = this._getGeneratorDefaults();
|
|
96
|
+
|
|
97
|
+
// Track explicit configuration (non-default values)
|
|
98
|
+
this.explicitConfig = {};
|
|
99
|
+
|
|
100
|
+
// Apply configurations in reverse precedence order (lowest to highest)
|
|
101
|
+
await this._loadBootstrapConfig();
|
|
102
|
+
await this._loadPackageJsonConfig();
|
|
103
|
+
await this._loadCustomConfigFile();
|
|
104
|
+
await this._loadCliConfigFile();
|
|
105
|
+
await this._loadEnvironmentVariables();
|
|
106
|
+
await this._loadCliArguments();
|
|
107
|
+
await this._loadCliOptions();
|
|
108
|
+
|
|
109
|
+
// Query configured MCP servers for unbounded parameter values
|
|
110
|
+
await this._queryMcpServers();
|
|
111
|
+
|
|
112
|
+
// Check if we should skip prompts
|
|
113
|
+
this.skipPrompts = this.options['skip-prompts'] ||
|
|
114
|
+
this._hasCompleteConfiguration();
|
|
115
|
+
|
|
116
|
+
return this.config;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Checks if prompting should be skipped
|
|
121
|
+
* @returns {boolean}
|
|
122
|
+
*/
|
|
123
|
+
shouldSkipPrompts() {
|
|
124
|
+
return this.skipPrompts;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Gets the final configuration, filling in any missing values with prompts
|
|
129
|
+
* @param {Object} promptAnswers - Answers from prompting phase
|
|
130
|
+
* @returns {Object} Complete configuration
|
|
131
|
+
*/
|
|
132
|
+
getFinalConfiguration(promptAnswers = {}) {
|
|
133
|
+
// Prompting has lowest precedence, so only use for missing values
|
|
134
|
+
const finalConfig = { ...promptAnswers };
|
|
135
|
+
|
|
136
|
+
// Override with explicit configuration (not defaults)
|
|
137
|
+
const explicitConfig = this.getExplicitConfiguration();
|
|
138
|
+
Object.keys(explicitConfig).forEach(key => {
|
|
139
|
+
if (explicitConfig[key] !== undefined && explicitConfig[key] !== null) {
|
|
140
|
+
finalConfig[key] = explicitConfig[key];
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Fill in missing values with defaults from this.config
|
|
145
|
+
Object.keys(this.config).forEach(key => {
|
|
146
|
+
if (finalConfig[key] === undefined || finalConfig[key] === null) {
|
|
147
|
+
finalConfig[key] = this.config[key];
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Ensure env var collections are properly merged (CLI over config file over registry)
|
|
152
|
+
// this.config already has the fully merged result from all sources
|
|
153
|
+
if (this.config.modelEnvVars && typeof this.config.modelEnvVars === 'object') {
|
|
154
|
+
finalConfig.modelEnvVars = { ...this.config.modelEnvVars }
|
|
155
|
+
}
|
|
156
|
+
if (this.config.serverEnvVars && typeof this.config.serverEnvVars === 'object') {
|
|
157
|
+
finalConfig.serverEnvVars = { ...this.config.serverEnvVars }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Ensure all parameters from the matrix are included in final config
|
|
161
|
+
// This is important for optional parameters that might be null
|
|
162
|
+
Object.entries(this.parameterMatrix).forEach(([param, config]) => {
|
|
163
|
+
if (finalConfig[param] === undefined) {
|
|
164
|
+
finalConfig[param] = this.config[param] || config.default;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Derive architecture, backend, and engine from deploymentConfig using DeploymentConfigResolver.
|
|
169
|
+
// In prompted mode the PromptRunner may do this, but in --skip-prompts
|
|
170
|
+
// mode we need to do it here so the values are available for downstream logic.
|
|
171
|
+
if (finalConfig.deploymentConfig) {
|
|
172
|
+
const parts = this.deploymentConfigResolver.decompose(finalConfig.deploymentConfig)
|
|
173
|
+
finalConfig.architecture = parts.architecture
|
|
174
|
+
finalConfig.backend = parts.backend
|
|
175
|
+
// For http architecture, engine comes from the --engine CLI option or prompt
|
|
176
|
+
if (parts.architecture === 'http') {
|
|
177
|
+
if (!finalConfig.engine) {
|
|
178
|
+
finalConfig.engine = parts.engine
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
finalConfig.engine = parts.engine
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// When skipping prompts, provide reasonable defaults for missing required parameters
|
|
186
|
+
if (this.skipPrompts) {
|
|
187
|
+
Object.entries(this.parameterMatrix).forEach(([param, config]) => {
|
|
188
|
+
if (config.required &&
|
|
189
|
+
(finalConfig[param] === null || finalConfig[param] === undefined)) {
|
|
190
|
+
|
|
191
|
+
// Provide reasonable defaults for missing required parameters
|
|
192
|
+
if (param === 'modelFormat') {
|
|
193
|
+
// Infer model format from architecture/engine (skip for transformers/triton)
|
|
194
|
+
const architecture = finalConfig.architecture || 'http'
|
|
195
|
+
if (architecture === 'http') {
|
|
196
|
+
const engine = finalConfig.engine || 'sklearn'
|
|
197
|
+
const formatMap = {
|
|
198
|
+
'sklearn': 'pkl',
|
|
199
|
+
'xgboost': 'json',
|
|
200
|
+
'tensorflow': 'keras'
|
|
201
|
+
}
|
|
202
|
+
finalConfig[param] = formatMap[engine] || 'pkl'
|
|
203
|
+
}
|
|
204
|
+
} else if (param === 'instanceType') {
|
|
205
|
+
// Default to ml.m5.large for http, ml.g5.xlarge for transformers/triton
|
|
206
|
+
const architecture = finalConfig.architecture || 'http'
|
|
207
|
+
finalConfig[param] = architecture === 'http' ? 'ml.m5.large' : 'ml.g5.xlarge'
|
|
208
|
+
} else if (param === 'projectName') {
|
|
209
|
+
// Generate project name
|
|
210
|
+
finalConfig[param] = this._generateProjectName(finalConfig.architecture)
|
|
211
|
+
} else if (config.default !== null) {
|
|
212
|
+
// Use default value if available
|
|
213
|
+
finalConfig[param] = config.default;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Always generate values for non-promptable required parameters that are missing
|
|
220
|
+
Object.entries(this.parameterMatrix).forEach(([param, config]) => {
|
|
221
|
+
if (config.required && !config.promptable &&
|
|
222
|
+
(finalConfig[param] === null || finalConfig[param] === undefined)) {
|
|
223
|
+
|
|
224
|
+
if (param === 'projectName') {
|
|
225
|
+
// Generate project name based on architecture or use default
|
|
226
|
+
finalConfig[param] = this._generateProjectName(finalConfig.architecture);
|
|
227
|
+
} else if (config.default !== null) {
|
|
228
|
+
// Use default value if available
|
|
229
|
+
finalConfig[param] = config.default;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Apply architecture-specific overrides
|
|
235
|
+
if (finalConfig.architecture === 'transformers') {
|
|
236
|
+
finalConfig.includeSampleModel = false;
|
|
237
|
+
}
|
|
238
|
+
if (finalConfig.architecture === 'diffusors') {
|
|
239
|
+
finalConfig.includeSampleModel = false;
|
|
240
|
+
}
|
|
241
|
+
if (finalConfig.architecture === 'triton') {
|
|
242
|
+
const backendMeta = tritonBackends[finalConfig.backend];
|
|
243
|
+
if (!backendMeta || !backendMeta.supportsSampleModel) {
|
|
244
|
+
finalConfig.includeSampleModel = false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Set destinationDir based on projectName if not explicitly provided via --project-dir
|
|
249
|
+
// This matches standard CLI behavior:
|
|
250
|
+
// - `ml-container-creator my-app` creates `./my-app/` subdirectory
|
|
251
|
+
// - `ml-container-creator my-app --project-dir /tmp` uses `/tmp/` directly
|
|
252
|
+
// - `yo generator --project-name my-app` uses current directory (option, not argument)
|
|
253
|
+
//
|
|
254
|
+
// Only create subdirectory when:
|
|
255
|
+
// 1. Project name was provided as a positional CLI argument (not option/config)
|
|
256
|
+
// 2. --project-dir was NOT explicitly provided
|
|
257
|
+
// 3. destinationDir is still the default '.'
|
|
258
|
+
|
|
259
|
+
const projectNameFromArgument = this.projectNameFromArgument || false;
|
|
260
|
+
const explicitDestination = explicitConfig.destinationDir;
|
|
261
|
+
|
|
262
|
+
if (projectNameFromArgument &&
|
|
263
|
+
!explicitDestination &&
|
|
264
|
+
finalConfig.destinationDir === '.') {
|
|
265
|
+
finalConfig.destinationDir = `./${finalConfig.projectName}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Generate CodeBuild project name if buildTarget is codebuild
|
|
269
|
+
if ((finalConfig.buildTarget === 'codebuild' || finalConfig.deployTarget === 'codebuild') && !finalConfig.codebuildProjectName) {
|
|
270
|
+
finalConfig.codebuildProjectName = this._generateCodeBuildProjectName(
|
|
271
|
+
finalConfig.projectName,
|
|
272
|
+
finalConfig.architecture
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Add build timestamp if not present
|
|
277
|
+
if (!finalConfig.buildTimestamp) {
|
|
278
|
+
finalConfig.buildTimestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Resolve HF_TOKEN environment variable references
|
|
282
|
+
// This happens after all configuration sources have been merged
|
|
283
|
+
if (finalConfig.hfToken) {
|
|
284
|
+
finalConfig.hfToken = this._resolveHfToken(finalConfig.hfToken);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Map awsRoleArn to roleArn for templates
|
|
288
|
+
if (finalConfig.awsRoleArn) {
|
|
289
|
+
finalConfig.roleArn = finalConfig.awsRoleArn;
|
|
290
|
+
delete finalConfig.awsRoleArn;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return finalConfig;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Gets only the explicit configuration (not defaults) for prompting
|
|
298
|
+
* @returns {Object} Explicit configuration only
|
|
299
|
+
*/
|
|
300
|
+
getExplicitConfiguration() {
|
|
301
|
+
return this.explicitConfig || {};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Gets the MCP source tracking information
|
|
306
|
+
* @returns {Object} Map of parameter names to their MCP source info
|
|
307
|
+
*/
|
|
308
|
+
getMcpSources() {
|
|
309
|
+
return this.mcpSources || {};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Returns the complete configuration object with all parameter families
|
|
314
|
+
* separated into named collections for validation layer consumption.
|
|
315
|
+
* @returns {{
|
|
316
|
+
* core: Object,
|
|
317
|
+
* endpointConfig: Object,
|
|
318
|
+
* icConfig: Object,
|
|
319
|
+
* modelEnvVars: Object,
|
|
320
|
+
* serverEnvVars: Object,
|
|
321
|
+
* manifest: Array<{param: string, value: *, source: string}>
|
|
322
|
+
* }}
|
|
323
|
+
*/
|
|
324
|
+
getFullConfiguration() {
|
|
325
|
+
const endpointParams = [
|
|
326
|
+
'endpointInitialInstanceCount',
|
|
327
|
+
'endpointDataCapturePercent',
|
|
328
|
+
'endpointVariantName',
|
|
329
|
+
'endpointVolumeSize'
|
|
330
|
+
]
|
|
331
|
+
const icParams = [
|
|
332
|
+
'icCpuCount',
|
|
333
|
+
'icMemorySize',
|
|
334
|
+
'icGpuCount',
|
|
335
|
+
'icCopyCount',
|
|
336
|
+
'icModelWeight'
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
const endpointConfig = {}
|
|
340
|
+
for (const param of endpointParams) {
|
|
341
|
+
const shortKey = param.replace('endpoint', '')
|
|
342
|
+
const key = shortKey.charAt(0).toLowerCase() + shortKey.slice(1)
|
|
343
|
+
if (this.config[param] !== undefined && this.config[param] !== null) {
|
|
344
|
+
endpointConfig[key] = this.config[param]
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const icConfig = {}
|
|
349
|
+
for (const param of icParams) {
|
|
350
|
+
const shortKey = param.replace('ic', '')
|
|
351
|
+
const key = shortKey.charAt(0).toLowerCase() + shortKey.slice(1)
|
|
352
|
+
if (this.config[param] !== undefined && this.config[param] !== null) {
|
|
353
|
+
icConfig[key] = this.config[param]
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Core parameters: everything that is NOT endpoint, iC, or env var collections
|
|
358
|
+
const excludedFromCore = new Set([
|
|
359
|
+
...endpointParams,
|
|
360
|
+
...icParams,
|
|
361
|
+
'modelEnvVars',
|
|
362
|
+
'serverEnvVars'
|
|
363
|
+
])
|
|
364
|
+
const core = {}
|
|
365
|
+
for (const [key, value] of Object.entries(this.config)) {
|
|
366
|
+
if (!excludedFromCore.has(key) && key !== '_sourceManifest') {
|
|
367
|
+
core[key] = value
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
core,
|
|
373
|
+
endpointConfig,
|
|
374
|
+
icConfig,
|
|
375
|
+
modelEnvVars: { ...(this.config.modelEnvVars || {}) },
|
|
376
|
+
serverEnvVars: { ...(this.config.serverEnvVars || {}) },
|
|
377
|
+
manifest: [...this._sourceManifest]
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Merge registry-provided environment variables with CLI-provided values.
|
|
383
|
+
* CLI values take precedence over registry values for the same key.
|
|
384
|
+
* Requirements: 3.3, 4.3
|
|
385
|
+
* @param {Object} registryModelEnvVars - Model env vars from registry
|
|
386
|
+
* @param {Object} registryServerEnvVars - Server env vars from registry
|
|
387
|
+
*/
|
|
388
|
+
mergeRegistryEnvVars(registryModelEnvVars = {}, registryServerEnvVars = {}) {
|
|
389
|
+
// Initialize collections if needed
|
|
390
|
+
if (!this.config.modelEnvVars || typeof this.config.modelEnvVars !== 'object') {
|
|
391
|
+
this.config.modelEnvVars = {}
|
|
392
|
+
}
|
|
393
|
+
if (!this.config.serverEnvVars || typeof this.config.serverEnvVars !== 'object') {
|
|
394
|
+
this.config.serverEnvVars = {}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Merge registry model env vars (CLI takes precedence)
|
|
398
|
+
Object.entries(registryModelEnvVars).forEach(([key, value]) => {
|
|
399
|
+
if (!(key in this.config.modelEnvVars)) {
|
|
400
|
+
this.config.modelEnvVars[key] = value
|
|
401
|
+
this._recordSource(`modelEnvVars.${key}`, value, 'registry')
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
// Merge registry server env vars (CLI takes precedence)
|
|
406
|
+
Object.entries(registryServerEnvVars).forEach(([key, value]) => {
|
|
407
|
+
if (!(key in this.config.serverEnvVars)) {
|
|
408
|
+
this.config.serverEnvVars[key] = value
|
|
409
|
+
this._recordSource(`serverEnvVars.${key}`, value, 'registry')
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Gets the parameter matrix configuration
|
|
416
|
+
* @private
|
|
417
|
+
*/
|
|
418
|
+
_getParameterMatrix() {
|
|
419
|
+
return {
|
|
420
|
+
deploymentConfig: {
|
|
421
|
+
cliOption: 'deployment-config',
|
|
422
|
+
envVar: null,
|
|
423
|
+
configFile: true,
|
|
424
|
+
packageJson: false,
|
|
425
|
+
mcp: false,
|
|
426
|
+
promptable: true,
|
|
427
|
+
required: true,
|
|
428
|
+
default: null,
|
|
429
|
+
valueSpace: 'bounded'
|
|
430
|
+
},
|
|
431
|
+
architecture: {
|
|
432
|
+
cliOption: null,
|
|
433
|
+
envVar: null,
|
|
434
|
+
configFile: false,
|
|
435
|
+
packageJson: false,
|
|
436
|
+
mcp: false,
|
|
437
|
+
promptable: false,
|
|
438
|
+
required: false,
|
|
439
|
+
default: null,
|
|
440
|
+
valueSpace: 'bounded'
|
|
441
|
+
},
|
|
442
|
+
backend: {
|
|
443
|
+
cliOption: null,
|
|
444
|
+
envVar: null,
|
|
445
|
+
configFile: false,
|
|
446
|
+
packageJson: false,
|
|
447
|
+
mcp: false,
|
|
448
|
+
promptable: false,
|
|
449
|
+
required: false,
|
|
450
|
+
default: null,
|
|
451
|
+
valueSpace: 'bounded'
|
|
452
|
+
},
|
|
453
|
+
engine: {
|
|
454
|
+
cliOption: 'engine',
|
|
455
|
+
envVar: null,
|
|
456
|
+
configFile: true,
|
|
457
|
+
packageJson: false,
|
|
458
|
+
mcp: false,
|
|
459
|
+
promptable: true,
|
|
460
|
+
required: false,
|
|
461
|
+
default: null,
|
|
462
|
+
valueSpace: 'bounded'
|
|
463
|
+
},
|
|
464
|
+
modelFormat: {
|
|
465
|
+
cliOption: 'model-format',
|
|
466
|
+
envVar: null,
|
|
467
|
+
configFile: true,
|
|
468
|
+
packageJson: false,
|
|
469
|
+
mcp: false,
|
|
470
|
+
promptable: true,
|
|
471
|
+
required: true,
|
|
472
|
+
default: null,
|
|
473
|
+
valueSpace: 'bounded'
|
|
474
|
+
},
|
|
475
|
+
modelName: {
|
|
476
|
+
cliOption: 'model-name',
|
|
477
|
+
envVar: null,
|
|
478
|
+
configFile: true,
|
|
479
|
+
packageJson: false,
|
|
480
|
+
mcp: false,
|
|
481
|
+
promptable: true,
|
|
482
|
+
required: false,
|
|
483
|
+
default: 'openai/gpt-oss-20b',
|
|
484
|
+
valueSpace: 'bounded'
|
|
485
|
+
},
|
|
486
|
+
includeSampleModel: {
|
|
487
|
+
cliOption: 'include-sample',
|
|
488
|
+
envVar: null,
|
|
489
|
+
configFile: true,
|
|
490
|
+
packageJson: false,
|
|
491
|
+
mcp: false,
|
|
492
|
+
promptable: true,
|
|
493
|
+
required: true,
|
|
494
|
+
default: false,
|
|
495
|
+
valueSpace: 'bounded'
|
|
496
|
+
},
|
|
497
|
+
includeTesting: {
|
|
498
|
+
cliOption: 'include-testing',
|
|
499
|
+
envVar: null,
|
|
500
|
+
configFile: true,
|
|
501
|
+
packageJson: false,
|
|
502
|
+
mcp: false,
|
|
503
|
+
promptable: false,
|
|
504
|
+
required: false,
|
|
505
|
+
default: true,
|
|
506
|
+
valueSpace: 'bounded'
|
|
507
|
+
},
|
|
508
|
+
instanceType: {
|
|
509
|
+
cliOption: 'instance-type',
|
|
510
|
+
envVar: 'ML_INSTANCE_TYPE',
|
|
511
|
+
configFile: true,
|
|
512
|
+
packageJson: false,
|
|
513
|
+
mcp: true,
|
|
514
|
+
promptable: true,
|
|
515
|
+
required: true,
|
|
516
|
+
default: null,
|
|
517
|
+
valueSpace: 'unbounded'
|
|
518
|
+
},
|
|
519
|
+
awsRegion: {
|
|
520
|
+
cliOption: 'region',
|
|
521
|
+
envVar: 'AWS_REGION',
|
|
522
|
+
ambientEnvVar: true, // AWS_REGION is commonly set in shells; treat as default, not explicit override
|
|
523
|
+
configFile: true,
|
|
524
|
+
packageJson: true,
|
|
525
|
+
mcp: true,
|
|
526
|
+
promptable: true,
|
|
527
|
+
required: false,
|
|
528
|
+
default: 'us-east-1',
|
|
529
|
+
valueSpace: 'unbounded'
|
|
530
|
+
},
|
|
531
|
+
awsRoleArn: {
|
|
532
|
+
cliOption: 'role-arn',
|
|
533
|
+
envVar: 'AWS_ROLE',
|
|
534
|
+
configFile: true,
|
|
535
|
+
packageJson: true,
|
|
536
|
+
mcp: true,
|
|
537
|
+
promptable: true,
|
|
538
|
+
required: false,
|
|
539
|
+
default: null,
|
|
540
|
+
valueSpace: 'unbounded'
|
|
541
|
+
},
|
|
542
|
+
configFile: {
|
|
543
|
+
cliOption: 'config',
|
|
544
|
+
envVar: 'ML_CONTAINER_CREATOR_CONFIG',
|
|
545
|
+
configFile: false,
|
|
546
|
+
packageJson: true,
|
|
547
|
+
mcp: false,
|
|
548
|
+
promptable: true,
|
|
549
|
+
required: false,
|
|
550
|
+
default: null,
|
|
551
|
+
valueSpace: 'bounded'
|
|
552
|
+
},
|
|
553
|
+
skipPrompts: {
|
|
554
|
+
cliOption: 'skip-prompts',
|
|
555
|
+
envVar: null,
|
|
556
|
+
configFile: false,
|
|
557
|
+
packageJson: false,
|
|
558
|
+
mcp: false,
|
|
559
|
+
promptable: false,
|
|
560
|
+
required: false,
|
|
561
|
+
default: false,
|
|
562
|
+
valueSpace: 'bounded'
|
|
563
|
+
},
|
|
564
|
+
projectName: {
|
|
565
|
+
cliOption: 'project-name',
|
|
566
|
+
envVar: null,
|
|
567
|
+
configFile: true,
|
|
568
|
+
packageJson: true,
|
|
569
|
+
mcp: false,
|
|
570
|
+
promptable: false,
|
|
571
|
+
required: true,
|
|
572
|
+
default: null,
|
|
573
|
+
valueSpace: 'bounded'
|
|
574
|
+
},
|
|
575
|
+
destinationDir: {
|
|
576
|
+
cliOption: 'project-dir',
|
|
577
|
+
envVar: null,
|
|
578
|
+
configFile: true,
|
|
579
|
+
packageJson: true,
|
|
580
|
+
mcp: false,
|
|
581
|
+
promptable: false,
|
|
582
|
+
required: true,
|
|
583
|
+
default: '.',
|
|
584
|
+
valueSpace: 'bounded'
|
|
585
|
+
},
|
|
586
|
+
buildTarget: {
|
|
587
|
+
cliOption: 'build-target',
|
|
588
|
+
envVar: 'ML_BUILD_TARGET',
|
|
589
|
+
configFile: true,
|
|
590
|
+
packageJson: false,
|
|
591
|
+
mcp: false,
|
|
592
|
+
promptable: true,
|
|
593
|
+
required: true,
|
|
594
|
+
default: 'codebuild',
|
|
595
|
+
valueSpace: 'bounded'
|
|
596
|
+
},
|
|
597
|
+
codebuildComputeType: {
|
|
598
|
+
cliOption: 'codebuild-compute-type',
|
|
599
|
+
envVar: 'ML_CODEBUILD_COMPUTE_TYPE',
|
|
600
|
+
configFile: true,
|
|
601
|
+
packageJson: false,
|
|
602
|
+
mcp: false,
|
|
603
|
+
promptable: true,
|
|
604
|
+
required: false,
|
|
605
|
+
default: 'BUILD_GENERAL1_MEDIUM',
|
|
606
|
+
valueSpace: 'bounded'
|
|
607
|
+
},
|
|
608
|
+
codebuildProjectName: {
|
|
609
|
+
cliOption: null,
|
|
610
|
+
envVar: null,
|
|
611
|
+
configFile: true,
|
|
612
|
+
packageJson: false,
|
|
613
|
+
mcp: false,
|
|
614
|
+
promptable: false,
|
|
615
|
+
required: false,
|
|
616
|
+
default: null,
|
|
617
|
+
valueSpace: 'bounded'
|
|
618
|
+
},
|
|
619
|
+
hfToken: {
|
|
620
|
+
cliOption: 'hf-token',
|
|
621
|
+
envVar: null,
|
|
622
|
+
configFile: true,
|
|
623
|
+
packageJson: false,
|
|
624
|
+
mcp: false,
|
|
625
|
+
promptable: true,
|
|
626
|
+
required: false,
|
|
627
|
+
default: null,
|
|
628
|
+
valueSpace: 'bounded'
|
|
629
|
+
},
|
|
630
|
+
deploymentTarget: {
|
|
631
|
+
cliOption: 'deployment-target',
|
|
632
|
+
envVar: 'ML_DEPLOYMENT_TARGET',
|
|
633
|
+
configFile: true,
|
|
634
|
+
packageJson: false,
|
|
635
|
+
mcp: false,
|
|
636
|
+
promptable: true,
|
|
637
|
+
required: true,
|
|
638
|
+
default: 'managed-inference',
|
|
639
|
+
valueSpace: 'bounded'
|
|
640
|
+
},
|
|
641
|
+
hyperPodCluster: {
|
|
642
|
+
cliOption: 'hyperpod-cluster',
|
|
643
|
+
envVar: null,
|
|
644
|
+
configFile: true,
|
|
645
|
+
packageJson: false,
|
|
646
|
+
mcp: true,
|
|
647
|
+
promptable: true,
|
|
648
|
+
required: false,
|
|
649
|
+
default: null,
|
|
650
|
+
valueSpace: 'unbounded'
|
|
651
|
+
},
|
|
652
|
+
hyperPodNamespace: {
|
|
653
|
+
cliOption: 'hyperpod-namespace',
|
|
654
|
+
envVar: null,
|
|
655
|
+
configFile: true,
|
|
656
|
+
packageJson: false,
|
|
657
|
+
mcp: false,
|
|
658
|
+
promptable: true,
|
|
659
|
+
required: false,
|
|
660
|
+
default: 'default',
|
|
661
|
+
valueSpace: 'bounded'
|
|
662
|
+
},
|
|
663
|
+
hyperPodReplicas: {
|
|
664
|
+
cliOption: 'hyperpod-replicas',
|
|
665
|
+
envVar: null,
|
|
666
|
+
configFile: true,
|
|
667
|
+
packageJson: false,
|
|
668
|
+
mcp: false,
|
|
669
|
+
promptable: true,
|
|
670
|
+
required: false,
|
|
671
|
+
default: 1,
|
|
672
|
+
valueSpace: 'bounded'
|
|
673
|
+
},
|
|
674
|
+
fsxVolumeHandle: {
|
|
675
|
+
cliOption: 'fsx-volume-handle',
|
|
676
|
+
envVar: null,
|
|
677
|
+
configFile: true,
|
|
678
|
+
packageJson: false,
|
|
679
|
+
mcp: false,
|
|
680
|
+
promptable: true,
|
|
681
|
+
required: false,
|
|
682
|
+
default: null,
|
|
683
|
+
valueSpace: 'bounded'
|
|
684
|
+
},
|
|
685
|
+
baseImage: {
|
|
686
|
+
cliOption: 'base-image',
|
|
687
|
+
envVar: null,
|
|
688
|
+
configFile: true,
|
|
689
|
+
packageJson: false,
|
|
690
|
+
mcp: true,
|
|
691
|
+
promptable: true,
|
|
692
|
+
required: false,
|
|
693
|
+
default: null,
|
|
694
|
+
valueSpace: 'unbounded'
|
|
695
|
+
},
|
|
696
|
+
asyncS3OutputPath: {
|
|
697
|
+
cliOption: 'async-s3-output-path',
|
|
698
|
+
envVar: 'ML_ASYNC_S3_OUTPUT_PATH',
|
|
699
|
+
configFile: true,
|
|
700
|
+
packageJson: false,
|
|
701
|
+
mcp: true,
|
|
702
|
+
promptable: true,
|
|
703
|
+
required: false,
|
|
704
|
+
default: null,
|
|
705
|
+
valueSpace: 'unbounded'
|
|
706
|
+
},
|
|
707
|
+
asyncSnsSuccessTopic: {
|
|
708
|
+
cliOption: 'async-sns-success-topic',
|
|
709
|
+
envVar: null,
|
|
710
|
+
configFile: true,
|
|
711
|
+
packageJson: false,
|
|
712
|
+
mcp: true,
|
|
713
|
+
promptable: true,
|
|
714
|
+
required: false,
|
|
715
|
+
default: null,
|
|
716
|
+
valueSpace: 'unbounded'
|
|
717
|
+
},
|
|
718
|
+
asyncSnsErrorTopic: {
|
|
719
|
+
cliOption: 'async-sns-error-topic',
|
|
720
|
+
envVar: null,
|
|
721
|
+
configFile: true,
|
|
722
|
+
packageJson: false,
|
|
723
|
+
mcp: true,
|
|
724
|
+
promptable: true,
|
|
725
|
+
required: false,
|
|
726
|
+
default: null,
|
|
727
|
+
valueSpace: 'unbounded'
|
|
728
|
+
},
|
|
729
|
+
asyncMaxConcurrentInvocations: {
|
|
730
|
+
cliOption: 'async-max-concurrent',
|
|
731
|
+
envVar: null,
|
|
732
|
+
configFile: true,
|
|
733
|
+
packageJson: false,
|
|
734
|
+
mcp: false,
|
|
735
|
+
promptable: true,
|
|
736
|
+
required: false,
|
|
737
|
+
default: 1,
|
|
738
|
+
valueSpace: 'bounded'
|
|
739
|
+
},
|
|
740
|
+
batchInputPath: {
|
|
741
|
+
cliOption: 'batch-input-path',
|
|
742
|
+
envVar: 'ML_BATCH_INPUT_PATH',
|
|
743
|
+
configFile: true,
|
|
744
|
+
packageJson: false,
|
|
745
|
+
mcp: true,
|
|
746
|
+
promptable: true,
|
|
747
|
+
required: false,
|
|
748
|
+
default: null,
|
|
749
|
+
valueSpace: 'unbounded'
|
|
750
|
+
},
|
|
751
|
+
batchOutputPath: {
|
|
752
|
+
cliOption: 'batch-output-path',
|
|
753
|
+
envVar: 'ML_BATCH_OUTPUT_PATH',
|
|
754
|
+
configFile: true,
|
|
755
|
+
packageJson: false,
|
|
756
|
+
mcp: true,
|
|
757
|
+
promptable: true,
|
|
758
|
+
required: false,
|
|
759
|
+
default: null,
|
|
760
|
+
valueSpace: 'unbounded'
|
|
761
|
+
},
|
|
762
|
+
batchInstanceCount: {
|
|
763
|
+
cliOption: 'batch-instance-count',
|
|
764
|
+
envVar: null,
|
|
765
|
+
configFile: true,
|
|
766
|
+
packageJson: false,
|
|
767
|
+
mcp: false,
|
|
768
|
+
promptable: true,
|
|
769
|
+
required: false,
|
|
770
|
+
default: 1,
|
|
771
|
+
valueSpace: 'bounded'
|
|
772
|
+
},
|
|
773
|
+
batchSplitType: {
|
|
774
|
+
cliOption: 'batch-split-type',
|
|
775
|
+
envVar: null,
|
|
776
|
+
configFile: true,
|
|
777
|
+
packageJson: false,
|
|
778
|
+
mcp: false,
|
|
779
|
+
promptable: true,
|
|
780
|
+
required: false,
|
|
781
|
+
default: 'Line',
|
|
782
|
+
valueSpace: 'bounded'
|
|
783
|
+
},
|
|
784
|
+
batchStrategy: {
|
|
785
|
+
cliOption: 'batch-strategy',
|
|
786
|
+
envVar: null,
|
|
787
|
+
configFile: true,
|
|
788
|
+
packageJson: false,
|
|
789
|
+
mcp: false,
|
|
790
|
+
promptable: true,
|
|
791
|
+
required: false,
|
|
792
|
+
default: 'MultiRecord',
|
|
793
|
+
valueSpace: 'bounded'
|
|
794
|
+
},
|
|
795
|
+
batchJoinSource: {
|
|
796
|
+
cliOption: 'batch-join-source',
|
|
797
|
+
envVar: null,
|
|
798
|
+
configFile: true,
|
|
799
|
+
packageJson: false,
|
|
800
|
+
mcp: false,
|
|
801
|
+
promptable: true,
|
|
802
|
+
required: false,
|
|
803
|
+
default: 'None',
|
|
804
|
+
valueSpace: 'bounded'
|
|
805
|
+
},
|
|
806
|
+
batchMaxConcurrentTransforms: {
|
|
807
|
+
cliOption: 'batch-max-concurrent',
|
|
808
|
+
envVar: null,
|
|
809
|
+
configFile: true,
|
|
810
|
+
packageJson: false,
|
|
811
|
+
mcp: false,
|
|
812
|
+
promptable: true,
|
|
813
|
+
required: false,
|
|
814
|
+
default: 1,
|
|
815
|
+
valueSpace: 'bounded'
|
|
816
|
+
},
|
|
817
|
+
batchMaxPayloadInMB: {
|
|
818
|
+
cliOption: 'batch-max-payload',
|
|
819
|
+
envVar: null,
|
|
820
|
+
configFile: true,
|
|
821
|
+
packageJson: false,
|
|
822
|
+
mcp: false,
|
|
823
|
+
promptable: true,
|
|
824
|
+
required: false,
|
|
825
|
+
default: 6,
|
|
826
|
+
valueSpace: 'bounded'
|
|
827
|
+
},
|
|
828
|
+
endpointInitialInstanceCount: {
|
|
829
|
+
cliOption: 'endpoint-initial-instance-count',
|
|
830
|
+
envVar: null,
|
|
831
|
+
configFile: true,
|
|
832
|
+
packageJson: false,
|
|
833
|
+
mcp: false,
|
|
834
|
+
promptable: false,
|
|
835
|
+
required: false,
|
|
836
|
+
default: 1,
|
|
837
|
+
valueSpace: 'bounded',
|
|
838
|
+
schemaValidated: true
|
|
839
|
+
},
|
|
840
|
+
endpointDataCapturePercent: {
|
|
841
|
+
cliOption: 'endpoint-data-capture-percent',
|
|
842
|
+
envVar: null,
|
|
843
|
+
configFile: true,
|
|
844
|
+
packageJson: false,
|
|
845
|
+
mcp: false,
|
|
846
|
+
promptable: false,
|
|
847
|
+
required: false,
|
|
848
|
+
default: 0,
|
|
849
|
+
valueSpace: 'bounded',
|
|
850
|
+
schemaValidated: true
|
|
851
|
+
},
|
|
852
|
+
endpointVariantName: {
|
|
853
|
+
cliOption: 'endpoint-variant-name',
|
|
854
|
+
envVar: null,
|
|
855
|
+
configFile: true,
|
|
856
|
+
packageJson: false,
|
|
857
|
+
mcp: false,
|
|
858
|
+
promptable: false,
|
|
859
|
+
required: false,
|
|
860
|
+
default: 'AllTraffic',
|
|
861
|
+
valueSpace: 'bounded',
|
|
862
|
+
schemaValidated: true
|
|
863
|
+
},
|
|
864
|
+
endpointVolumeSize: {
|
|
865
|
+
cliOption: 'endpoint-volume-size',
|
|
866
|
+
envVar: null,
|
|
867
|
+
configFile: true,
|
|
868
|
+
packageJson: false,
|
|
869
|
+
mcp: false,
|
|
870
|
+
promptable: false,
|
|
871
|
+
required: false,
|
|
872
|
+
default: null,
|
|
873
|
+
valueSpace: 'bounded',
|
|
874
|
+
schemaValidated: true
|
|
875
|
+
},
|
|
876
|
+
icCpuCount: {
|
|
877
|
+
cliOption: 'ic-cpu-count',
|
|
878
|
+
envVar: null,
|
|
879
|
+
configFile: true,
|
|
880
|
+
packageJson: false,
|
|
881
|
+
mcp: false,
|
|
882
|
+
promptable: false,
|
|
883
|
+
required: false,
|
|
884
|
+
default: null,
|
|
885
|
+
valueSpace: 'bounded',
|
|
886
|
+
schemaValidated: true
|
|
887
|
+
},
|
|
888
|
+
icMemorySize: {
|
|
889
|
+
cliOption: 'ic-memory-size',
|
|
890
|
+
envVar: null,
|
|
891
|
+
configFile: true,
|
|
892
|
+
packageJson: false,
|
|
893
|
+
mcp: false,
|
|
894
|
+
promptable: false,
|
|
895
|
+
required: false,
|
|
896
|
+
default: null,
|
|
897
|
+
valueSpace: 'bounded',
|
|
898
|
+
schemaValidated: true
|
|
899
|
+
},
|
|
900
|
+
icGpuCount: {
|
|
901
|
+
cliOption: 'ic-gpu-count',
|
|
902
|
+
envVar: null,
|
|
903
|
+
configFile: true,
|
|
904
|
+
packageJson: false,
|
|
905
|
+
mcp: false,
|
|
906
|
+
promptable: false,
|
|
907
|
+
required: false,
|
|
908
|
+
default: null,
|
|
909
|
+
valueSpace: 'bounded',
|
|
910
|
+
schemaValidated: true
|
|
911
|
+
},
|
|
912
|
+
icCopyCount: {
|
|
913
|
+
cliOption: 'ic-copy-count',
|
|
914
|
+
envVar: null,
|
|
915
|
+
configFile: true,
|
|
916
|
+
packageJson: false,
|
|
917
|
+
mcp: false,
|
|
918
|
+
promptable: false,
|
|
919
|
+
required: false,
|
|
920
|
+
default: 1,
|
|
921
|
+
valueSpace: 'bounded',
|
|
922
|
+
schemaValidated: true
|
|
923
|
+
},
|
|
924
|
+
icModelWeight: {
|
|
925
|
+
cliOption: 'ic-model-weight',
|
|
926
|
+
envVar: null,
|
|
927
|
+
configFile: true,
|
|
928
|
+
packageJson: false,
|
|
929
|
+
mcp: false,
|
|
930
|
+
promptable: false,
|
|
931
|
+
required: false,
|
|
932
|
+
default: 1.0,
|
|
933
|
+
valueSpace: 'bounded',
|
|
934
|
+
schemaValidated: true
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Checks if a parameter source is supported according to the matrix
|
|
941
|
+
* @private
|
|
942
|
+
*/
|
|
943
|
+
_isSourceSupported(parameter, source) {
|
|
944
|
+
const paramConfig = this.parameterMatrix[parameter];
|
|
945
|
+
if (!paramConfig) return false;
|
|
946
|
+
|
|
947
|
+
switch (source) {
|
|
948
|
+
case 'envVar':
|
|
949
|
+
return paramConfig.envVar !== null;
|
|
950
|
+
case 'configFile':
|
|
951
|
+
return paramConfig.configFile === true;
|
|
952
|
+
case 'packageJson':
|
|
953
|
+
return paramConfig.packageJson === true;
|
|
954
|
+
case 'cliOption':
|
|
955
|
+
return paramConfig.cliOption !== null;
|
|
956
|
+
default:
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Parses a value according to its expected type
|
|
963
|
+
* @private
|
|
964
|
+
*/
|
|
965
|
+
_parseValue(parameter, value) {
|
|
966
|
+
// Handle boolean parameters
|
|
967
|
+
if (parameter === 'includeSampleModel' || parameter === 'includeTesting' || parameter === 'skipPrompts') {
|
|
968
|
+
return value === true || value === 'true';
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Handle array parameters (if any in the future)
|
|
972
|
+
if (parameter === 'testTypes' && typeof value === 'string') {
|
|
973
|
+
return value.split(',').map(s => s.trim());
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Coerce numeric parameters from CLI strings to numbers.
|
|
977
|
+
// CLI always passes values as strings; we coerce when:
|
|
978
|
+
// 1. The default is already a number (e.g. endpointInitialInstanceCount default: 1)
|
|
979
|
+
// 2. The parameter is schema-validated, default is null, and the value is purely numeric
|
|
980
|
+
// (string defaults like 'AllTraffic' won't match since their default type is string)
|
|
981
|
+
const paramConfig = this.parameterMatrix[parameter]
|
|
982
|
+
if (paramConfig && typeof value === 'string') {
|
|
983
|
+
const hasNumericDefault = (typeof paramConfig.default === 'number')
|
|
984
|
+
const isNullDefaultNumericParam = (paramConfig.default === null &&
|
|
985
|
+
paramConfig.schemaValidated &&
|
|
986
|
+
/^-?\d+(\.\d+)?$/.test(value))
|
|
987
|
+
if (hasNumericDefault || isNullDefaultNumericParam) {
|
|
988
|
+
const num = Number(value)
|
|
989
|
+
if (!isNaN(num)) {
|
|
990
|
+
return num
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Handle string parameters
|
|
996
|
+
return value;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Generator defaults (lowest precedence before prompting)
|
|
1000
|
+
* @private
|
|
1001
|
+
*/
|
|
1002
|
+
_getGeneratorDefaults() {
|
|
1003
|
+
const defaults = {};
|
|
1004
|
+
|
|
1005
|
+
// Apply defaults from parameter matrix
|
|
1006
|
+
Object.entries(this.parameterMatrix).forEach(([param, config]) => {
|
|
1007
|
+
if (config.default !== null) {
|
|
1008
|
+
defaults[param] = config.default;
|
|
1009
|
+
this._recordSource(param, config.default, 'default')
|
|
1010
|
+
} else {
|
|
1011
|
+
defaults[param] = null;
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
// Add legacy parameters that aren't in the matrix but are still used internally
|
|
1016
|
+
defaults.testTypes = null;
|
|
1017
|
+
defaults.includeTesting = true;
|
|
1018
|
+
|
|
1019
|
+
// Collection parameters for env vars (not in matrix, handled separately)
|
|
1020
|
+
defaults.modelEnvVars = {};
|
|
1021
|
+
defaults.serverEnvVars = {};
|
|
1022
|
+
|
|
1023
|
+
return defaults;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Load from bootstrap config (~/.ml-container-creator/config.json)
|
|
1028
|
+
* Reads the active profile and maps its keys to ConfigManager config keys.
|
|
1029
|
+
* Sits above generator defaults but below all other configuration sources.
|
|
1030
|
+
* @private
|
|
1031
|
+
*/
|
|
1032
|
+
async _loadBootstrapConfig() {
|
|
1033
|
+
try {
|
|
1034
|
+
const bootstrapConfig = new BootstrapConfig();
|
|
1035
|
+
const activeProfile = bootstrapConfig.getActiveProfile();
|
|
1036
|
+
if (!activeProfile) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const profileConfig = activeProfile.config;
|
|
1041
|
+
const mapped = {};
|
|
1042
|
+
|
|
1043
|
+
if (profileConfig.roleArn) {
|
|
1044
|
+
mapped.awsRoleArn = profileConfig.roleArn;
|
|
1045
|
+
}
|
|
1046
|
+
if (profileConfig.awsRegion) {
|
|
1047
|
+
mapped.awsRegion = profileConfig.awsRegion;
|
|
1048
|
+
}
|
|
1049
|
+
if (profileConfig.awsProfile) {
|
|
1050
|
+
mapped.awsProfile = profileConfig.awsProfile;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
this._mergeConfig(mapped);
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
// Ignore errors — config file may not exist or may be malformed
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Load from package.json "ml-container-creator" section (filtered by matrix)
|
|
1061
|
+
* @private
|
|
1062
|
+
*/
|
|
1063
|
+
async _loadPackageJsonConfig() {
|
|
1064
|
+
try {
|
|
1065
|
+
const packageJsonPath = path.resolve(process.cwd(), 'package.json');
|
|
1066
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
1067
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
1068
|
+
const generatorConfig = packageJson['ml-container-creator'];
|
|
1069
|
+
if (generatorConfig) {
|
|
1070
|
+
// Filter config to only include parameters supported in package.json
|
|
1071
|
+
const filteredConfig = {};
|
|
1072
|
+
Object.entries(generatorConfig).forEach(([key, value]) => {
|
|
1073
|
+
if (this._isSourceSupported(key, 'packageJson')) {
|
|
1074
|
+
filteredConfig[key] = this._parseValue(key, value);
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
this._mergeConfig(filteredConfig);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
// Ignore errors - this is optional
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Load from config/mcp.json
|
|
1087
|
+
* @private
|
|
1088
|
+
*/
|
|
1089
|
+
async _loadCustomConfigFile() {
|
|
1090
|
+
try {
|
|
1091
|
+
const configPath = path.join(GENERATOR_ROOT, 'config', 'mcp.json');
|
|
1092
|
+
if (fs.existsSync(configPath)) {
|
|
1093
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1094
|
+
this._mergeConfig(config);
|
|
1095
|
+
}
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
// Ignore errors - this is optional
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Load from CLI --config file or --config-json inline string.
|
|
1103
|
+
*
|
|
1104
|
+
* --config-json accepts either:
|
|
1105
|
+
* 1. An inline JSON string: --config-json='{"deploymentConfig":"transformers-vllm"}'
|
|
1106
|
+
* 2. A path to a JSON file: --config-json=config.json
|
|
1107
|
+
*
|
|
1108
|
+
* When both --config and --config-json are provided, --config-json wins
|
|
1109
|
+
* (it is applied second, so its values override --config values).
|
|
1110
|
+
*
|
|
1111
|
+
* Also checks the ML_CONTAINER_CREATOR_CONFIG environment variable as a
|
|
1112
|
+
* fallback for --config.
|
|
1113
|
+
*
|
|
1114
|
+
* @private
|
|
1115
|
+
*/
|
|
1116
|
+
async _loadCliConfigFile() {
|
|
1117
|
+
let configFile = this.options.config;
|
|
1118
|
+
|
|
1119
|
+
// Check environment variable if CLI option not provided
|
|
1120
|
+
if (!configFile && process.env.ML_CONTAINER_CREATOR_CONFIG) {
|
|
1121
|
+
configFile = process.env.ML_CONTAINER_CREATOR_CONFIG;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (configFile) {
|
|
1125
|
+
this._loadConfigFromFile(configFile);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// --config-json: inline JSON string or path to a JSON file
|
|
1129
|
+
const configJson = this.options['config-json'];
|
|
1130
|
+
if (configJson) {
|
|
1131
|
+
this._loadConfigFromJson(configJson);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Load configuration from a JSON file path.
|
|
1137
|
+
* @param {string} configFile - Path to the JSON config file
|
|
1138
|
+
* @private
|
|
1139
|
+
*/
|
|
1140
|
+
_loadConfigFromFile(configFile) {
|
|
1141
|
+
try {
|
|
1142
|
+
const configPath = path.resolve(configFile);
|
|
1143
|
+
if (!fs.existsSync(configPath)) {
|
|
1144
|
+
throw new ConfigurationError(
|
|
1145
|
+
`Config file not found: ${configPath}`,
|
|
1146
|
+
'configFile',
|
|
1147
|
+
'cli'
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Check if file is readable
|
|
1152
|
+
try {
|
|
1153
|
+
fs.accessSync(configPath, fs.constants.R_OK);
|
|
1154
|
+
} catch (accessError) {
|
|
1155
|
+
throw new ConfigurationError(
|
|
1156
|
+
`Config file is not readable: ${configPath}`,
|
|
1157
|
+
'configFile',
|
|
1158
|
+
'cli'
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1163
|
+
this._applyJsonConfig(config);
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
if (error instanceof ConfigurationError) {
|
|
1166
|
+
throw error;
|
|
1167
|
+
} else {
|
|
1168
|
+
throw new ConfigurationError(
|
|
1169
|
+
`Failed to load config file ${configFile}: ${error.message}`,
|
|
1170
|
+
'configFile',
|
|
1171
|
+
'cli'
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Load configuration from an inline JSON string or a JSON file path.
|
|
1179
|
+
* Tries to parse as JSON first; if that fails and the value looks like
|
|
1180
|
+
* a file path, reads and parses the file instead.
|
|
1181
|
+
*
|
|
1182
|
+
* @param {string} configJson - Inline JSON string or path to a JSON file
|
|
1183
|
+
* @private
|
|
1184
|
+
*/
|
|
1185
|
+
_loadConfigFromJson(configJson) {
|
|
1186
|
+
let config;
|
|
1187
|
+
try {
|
|
1188
|
+
config = JSON.parse(configJson);
|
|
1189
|
+
} catch {
|
|
1190
|
+
// Not valid JSON — try as a file path
|
|
1191
|
+
try {
|
|
1192
|
+
const configPath = path.resolve(configJson);
|
|
1193
|
+
if (!fs.existsSync(configPath)) {
|
|
1194
|
+
throw new ConfigurationError(
|
|
1195
|
+
`--config-json value is not valid JSON and file not found: ${configJson}`,
|
|
1196
|
+
'configJson',
|
|
1197
|
+
'cli'
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
if (error instanceof ConfigurationError) {
|
|
1203
|
+
throw error;
|
|
1204
|
+
}
|
|
1205
|
+
throw new ConfigurationError(
|
|
1206
|
+
`Failed to parse --config-json: ${error.message}`,
|
|
1207
|
+
'configJson',
|
|
1208
|
+
'cli'
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
this._applyJsonConfig(config);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Apply a parsed JSON config object, filtering to supported parameters.
|
|
1217
|
+
* Handles nested objects for endpoint, iC, and env var configuration.
|
|
1218
|
+
* @param {Object} config - Parsed JSON config object
|
|
1219
|
+
* @private
|
|
1220
|
+
*/
|
|
1221
|
+
_applyJsonConfig(config) {
|
|
1222
|
+
const filteredConfig = {};
|
|
1223
|
+
Object.entries(config).forEach(([key, value]) => {
|
|
1224
|
+
// Handle nested endpointConfig object
|
|
1225
|
+
if (key === 'endpointConfig' && typeof value === 'object' && value !== null) {
|
|
1226
|
+
const endpointMapping = {
|
|
1227
|
+
initialInstanceCount: 'endpointInitialInstanceCount',
|
|
1228
|
+
dataCapturePercent: 'endpointDataCapturePercent',
|
|
1229
|
+
variantName: 'endpointVariantName',
|
|
1230
|
+
volumeSize: 'endpointVolumeSize'
|
|
1231
|
+
}
|
|
1232
|
+
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
|
|
1233
|
+
const flatKey = endpointMapping[nestedKey]
|
|
1234
|
+
if (flatKey && this._isSourceSupported(flatKey, 'configFile')) {
|
|
1235
|
+
filteredConfig[flatKey] = nestedValue
|
|
1236
|
+
this._recordSource(flatKey, nestedValue, 'config-file')
|
|
1237
|
+
}
|
|
1238
|
+
})
|
|
1239
|
+
return
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Handle nested icConfig object
|
|
1243
|
+
if (key === 'icConfig' && typeof value === 'object' && value !== null) {
|
|
1244
|
+
const icMapping = {
|
|
1245
|
+
cpuCount: 'icCpuCount',
|
|
1246
|
+
memorySize: 'icMemorySize',
|
|
1247
|
+
gpuCount: 'icGpuCount',
|
|
1248
|
+
copyCount: 'icCopyCount',
|
|
1249
|
+
modelWeight: 'icModelWeight'
|
|
1250
|
+
}
|
|
1251
|
+
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
|
|
1252
|
+
const flatKey = icMapping[nestedKey]
|
|
1253
|
+
if (flatKey && this._isSourceSupported(flatKey, 'configFile')) {
|
|
1254
|
+
filteredConfig[flatKey] = nestedValue
|
|
1255
|
+
this._recordSource(flatKey, nestedValue, 'config-file')
|
|
1256
|
+
}
|
|
1257
|
+
})
|
|
1258
|
+
return
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Handle modelEnvVars object (merge with CLI, CLI takes precedence)
|
|
1262
|
+
if (key === 'modelEnvVars' && typeof value === 'object' && value !== null) {
|
|
1263
|
+
if (!this.config.modelEnvVars) {
|
|
1264
|
+
this.config.modelEnvVars = {}
|
|
1265
|
+
}
|
|
1266
|
+
// Only set keys not already provided by CLI (CLI has higher precedence)
|
|
1267
|
+
const cliModelEnvVars = (this.explicitConfig && this.explicitConfig.modelEnvVars) || {}
|
|
1268
|
+
Object.entries(value).forEach(([envKey, envValue]) => {
|
|
1269
|
+
if (!(envKey in cliModelEnvVars)) {
|
|
1270
|
+
this.config.modelEnvVars[envKey] = envValue
|
|
1271
|
+
this._recordSource(`modelEnvVars.${envKey}`, envValue, 'config-file')
|
|
1272
|
+
}
|
|
1273
|
+
})
|
|
1274
|
+
return
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// Handle serverEnvVars object (merge with CLI, CLI takes precedence)
|
|
1278
|
+
if (key === 'serverEnvVars' && typeof value === 'object' && value !== null) {
|
|
1279
|
+
if (!this.config.serverEnvVars) {
|
|
1280
|
+
this.config.serverEnvVars = {}
|
|
1281
|
+
}
|
|
1282
|
+
// Only set keys not already provided by CLI (CLI has higher precedence)
|
|
1283
|
+
const cliServerEnvVars = (this.explicitConfig && this.explicitConfig.serverEnvVars) || {}
|
|
1284
|
+
Object.entries(value).forEach(([envKey, envValue]) => {
|
|
1285
|
+
if (!(envKey in cliServerEnvVars)) {
|
|
1286
|
+
this.config.serverEnvVars[envKey] = envValue
|
|
1287
|
+
this._recordSource(`serverEnvVars.${envKey}`, envValue, 'config-file')
|
|
1288
|
+
}
|
|
1289
|
+
})
|
|
1290
|
+
return
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (this._isSourceSupported(key, 'configFile')) {
|
|
1294
|
+
filteredConfig[key] = this._parseValue(key, value);
|
|
1295
|
+
this._recordSource(key, this._parseValue(key, value), 'config-file')
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
this._mergeConfig(filteredConfig);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Load from environment variables (filtered by matrix)
|
|
1303
|
+
* @private
|
|
1304
|
+
*/
|
|
1305
|
+
async _loadEnvironmentVariables() {
|
|
1306
|
+
// Build environment variable mapping from parameter matrix
|
|
1307
|
+
const envMapping = {};
|
|
1308
|
+
Object.entries(this.parameterMatrix).forEach(([param, config]) => {
|
|
1309
|
+
if (config.envVar) {
|
|
1310
|
+
envMapping[config.envVar] = { param, ambient: config.ambientEnvVar === true };
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
Object.entries(envMapping).forEach(([envVar, { param: configKey, ambient }]) => {
|
|
1315
|
+
const value = process.env[envVar];
|
|
1316
|
+
if (value !== undefined && value !== '' && this._isSourceSupported(configKey, 'envVar')) {
|
|
1317
|
+
this.config[configKey] = this._parseValue(configKey, value);
|
|
1318
|
+
this._recordSource(configKey, this._parseValue(configKey, value), 'env-var')
|
|
1319
|
+
// Track as explicit configuration — unless the env var is ambient
|
|
1320
|
+
// (e.g. AWS_REGION is commonly set in shells as a default, not an override)
|
|
1321
|
+
if (!ambient) {
|
|
1322
|
+
if (!this.explicitConfig) {
|
|
1323
|
+
this.explicitConfig = {};
|
|
1324
|
+
}
|
|
1325
|
+
this.explicitConfig[configKey] = this._parseValue(configKey, value);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Load from CLI arguments (positional)
|
|
1333
|
+
* @private
|
|
1334
|
+
*/
|
|
1335
|
+
async _loadCliArguments() {
|
|
1336
|
+
// First positional argument is project name
|
|
1337
|
+
if (this.args && this.args.length > 0) {
|
|
1338
|
+
this.config.projectName = this.args[0];
|
|
1339
|
+
// Track as explicit configuration
|
|
1340
|
+
if (!this.explicitConfig) {
|
|
1341
|
+
this.explicitConfig = {};
|
|
1342
|
+
}
|
|
1343
|
+
this.explicitConfig.projectName = this.args[0];
|
|
1344
|
+
// Track that project name came from positional argument (for subdirectory creation)
|
|
1345
|
+
this.projectNameFromArgument = true;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Load from CLI options (highest precedence, filtered by matrix)
|
|
1351
|
+
* @private
|
|
1352
|
+
*/
|
|
1353
|
+
async _loadCliOptions() {
|
|
1354
|
+
const options = this.options;
|
|
1355
|
+
|
|
1356
|
+
// Build CLI option mapping from parameter matrix
|
|
1357
|
+
Object.entries(this.parameterMatrix).forEach(([param, config]) => {
|
|
1358
|
+
if (config.cliOption && options[config.cliOption] !== undefined) {
|
|
1359
|
+
this.config[param] = this._parseValue(param, options[config.cliOption]);
|
|
1360
|
+
this._recordSource(param, this._parseValue(param, options[config.cliOption]), 'cli')
|
|
1361
|
+
// Track as explicit configuration
|
|
1362
|
+
if (!this.explicitConfig) {
|
|
1363
|
+
this.explicitConfig = {};
|
|
1364
|
+
}
|
|
1365
|
+
this.explicitConfig[param] = this._parseValue(param, options[config.cliOption]);
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
// Parse --model-env KEY=VALUE pairs
|
|
1370
|
+
this._parseEnvVarOptions('model-env', 'modelEnvVars');
|
|
1371
|
+
|
|
1372
|
+
// Parse --server-env KEY=VALUE pairs
|
|
1373
|
+
this._parseEnvVarOptions('server-env', 'serverEnvVars');
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Parse --model-env or --server-env CLI options into env var collections.
|
|
1378
|
+
* Supports both array (multiple flags) and single string values.
|
|
1379
|
+
* Performs eager format validation at parse time.
|
|
1380
|
+
* @param {string} optionName - CLI option name (e.g., 'model-env')
|
|
1381
|
+
* @param {string} configKey - Config key to store results (e.g., 'modelEnvVars')
|
|
1382
|
+
* @private
|
|
1383
|
+
*/
|
|
1384
|
+
_parseEnvVarOptions(optionName, configKey) {
|
|
1385
|
+
const rawValue = this.options[optionName];
|
|
1386
|
+
if (rawValue === undefined || rawValue === null) {
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// Normalize to array (may receive a single string or an array)
|
|
1391
|
+
const values = Array.isArray(rawValue) ? rawValue : [rawValue];
|
|
1392
|
+
|
|
1393
|
+
// Initialize collection if not already present
|
|
1394
|
+
if (!this.config[configKey] || typeof this.config[configKey] !== 'object') {
|
|
1395
|
+
this.config[configKey] = {};
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
for (const entry of values) {
|
|
1399
|
+
if (typeof entry !== 'string' || entry.trim() === '') {
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
const { key, value } = parseKeyValue(entry);
|
|
1403
|
+
this.config[configKey][key] = value;
|
|
1404
|
+
this._recordSource(`${configKey}.${key}`, value, 'cli')
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Track as explicit configuration
|
|
1408
|
+
if (Object.keys(this.config[configKey]).length > 0) {
|
|
1409
|
+
if (!this.explicitConfig) {
|
|
1410
|
+
this.explicitConfig = {};
|
|
1411
|
+
}
|
|
1412
|
+
this.explicitConfig[configKey] = { ...this.config[configKey] };
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/**
|
|
1417
|
+
* Query configured MCP servers for unbounded parameter values.
|
|
1418
|
+
* Reads mcpServers from config/mcp.json, spawns each one,
|
|
1419
|
+
* and stores results in mcpSources/mcpChoices.
|
|
1420
|
+
* @private
|
|
1421
|
+
*/
|
|
1422
|
+
async _queryMcpServers() {
|
|
1423
|
+
// No-op: MCP queries now happen on-demand during prompting
|
|
1424
|
+
// via queryMcpServer(). This method is kept for backward compatibility.
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* Query a single named MCP server on-demand with the given context.
|
|
1429
|
+
* Stores results in mcpSources/mcpChoices and returns the result.
|
|
1430
|
+
* @param {string} serverName - Name of the server in mcpServers config
|
|
1431
|
+
* @param {object} context - Context to pass to the MCP tool (e.g. { regionSearch: 'europe' })
|
|
1432
|
+
* @returns {Promise<{ values: object, choices: object } | null>}
|
|
1433
|
+
*/
|
|
1434
|
+
async queryMcpServer(serverName, context = {}) {
|
|
1435
|
+
let mcpServerConfigs;
|
|
1436
|
+
try {
|
|
1437
|
+
const configPath = path.join(GENERATOR_ROOT, 'config', 'mcp.json');
|
|
1438
|
+
if (!fs.existsSync(configPath)) return null;
|
|
1439
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1440
|
+
mcpServerConfigs = config.mcpServers;
|
|
1441
|
+
} catch {
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (!mcpServerConfigs || !mcpServerConfigs[serverName]) return null;
|
|
1446
|
+
|
|
1447
|
+
const smart = this.options.smart === true;
|
|
1448
|
+
const discover = this.options.discover === true;
|
|
1449
|
+
const serverConfig = mcpServerConfigs[serverName];
|
|
1450
|
+
|
|
1451
|
+
// Build a custom McpClient that passes context through
|
|
1452
|
+
const client = new McpClient(serverConfig, {
|
|
1453
|
+
timeout: 15000,
|
|
1454
|
+
parameterMatrix: this.parameterMatrix,
|
|
1455
|
+
smart,
|
|
1456
|
+
discover
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
// Override the _buildContext to merge our search context
|
|
1460
|
+
const origBuildContext = client._buildContext.bind(client);
|
|
1461
|
+
client._buildContext = () => ({ ...origBuildContext(), ...context });
|
|
1462
|
+
|
|
1463
|
+
try {
|
|
1464
|
+
const result = await client.query();
|
|
1465
|
+
await client.close();
|
|
1466
|
+
|
|
1467
|
+
if (!result) {
|
|
1468
|
+
const diag = client.getDiagnosticMessage();
|
|
1469
|
+
if (diag) console.log(` ⚠️ ${serverName}: ${diag}`);
|
|
1470
|
+
return null;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// Store values
|
|
1474
|
+
for (const [param, value] of Object.entries(result.values || {})) {
|
|
1475
|
+
const paramConfig = this.parameterMatrix[param];
|
|
1476
|
+
if (paramConfig && paramConfig.valueSpace === 'unbounded' && paramConfig.mcp === true) {
|
|
1477
|
+
this.mcpSources[param] = {
|
|
1478
|
+
server: serverName,
|
|
1479
|
+
value,
|
|
1480
|
+
timestamp: new Date().toISOString()
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Store choices
|
|
1486
|
+
for (const [param, choices] of Object.entries(result.choices || {})) {
|
|
1487
|
+
const paramConfig = this.parameterMatrix[param];
|
|
1488
|
+
if (paramConfig && paramConfig.valueSpace === 'unbounded' && paramConfig.mcp === true && Array.isArray(choices)) {
|
|
1489
|
+
this.mcpChoices[param] = choices;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
return result;
|
|
1494
|
+
} catch (err) {
|
|
1495
|
+
await client.close().catch(() => {});
|
|
1496
|
+
console.log(` ⚠️ ${serverName}: ${err.message}`);
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* Get the names of configured MCP servers.
|
|
1503
|
+
* @returns {string[]}
|
|
1504
|
+
*/
|
|
1505
|
+
getMcpServerNames() {
|
|
1506
|
+
try {
|
|
1507
|
+
const configPath = path.join(GENERATOR_ROOT, 'config', 'mcp.json');
|
|
1508
|
+
if (!fs.existsSync(configPath)) return [];
|
|
1509
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1510
|
+
return Object.keys(config.mcpServers || {});
|
|
1511
|
+
} catch {
|
|
1512
|
+
return [];
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Merges configuration object into current config
|
|
1518
|
+
* @private
|
|
1519
|
+
*/
|
|
1520
|
+
_mergeConfig(newConfig) {
|
|
1521
|
+
Object.keys(newConfig).forEach(key => {
|
|
1522
|
+
if (newConfig[key] !== undefined && newConfig[key] !== null) {
|
|
1523
|
+
this.config[key] = newConfig[key];
|
|
1524
|
+
// Track as explicit configuration (not default)
|
|
1525
|
+
if (!this.explicitConfig) {
|
|
1526
|
+
this.explicitConfig = {};
|
|
1527
|
+
}
|
|
1528
|
+
this.explicitConfig[key] = newConfig[key];
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* Records a source manifest entry for a parameter.
|
|
1535
|
+
* If the parameter already has an entry, it is replaced (higher-precedence wins).
|
|
1536
|
+
* @param {string} param - Parameter name
|
|
1537
|
+
* @param {*} value - Parameter value
|
|
1538
|
+
* @param {string} source - Source identifier (cli, config-file, registry, env-var, default)
|
|
1539
|
+
* @private
|
|
1540
|
+
*/
|
|
1541
|
+
_recordSource(param, value, source) {
|
|
1542
|
+
const existingIndex = this._sourceManifest.findIndex(entry => entry.param === param)
|
|
1543
|
+
if (existingIndex >= 0) {
|
|
1544
|
+
this._sourceManifest[existingIndex] = { param, value, source }
|
|
1545
|
+
} else {
|
|
1546
|
+
this._sourceManifest.push({ param, value, source })
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Checks if we have enough configuration to skip prompts
|
|
1552
|
+
* Non-promptable parameters are not required for this check since they can be auto-generated
|
|
1553
|
+
* @private
|
|
1554
|
+
*/
|
|
1555
|
+
_hasCompleteConfiguration() {
|
|
1556
|
+
// Only check promptable required parameters
|
|
1557
|
+
const promptableRequired = Object.entries(this.parameterMatrix)
|
|
1558
|
+
.filter(([_param, config]) => config.required && config.promptable)
|
|
1559
|
+
.map(([param]) => param);
|
|
1560
|
+
|
|
1561
|
+
// Special case: modelFormat is not required for transformers/triton/diffusors architectures
|
|
1562
|
+
const requiredForConfig = promptableRequired.filter(param => {
|
|
1563
|
+
if (param === 'modelFormat') {
|
|
1564
|
+
const architecture = this.config.architecture
|
|
1565
|
+
if (architecture === 'transformers' || architecture === 'triton' || architecture === 'diffusors') {
|
|
1566
|
+
return false
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return true;
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
return requiredForConfig.every(key =>
|
|
1573
|
+
this.config[key] !== undefined && this.config[key] !== null
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Validates the current configuration against the parameter matrix
|
|
1579
|
+
* Only reports errors for parameters that cannot be resolved through prompting or auto-generation
|
|
1580
|
+
* @returns {Array} Array of validation errors
|
|
1581
|
+
*/
|
|
1582
|
+
validateConfiguration() {
|
|
1583
|
+
const errors = [];
|
|
1584
|
+
|
|
1585
|
+
// Old-format deployment-config migration messages
|
|
1586
|
+
const oldFormatMigration = {
|
|
1587
|
+
'sklearn-flask': 'Use --deployment-config=http-flask --engine=sklearn instead',
|
|
1588
|
+
'sklearn-fastapi': 'Use --deployment-config=http-fastapi --engine=sklearn instead',
|
|
1589
|
+
'xgboost-flask': 'Use --deployment-config=http-flask --engine=xgboost instead',
|
|
1590
|
+
'xgboost-fastapi': 'Use --deployment-config=http-fastapi --engine=xgboost instead',
|
|
1591
|
+
'tensorflow-flask': 'Use --deployment-config=http-flask --engine=tensorflow instead',
|
|
1592
|
+
'tensorflow-fastapi': 'Use --deployment-config=http-fastapi --engine=tensorflow instead'
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// Validate deployment-config
|
|
1596
|
+
if (this.config.deploymentConfig) {
|
|
1597
|
+
const migrationMsg = oldFormatMigration[this.config.deploymentConfig]
|
|
1598
|
+
if (migrationMsg) {
|
|
1599
|
+
errors.push(`Unsupported deployment-config: ${this.config.deploymentConfig}. This value has been replaced. ${migrationMsg}`)
|
|
1600
|
+
} else if (!this.deploymentConfigResolver.isValid(this.config.deploymentConfig)) {
|
|
1601
|
+
const valid = this.deploymentConfigResolver.getAllConfigs().join(', ')
|
|
1602
|
+
errors.push(`Unsupported deployment-config: ${this.config.deploymentConfig}. Valid configs: ${valid}`)
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// Validate engine (only valid for http architecture)
|
|
1607
|
+
if (this.config.engine) {
|
|
1608
|
+
const validEngines = ['sklearn', 'xgboost', 'tensorflow']
|
|
1609
|
+
if (!validEngines.includes(this.config.engine)) {
|
|
1610
|
+
errors.push(`Unsupported engine: ${this.config.engine}. Supported: ${validEngines.join(', ')}`)
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// Validate model format based on architecture/engine
|
|
1615
|
+
if (this.config.modelFormat && this.config.deploymentConfig) {
|
|
1616
|
+
try {
|
|
1617
|
+
const parts = this.deploymentConfigResolver.decompose(this.config.deploymentConfig)
|
|
1618
|
+
if (parts.architecture === 'http') {
|
|
1619
|
+
const engine = this.config.engine || parts.engine
|
|
1620
|
+
if (engine) {
|
|
1621
|
+
const supportedOptions = this._getSupportedOptions()
|
|
1622
|
+
const validFormats = supportedOptions.modelFormats[engine] || []
|
|
1623
|
+
if (validFormats.length > 0 && !validFormats.includes(this.config.modelFormat)) {
|
|
1624
|
+
errors.push(`Unsupported model format '${this.config.modelFormat}' for engine '${engine}'. Supported: ${validFormats.join(', ')}`)
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
} catch {
|
|
1629
|
+
// deploymentConfig already flagged as invalid above
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// Validate AWS Role ARN format if provided
|
|
1634
|
+
if (this.config.awsRoleArn) {
|
|
1635
|
+
try {
|
|
1636
|
+
this._isValidArn(this.config.awsRoleArn);
|
|
1637
|
+
} catch (error) {
|
|
1638
|
+
if (error instanceof ValidationError) {
|
|
1639
|
+
errors.push(error.message);
|
|
1640
|
+
} else {
|
|
1641
|
+
errors.push(`Invalid AWS Role ARN format: ${this.config.awsRoleArn}. Expected format: arn:aws:iam::123456789012:role/RoleName`);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// Validate build target (renamed from deployTarget)
|
|
1647
|
+
const buildTarget = this.config.buildTarget || this.config.deployTarget;
|
|
1648
|
+
if (buildTarget && !this._getSupportedOptions().buildTargets.includes(buildTarget)) {
|
|
1649
|
+
errors.push(`Unsupported build target: ${buildTarget}. Supported targets: ${this._getSupportedOptions().buildTargets.join(', ')}`);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Validate CodeBuild compute type
|
|
1653
|
+
if (this.config.codebuildComputeType && !this._getSupportedOptions().codebuildComputeTypes.includes(this.config.codebuildComputeType)) {
|
|
1654
|
+
errors.push(`Unsupported CodeBuild compute type: ${this.config.codebuildComputeType}. Supported types: ${this._getSupportedOptions().codebuildComputeTypes.join(', ')}`);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// Validate CodeBuild project name format
|
|
1658
|
+
if (this.config.codebuildProjectName) {
|
|
1659
|
+
const projectNamePattern = /^[a-zA-Z0-9][a-zA-Z0-9\-_]{1,254}$/;
|
|
1660
|
+
if (!projectNamePattern.test(this.config.codebuildProjectName)) {
|
|
1661
|
+
errors.push(`Invalid CodeBuild project name: ${this.config.codebuildProjectName}. Project names must be 2-255 characters, start with a letter or number, and contain only letters, numbers, hyphens, and underscores.`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// Only validate required parameters if we're skipping prompts
|
|
1666
|
+
// If prompts are available, missing parameters can be collected later
|
|
1667
|
+
if (this.skipPrompts) {
|
|
1668
|
+
Object.entries(this.parameterMatrix).forEach(([param, config]) => {
|
|
1669
|
+
if (config.required &&
|
|
1670
|
+
(this.config[param] === null || this.config[param] === undefined)) {
|
|
1671
|
+
|
|
1672
|
+
// Special case: modelFormat is not required for transformers/triton/diffusors
|
|
1673
|
+
if (param === 'modelFormat') {
|
|
1674
|
+
try {
|
|
1675
|
+
const parts = this.deploymentConfigResolver.decompose(this.config.deploymentConfig)
|
|
1676
|
+
if (parts.architecture === 'transformers' || parts.architecture === 'triton' || parts.architecture === 'diffusors') {
|
|
1677
|
+
return
|
|
1678
|
+
}
|
|
1679
|
+
} catch {
|
|
1680
|
+
// If deploymentConfig is invalid, skip this check
|
|
1681
|
+
return
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// Only error for promptable required parameters that have no default and can't be auto-generated
|
|
1686
|
+
if (config.promptable && config.default === null && !this._canAutoGenerate(param)) {
|
|
1687
|
+
errors.push(`Required parameter '${param}' is missing and prompts are disabled`);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
// Validate that modelName is provided for diffusors architecture
|
|
1693
|
+
if (this.config.deploymentConfig) {
|
|
1694
|
+
try {
|
|
1695
|
+
const parts = this.deploymentConfigResolver.decompose(this.config.deploymentConfig)
|
|
1696
|
+
if (parts.architecture === 'diffusors') {
|
|
1697
|
+
const explicitModelName = this.explicitConfig && this.explicitConfig.modelName
|
|
1698
|
+
if (!explicitModelName) {
|
|
1699
|
+
errors.push('Model name is required for diffusors architecture. Use --model-name to specify a HuggingFace diffusion model.')
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
} catch {
|
|
1703
|
+
// deploymentConfig already flagged as invalid above
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// Validate schema-validated parameters (endpoint, iC)
|
|
1709
|
+
Object.entries(this.parameterMatrix).forEach(([param, config]) => {
|
|
1710
|
+
if (config.schemaValidated && this.config[param] !== null && this.config[param] !== undefined) {
|
|
1711
|
+
const result = this.schemaValidator.validate(param, this.config[param], this.config.deploymentTarget)
|
|
1712
|
+
if (!result.valid) {
|
|
1713
|
+
errors.push(result.error)
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
})
|
|
1717
|
+
|
|
1718
|
+
return errors;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
/**
|
|
1722
|
+
* Validates required parameters before file generation
|
|
1723
|
+
* This is called after all configuration sources have been processed and prompting is complete
|
|
1724
|
+
* @param {Object} finalConfig - The complete configuration object
|
|
1725
|
+
* @returns {Array} Array of validation errors for missing required parameters
|
|
1726
|
+
*/
|
|
1727
|
+
validateRequiredParameters(finalConfig) {
|
|
1728
|
+
const errors = [];
|
|
1729
|
+
|
|
1730
|
+
// First, validate individual parameter values
|
|
1731
|
+
Object.entries(finalConfig).forEach(([param, value]) => {
|
|
1732
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
1733
|
+
try {
|
|
1734
|
+
this._validateParameterValue(param, value, finalConfig);
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
if (error instanceof ValidationError) {
|
|
1737
|
+
errors.push(error.message);
|
|
1738
|
+
} else {
|
|
1739
|
+
errors.push(`Invalid value for parameter '${param}': ${error.message}`);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
// Then, validate required parameters are present
|
|
1746
|
+
Object.entries(this.parameterMatrix).forEach(([param, config]) => {
|
|
1747
|
+
if (config.required) {
|
|
1748
|
+
const value = finalConfig[param];
|
|
1749
|
+
const isEmpty = value === null || value === undefined || value === '';
|
|
1750
|
+
|
|
1751
|
+
// Special case: modelFormat is not required for transformers/triton/diffusors
|
|
1752
|
+
if (param === 'modelFormat' && (finalConfig.architecture === 'transformers' || finalConfig.architecture === 'triton' || finalConfig.architecture === 'diffusors')) {
|
|
1753
|
+
return; // Skip validation
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// Special case: instanceType is not required for hyperpod-eks
|
|
1757
|
+
// when not provided (backward compatibility) — but it IS prompted now
|
|
1758
|
+
// so it should normally be present
|
|
1759
|
+
if (param === 'instanceType' && finalConfig.deploymentTarget === 'hyperpod-eks' && !finalConfig.instanceType) {
|
|
1760
|
+
return; // Skip validation only if truly missing for backward compat
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
if (isEmpty) {
|
|
1764
|
+
if (config.promptable) {
|
|
1765
|
+
// Promptable required parameter is missing - this should not happen after prompting
|
|
1766
|
+
errors.push(`Required parameter '${param}' is missing. This parameter is required for ${finalConfig.architecture || 'the selected'} architecture.`);
|
|
1767
|
+
} else {
|
|
1768
|
+
// Non-promptable required parameter is missing - this is a configuration error
|
|
1769
|
+
errors.push(`Required non-promptable parameter '${param}' is missing. This parameter must be provided through CLI options, environment variables, or configuration files.`);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
// Finally, validate parameter combinations and dependencies
|
|
1776
|
+
const combinationErrors = this._validateParameterCombinations(finalConfig);
|
|
1777
|
+
errors.push(...combinationErrors);
|
|
1778
|
+
|
|
1779
|
+
return errors;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/**
|
|
1783
|
+
* Validates parameter combinations and dependencies
|
|
1784
|
+
* @param {Object} config - The configuration object to validate
|
|
1785
|
+
* @returns {Array} Array of validation errors for invalid combinations
|
|
1786
|
+
* @private
|
|
1787
|
+
*/
|
|
1788
|
+
_validateParameterCombinations(config) {
|
|
1789
|
+
const errors = [];
|
|
1790
|
+
|
|
1791
|
+
// Additional combination validations that aren't covered by individual parameter validation
|
|
1792
|
+
// For example, complex business rules that involve multiple parameters
|
|
1793
|
+
|
|
1794
|
+
// Validate that transformers architecture has sample model disabled
|
|
1795
|
+
if (config.architecture === 'transformers' && config.includeSampleModel === true) {
|
|
1796
|
+
errors.push(`Architecture '${config.architecture}' does not support sample models. The 'includeSampleModel' parameter will be automatically set to false.`);
|
|
1797
|
+
}
|
|
1798
|
+
// Validate that diffusors architecture has sample model disabled
|
|
1799
|
+
if (config.architecture === 'diffusors' && config.includeSampleModel === true) {
|
|
1800
|
+
errors.push(`Architecture '${config.architecture}' does not support sample models. The 'includeSampleModel' parameter will be automatically set to false.`);
|
|
1801
|
+
}
|
|
1802
|
+
// Validate that ineligible Triton backends have sample model disabled
|
|
1803
|
+
if (config.architecture === 'triton' && config.includeSampleModel === true) {
|
|
1804
|
+
const backendMeta = tritonBackends[config.backend];
|
|
1805
|
+
if (!backendMeta || !backendMeta.supportsSampleModel) {
|
|
1806
|
+
errors.push(`Triton backend '${config.backend}' does not support sample models. The 'includeSampleModel' parameter will be automatically set to false.`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
return errors;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* Checks if a parameter can be auto-generated when missing
|
|
1815
|
+
* @param {string} param - Parameter name
|
|
1816
|
+
* @returns {boolean} True if parameter can be auto-generated
|
|
1817
|
+
* @private
|
|
1818
|
+
*/
|
|
1819
|
+
_canAutoGenerate(param) {
|
|
1820
|
+
// Parameters that can be auto-generated even when missing
|
|
1821
|
+
const autoGeneratable = [
|
|
1822
|
+
'modelFormat', // Can be inferred from engine
|
|
1823
|
+
'includeSampleModel', // Has default
|
|
1824
|
+
'includeTesting', // Has default
|
|
1825
|
+
'instanceType' // Has default
|
|
1826
|
+
];
|
|
1827
|
+
|
|
1828
|
+
return autoGeneratable.includes(param);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* Generates a project name based on framework
|
|
1833
|
+
* @param {string} framework - The ML framework
|
|
1834
|
+
* @returns {string} Generated project name
|
|
1835
|
+
* @private
|
|
1836
|
+
*/
|
|
1837
|
+
_generateProjectName(architecture) {
|
|
1838
|
+
const adjectives = [
|
|
1839
|
+
'smart', 'fast', 'clever', 'bright', 'swift', 'agile', 'sharp', 'quick',
|
|
1840
|
+
'wise', 'keen', 'bold', 'sleek', 'neat', 'cool', 'fresh', 'prime'
|
|
1841
|
+
];
|
|
1842
|
+
|
|
1843
|
+
const architectureNames = {
|
|
1844
|
+
'http': ['http', 'api', 'serve'],
|
|
1845
|
+
'transformers': ['llm', 'transformer', 'gpt', 'bert', 'ai'],
|
|
1846
|
+
'triton': ['triton', 'inference', 'nvidia'],
|
|
1847
|
+
'diffusors': ['diffusion', 'image', 'vllm-omni']
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
const suffixes = [
|
|
1851
|
+
'model', 'predictor', 'classifier', 'engine', 'service', 'api',
|
|
1852
|
+
'container', 'deployment', 'inference', 'ml', 'ai', 'bot'
|
|
1853
|
+
];
|
|
1854
|
+
|
|
1855
|
+
// Get random elements
|
|
1856
|
+
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
|
|
1857
|
+
const archName = architectureNames[architecture] ?
|
|
1858
|
+
architectureNames[architecture][Math.floor(Math.random() * architectureNames[architecture].length)] :
|
|
1859
|
+
'ml';
|
|
1860
|
+
const suffix = suffixes[Math.floor(Math.random() * suffixes.length)];
|
|
1861
|
+
|
|
1862
|
+
return `${adjective}-${archName}-${suffix}`;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
/**
|
|
1866
|
+
* Generates a descriptive CodeBuild project name
|
|
1867
|
+
* @param {string} projectName - The main project name
|
|
1868
|
+
* @param {string} framework - The ML framework being used
|
|
1869
|
+
* @returns {string} Generated CodeBuild project name
|
|
1870
|
+
* @private
|
|
1871
|
+
*/
|
|
1872
|
+
_generateCodeBuildProjectName(projectName, architecture) {
|
|
1873
|
+
const architectureMap = {
|
|
1874
|
+
'http': 'http',
|
|
1875
|
+
'transformers': 'llm',
|
|
1876
|
+
'triton': 'triton',
|
|
1877
|
+
'diffusors': 'diffusion'
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
const archName = architectureMap[architecture] || 'ml';
|
|
1881
|
+
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
|
|
1882
|
+
|
|
1883
|
+
// Create a descriptive name that indicates it's a build project
|
|
1884
|
+
const buildProjectName = `${projectName}-${archName}-build-${timestamp}`;
|
|
1885
|
+
|
|
1886
|
+
// Ensure it meets AWS CodeBuild naming requirements (2-255 chars, alphanumeric + hyphens/underscores)
|
|
1887
|
+
return buildProjectName
|
|
1888
|
+
.toLowerCase()
|
|
1889
|
+
.replace(/[^a-z0-9\-_]/g, '-') // Replace invalid chars with hyphens
|
|
1890
|
+
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
|
1891
|
+
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
|
|
1892
|
+
.slice(0, 255); // Ensure max length
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
/**
|
|
1896
|
+
* Validates a single parameter value
|
|
1897
|
+
* @param {string} parameter - Parameter name
|
|
1898
|
+
* @param {*} value - Parameter value
|
|
1899
|
+
* @param {Object} context - Additional context (e.g., other parameter values)
|
|
1900
|
+
* @throws {ValidationError} If parameter value is invalid
|
|
1901
|
+
* @private
|
|
1902
|
+
*/
|
|
1903
|
+
_validateParameterValue(parameter, value, context = {}) {
|
|
1904
|
+
const supportedOptions = this._getSupportedOptions();
|
|
1905
|
+
|
|
1906
|
+
switch (parameter) {
|
|
1907
|
+
case 'deploymentConfig':
|
|
1908
|
+
if (value) {
|
|
1909
|
+
// Check for old-format configs with migration messages
|
|
1910
|
+
const oldFormatMigration = {
|
|
1911
|
+
'sklearn-flask': 'Use --deployment-config=http-flask --engine=sklearn instead',
|
|
1912
|
+
'sklearn-fastapi': 'Use --deployment-config=http-fastapi --engine=sklearn instead',
|
|
1913
|
+
'xgboost-flask': 'Use --deployment-config=http-flask --engine=xgboost instead',
|
|
1914
|
+
'xgboost-fastapi': 'Use --deployment-config=http-fastapi --engine=xgboost instead',
|
|
1915
|
+
'tensorflow-flask': 'Use --deployment-config=http-flask --engine=tensorflow instead',
|
|
1916
|
+
'tensorflow-fastapi': 'Use --deployment-config=http-fastapi --engine=tensorflow instead'
|
|
1917
|
+
}
|
|
1918
|
+
const migrationMsg = oldFormatMigration[value]
|
|
1919
|
+
if (migrationMsg) {
|
|
1920
|
+
throw new ValidationError(
|
|
1921
|
+
`Unsupported deployment-config: ${value}. This value has been replaced. ${migrationMsg}`,
|
|
1922
|
+
parameter,
|
|
1923
|
+
value
|
|
1924
|
+
)
|
|
1925
|
+
}
|
|
1926
|
+
if (!this.deploymentConfigResolver.isValid(value)) {
|
|
1927
|
+
const valid = this.deploymentConfigResolver.getAllConfigs().join(', ')
|
|
1928
|
+
throw new ValidationError(
|
|
1929
|
+
`Unsupported deployment-config: ${value}. Valid configs: ${valid}`,
|
|
1930
|
+
parameter,
|
|
1931
|
+
value
|
|
1932
|
+
)
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
break
|
|
1936
|
+
|
|
1937
|
+
case 'engine':
|
|
1938
|
+
if (value) {
|
|
1939
|
+
const validEngines = ['sklearn', 'xgboost', 'tensorflow']
|
|
1940
|
+
if (!validEngines.includes(value)) {
|
|
1941
|
+
throw new ValidationError(
|
|
1942
|
+
`Unsupported engine: ${value}. Supported: ${validEngines.join(', ')}`,
|
|
1943
|
+
parameter,
|
|
1944
|
+
value
|
|
1945
|
+
)
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
break
|
|
1949
|
+
|
|
1950
|
+
case 'modelFormat':
|
|
1951
|
+
if (value && context.architecture === 'http' && context.engine) {
|
|
1952
|
+
const validFormats = supportedOptions.modelFormats[context.engine] || [];
|
|
1953
|
+
if (validFormats.length > 0 && !validFormats.includes(value)) {
|
|
1954
|
+
throw new ValidationError(
|
|
1955
|
+
`Model format '${value}' is not compatible with engine '${context.engine}'. Compatible formats: ${validFormats.join(', ')}`,
|
|
1956
|
+
parameter,
|
|
1957
|
+
value
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
break;
|
|
1962
|
+
|
|
1963
|
+
case 'instanceType':
|
|
1964
|
+
if (value) {
|
|
1965
|
+
// Validate AWS SageMaker instance type format
|
|
1966
|
+
const instancePattern = /^ml\.[a-z0-9]+\.(nano|micro|small|medium|large|xlarge|[0-9]+xlarge)$/;
|
|
1967
|
+
if (!instancePattern.test(value)) {
|
|
1968
|
+
throw new ValidationError(
|
|
1969
|
+
`Invalid instance type format: ${value}. Expected format: ml.{family}.{size} (e.g., ml.m5.large, ml.g4dn.xlarge)`,
|
|
1970
|
+
parameter,
|
|
1971
|
+
value
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
// Warn about CPU instances for transformers/triton (but don't block)
|
|
1975
|
+
if (context.architecture === 'transformers' || context.architecture === 'triton') {
|
|
1976
|
+
const cpuFamilies = ['t2', 't3', 't3a', 't4g', 'm4', 'm5', 'm5a', 'm5ad', 'm5d', 'm5dn', 'm5n', 'm5zn', 'm6a', 'm6g', 'm6gd', 'm6i', 'm6id', 'm6idn', 'm6in', 'c4', 'c5', 'c5a', 'c5ad', 'c5d', 'c5n', 'c6a', 'c6g', 'c6gd', 'c6gn', 'c6i', 'c6id', 'c6in', 'r4', 'r5', 'r5a', 'r5ad', 'r5b', 'r5d', 'r5dn', 'r5n', 'r6a', 'r6g', 'r6gd', 'r6i', 'r6id', 'r6idn', 'r6in'];
|
|
1977
|
+
const instanceFamily = value.split('.')[1];
|
|
1978
|
+
if (cpuFamilies.includes(instanceFamily)) {
|
|
1979
|
+
console.warn(`⚠️ Warning: Using CPU instance ${value} with ${context.architecture} architecture. GPU instances are recommended for better performance.`);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
break;
|
|
1984
|
+
|
|
1985
|
+
case 'awsRegion':
|
|
1986
|
+
if (value && !supportedOptions.awsRegions.includes(value)) {
|
|
1987
|
+
throw new ValidationError(
|
|
1988
|
+
`Unsupported AWS region: ${value}. Supported regions: ${supportedOptions.awsRegions.join(', ')}`,
|
|
1989
|
+
parameter,
|
|
1990
|
+
value
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
break;
|
|
1994
|
+
|
|
1995
|
+
case 'awsRoleArn':
|
|
1996
|
+
if (value) {
|
|
1997
|
+
this._isValidArn(value);
|
|
1998
|
+
}
|
|
1999
|
+
break;
|
|
2000
|
+
|
|
2001
|
+
case 'buildTarget':
|
|
2002
|
+
case 'deployTarget':
|
|
2003
|
+
if (value && !supportedOptions.buildTargets.includes(value)) {
|
|
2004
|
+
throw new ValidationError(
|
|
2005
|
+
`Unsupported build target: ${value}. Supported targets: ${supportedOptions.buildTargets.join(', ')}`,
|
|
2006
|
+
parameter,
|
|
2007
|
+
value
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
break;
|
|
2011
|
+
|
|
2012
|
+
case 'codebuildComputeType':
|
|
2013
|
+
if (value && !supportedOptions.codebuildComputeTypes.includes(value)) {
|
|
2014
|
+
throw new ValidationError(
|
|
2015
|
+
`Unsupported CodeBuild compute type: ${value}. Supported types: ${supportedOptions.codebuildComputeTypes.join(', ')}`,
|
|
2016
|
+
parameter,
|
|
2017
|
+
value
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
break;
|
|
2021
|
+
|
|
2022
|
+
case 'codebuildProjectName':
|
|
2023
|
+
if (value) {
|
|
2024
|
+
// AWS CodeBuild project names must follow specific naming rules
|
|
2025
|
+
const projectNamePattern = /^[a-zA-Z0-9][a-zA-Z0-9\-_]{1,254}$/;
|
|
2026
|
+
if (!projectNamePattern.test(value)) {
|
|
2027
|
+
throw new ValidationError(
|
|
2028
|
+
`Invalid CodeBuild project name: ${value}. Project names must be 2-255 characters, start with a letter or number, and contain only letters, numbers, hyphens, and underscores.`,
|
|
2029
|
+
parameter,
|
|
2030
|
+
value
|
|
2031
|
+
);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
break;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
/**
|
|
2039
|
+
* Resolves HF_TOKEN references to actual token values
|
|
2040
|
+
* @param {string} tokenValue - The token value or "$HF_TOKEN" reference
|
|
2041
|
+
* @returns {string|null} Resolved token value
|
|
2042
|
+
* @private
|
|
2043
|
+
*/
|
|
2044
|
+
_resolveHfToken(tokenValue) {
|
|
2045
|
+
if (!tokenValue || tokenValue.trim() === '') {
|
|
2046
|
+
return null;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// Check if it's an environment variable reference
|
|
2050
|
+
if (tokenValue.trim() === '$HF_TOKEN') {
|
|
2051
|
+
const envToken = process.env.HF_TOKEN;
|
|
2052
|
+
if (!envToken) {
|
|
2053
|
+
console.warn('⚠️ Warning: $HF_TOKEN specified but HF_TOKEN environment variable is not set');
|
|
2054
|
+
console.warn(' The container will be built without authentication.');
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
return envToken;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// Direct token value
|
|
2061
|
+
return tokenValue;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
/**
|
|
2065
|
+
* Validates AWS Role ARN format
|
|
2066
|
+
* @param {string} arn - The ARN to validate
|
|
2067
|
+
* @throws {ValidationError} If ARN format is invalid
|
|
2068
|
+
* @private
|
|
2069
|
+
*/
|
|
2070
|
+
_isValidArn(arn) {
|
|
2071
|
+
const arnPattern = /^arn:aws:iam::\d{12}:role\/[\w+=,.@-]+$/;
|
|
2072
|
+
if (!arnPattern.test(arn)) {
|
|
2073
|
+
throw new ValidationError(
|
|
2074
|
+
`Invalid AWS Role ARN format: ${arn}. Expected format: arn:aws:iam::123456789012:role/RoleName`,
|
|
2075
|
+
'awsRoleArn',
|
|
2076
|
+
arn
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
return true;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
/**
|
|
2083
|
+
* Gets supported options for validation
|
|
2084
|
+
* @private
|
|
2085
|
+
*/
|
|
2086
|
+
_getSupportedOptions() {
|
|
2087
|
+
return {
|
|
2088
|
+
deploymentConfigs: this.deploymentConfigResolver.getAllConfigs(),
|
|
2089
|
+
engines: ['sklearn', 'xgboost', 'tensorflow'],
|
|
2090
|
+
modelFormats: {
|
|
2091
|
+
'sklearn': ['pkl', 'joblib'],
|
|
2092
|
+
'xgboost': ['json', 'model', 'ubj'],
|
|
2093
|
+
'tensorflow': ['keras', 'h5', 'SavedModel']
|
|
2094
|
+
},
|
|
2095
|
+
buildTargets: ['codebuild'],
|
|
2096
|
+
codebuildComputeTypes: ['BUILD_GENERAL1_SMALL', 'BUILD_GENERAL1_MEDIUM', 'BUILD_GENERAL1_LARGE'],
|
|
2097
|
+
awsRegions: [
|
|
2098
|
+
'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
|
|
2099
|
+
'eu-west-1', 'eu-west-2', 'eu-central-1', 'eu-north-1',
|
|
2100
|
+
'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1',
|
|
2101
|
+
'ca-central-1', 'sa-east-1'
|
|
2102
|
+
]
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
|