badgr-cli 1.0.23 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badgr-cli",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "Badgr, run or serve GPU workloads from one command",
5
5
  "type": "module",
6
6
  "bin": {
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')} 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
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('# SimplestBadgr picks the GPU (RunPod, reliable):')}
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('# Opt into cheaper budget providers (Vast.ai etc.):')}
39
- badgr run python train.py --cheap
40
- badgr serve meta-llama/Llama-3.1-8B-Instruct --cheap
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 on RunPod)
57
- --cheap Search budget providers too (Vast.ai etc.) for lower prices
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
- --cheap Search budget providers too (Vast.ai etc.) for lower prices
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': {
@@ -18,7 +18,6 @@ export function parseRunArgs(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; }
22
21
  if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
23
22
  if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
24
23
  if (args[i] === '--detach') { flags.detach = true; i++; continue; }
@@ -349,9 +348,8 @@ export async function runCommand(config, args, chalk) {
349
348
  const maxRuntimeMs = flags.maxRuntime ? flags.maxRuntime * 60 * 1000 : null;
350
349
  const maxCost = flags.maxCost ?? null;
351
350
 
352
- // Resolve effective tier: --cheap and --tier 2 opt into budget providers;
353
- // everything else defaults to tier 1 (RunPod only) for reliability.
354
- 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')
355
353
  ? '2'
356
354
  : (flags.tier || '1');
357
355
 
@@ -367,7 +365,7 @@ export async function runCommand(config, args, chalk) {
367
365
  console.log(chalk.bold(`\n⚡ Running ${workloadLabel}\n`));
368
366
  if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
369
367
  if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
370
- if (effectiveTier === '2') console.log(` ${chalk.dim('(budget modesearching all providers)')}`);
368
+ if (effectiveTier === '2') console.log(` ${chalk.dim('(tier 2marketplace routing)')}`)
371
369
  console.log();
372
370
  process.stdout.write(chalk.dim(' Finding best GPU...'));
373
371
 
@@ -394,7 +392,7 @@ export async function runCommand(config, args, chalk) {
394
392
 
395
393
  if (effectiveTier === '2' && process.stdin.isTTY) {
396
394
  // Budget mode: confirm because the user is being routed to a less reliable provider.
397
- const answer = await askConfirm(` Press ${chalk.bold('Enter')} to run on budget provider, or ${chalk.bold('q')} to cancel: `);
395
+ const answer = await askConfirm(` Press ${chalk.bold('Enter')} to run on tier 2 (marketplace), or ${chalk.bold('q')} to cancel: `);
398
396
  if (answer.toLowerCase() === 'q') {
399
397
  console.log(chalk.dim('\n Cancelled.\n'));
400
398
  process.exit(0);
@@ -459,20 +457,20 @@ export async function runCommand(config, args, chalk) {
459
457
  process.exit(1);
460
458
  }
461
459
 
462
- // Tier 1 (RunPod) is out of capacity — offer budget providers.
460
+ // Tier 1 out of capacity — offer tier 2 marketplace routing.
463
461
  if (process.stdin.isTTY) {
464
462
  const answer = await askConfirm(
465
- `\n RunPod has no suitable capacity. Try budget marketplace GPUs? (Vast.ai, Salad) [${chalk.bold('Enter')}/${chalk.bold('q')}]: `
463
+ `\n No tier 1 capacity available. Try tier 2 marketplace routing? [${chalk.bold('Enter')}/${chalk.bold('q')}]: `
466
464
  );
467
465
  if (answer.toLowerCase() === 'q') {
468
466
  console.log(chalk.dim('\n Cancelled.\n'));
469
467
  process.exit(0);
470
468
  }
471
469
  } else {
472
- console.log(chalk.dim('\n RunPod capacity unavailable falling back to budget providers...\n'));
470
+ console.log(chalk.dim('\n No tier 1 capacity — trying tier 2 marketplace routing...\n'));
473
471
  }
474
472
 
475
- console.log(chalk.dim(' Searching budget providers (Vast.ai, Salad)...'));
473
+ console.log(chalk.dim(' Searching tier 2 capacity...'));
476
474
  try {
477
475
  dep = await callApi('/run', {
478
476
  method: 'POST',
@@ -487,13 +485,14 @@ export async function runCommand(config, args, chalk) {
487
485
  console.error(chalk.dim(' Try `badgr capacity` to see what\'s available, or try again shortly.'));
488
486
  } else if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
489
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)
490
489
  if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
491
490
  if (d2?.debug_error) console.error(chalk.dim(` Provider detail: ${d2.debug_error}`));
492
491
  } else {
493
492
  console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.\n`));
494
493
  }
495
494
  } else {
496
- console.error(chalk.red(`\n ✗ Could not start job on budget providers: ${err2.message}\n`));
495
+ console.error(chalk.red(`\n ✗ Could not start job on tier 2: ${err2.message}\n`));
497
496
  console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.`));
498
497
  }
499
498
  process.exit(1);
@@ -23,7 +23,6 @@ export function parseServeArgs(args) {
23
23
  if (args[i] === '--count') { flags.count = parseInt(args[++i], 10); i++; continue; }
24
24
  if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
25
25
  if (args[i] === '--tier') { flags.tier = args[++i]; i++; continue; }
26
- if (args[i] === '--cheap') { flags.cheap = true; i++; continue; }
27
26
  if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
28
27
  if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
29
28
  if (args[i] === '--no-wait') { flags.noWait = true; i++; continue; }
@@ -75,15 +74,15 @@ export async function serveCommand(config, args, chalk) {
75
74
  const gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : 'AUTO';
76
75
  const gpuLabel = gpu === 'AUTO' ? 'auto' : gpu;
77
76
 
78
- // Default to tier 1 (RunPod) for reliability; --cheap or --tier 2 opts into budget providers.
79
- const effectiveTier = (flags.cheap || flags.tier === '2' || flags.tier === 'tier2' || flags.tier === 'tier-2')
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')
80
79
  ? '2'
81
80
  : (flags.tier || '1');
82
81
 
83
82
  console.log(chalk.bold('\nServing model\n'));
84
83
  console.log(` ${chalk.bold('Model:')} ${model}`);
85
84
  console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
86
- if (effectiveTier === '2') console.log(` ${chalk.dim('(budget modesearching all providers)')}`);
85
+ if (effectiveTier === '2') console.log(` ${chalk.dim('(tier 2marketplace routing)')}`);
87
86
  console.log();
88
87
  process.stdout.write(chalk.dim(' Finding GPU capacity...\n'));
89
88
 
@@ -118,20 +117,20 @@ export async function serveCommand(config, args, chalk) {
118
117
  process.exit(1);
119
118
  }
120
119
 
121
- // Tier 1 (RunPod) out of capacity — offer budget providers.
120
+ // Tier 1 out of capacity — offer tier 2 marketplace routing.
122
121
  if (process.stdin.isTTY) {
123
122
  const answer = await askConfirm(
124
- `\n RunPod has no suitable capacity. Try budget marketplace GPUs? (Vast.ai, Salad) [${chalk.bold('Enter')}/${chalk.bold('q')}]: `
123
+ `\n No tier 1 capacity available. Try tier 2 marketplace routing? [${chalk.bold('Enter')}/${chalk.bold('q')}]: `
125
124
  );
126
125
  if (answer.toLowerCase() === 'q') {
127
126
  console.log(chalk.dim('\n Cancelled.\n'));
128
127
  process.exit(0);
129
128
  }
130
129
  } else {
131
- console.log(chalk.dim('\n RunPod capacity unavailable falling back to budget providers...\n'));
130
+ console.log(chalk.dim('\n No tier 1 capacity — trying tier 2 marketplace routing...\n'));
132
131
  }
133
132
 
134
- console.log(chalk.dim(' Searching budget providers (Vast.ai, Salad)...'));
133
+ console.log(chalk.dim(' Searching tier 2 capacity...'));
135
134
  try {
136
135
  dep = await callApi('/serve', {
137
136
  method: 'POST',
@@ -148,7 +147,7 @@ export async function serveCommand(config, args, chalk) {
148
147
  console.error(chalk.red(`\n ✗ Budget provider found capacity but could not start the endpoint. Please try again.\n`));
149
148
  console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
150
149
  } else {
151
- console.error(chalk.red(`\n ✗ Failed to start endpoint on budget providers: ${err2.message}\n`));
150
+ console.error(chalk.red(`\n ✗ Could not start endpoint on tier 2: ${err2.message}\n`));
152
151
  console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
153
152
  }
154
153
  process.exit(1);
@@ -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(` Provider: RunPod (Tier 1 — reliable)`));
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,7 +101,7 @@ export async function testCommand(config, chalk) {
59
101
  let depId;
60
102
 
61
103
  // ── 1. Provision ─────────────────────────────────────────────────────────
62
- process.stdout.write(chalk.dim(' Provisioning GPU (RunPod)...'));
104
+ process.stdout.write(chalk.dim(` Provisioning GPU (${tierLabel})...\n`));
63
105
  let dep;
64
106
  const baseBody = {
65
107
  command: TEST_COMMAND,
@@ -72,12 +114,12 @@ export async function testCommand(config, chalk) {
72
114
  method: 'POST',
73
115
  apiKey: config.apiKey,
74
116
  baseUrl: config.baseUrl,
75
- body: { ...baseBody, tier: '1' },
117
+ body: { ...baseBody, tier },
76
118
  });
77
119
  } catch (err) {
78
- if (err.errorData?.code === 'NO_CAPACITY_MATCH') {
120
+ if (err.errorData?.code === 'NO_CAPACITY_MATCH' && tier === '1') {
79
121
  process.stdout.write('\n');
80
- process.stdout.write(chalk.dim(' RunPod unavailable, trying budget providers...'));
122
+ process.stdout.write(chalk.dim(' No tier 1 capacity — trying tier 2 marketplace routing...'));
81
123
  try {
82
124
  dep = await callApi('/run', {
83
125
  method: 'POST',
@@ -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 --cheap flag', () => {
208
- const { flags } = parseServeArgs(['my/model', '--cheap']);
209
- expect(flags.cheap).toBe(true);
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('--cheap defaults to falsy when not passed', () => {
207
+ it('--tier defaults to undefined when not passed', () => {
213
208
  const { flags } = parseServeArgs(['my/model']);
214
- expect(flags.cheap).toBeFalsy();
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');