badgr-cli 1.0.32 → 1.0.34
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/README.md +27 -16
- package/package.json +3 -3
- package/src/api.js +2 -2
- package/src/commands/receipts.js +4 -0
- package/src/commands/run.js +50 -43
- package/src/commands/serve.js +66 -7
- package/src/errors.js +299 -0
- package/src/fallback.js +73 -33
- package/src/router.js +2 -1
- package/tests/commands.test.js +12 -0
- package/tests/errors.test.js +130 -0
- package/tests/launch-readiness.test.js +326 -0
- package/tests/run-lifecycle.test.js +14 -4
package/README.md
CHANGED
|
@@ -61,11 +61,11 @@ badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S --region EU
|
|
|
61
61
|
|
|
62
62
|
| Flag | Default | Description |
|
|
63
63
|
|------|---------|-------------|
|
|
64
|
-
| `--gpu <type>` | auto | GPU type — Badgr
|
|
64
|
+
| `--gpu <type>` | auto | GPU type override — Badgr Auto selects based on model size if omitted |
|
|
65
65
|
| `--image <img>` | — | Serve a custom container instead of a HuggingFace model |
|
|
66
66
|
| `--task <task>` | — | vLLM task override, e.g. `embed` for embedding models |
|
|
67
67
|
| `--env KEY=VALUE` | — | Environment variable (repeatable) |
|
|
68
|
-
| `--tier 1\|2` | 1 | `1` =
|
|
68
|
+
| `--tier 1\|2` | 1 | `1` = reliable execution (default); `2` = lower-cost burst capacity |
|
|
69
69
|
| `--count <n>` | 1 | Number of GPUs (1–8) |
|
|
70
70
|
| `--region US\|EU\|AU` | — | Optional region preference. If omitted, Badgr chooses best available capacity. |
|
|
71
71
|
| `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
|
|
@@ -83,10 +83,11 @@ badgr run python train.py --gpu A100 --env HF_TOKEN=$HF_TOKEN --max-runtime 60
|
|
|
83
83
|
|
|
84
84
|
| Flag | Default | Description |
|
|
85
85
|
|------|---------|-------------|
|
|
86
|
-
| `--gpu <type>` | auto | GPU type — Badgr
|
|
86
|
+
| `--gpu <type>` | auto | GPU type override — Badgr Auto selects if omitted |
|
|
87
|
+
| `--min-vram <GB>` | — | Minimum VRAM in GB — optional constraint for Auto routing |
|
|
87
88
|
| `--image <img>` | python:3.11-slim | Docker image |
|
|
88
89
|
| `--env KEY=VALUE` | — | Environment variable (repeatable) |
|
|
89
|
-
| `--tier 1\|2` | 1 | `1` =
|
|
90
|
+
| `--tier 1\|2` | 1 | `1` = reliable execution (default); `2` = lower-cost burst capacity |
|
|
90
91
|
| `--count <n>` | 1 | Number of GPUs |
|
|
91
92
|
| `--region US\|EU\|AU` | — | Optional region preference. If omitted, Badgr chooses best available capacity. |
|
|
92
93
|
| `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
|
|
@@ -98,16 +99,18 @@ badgr run python train.py --gpu A100 --env HF_TOKEN=$HF_TOKEN --max-runtime 60
|
|
|
98
99
|
|
|
99
100
|
## Routing
|
|
100
101
|
|
|
101
|
-
Badgr
|
|
102
|
+
Badgr Auto selects the best eligible route based on GPU type, VRAM, availability, region, workload requirements, and reliability. Advanced users can optionally choose an execution tier or hardware constraint.
|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
Most users should use the default Badgr Auto route. Tiers are an optional advanced control.
|
|
104
105
|
|
|
105
|
-
**Tier
|
|
106
|
+
**Tier 1** (default) — Reliable execution. Best for production workloads, model serving, and jobs where startup reliability matters most. Uses managed routing, readiness checks, fallback, and teardown controls.
|
|
107
|
+
|
|
108
|
+
**Tier 2** — Lower-cost burst execution. An optional advanced control for cost-sensitive workloads. Availability may vary.
|
|
106
109
|
|
|
107
110
|
Preview before provisioning:
|
|
108
111
|
|
|
109
112
|
```bash
|
|
110
|
-
badgr serve
|
|
113
|
+
badgr serve meta-llama/Llama-3.1-8B-Instruct --dry-run
|
|
111
114
|
```
|
|
112
115
|
|
|
113
116
|
---
|
|
@@ -154,15 +157,23 @@ const client = new OpenAI({
|
|
|
154
157
|
|
|
155
158
|
## GPU options
|
|
156
159
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
|
162
|
-
|
|
163
|
-
|
|
|
160
|
+
Badgr Auto selects the best eligible GPU for your workload. Add `--gpu <type>` or `--min-vram <GB>` only when you need more control.
|
|
161
|
+
|
|
162
|
+
Available GPU types may vary by region and current capacity. Run `badgr capacity` or use `--dry-run` to confirm availability before provisioning.
|
|
163
|
+
|
|
164
|
+
| Flag value | GPU | VRAM | Best for |
|
|
165
|
+
|-----------|-----|------|---------|
|
|
166
|
+
| RTX_3090 | NVIDIA RTX 3090 | 24 GB | Dev, inference |
|
|
167
|
+
| RTX_4090 | NVIDIA RTX 4090 | 24 GB | Inference, training, dev |
|
|
168
|
+
| L40S | NVIDIA L40S | 48 GB | Inference, vLLM, embeddings |
|
|
169
|
+
| A100 | NVIDIA A100 | 40–80 GB | Training, inference |
|
|
170
|
+
| H100 | NVIDIA H100 | 80 GB | Large model training |
|
|
171
|
+
|
|
172
|
+
Additional GPU types may be routable depending on current capacity — check with `badgr capacity`.
|
|
173
|
+
|
|
174
|
+
Pricing is confirmed before provisioning. Use `--dry-run` to see pricing before committing.
|
|
164
175
|
|
|
165
|
-
|
|
176
|
+
Full GPU support details: see [GPU_SUPPORT.md](../../GPU_SUPPORT.md) in the repo root.
|
|
166
177
|
|
|
167
178
|
---
|
|
168
179
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "badgr-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.34",
|
|
4
4
|
"description": "Badgr, run or serve GPU workloads from one command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"test:watch": "vitest"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@inquirer/prompts": "^5.
|
|
15
|
+
"@inquirer/prompts": "^8.5.2",
|
|
16
16
|
"chalk": "^5.3.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"vitest": "^1.
|
|
19
|
+
"vitest": "^4.1.8"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=18.0.0"
|
package/src/api.js
CHANGED
|
@@ -105,7 +105,7 @@ export function runJob(config, body) {
|
|
|
105
105
|
apiKey: config.apiKey,
|
|
106
106
|
baseUrl: config.baseUrl,
|
|
107
107
|
body,
|
|
108
|
-
timeoutMs:
|
|
108
|
+
timeoutMs: 75_000,
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -115,7 +115,7 @@ export function serveModel(config, body) {
|
|
|
115
115
|
apiKey: config.apiKey,
|
|
116
116
|
baseUrl: config.baseUrl,
|
|
117
117
|
body,
|
|
118
|
-
timeoutMs:
|
|
118
|
+
timeoutMs: 75_000,
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
|
package/src/commands/receipts.js
CHANGED
|
@@ -33,6 +33,10 @@ function printReceipt(r, chalk) {
|
|
|
33
33
|
if (r.retries !== undefined) console.log(` ${chalk.bold('retries:')} ${r.retries}`);
|
|
34
34
|
if (r.route?.ratePerHour) console.log(` ${chalk.bold('rate:')} $${r.route.ratePerHour.toFixed(4)}/hr`);
|
|
35
35
|
|
|
36
|
+
if (r.failureCategory ?? r.failure_category ?? r.failure_reason) {
|
|
37
|
+
const cat = r.failureCategory ?? r.failure_category ?? r.failure_reason;
|
|
38
|
+
console.log(` ${chalk.bold('failure:')} ${chalk.red(cat)}`);
|
|
39
|
+
}
|
|
36
40
|
if (r.status) console.log(` ${chalk.bold('status:')} ${r.status}`);
|
|
37
41
|
console.log(` ${chalk.dim(ts)}`);
|
|
38
42
|
console.log();
|
package/src/commands/run.js
CHANGED
|
@@ -2,6 +2,7 @@ import { requireApiKey } from '../config.js';
|
|
|
2
2
|
import { callApi, terminateDeployment } from '../api.js';
|
|
3
3
|
import { addReceipt, updateReceipt, generateReceiptId } from '../store.js';
|
|
4
4
|
import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD } from '../fallback.js';
|
|
5
|
+
import { formatCliError } from '../errors.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* badgr run python train.py # gpu=auto, attached
|
|
@@ -28,6 +29,7 @@ export function parseRunArgs(args) {
|
|
|
28
29
|
if (args[i] === '--no-expanded-search') { flags.noFallback = true; i++; continue; }
|
|
29
30
|
if (args[i] === '--max-runtime') { flags.maxRuntime = parseFloat(args[++i]); i++; continue; }
|
|
30
31
|
if (args[i] === '--max-cost') { flags.maxCost = parseFloat(args[++i]); i++; continue; }
|
|
32
|
+
if (args[i] === '--min-vram') { flags.minVram = parseFloat(args[++i]); i++; continue; }
|
|
31
33
|
if (args[i] === '--env') {
|
|
32
34
|
const kv = args[++i]; i++;
|
|
33
35
|
if (!flags.env) flags.env = [];
|
|
@@ -324,9 +326,33 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
|
|
|
324
326
|
return { status: 'interrupted', reason: 'signal', exitCode: null, runtimeMs: Date.now() - startMs, failureType: null };
|
|
325
327
|
}
|
|
326
328
|
|
|
329
|
+
// Known badgr run flags — used to detect broken shell line continuation.
|
|
330
|
+
const _KNOWN_RUN_FLAGS = new Set([
|
|
331
|
+
'--gpu', '--image', '--count', '--region', '--tier', '--max-price', '--name',
|
|
332
|
+
'--detach', '--fallback', '--no-fallback', '--strict-capacity',
|
|
333
|
+
'--no-expanded-search', '--max-runtime', '--max-cost', '--min-vram', '--env',
|
|
334
|
+
]);
|
|
335
|
+
|
|
327
336
|
export async function runCommand(config, args, chalk) {
|
|
328
337
|
const { flags, positional } = parseRunArgs(args);
|
|
329
338
|
|
|
339
|
+
// Detect flags that ended up in the command because of broken shell line continuation
|
|
340
|
+
// (e.g. `\ ` with trailing space instead of `\<newline>`).
|
|
341
|
+
const misplaced = positional.filter(a => _KNOWN_RUN_FLAGS.has(a));
|
|
342
|
+
if (misplaced.length > 0) {
|
|
343
|
+
console.error(chalk.red(`\n ✗ These look like badgr flags but were treated as command arguments:`));
|
|
344
|
+
console.error(chalk.red(` ${misplaced.join(', ')}`));
|
|
345
|
+
console.error(chalk.dim(''));
|
|
346
|
+
console.error(chalk.dim(' This usually means a line continuation has a trailing space.'));
|
|
347
|
+
console.error(chalk.dim(' Use a single line, or end each continued line with \\ and no space after:'));
|
|
348
|
+
console.error(chalk.dim(''));
|
|
349
|
+
console.error(chalk.dim(' badgr run python script.py \\'));
|
|
350
|
+
console.error(chalk.dim(' --gpu RTX_4090 --max-cost 10 --max-runtime 60'));
|
|
351
|
+
console.error(chalk.dim(''));
|
|
352
|
+
process.exitCode = 1;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
330
356
|
if (positional.length === 0 && !flags.image) {
|
|
331
357
|
console.error(chalk.red('Usage: badgr run <command...>'));
|
|
332
358
|
console.error(chalk.red(' badgr run --image my/image:latest'));
|
|
@@ -356,7 +382,6 @@ export async function runCommand(config, args, chalk) {
|
|
|
356
382
|
process.exitCode = 1;
|
|
357
383
|
return;
|
|
358
384
|
}
|
|
359
|
-
|
|
360
385
|
const command = positional.length > 0 ? positional : undefined;
|
|
361
386
|
const cmdStr = command ? command.join(' ') : '';
|
|
362
387
|
const isSmoke = cmdStr.length < 80 && /print\s*\(|['"]hello/i.test(cmdStr);
|
|
@@ -368,39 +393,26 @@ export async function runCommand(config, args, chalk) {
|
|
|
368
393
|
const envObj = parseEnvFlag(flags.env);
|
|
369
394
|
const effectiveTier = normalizeTier(flags.tier);
|
|
370
395
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
console.log();
|
|
387
|
-
} else {
|
|
388
|
-
console.log(chalk.bold('\n⚡ Running GPU job\n'));
|
|
389
|
-
if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
|
|
390
|
-
if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
|
|
391
|
-
console.log(` ${chalk.bold('GPU:')} ${gpu}`);
|
|
392
|
-
if (flags.maxRuntime) console.log(` ${chalk.bold('Max runtime:')} ${flags.maxRuntime}min`);
|
|
393
|
-
if (maxCost) console.log(` ${chalk.bold('Max cost:')} $${maxCost.toFixed(2)}`);
|
|
394
|
-
if (flags.maxPrice) console.log(` ${chalk.bold('Max price:')} $${flags.maxPrice.toFixed(2)}/hr`);
|
|
395
|
-
if (detach) console.log(` ${chalk.dim('(detached — returns immediately)')}`);
|
|
396
|
-
if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
|
|
397
|
-
console.log();
|
|
396
|
+
const gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : undefined;
|
|
397
|
+
|
|
398
|
+
console.log(chalk.bold('\n⚡ Running GPU job\n'));
|
|
399
|
+
if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
|
|
400
|
+
if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
|
|
401
|
+
if (gpu) console.log(` ${chalk.bold('GPU:')} ${gpu}`);
|
|
402
|
+
else console.log(` ${chalk.bold('GPU:')} ${chalk.dim('auto')}`);
|
|
403
|
+
if (flags.minVram) console.log(` ${chalk.bold('Min VRAM:')} ${flags.minVram} GB`);
|
|
404
|
+
if (flags.maxRuntime) console.log(` ${chalk.bold('Max runtime:')} ${flags.maxRuntime}min`);
|
|
405
|
+
if (maxCost) console.log(` ${chalk.bold('Max cost:')} $${maxCost.toFixed(2)}`);
|
|
406
|
+
if (flags.maxPrice) console.log(` ${chalk.bold('Max price:')} $${flags.maxPrice.toFixed(2)}/hr`);
|
|
407
|
+
if (detach) console.log(` ${chalk.dim('(detached — returns immediately)')}`);
|
|
408
|
+
if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
|
|
409
|
+
console.log();
|
|
398
410
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
411
|
+
if (!detach && !flags.maxRuntime && !maxCost) {
|
|
412
|
+
console.log(chalk.dim(' Tip: add --max-runtime 60 or --max-cost 5.00 to cap spend automatically'));
|
|
402
413
|
}
|
|
403
414
|
|
|
415
|
+
|
|
404
416
|
console.log(chalk.dim(' Finding suitable capacity...'));
|
|
405
417
|
if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
|
|
406
418
|
console.log(chalk.dim(` API: ${config.baseUrl}`));
|
|
@@ -411,7 +423,8 @@ export async function runCommand(config, args, chalk) {
|
|
|
411
423
|
return {
|
|
412
424
|
command,
|
|
413
425
|
image,
|
|
414
|
-
gpu:
|
|
426
|
+
gpu: gpuOverride || gpu || 'auto',
|
|
427
|
+
...(flags.minVram ? { min_vram: flags.minVram } : {}),
|
|
415
428
|
gpu_count: flags.count || 1,
|
|
416
429
|
...(effectiveRegion ? { region: effectiveRegion } : {}),
|
|
417
430
|
max_price_per_hour: flags.maxPrice,
|
|
@@ -526,8 +539,8 @@ export async function runCommand(config, args, chalk) {
|
|
|
526
539
|
shuttingDown = true;
|
|
527
540
|
try {
|
|
528
541
|
await teardown(reason);
|
|
529
|
-
} catch
|
|
530
|
-
console.error(
|
|
542
|
+
} catch {
|
|
543
|
+
console.error(formatCliError('TEARDOWN_FAILED', { deploymentId: dep.deployment_id, receiptId: rcptId }, chalk));
|
|
531
544
|
}
|
|
532
545
|
process.exit(reason === 'interrupted' ? 0 : 1);
|
|
533
546
|
}
|
|
@@ -542,10 +555,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
542
555
|
|
|
543
556
|
if (dep.status === 'failed') {
|
|
544
557
|
process.removeListener('SIGINT', handleShutdown);
|
|
545
|
-
console.error(
|
|
546
|
-
console.error(chalk.dim(' The backend retried automatically. All attempts failed.'));
|
|
547
|
-
console.error(chalk.dim(` Contact support with receipt ID: ${rcptId}`));
|
|
548
|
-
console.log();
|
|
558
|
+
console.error(formatCliError('JOB_INFRASTRUCTURE_FAILURE', { receiptId: rcptId }, chalk));
|
|
549
559
|
process.exitCode = 1;
|
|
550
560
|
return;
|
|
551
561
|
}
|
|
@@ -597,13 +607,10 @@ export async function runCommand(config, args, chalk) {
|
|
|
597
607
|
|
|
598
608
|
if (finalStatus === 'failed' || (exitCode !== null && exitCode !== 0)) {
|
|
599
609
|
if (failureType === 'infrastructure') {
|
|
600
|
-
console.error(
|
|
601
|
-
console.error(chalk.dim(' Contact support with your receipt ID for a refund.'));
|
|
610
|
+
console.error(formatCliError('JOB_INFRASTRUCTURE_FAILURE', { receiptId: rcptId }, chalk));
|
|
602
611
|
} else {
|
|
603
|
-
console.error(
|
|
604
|
-
console.error(chalk.dim(` Check logs: badgr logs ${dep.deployment_id}`));
|
|
612
|
+
console.error(formatCliError('JOB_FAILED', { exitCode, deploymentId: dep.deployment_id }, chalk));
|
|
605
613
|
}
|
|
606
|
-
console.log();
|
|
607
614
|
process.exitCode = exitCode ?? 1;
|
|
608
615
|
return;
|
|
609
616
|
}
|
package/src/commands/serve.js
CHANGED
|
@@ -2,6 +2,7 @@ import { requireApiKey } from '../config.js';
|
|
|
2
2
|
import { callApi } from '../api.js';
|
|
3
3
|
import { addDeployment, addReceipt, updateReceipt, generateReceiptId } from '../store.js';
|
|
4
4
|
import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD } from '../fallback.js';
|
|
5
|
+
import { formatCliError } from '../errors.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* badgr serve meta-llama/Llama-3.1-8B-Instruct
|
|
@@ -27,6 +28,7 @@ export function parseServeArgs(args) {
|
|
|
27
28
|
if (args[i] === '--no-wait') { flags.noWait = true; i++; continue; }
|
|
28
29
|
if (args[i] === '--max-cost') { flags.maxCost = parseFloat(args[++i]); i++; continue; }
|
|
29
30
|
if (args[i] === '--health-path') { flags.healthPath = args[++i]; i++; continue; }
|
|
31
|
+
if (args[i] === '--check-nodes') { flags.checkNodes = args[++i]; i++; continue; }
|
|
30
32
|
// All three aliases map to noMarketplaceFallback
|
|
31
33
|
if (args[i] === '--no-fallback' ||
|
|
32
34
|
args[i] === '--strict-capacity' ||
|
|
@@ -128,10 +130,61 @@ async function waitForEndpoint(endpointUrl, deploymentId, config, timeoutMs = 5
|
|
|
128
130
|
return { ready: false, timedOut: true, depFailed: false };
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
async function validateComfyNodes(baseUrl, nodeList, chalk) {
|
|
134
|
+
const nodes = nodeList.split(',').map(n => n.trim()).filter(Boolean);
|
|
135
|
+
if (!nodes.length) return;
|
|
136
|
+
const objectInfoUrl = baseUrl.replace(/\/v1\/?$/, '') + '/object_info';
|
|
137
|
+
console.log(chalk.dim(`\n Checking custom nodes via ${objectInfoUrl}…`));
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch(objectInfoUrl, { signal: AbortSignal.timeout(15_000) });
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
console.log(chalk.yellow(` ⚠ Could not verify nodes (HTTP ${res.status})`));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const available = await res.json();
|
|
145
|
+
const found = nodes.filter(n => Object.prototype.hasOwnProperty.call(available, n));
|
|
146
|
+
const missing = nodes.filter(n => !Object.prototype.hasOwnProperty.call(available, n));
|
|
147
|
+
if (found.length) console.log(chalk.green(` ✓ Nodes confirmed: ${found.join(', ')}`));
|
|
148
|
+
if (missing.length) {
|
|
149
|
+
console.log(chalk.yellow(` ⚠ Nodes not found: ${missing.join(', ')}`));
|
|
150
|
+
console.log(chalk.dim(' These may not be installed in this image.'));
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.log(chalk.yellow(` ⚠ Node check failed: ${err.message}`));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Known badgr serve flags — used to detect broken shell line continuation.
|
|
158
|
+
const _KNOWN_SERVE_FLAGS = new Set([
|
|
159
|
+
'--gpu', '--image', '--task', '--count', '--region', '--tier', '--max-price',
|
|
160
|
+
'--name', '--no-wait', '--max-cost', '--health-path', '--check-nodes',
|
|
161
|
+
'--no-fallback', '--strict-capacity', '--no-expanded-search', '--env',
|
|
162
|
+
]);
|
|
163
|
+
|
|
131
164
|
export async function serveCommand(config, args, chalk) {
|
|
132
165
|
const { model, flags } = parseServeArgs(args);
|
|
133
166
|
const customImage = flags.image || null;
|
|
134
167
|
|
|
168
|
+
// Detect flags that ended up as positional args due to broken shell line continuation
|
|
169
|
+
// (e.g. `\ ` with a trailing space instead of `\<newline>`).
|
|
170
|
+
// After parseServeArgs, only the model name should be in positional. Any extra token
|
|
171
|
+
// that matches a known badgr flag name was not parsed — likely a syntax mistake.
|
|
172
|
+
const servePositional = args.filter(a => !a.startsWith('-'));
|
|
173
|
+
const misplaced = servePositional.slice(1).filter(a => _KNOWN_SERVE_FLAGS.has(a));
|
|
174
|
+
if (misplaced.length > 0) {
|
|
175
|
+
console.error(chalk.red(`\n ✗ These look like badgr flags but were treated as arguments:`));
|
|
176
|
+
console.error(chalk.red(` ${misplaced.join(', ')}`));
|
|
177
|
+
console.error(chalk.dim(''));
|
|
178
|
+
console.error(chalk.dim(' This usually means a line continuation has a trailing space.'));
|
|
179
|
+
console.error(chalk.dim(' Use a single line, or end each continued line with \\ and no space after:'));
|
|
180
|
+
console.error(chalk.dim(''));
|
|
181
|
+
console.error(chalk.dim(' badgr serve meta-llama/Llama-3.1-8B-Instruct \\'));
|
|
182
|
+
console.error(chalk.dim(' --gpu L40S --max-cost 10'));
|
|
183
|
+
console.error(chalk.dim(''));
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
135
188
|
if (!model && !customImage) {
|
|
136
189
|
console.error(chalk.red('Usage: badgr serve <model>'));
|
|
137
190
|
console.error(chalk.red(' badgr serve meta-llama/Llama-3.1-8B-Instruct'));
|
|
@@ -302,9 +355,10 @@ export async function serveCommand(config, args, chalk) {
|
|
|
302
355
|
timeoutMs: 10_000,
|
|
303
356
|
});
|
|
304
357
|
if (['failed', 'terminated', 'error'].includes(latest.status)) {
|
|
305
|
-
console.error(
|
|
306
|
-
|
|
307
|
-
|
|
358
|
+
console.error(formatCliError('HEALTH_CHECK_DEPLOY_FAILED', {
|
|
359
|
+
deploymentId: dep.deployment_id,
|
|
360
|
+
failReason: latest.error || latest.status,
|
|
361
|
+
}, chalk));
|
|
308
362
|
updateReceipt(rcptId, { status: 'failed', failReason: latest.error || latest.status });
|
|
309
363
|
process.exitCode = 1;
|
|
310
364
|
return;
|
|
@@ -317,10 +371,10 @@ export async function serveCommand(config, args, chalk) {
|
|
|
317
371
|
process.stdout.write('\n');
|
|
318
372
|
|
|
319
373
|
if (healthResult.depFailed) {
|
|
320
|
-
console.error(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
));
|
|
374
|
+
console.error(formatCliError('HEALTH_CHECK_DEPLOY_FAILED', {
|
|
375
|
+
deploymentId: dep.deployment_id,
|
|
376
|
+
failReason: healthResult.failReason,
|
|
377
|
+
}, chalk));
|
|
324
378
|
updateReceipt(rcptId, { status: 'failed', failReason: healthResult.failReason });
|
|
325
379
|
process.exitCode = 1;
|
|
326
380
|
return;
|
|
@@ -330,6 +384,11 @@ export async function serveCommand(config, args, chalk) {
|
|
|
330
384
|
if (!endpointReady) updateReceipt(rcptId, { status: 'health_check_timeout' });
|
|
331
385
|
}
|
|
332
386
|
|
|
387
|
+
// ── Custom-node validation (ComfyUI) ─────────────────────────────────────
|
|
388
|
+
if (flags.checkNodes && endpointReady) {
|
|
389
|
+
await validateComfyNodes(endpointUrl, flags.checkNodes, chalk);
|
|
390
|
+
}
|
|
391
|
+
|
|
333
392
|
// ── Update receipt with final state ───────────────────────────────────────
|
|
334
393
|
updateReceipt(rcptId, {
|
|
335
394
|
status: endpointReady ? 'ready' : 'starting',
|