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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badgr-cli",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "Badgr, run or serve GPU workloads from one command",
5
5
  "type": "module",
6
6
  "bin": {
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 cheapest available)
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
 
@@ -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 cheapest available GPU right now.
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 ✗ Low-cost provider failed. Primary provider also unavailable.\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 ✗ Low-cost provider failed. Primary provider also unavailable.\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 === 'low_cost_provider_failed') {
475
- console.log(chalk.yellow(' ℹ Low-cost provider failed. Using primary provider instead.\n'));
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
 
@@ -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 ✗ Low-cost provider failed. Primary provider also unavailable.\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 ✗ Low-cost provider failed. Primary provider also unavailable.\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 === 'low_cost_provider_failed') {
165
- console.log(chalk.yellow(' ℹ Low-cost provider failed. Using primary provider instead.\n'));
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}`)}`);
@@ -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', () => {