badgr-cli 1.0.22 → 1.0.25
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/package.json +1 -1
- package/src/badgr.js +19 -19
- package/src/commands/run.js +29 -39
- package/src/commands/serve.js +32 -29
- package/src/commands/status.js +2 -2
- package/src/commands/test-run.js +79 -19
- package/tests/commands.test.js +29 -16
package/package.json
CHANGED
package/src/badgr.js
CHANGED
|
@@ -17,27 +17,27 @@ const HELP = `
|
|
|
17
17
|
${chalk.bold('badgr')} — run or serve GPU workloads from one command
|
|
18
18
|
|
|
19
19
|
${chalk.bold('COMMANDS')}
|
|
20
|
-
${chalk.cyan('badgr login')}
|
|
21
|
-
${chalk.cyan('badgr run <command>')}
|
|
22
|
-
${chalk.cyan('badgr serve <model>')}
|
|
23
|
-
${chalk.cyan('badgr status')}
|
|
24
|
-
${chalk.cyan('badgr logs <id>')}
|
|
25
|
-
${chalk.cyan('badgr down <id>')}
|
|
26
|
-
${chalk.cyan('badgr receipts')}
|
|
27
|
-
${chalk.cyan('badgr test')}
|
|
28
|
-
${chalk.cyan('badgr capacity')}
|
|
20
|
+
${chalk.cyan('badgr login')} Authenticate with your API key
|
|
21
|
+
${chalk.cyan('badgr run <command>')} Run a one-off GPU job
|
|
22
|
+
${chalk.cyan('badgr serve <model>')} Serve a model with an OpenAI-compatible endpoint
|
|
23
|
+
${chalk.cyan('badgr status')} Show what's running and what's billing
|
|
24
|
+
${chalk.cyan('badgr logs <id>')} Stream logs for a running job or endpoint
|
|
25
|
+
${chalk.cyan('badgr down <id>')} Stop a deployment and end billing
|
|
26
|
+
${chalk.cyan('badgr receipts')} Show cost history
|
|
27
|
+
${chalk.cyan('badgr test')} Run an end-to-end test (provision → run → teardown)
|
|
28
|
+
${chalk.cyan('badgr capacity')} Check what GPU capacity is available right now
|
|
29
29
|
|
|
30
30
|
${chalk.bold('EXAMPLES')}
|
|
31
31
|
${chalk.dim('# Verify the stack works end-to-end:')}
|
|
32
32
|
badgr test
|
|
33
33
|
|
|
34
|
-
${chalk.dim('#
|
|
34
|
+
${chalk.dim('# Tier 1 — managed provider routing (default):')}
|
|
35
35
|
badgr run python train.py
|
|
36
36
|
badgr serve meta-llama/Llama-3.1-8B-Instruct
|
|
37
37
|
|
|
38
|
-
${chalk.dim('#
|
|
39
|
-
badgr run python train.py --
|
|
40
|
-
badgr serve meta-llama/Llama-3.1-8B-Instruct --
|
|
38
|
+
${chalk.dim('# Tier 2 — marketplace routing, lower-cost options:')}
|
|
39
|
+
badgr run python train.py --tier 2
|
|
40
|
+
badgr serve meta-llama/Llama-3.1-8B-Instruct --tier 2
|
|
41
41
|
|
|
42
42
|
${chalk.dim('# Pin a specific GPU:')}
|
|
43
43
|
badgr run python train.py --gpu A100
|
|
@@ -53,12 +53,12 @@ ${chalk.bold('EXAMPLES')}
|
|
|
53
53
|
badgr receipts dep-abc123
|
|
54
54
|
|
|
55
55
|
${chalk.bold('badgr run OPTIONS')}
|
|
56
|
-
--gpu <type> GPU type (default: auto — Badgr picks best available
|
|
57
|
-
--
|
|
56
|
+
--gpu <type> GPU type (default: auto — Badgr picks best available)
|
|
57
|
+
--tier 1 Managed provider routing (default)
|
|
58
|
+
--tier 2 Marketplace provider routing, lower-cost options
|
|
58
59
|
--image <image> Docker image (default: python:3.11-slim)
|
|
59
60
|
--count <n> Number of GPUs (default: 1)
|
|
60
61
|
--region US|EU|AU Region preference
|
|
61
|
-
--tier 1|2 Provider tier: 1 = reliable (default), 2 = budget
|
|
62
62
|
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
63
63
|
--max-runtime <min> Auto-stop after N minutes (recommended)
|
|
64
64
|
--max-cost <$> Auto-stop when spend reaches this amount
|
|
@@ -66,10 +66,10 @@ ${chalk.bold('badgr run OPTIONS')}
|
|
|
66
66
|
|
|
67
67
|
${chalk.bold('badgr serve OPTIONS')}
|
|
68
68
|
--gpu <type> GPU type (default: auto — inferred from model size)
|
|
69
|
-
--
|
|
69
|
+
--tier 1 Managed provider routing (default)
|
|
70
|
+
--tier 2 Marketplace provider routing, lower-cost options
|
|
70
71
|
--count <n> Number of GPUs (default: 1)
|
|
71
72
|
--region US|EU|AU Region preference
|
|
72
|
-
--tier 1|2 Provider tier: 1 = reliable (default), 2 = budget
|
|
73
73
|
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
74
74
|
--no-wait Skip endpoint health check
|
|
75
75
|
|
|
@@ -103,7 +103,7 @@ async function main() {
|
|
|
103
103
|
case 'receipts': return receiptsCommand(config, rest, chalk);
|
|
104
104
|
case 'models': return modelsCommand(config, chalk);
|
|
105
105
|
case 'capacity': return capacityCommand(config, rest, chalk);
|
|
106
|
-
case 'test': return testCommand(config, chalk);
|
|
106
|
+
case 'test': return testCommand(config, rest, chalk);
|
|
107
107
|
// legacy aliases kept for compatibility
|
|
108
108
|
case 'up': return upCommand(config, rest, chalk);
|
|
109
109
|
case 'config': {
|
package/src/commands/run.js
CHANGED
|
@@ -2,7 +2,6 @@ import readline from 'readline';
|
|
|
2
2
|
import { requireApiKey } from '../config.js';
|
|
3
3
|
import { callApi, terminateDeployment } from '../api.js';
|
|
4
4
|
import { addReceipt, updateReceipt, generateReceiptId } from '../store.js';
|
|
5
|
-
import { rankAlternatives, promptFallback } from '../fallback.js';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* badgr run python train.py # gpu=auto, attached
|
|
@@ -19,7 +18,6 @@ export function parseRunArgs(args) {
|
|
|
19
18
|
if (args[i] === '--count') { flags.count = parseInt(args[++i], 10); i++; continue; }
|
|
20
19
|
if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
|
|
21
20
|
if (args[i] === '--tier') { flags.tier = args[++i]; i++; continue; }
|
|
22
|
-
if (args[i] === '--cheap') { flags.cheap = true; i++; continue; }
|
|
23
21
|
if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
|
|
24
22
|
if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
|
|
25
23
|
if (args[i] === '--detach') { flags.detach = true; i++; continue; }
|
|
@@ -350,9 +348,8 @@ export async function runCommand(config, args, chalk) {
|
|
|
350
348
|
const maxRuntimeMs = flags.maxRuntime ? flags.maxRuntime * 60 * 1000 : null;
|
|
351
349
|
const maxCost = flags.maxCost ?? null;
|
|
352
350
|
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
const effectiveTier = (flags.cheap || flags.tier === '2' || flags.tier === 'tier2' || flags.tier === 'tier-2')
|
|
351
|
+
// Tier 1 = managed routing (default). Tier 2 = marketplace routing, opt-in via --tier 2.
|
|
352
|
+
const effectiveTier = (flags.tier === '2' || flags.tier === 'tier2' || flags.tier === 'tier-2')
|
|
356
353
|
? '2'
|
|
357
354
|
: (flags.tier || '1');
|
|
358
355
|
|
|
@@ -368,7 +365,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
368
365
|
console.log(chalk.bold(`\n⚡ Running ${workloadLabel}\n`));
|
|
369
366
|
if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
|
|
370
367
|
if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
|
|
371
|
-
if (effectiveTier === '2') console.log(` ${chalk.dim('(
|
|
368
|
+
if (effectiveTier === '2') console.log(` ${chalk.dim('(tier 2 — marketplace routing)')}`)
|
|
372
369
|
console.log();
|
|
373
370
|
process.stdout.write(chalk.dim(' Finding best GPU...'));
|
|
374
371
|
|
|
@@ -395,7 +392,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
395
392
|
|
|
396
393
|
if (effectiveTier === '2' && process.stdin.isTTY) {
|
|
397
394
|
// Budget mode: confirm because the user is being routed to a less reliable provider.
|
|
398
|
-
const answer = await askConfirm(` Press ${chalk.bold('Enter')} to run on
|
|
395
|
+
const answer = await askConfirm(` Press ${chalk.bold('Enter')} to run on tier 2 (marketplace), or ${chalk.bold('q')} to cancel: `);
|
|
399
396
|
if (answer.toLowerCase() === 'q') {
|
|
400
397
|
console.log(chalk.dim('\n Cancelled.\n'));
|
|
401
398
|
process.exit(0);
|
|
@@ -454,56 +451,49 @@ export async function runCommand(config, args, chalk) {
|
|
|
454
451
|
} catch (err) {
|
|
455
452
|
const d = err.errorData;
|
|
456
453
|
if (d?.code === 'NO_CAPACITY_MATCH') {
|
|
457
|
-
if (
|
|
458
|
-
console.error(chalk.red(
|
|
459
|
-
console.error(chalk.dim(' Pass --fallback closest to auto-select an alternative.'));
|
|
460
|
-
process.exit(1);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const pool = [
|
|
464
|
-
...(Array.isArray(d.same_gpu_other_regions) ? d.same_gpu_other_regions : []),
|
|
465
|
-
...(Array.isArray(d.alternatives) ? d.alternatives : []),
|
|
466
|
-
];
|
|
467
|
-
|
|
468
|
-
if (pool.length === 0) {
|
|
469
|
-
console.error(chalk.red(`\n ✗ ${gpu} isn't available right now and no alternatives were found.\n`));
|
|
454
|
+
if (effectiveTier === '2') {
|
|
455
|
+
console.error(chalk.red('\n ✗ No GPU capacity available right now on any provider.\n'));
|
|
470
456
|
console.error(chalk.dim(' Try `badgr capacity` to see what\'s available, or try again shortly.'));
|
|
471
457
|
process.exit(1);
|
|
472
458
|
}
|
|
473
459
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
460
|
+
// Tier 1 out of capacity — offer tier 2 marketplace routing.
|
|
461
|
+
if (process.stdin.isTTY) {
|
|
462
|
+
const answer = await askConfirm(
|
|
463
|
+
`\n No tier 1 capacity available. Try tier 2 marketplace routing? [${chalk.bold('Enter')}/${chalk.bold('q')}]: `
|
|
464
|
+
);
|
|
465
|
+
if (answer.toLowerCase() === 'q') {
|
|
466
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
467
|
+
process.exit(0);
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
console.log(chalk.dim('\n No tier 1 capacity — trying tier 2 marketplace routing...\n'));
|
|
480
471
|
}
|
|
481
472
|
|
|
482
|
-
console.log(chalk.dim(
|
|
483
|
-
|
|
473
|
+
console.log(chalk.dim(' Searching tier 2 capacity...'));
|
|
484
474
|
try {
|
|
485
475
|
dep = await callApi('/run', {
|
|
486
476
|
method: 'POST',
|
|
487
477
|
apiKey: config.apiKey,
|
|
488
478
|
baseUrl: config.baseUrl,
|
|
489
|
-
body: buildBody(
|
|
479
|
+
body: { ...buildBody(), tier: '2' },
|
|
490
480
|
});
|
|
491
481
|
} catch (err2) {
|
|
492
482
|
const d2 = err2.errorData;
|
|
493
|
-
if (d2?.code === '
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
483
|
+
if (d2?.code === 'NO_CAPACITY_MATCH') {
|
|
484
|
+
console.error(chalk.red('\n ✗ No GPU capacity available right now on any provider.\n'));
|
|
485
|
+
console.error(chalk.dim(' Try `badgr capacity` to see what\'s available, or try again shortly.'));
|
|
486
|
+
} else if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
487
|
+
console.error(chalk.red(`\n ✗ Budget provider found capacity but could not start the machine. Please try again.\n`));
|
|
488
|
+
// (error detail kept below)
|
|
499
489
|
if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
|
|
500
490
|
if (d2?.debug_error) console.error(chalk.dim(` Provider detail: ${d2.debug_error}`));
|
|
491
|
+
} else {
|
|
492
|
+
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.\n`));
|
|
501
493
|
}
|
|
502
494
|
} else {
|
|
503
|
-
console.error(chalk.red(`\n ✗
|
|
504
|
-
|
|
505
|
-
if (!(process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true')) {
|
|
506
|
-
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.\n`));
|
|
495
|
+
console.error(chalk.red(`\n ✗ Could not start job on tier 2: ${err2.message}\n`));
|
|
496
|
+
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.`));
|
|
507
497
|
}
|
|
508
498
|
process.exit(1);
|
|
509
499
|
}
|
package/src/commands/serve.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
1
2
|
import { requireApiKey } from '../config.js';
|
|
2
3
|
import { callApi } from '../api.js';
|
|
3
4
|
import { addDeployment, addReceipt, updateReceipt, generateReceiptId } from '../store.js';
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
function askConfirm(prompt) {
|
|
7
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
return new Promise(resolve => rl.question(prompt, ans => { rl.close(); resolve(ans.trim()); }));
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* badgr serve meta-llama/Llama-3.1-8B-Instruct
|
|
@@ -18,7 +23,6 @@ export function parseServeArgs(args) {
|
|
|
18
23
|
if (args[i] === '--count') { flags.count = parseInt(args[++i], 10); i++; continue; }
|
|
19
24
|
if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
|
|
20
25
|
if (args[i] === '--tier') { flags.tier = args[++i]; i++; continue; }
|
|
21
|
-
if (args[i] === '--cheap') { flags.cheap = true; i++; continue; }
|
|
22
26
|
if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
|
|
23
27
|
if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
|
|
24
28
|
if (args[i] === '--no-wait') { flags.noWait = true; i++; continue; }
|
|
@@ -70,15 +74,15 @@ export async function serveCommand(config, args, chalk) {
|
|
|
70
74
|
const gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : 'AUTO';
|
|
71
75
|
const gpuLabel = gpu === 'AUTO' ? 'auto' : gpu;
|
|
72
76
|
|
|
73
|
-
//
|
|
74
|
-
const effectiveTier = (flags.
|
|
77
|
+
// Tier 1 = managed routing (default). Tier 2 = marketplace routing, opt-in via --tier 2.
|
|
78
|
+
const effectiveTier = (flags.tier === '2' || flags.tier === 'tier2' || flags.tier === 'tier-2')
|
|
75
79
|
? '2'
|
|
76
80
|
: (flags.tier || '1');
|
|
77
81
|
|
|
78
82
|
console.log(chalk.bold('\nServing model\n'));
|
|
79
83
|
console.log(` ${chalk.bold('Model:')} ${model}`);
|
|
80
84
|
console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
|
|
81
|
-
if (effectiveTier === '2') console.log(` ${chalk.dim('(
|
|
85
|
+
if (effectiveTier === '2') console.log(` ${chalk.dim('(tier 2 — marketplace routing)')}`);
|
|
82
86
|
console.log();
|
|
83
87
|
process.stdout.write(chalk.dim(' Finding GPU capacity...\n'));
|
|
84
88
|
|
|
@@ -107,46 +111,45 @@ export async function serveCommand(config, args, chalk) {
|
|
|
107
111
|
} catch (err) {
|
|
108
112
|
const d = err.errorData;
|
|
109
113
|
if (d?.code === 'NO_CAPACITY_MATCH') {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
...(Array.isArray(d.alternatives) ? d.alternatives : []),
|
|
113
|
-
];
|
|
114
|
-
|
|
115
|
-
if (pool.length === 0) {
|
|
116
|
-
console.error(chalk.red(`\n ✗ No GPU capacity available right now.\n`));
|
|
114
|
+
if (effectiveTier === '2') {
|
|
115
|
+
console.error(chalk.red('\n ✗ No GPU capacity available right now on any provider.\n'));
|
|
117
116
|
console.error(chalk.dim(' Run `badgr capacity` to see alternatives, or try again shortly.'));
|
|
118
117
|
process.exit(1);
|
|
119
118
|
}
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
// Tier 1 out of capacity — offer tier 2 marketplace routing.
|
|
121
|
+
if (process.stdin.isTTY) {
|
|
122
|
+
const answer = await askConfirm(
|
|
123
|
+
`\n No tier 1 capacity available. Try tier 2 marketplace routing? [${chalk.bold('Enter')}/${chalk.bold('q')}]: `
|
|
124
|
+
);
|
|
125
|
+
if (answer.toLowerCase() === 'q') {
|
|
126
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
console.log(chalk.dim('\n No tier 1 capacity — trying tier 2 marketplace routing...\n'));
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
console.log(chalk.dim(
|
|
130
|
-
|
|
133
|
+
console.log(chalk.dim(' Searching tier 2 capacity...'));
|
|
131
134
|
try {
|
|
132
135
|
dep = await callApi('/serve', {
|
|
133
136
|
method: 'POST',
|
|
134
137
|
apiKey: config.apiKey,
|
|
135
138
|
baseUrl: config.baseUrl,
|
|
136
|
-
body: buildBody(
|
|
139
|
+
body: { ...buildBody(), tier: '2' },
|
|
137
140
|
});
|
|
138
141
|
} catch (err2) {
|
|
139
142
|
const d2 = err2.errorData;
|
|
140
|
-
if (d2?.code === '
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
if (d2?.code === 'NO_CAPACITY_MATCH') {
|
|
144
|
+
console.error(chalk.red('\n ✗ No GPU capacity available right now on any provider.\n'));
|
|
145
|
+
console.error(chalk.dim(' Run `badgr capacity` to see alternatives, or try again shortly.'));
|
|
146
|
+
} else if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
147
|
+
console.error(chalk.red(`\n ✗ Budget provider found capacity but could not start the endpoint. Please try again.\n`));
|
|
148
|
+
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
|
|
146
149
|
} else {
|
|
147
|
-
console.error(chalk.red(`\n ✗
|
|
150
|
+
console.error(chalk.red(`\n ✗ Could not start endpoint on tier 2: ${err2.message}\n`));
|
|
151
|
+
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
|
|
148
152
|
}
|
|
149
|
-
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
|
|
150
153
|
process.exit(1);
|
|
151
154
|
}
|
|
152
155
|
} else if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
|
package/src/commands/status.js
CHANGED
|
@@ -47,7 +47,7 @@ export async function statusCommand(config, args, chalk) {
|
|
|
47
47
|
|
|
48
48
|
console.log(chalk.bold('\nRunning now:\n'));
|
|
49
49
|
for (const d of running) {
|
|
50
|
-
const id = d.
|
|
50
|
+
const id = d.deployment_id || d.name;
|
|
51
51
|
const type = d.workload_type === 'endpoint' ? 'endpoint' : 'job';
|
|
52
52
|
const gpu = d.gpu_type || '—';
|
|
53
53
|
const rate = d.cost_per_hour > 0 ? chalk.yellow(`$${d.cost_per_hour.toFixed(2)}/hr`) : '';
|
|
@@ -74,7 +74,7 @@ export async function statusCommand(config, args, chalk) {
|
|
|
74
74
|
|
|
75
75
|
console.log(chalk.bold('Stop billing:\n'));
|
|
76
76
|
for (const d of running) {
|
|
77
|
-
const id = d.
|
|
77
|
+
const id = d.deployment_id || d.name;
|
|
78
78
|
console.log(` ${chalk.cyan(`badgr down ${id}`)}`);
|
|
79
79
|
}
|
|
80
80
|
console.log();
|
package/src/commands/test-run.js
CHANGED
|
@@ -9,6 +9,20 @@ const TEST_COMMAND = ['python', '-c', "print('hello from badgr')"];
|
|
|
9
9
|
const TEST_IMAGE = 'python:3.11-slim';
|
|
10
10
|
const EXPECTED_OUTPUT = 'hello from badgr';
|
|
11
11
|
|
|
12
|
+
// --provider flag resolves to a backend tier value.
|
|
13
|
+
// 'tier1' → managed routing (default), 'tier2' → marketplace routing, 'secondary' → secondary dispatch.
|
|
14
|
+
const PROVIDER_TO_TIER = { tier1: '1', tier2: '2', secondary: 'modal' };
|
|
15
|
+
|
|
16
|
+
export function parseTestArgs(args) {
|
|
17
|
+
const flags = {};
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < args.length) {
|
|
20
|
+
if (args[i] === '--provider' && args[i + 1]) { flags.provider = args[++i]; i++; continue; }
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
return flags;
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
function step(chalk, ok, msg, detail = '') {
|
|
13
27
|
const icon = ok ? chalk.green('✓') : chalk.red('✗');
|
|
14
28
|
const suffix = detail ? chalk.dim(` — ${detail}`) : '';
|
|
@@ -46,12 +60,40 @@ async function pollLogs(config, depId, expected, timeoutMs) {
|
|
|
46
60
|
return false;
|
|
47
61
|
}
|
|
48
62
|
|
|
49
|
-
export async function testCommand(config, chalk) {
|
|
63
|
+
export async function testCommand(config, args, chalk) {
|
|
50
64
|
requireApiKey(config);
|
|
51
65
|
|
|
66
|
+
const flags = parseTestArgs(Array.isArray(args) ? args : []);
|
|
67
|
+
const providerKey = flags.provider ? flags.provider.toLowerCase() : 'tier1';
|
|
68
|
+
|
|
69
|
+
if (providerKey === 'secondary') {
|
|
70
|
+
// Secondary dispatch provider uses a webhook model, not direct GPU rental.
|
|
71
|
+
// Verify the backend reports it as configured.
|
|
72
|
+
console.log(chalk.bold('\n⚡ Testing secondary dispatch provider\n'));
|
|
73
|
+
let routes;
|
|
74
|
+
try {
|
|
75
|
+
routes = await callApi('/compute/routes', { apiKey: config.apiKey, baseUrl: config.baseUrl });
|
|
76
|
+
} catch {
|
|
77
|
+
routes = null;
|
|
78
|
+
}
|
|
79
|
+
const secondaryRoute = Array.isArray(routes) ? routes.find(r => r.name === 'modal') : null;
|
|
80
|
+
if (secondaryRoute?.available) {
|
|
81
|
+
step(chalk, true, 'Secondary provider configured');
|
|
82
|
+
console.log(chalk.green('\n ✓ Secondary dispatch provider is ready\n'));
|
|
83
|
+
} else {
|
|
84
|
+
step(chalk, false, 'Secondary provider configured', 'contact support to enable secondary dispatch');
|
|
85
|
+
console.log(chalk.red('\n ✗ Secondary dispatch provider is not configured\n'));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const tier = PROVIDER_TO_TIER[providerKey] ?? '1';
|
|
92
|
+
const tierLabel = tier === '1' ? 'tier 1 (managed routing)' : 'tier 2 (marketplace routing)';
|
|
93
|
+
|
|
52
94
|
console.log(chalk.bold('\n⚡ Running end-to-end test\n'));
|
|
53
95
|
console.log(chalk.dim(` Command: ${TEST_COMMAND.join(' ')}`));
|
|
54
|
-
console.log(chalk.dim(`
|
|
96
|
+
console.log(chalk.dim(` Routing: ${tier === '1' ? 'tier 1 — managed provider routing' : 'tier 2 — marketplace routing'}`));
|
|
55
97
|
console.log(chalk.dim(` Budget: max $${TEST_MAX_PRICE.toFixed(2)}/hr · 2 minute cap (~$0.05 max)`));
|
|
56
98
|
console.log();
|
|
57
99
|
|
|
@@ -59,32 +101,50 @@ export async function testCommand(config, chalk) {
|
|
|
59
101
|
let depId;
|
|
60
102
|
|
|
61
103
|
// ── 1. Provision ─────────────────────────────────────────────────────────
|
|
62
|
-
process.stdout.write(chalk.dim(
|
|
104
|
+
process.stdout.write(chalk.dim(` Provisioning GPU (${tierLabel})...\n`));
|
|
63
105
|
let dep;
|
|
106
|
+
const baseBody = {
|
|
107
|
+
command: TEST_COMMAND,
|
|
108
|
+
image: TEST_IMAGE,
|
|
109
|
+
gpu: 'RTX_3080',
|
|
110
|
+
max_price_per_hour: TEST_MAX_PRICE,
|
|
111
|
+
};
|
|
64
112
|
try {
|
|
65
113
|
dep = await callApi('/run', {
|
|
66
114
|
method: 'POST',
|
|
67
115
|
apiKey: config.apiKey,
|
|
68
116
|
baseUrl: config.baseUrl,
|
|
69
|
-
body: {
|
|
70
|
-
command: TEST_COMMAND,
|
|
71
|
-
image: TEST_IMAGE,
|
|
72
|
-
tier: '1',
|
|
73
|
-
// smoke_test workload → cheapest reliable GPU with ≥4 GB VRAM
|
|
74
|
-
gpu: 'RTX_3080',
|
|
75
|
-
max_price_per_hour: TEST_MAX_PRICE,
|
|
76
|
-
},
|
|
117
|
+
body: { ...baseBody, tier },
|
|
77
118
|
});
|
|
78
|
-
depId = dep.deployment_id;
|
|
79
|
-
process.stdout.write('\n');
|
|
80
|
-
step(chalk, true, 'Provisioned', `${dep.deployment_id} on ${dep.gpu_type}`);
|
|
81
119
|
} catch (err) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
120
|
+
if (err.errorData?.code === 'NO_CAPACITY_MATCH' && tier === '1') {
|
|
121
|
+
process.stdout.write('\n');
|
|
122
|
+
process.stdout.write(chalk.dim(' No tier 1 capacity — trying tier 2 marketplace routing...'));
|
|
123
|
+
try {
|
|
124
|
+
dep = await callApi('/run', {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
apiKey: config.apiKey,
|
|
127
|
+
baseUrl: config.baseUrl,
|
|
128
|
+
body: { ...baseBody, tier: '2' },
|
|
129
|
+
});
|
|
130
|
+
} catch (err2) {
|
|
131
|
+
process.stdout.write('\n');
|
|
132
|
+
step(chalk, false, 'Provisioned', err2.message);
|
|
133
|
+
console.log();
|
|
134
|
+
console.error(chalk.red(' Test failed — no GPU capacity available on any provider.\n'));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
process.stdout.write('\n');
|
|
139
|
+
step(chalk, false, 'Provisioned', err.message);
|
|
140
|
+
console.log();
|
|
141
|
+
console.error(chalk.red(' Test failed — could not provision GPU.\n'));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
87
144
|
}
|
|
145
|
+
depId = dep.deployment_id;
|
|
146
|
+
process.stdout.write('\n');
|
|
147
|
+
step(chalk, true, 'Provisioned', `${dep.deployment_id} on ${dep.gpu_type}`);
|
|
88
148
|
|
|
89
149
|
// ── 2. Container started ─────────────────────────────────────────────────
|
|
90
150
|
process.stdout.write(chalk.dim(' Waiting for container to start...'));
|
package/tests/commands.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { parseRunArgs, classifyFailure, inferWorkload } from '../src/commands/run.js';
|
|
3
3
|
import { parseServeArgs } from '../src/commands/serve.js';
|
|
4
|
-
import { testCommand } from '../src/commands/test-run.js';
|
|
4
|
+
import { testCommand, parseTestArgs } from '../src/commands/test-run.js';
|
|
5
5
|
import { rankAlternatives, diffDescription, promptFallback } from '../src/fallback.js';
|
|
6
6
|
|
|
7
7
|
describe('parseRunArgs', () => {
|
|
@@ -73,20 +73,15 @@ describe('parseRunArgs', () => {
|
|
|
73
73
|
expect(flags.maxCost).toBe(5.0);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
it('parses --cheap flag', () => {
|
|
77
|
-
const { flags } = parseRunArgs(['python', 'train.py', '--cheap']);
|
|
78
|
-
expect(flags.cheap).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('--cheap defaults to falsy when not passed', () => {
|
|
82
|
-
const { flags } = parseRunArgs(['python', 'train.py']);
|
|
83
|
-
expect(flags.cheap).toBeFalsy();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
76
|
it('parses --tier 2 flag', () => {
|
|
87
77
|
const { flags } = parseRunArgs(['python', 'train.py', '--tier', '2']);
|
|
88
78
|
expect(flags.tier).toBe('2');
|
|
89
79
|
});
|
|
80
|
+
|
|
81
|
+
it('--tier defaults to undefined when not passed', () => {
|
|
82
|
+
const { flags } = parseRunArgs(['python', 'train.py']);
|
|
83
|
+
expect(flags.tier).toBeUndefined();
|
|
84
|
+
});
|
|
90
85
|
});
|
|
91
86
|
|
|
92
87
|
describe('classifyFailure', () => {
|
|
@@ -204,14 +199,14 @@ describe('parseServeArgs', () => {
|
|
|
204
199
|
expect(model).toBeNull();
|
|
205
200
|
});
|
|
206
201
|
|
|
207
|
-
it('parses --
|
|
208
|
-
const { flags } = parseServeArgs(['my/model', '--
|
|
209
|
-
expect(flags.
|
|
202
|
+
it('parses --tier 2 flag', () => {
|
|
203
|
+
const { flags } = parseServeArgs(['my/model', '--tier', '2']);
|
|
204
|
+
expect(flags.tier).toBe('2');
|
|
210
205
|
});
|
|
211
206
|
|
|
212
|
-
it('--
|
|
207
|
+
it('--tier defaults to undefined when not passed', () => {
|
|
213
208
|
const { flags } = parseServeArgs(['my/model']);
|
|
214
|
-
expect(flags.
|
|
209
|
+
expect(flags.tier).toBeUndefined();
|
|
215
210
|
});
|
|
216
211
|
});
|
|
217
212
|
|
|
@@ -221,6 +216,24 @@ describe('testCommand', () => {
|
|
|
221
216
|
});
|
|
222
217
|
});
|
|
223
218
|
|
|
219
|
+
describe('parseTestArgs', () => {
|
|
220
|
+
it('returns empty flags for no args', () => {
|
|
221
|
+
expect(parseTestArgs([])).toEqual({});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('parses --provider tier1', () => {
|
|
225
|
+
expect(parseTestArgs(['--provider', 'tier1'])).toEqual({ provider: 'tier1' });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('parses --provider tier2', () => {
|
|
229
|
+
expect(parseTestArgs(['--provider', 'tier2'])).toEqual({ provider: 'tier2' });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('parses --provider secondary', () => {
|
|
233
|
+
expect(parseTestArgs(['--provider', 'secondary'])).toEqual({ provider: 'secondary' });
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
224
237
|
describe('inferWorkload', () => {
|
|
225
238
|
it('returns general for empty command', () => {
|
|
226
239
|
expect(inferWorkload([])).toBe('general');
|