badgr-cli 1.0.19 → 1.0.21
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 +10 -2
- package/src/commands/run.js +20 -8
- package/src/commands/serve.js +14 -4
- package/tests/commands.test.js +25 -0
package/package.json
CHANGED
package/src/badgr.js
CHANGED
|
@@ -26,10 +26,14 @@ ${chalk.bold('COMMANDS')}
|
|
|
26
26
|
${chalk.cyan('badgr capacity')} Check what GPU capacity is available right now
|
|
27
27
|
|
|
28
28
|
${chalk.bold('EXAMPLES')}
|
|
29
|
-
${chalk.dim('# Simplest — Badgr picks the GPU:')}
|
|
29
|
+
${chalk.dim('# Simplest — Badgr picks the GPU (RunPod, reliable):')}
|
|
30
30
|
badgr run python train.py
|
|
31
31
|
badgr serve meta-llama/Llama-3.1-8B-Instruct
|
|
32
32
|
|
|
33
|
+
${chalk.dim('# Opt into cheaper budget providers (Vast.ai etc.):')}
|
|
34
|
+
badgr run python train.py --cheap
|
|
35
|
+
badgr serve meta-llama/Llama-3.1-8B-Instruct --cheap
|
|
36
|
+
|
|
33
37
|
${chalk.dim('# Pin a specific GPU:')}
|
|
34
38
|
badgr run python train.py --gpu A100
|
|
35
39
|
badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
|
|
@@ -44,10 +48,12 @@ ${chalk.bold('EXAMPLES')}
|
|
|
44
48
|
badgr receipts dep-abc123
|
|
45
49
|
|
|
46
50
|
${chalk.bold('badgr run OPTIONS')}
|
|
47
|
-
--gpu <type> GPU type (default: auto — Badgr picks
|
|
51
|
+
--gpu <type> GPU type (default: auto — Badgr picks best available on RunPod)
|
|
52
|
+
--cheap Search budget providers too (Vast.ai etc.) for lower prices
|
|
48
53
|
--image <image> Docker image (default: python:3.11-slim)
|
|
49
54
|
--count <n> Number of GPUs (default: 1)
|
|
50
55
|
--region US|EU|AU Region preference
|
|
56
|
+
--tier 1|2 Provider tier: 1 = reliable (default), 2 = budget
|
|
51
57
|
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
52
58
|
--max-runtime <min> Auto-stop after N minutes (recommended)
|
|
53
59
|
--max-cost <$> Auto-stop when spend reaches this amount
|
|
@@ -55,8 +61,10 @@ ${chalk.bold('badgr run OPTIONS')}
|
|
|
55
61
|
|
|
56
62
|
${chalk.bold('badgr serve OPTIONS')}
|
|
57
63
|
--gpu <type> GPU type (default: auto — inferred from model size)
|
|
64
|
+
--cheap Search budget providers too (Vast.ai etc.) for lower prices
|
|
58
65
|
--count <n> Number of GPUs (default: 1)
|
|
59
66
|
--region US|EU|AU Region preference
|
|
67
|
+
--tier 1|2 Provider tier: 1 = reliable (default), 2 = budget
|
|
60
68
|
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
61
69
|
--no-wait Skip endpoint health check
|
|
62
70
|
|
package/src/commands/run.js
CHANGED
|
@@ -18,6 +18,8 @@ export function parseRunArgs(args) {
|
|
|
18
18
|
if (args[i] === '--image') { flags.image = args[++i]; i++; continue; }
|
|
19
19
|
if (args[i] === '--count') { flags.count = parseInt(args[++i], 10); i++; continue; }
|
|
20
20
|
if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
|
|
21
|
+
if (args[i] === '--tier') { flags.tier = args[++i]; i++; continue; }
|
|
22
|
+
if (args[i] === '--cheap') { flags.cheap = true; i++; continue; }
|
|
21
23
|
if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
|
|
22
24
|
if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
|
|
23
25
|
if (args[i] === '--detach') { flags.detach = true; i++; continue; }
|
|
@@ -268,11 +270,12 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
|
|
|
268
270
|
}
|
|
269
271
|
}
|
|
270
272
|
|
|
271
|
-
// Ask the backend for the
|
|
273
|
+
// Ask the backend for the best available GPU right now.
|
|
274
|
+
// Defaults to tier 1 (RunPod) for reliability; pass tier='2' for budget providers.
|
|
272
275
|
// Returns { gpu, region, price } or null when nothing is available.
|
|
273
|
-
async function findAutoGpu(config, chalk) {
|
|
276
|
+
async function findAutoGpu(config, chalk, tier = '1') {
|
|
274
277
|
try {
|
|
275
|
-
const params = new URLSearchParams({ max_price: '10' });
|
|
278
|
+
const params = new URLSearchParams({ max_price: '10', tier });
|
|
276
279
|
return await callApi(`/capacity/auto?${params}`, {
|
|
277
280
|
apiKey: config.apiKey,
|
|
278
281
|
baseUrl: config.baseUrl,
|
|
@@ -306,6 +309,12 @@ export async function runCommand(config, args, chalk) {
|
|
|
306
309
|
const maxRuntimeMs = flags.maxRuntime ? flags.maxRuntime * 60 * 1000 : null;
|
|
307
310
|
const maxCost = flags.maxCost ?? null;
|
|
308
311
|
|
|
312
|
+
// Resolve effective tier: --cheap and --tier 2 opt into budget providers;
|
|
313
|
+
// everything else defaults to tier 1 (RunPod only) for reliability.
|
|
314
|
+
const effectiveTier = (flags.cheap || flags.tier === '2' || flags.tier === 'tier2' || flags.tier === 'tier-2')
|
|
315
|
+
? '2'
|
|
316
|
+
: (flags.tier || '1');
|
|
317
|
+
|
|
309
318
|
// ── Auto GPU selection (no --gpu specified) ────────────────────────────────
|
|
310
319
|
let gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : null;
|
|
311
320
|
let autoRegion = null;
|
|
@@ -314,12 +323,13 @@ export async function runCommand(config, args, chalk) {
|
|
|
314
323
|
console.log(chalk.bold('\n⚡ Running GPU job\n'));
|
|
315
324
|
if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
|
|
316
325
|
if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
|
|
326
|
+
if (effectiveTier === '2') console.log(` ${chalk.dim('(budget mode — searching all providers)')}`);
|
|
317
327
|
console.log();
|
|
318
328
|
process.stdout.write(chalk.dim(' Finding GPU...'));
|
|
319
329
|
|
|
320
330
|
let best;
|
|
321
331
|
try {
|
|
322
|
-
best = await findAutoGpu(config, chalk);
|
|
332
|
+
best = await findAutoGpu(config, chalk, effectiveTier);
|
|
323
333
|
} catch (err) {
|
|
324
334
|
process.stdout.write('\n');
|
|
325
335
|
console.error(chalk.red(`\n ✗ Could not find GPU capacity: ${err.message}\n`));
|
|
@@ -384,6 +394,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
384
394
|
...(effectiveRegion ? { region: effectiveRegion } : {}),
|
|
385
395
|
max_price_per_hour: flags.maxPrice,
|
|
386
396
|
name: flags.name,
|
|
397
|
+
tier: effectiveTier,
|
|
387
398
|
};
|
|
388
399
|
}
|
|
389
400
|
|
|
@@ -436,7 +447,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
436
447
|
const d2 = err2.errorData;
|
|
437
448
|
if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
438
449
|
if (d2?.low_cost_provider_failed) {
|
|
439
|
-
console.error(chalk.red(`\n ✗
|
|
450
|
+
console.error(chalk.red(`\n ✗ Tier 2 unavailable. Tier 1 also unavailable. Try again shortly.\n`));
|
|
440
451
|
} else {
|
|
441
452
|
console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity but could not start the machine. Please try again.\n`));
|
|
442
453
|
}
|
|
@@ -453,7 +464,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
453
464
|
}
|
|
454
465
|
} else if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
455
466
|
if (d?.low_cost_provider_failed) {
|
|
456
|
-
console.error(chalk.red(`\n ✗
|
|
467
|
+
console.error(chalk.red(`\n ✗ Tier 2 unavailable. Tier 1 also unavailable. Try again shortly.\n`));
|
|
457
468
|
} else {
|
|
458
469
|
console.error(chalk.red(`\n ✗ Badgr found ${gpu} capacity but could not start the machine. Please try again.\n`));
|
|
459
470
|
}
|
|
@@ -471,8 +482,8 @@ export async function runCommand(config, args, chalk) {
|
|
|
471
482
|
}
|
|
472
483
|
}
|
|
473
484
|
|
|
474
|
-
if (dep.provider_fallback_note === '
|
|
475
|
-
console.log(chalk.yellow(' ℹ
|
|
485
|
+
if (dep.provider_fallback_note === 'tier2_failed_using_tier1') {
|
|
486
|
+
console.log(chalk.yellow(' ℹ Tier 2 unavailable. Running on Tier 1 instead.\n'));
|
|
476
487
|
}
|
|
477
488
|
|
|
478
489
|
const rcptId = dep.receipt_id || generateReceiptId();
|
|
@@ -488,6 +499,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
488
499
|
console.log();
|
|
489
500
|
console.log(chalk.bold(` Job ID: ${chalk.cyan(dep.deployment_id)}`));
|
|
490
501
|
console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
|
|
502
|
+
if (dep.tier) console.log(` ${chalk.bold('Tier:')} ${dep.tier}`);
|
|
491
503
|
if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
|
|
492
504
|
console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
|
|
493
505
|
|
package/src/commands/serve.js
CHANGED
|
@@ -17,6 +17,8 @@ export function parseServeArgs(args) {
|
|
|
17
17
|
if (args[i] === '--gpu') { flags.gpu = args[++i]; i++; continue; }
|
|
18
18
|
if (args[i] === '--count') { flags.count = parseInt(args[++i], 10); i++; continue; }
|
|
19
19
|
if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
|
|
20
|
+
if (args[i] === '--tier') { flags.tier = args[++i]; i++; continue; }
|
|
21
|
+
if (args[i] === '--cheap') { flags.cheap = true; i++; continue; }
|
|
20
22
|
if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
|
|
21
23
|
if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
|
|
22
24
|
if (args[i] === '--no-wait') { flags.noWait = true; i++; continue; }
|
|
@@ -63,9 +65,15 @@ export async function serveCommand(config, args, chalk) {
|
|
|
63
65
|
const gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : 'AUTO';
|
|
64
66
|
const gpuLabel = gpu === 'AUTO' ? 'auto' : gpu;
|
|
65
67
|
|
|
68
|
+
// Default to tier 1 (RunPod) for reliability; --cheap or --tier 2 opts into budget providers.
|
|
69
|
+
const effectiveTier = (flags.cheap || flags.tier === '2' || flags.tier === 'tier2' || flags.tier === 'tier-2')
|
|
70
|
+
? '2'
|
|
71
|
+
: (flags.tier || '1');
|
|
72
|
+
|
|
66
73
|
console.log(chalk.bold('\nServing model\n'));
|
|
67
74
|
console.log(` ${chalk.bold('Model:')} ${model}`);
|
|
68
75
|
console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
|
|
76
|
+
if (effectiveTier === '2') console.log(` ${chalk.dim('(budget mode — searching all providers)')}`);
|
|
69
77
|
console.log();
|
|
70
78
|
process.stdout.write(chalk.dim(' Finding GPU capacity...\n'));
|
|
71
79
|
|
|
@@ -79,6 +87,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
79
87
|
...(regionOverride || effectiveRegion ? { region: regionOverride || effectiveRegion } : {}),
|
|
80
88
|
max_price_per_hour: flags.maxPrice,
|
|
81
89
|
name: flags.name,
|
|
90
|
+
tier: effectiveTier,
|
|
82
91
|
};
|
|
83
92
|
}
|
|
84
93
|
|
|
@@ -125,7 +134,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
125
134
|
const d2 = err2.errorData;
|
|
126
135
|
if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
127
136
|
if (d2?.low_cost_provider_failed) {
|
|
128
|
-
console.error(chalk.red(`\n ✗
|
|
137
|
+
console.error(chalk.red(`\n ✗ Tier 2 unavailable. Tier 1 also unavailable. Try again shortly.\n`));
|
|
129
138
|
} else {
|
|
130
139
|
console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity but could not start the endpoint. Please try again.\n`));
|
|
131
140
|
}
|
|
@@ -137,7 +146,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
137
146
|
}
|
|
138
147
|
} else if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
139
148
|
if (d?.low_cost_provider_failed) {
|
|
140
|
-
console.error(chalk.red(`\n ✗
|
|
149
|
+
console.error(chalk.red(`\n ✗ Tier 2 unavailable. Tier 1 also unavailable. Try again shortly.\n`));
|
|
141
150
|
} else {
|
|
142
151
|
console.error(chalk.red(`\n ✗ Badgr found capacity but could not start the endpoint. Please try again.\n`));
|
|
143
152
|
}
|
|
@@ -161,8 +170,8 @@ export async function serveCommand(config, args, chalk) {
|
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
172
|
|
|
164
|
-
if (dep.provider_fallback_note === '
|
|
165
|
-
console.log(chalk.yellow(' ℹ
|
|
173
|
+
if (dep.provider_fallback_note === 'tier2_failed_using_tier1') {
|
|
174
|
+
console.log(chalk.yellow(' ℹ Tier 2 unavailable. Running on Tier 1 instead.\n'));
|
|
166
175
|
}
|
|
167
176
|
|
|
168
177
|
addDeployment({
|
|
@@ -215,6 +224,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
215
224
|
console.log(` ${chalk.bold('Base URL:')} ${chalk.cyan(endpointUrl)}`);
|
|
216
225
|
console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
|
|
217
226
|
console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
|
|
227
|
+
if (dep.tier) console.log(` ${chalk.bold('Tier:')} ${dep.tier}`);
|
|
218
228
|
if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
|
|
219
229
|
console.log(` ${chalk.bold('Logs:')} ${chalk.dim(`badgr logs ${dep.deployment_id}`)}`);
|
|
220
230
|
console.log(` ${chalk.bold('Stop billing:')} ${chalk.dim(`badgr down ${dep.deployment_id}`)}`);
|
package/tests/commands.test.js
CHANGED
|
@@ -71,6 +71,21 @@ describe('parseRunArgs', () => {
|
|
|
71
71
|
const { flags } = parseRunArgs(['python', 'train.py', '--max-cost', '5.00']);
|
|
72
72
|
expect(flags.maxCost).toBe(5.0);
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
it('parses --cheap flag', () => {
|
|
76
|
+
const { flags } = parseRunArgs(['python', 'train.py', '--cheap']);
|
|
77
|
+
expect(flags.cheap).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('--cheap defaults to falsy when not passed', () => {
|
|
81
|
+
const { flags } = parseRunArgs(['python', 'train.py']);
|
|
82
|
+
expect(flags.cheap).toBeFalsy();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('parses --tier 2 flag', () => {
|
|
86
|
+
const { flags } = parseRunArgs(['python', 'train.py', '--tier', '2']);
|
|
87
|
+
expect(flags.tier).toBe('2');
|
|
88
|
+
});
|
|
74
89
|
});
|
|
75
90
|
|
|
76
91
|
describe('classifyFailure', () => {
|
|
@@ -187,6 +202,16 @@ describe('parseServeArgs', () => {
|
|
|
187
202
|
const { model } = parseServeArgs(['--gpu', 'RTX_4090']);
|
|
188
203
|
expect(model).toBeNull();
|
|
189
204
|
});
|
|
205
|
+
|
|
206
|
+
it('parses --cheap flag', () => {
|
|
207
|
+
const { flags } = parseServeArgs(['my/model', '--cheap']);
|
|
208
|
+
expect(flags.cheap).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('--cheap defaults to falsy when not passed', () => {
|
|
212
|
+
const { flags } = parseServeArgs(['my/model']);
|
|
213
|
+
expect(flags.cheap).toBeFalsy();
|
|
214
|
+
});
|
|
190
215
|
});
|
|
191
216
|
|
|
192
217
|
describe('promptFallback output', () => {
|