badgr-cli 1.0.20 → 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.20",
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
 
@@ -19,6 +19,7 @@ export function parseRunArgs(args) {
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
21
  if (args[i] === '--tier') { flags.tier = args[++i]; i++; continue; }
22
+ if (args[i] === '--cheap') { flags.cheap = true; i++; continue; }
22
23
  if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
23
24
  if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
24
25
  if (args[i] === '--detach') { flags.detach = true; i++; continue; }
@@ -269,11 +270,12 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
269
270
  }
270
271
  }
271
272
 
272
- // 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.
273
275
  // Returns { gpu, region, price } or null when nothing is available.
274
- async function findAutoGpu(config, chalk) {
276
+ async function findAutoGpu(config, chalk, tier = '1') {
275
277
  try {
276
- const params = new URLSearchParams({ max_price: '10' });
278
+ const params = new URLSearchParams({ max_price: '10', tier });
277
279
  return await callApi(`/capacity/auto?${params}`, {
278
280
  apiKey: config.apiKey,
279
281
  baseUrl: config.baseUrl,
@@ -307,6 +309,12 @@ export async function runCommand(config, args, chalk) {
307
309
  const maxRuntimeMs = flags.maxRuntime ? flags.maxRuntime * 60 * 1000 : null;
308
310
  const maxCost = flags.maxCost ?? null;
309
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
+
310
318
  // ── Auto GPU selection (no --gpu specified) ────────────────────────────────
311
319
  let gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : null;
312
320
  let autoRegion = null;
@@ -315,12 +323,13 @@ export async function runCommand(config, args, chalk) {
315
323
  console.log(chalk.bold('\n⚡ Running GPU job\n'));
316
324
  if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
317
325
  if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
326
+ if (effectiveTier === '2') console.log(` ${chalk.dim('(budget mode — searching all providers)')}`);
318
327
  console.log();
319
328
  process.stdout.write(chalk.dim(' Finding GPU...'));
320
329
 
321
330
  let best;
322
331
  try {
323
- best = await findAutoGpu(config, chalk);
332
+ best = await findAutoGpu(config, chalk, effectiveTier);
324
333
  } catch (err) {
325
334
  process.stdout.write('\n');
326
335
  console.error(chalk.red(`\n ✗ Could not find GPU capacity: ${err.message}\n`));
@@ -385,7 +394,7 @@ export async function runCommand(config, args, chalk) {
385
394
  ...(effectiveRegion ? { region: effectiveRegion } : {}),
386
395
  max_price_per_hour: flags.maxPrice,
387
396
  name: flags.name,
388
- ...(flags.tier ? { tier: flags.tier } : {}),
397
+ tier: effectiveTier,
389
398
  };
390
399
  }
391
400
 
@@ -18,6 +18,7 @@ export function parseServeArgs(args) {
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
20
  if (args[i] === '--tier') { flags.tier = args[++i]; i++; continue; }
21
+ if (args[i] === '--cheap') { flags.cheap = true; i++; continue; }
21
22
  if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
22
23
  if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
23
24
  if (args[i] === '--no-wait') { flags.noWait = true; i++; continue; }
@@ -64,9 +65,15 @@ export async function serveCommand(config, args, chalk) {
64
65
  const gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : 'AUTO';
65
66
  const gpuLabel = gpu === 'AUTO' ? 'auto' : gpu;
66
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
+
67
73
  console.log(chalk.bold('\nServing model\n'));
68
74
  console.log(` ${chalk.bold('Model:')} ${model}`);
69
75
  console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
76
+ if (effectiveTier === '2') console.log(` ${chalk.dim('(budget mode — searching all providers)')}`);
70
77
  console.log();
71
78
  process.stdout.write(chalk.dim(' Finding GPU capacity...\n'));
72
79
 
@@ -80,7 +87,7 @@ export async function serveCommand(config, args, chalk) {
80
87
  ...(regionOverride || effectiveRegion ? { region: regionOverride || effectiveRegion } : {}),
81
88
  max_price_per_hour: flags.maxPrice,
82
89
  name: flags.name,
83
- ...(flags.tier ? { tier: flags.tier } : {}),
90
+ tier: effectiveTier,
84
91
  };
85
92
  }
86
93
 
@@ -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', () => {