badgr-cli 1.0.18 → 1.0.20

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.18",
3
+ "version": "1.0.20",
4
4
  "description": "Badgr, run or serve GPU workloads from one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@ 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; }
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] === '--detach') { flags.detach = true; i++; continue; }
@@ -81,6 +82,42 @@ function renderStatusBar(chalk, { elapsedMs, ratePerHour, gpuUtil, cpuUtil, maxR
81
82
  return chalk.dim(' ' + parts.join(' • '));
82
83
  }
83
84
 
85
+ // Wait for status to leave 'starting' (smoke check running on backend).
86
+ // Returns the final dep object with status 'running' or 'failed'.
87
+ async function waitForRunning(config, depId, chalk) {
88
+ const POLL_MS = 3000;
89
+ const TIMEOUT_MS = 120_000; // 2 min max for smoke check
90
+ const startMs = Date.now();
91
+ let dots = 0;
92
+
93
+ process.stdout.write(chalk.dim(' Waiting for container to start'));
94
+ const ticker = setInterval(() => {
95
+ process.stdout.write('.');
96
+ dots++;
97
+ }, 1000);
98
+
99
+ try {
100
+ while (Date.now() - startMs < TIMEOUT_MS) {
101
+ await new Promise(r => setTimeout(r, POLL_MS));
102
+ const dep = await callApi(`/deployments/${depId}`, {
103
+ apiKey: config.apiKey,
104
+ baseUrl: config.baseUrl,
105
+ });
106
+ if (dep.status !== 'starting') {
107
+ clearInterval(ticker);
108
+ process.stdout.write('\n');
109
+ return dep;
110
+ }
111
+ }
112
+ } finally {
113
+ clearInterval(ticker);
114
+ if (dots > 0) process.stdout.write('\n');
115
+ }
116
+
117
+ // Timed out waiting — return whatever we have
118
+ return await callApi(`/deployments/${depId}`, { apiKey: config.apiKey, baseUrl: config.baseUrl });
119
+ }
120
+
84
121
  async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost = null, ratePerHour = 0, onTeardown }) {
85
122
  const TERMINAL = new Set(['stopped', 'failed', 'completed']);
86
123
  const POLL_MS = 4000;
@@ -129,14 +166,14 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
129
166
  if (maxCost !== null && spentSoFar >= maxCost) {
130
167
  tearing = true;
131
168
  stopTicker();
132
- onTeardown('max-cost');
169
+ await onTeardown('max-cost');
133
170
  return { status: 'timeout', exitCode: null, runtimeMs: elapsedMs, failureType: null };
134
171
  }
135
172
 
136
173
  if (maxRuntimeMs !== null && elapsedMs >= maxRuntimeMs) {
137
174
  tearing = true;
138
175
  stopTicker();
139
- onTeardown('max-runtime');
176
+ await onTeardown('max-runtime');
140
177
  return { status: 'timeout', exitCode: null, runtimeMs: elapsedMs, failureType: null };
141
178
  }
142
179
 
@@ -158,7 +195,7 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
158
195
  } else if (consecutiveErrs >= HEARTBEAT_KILL_POLLS) {
159
196
  tearing = true;
160
197
  stopTicker();
161
- onTeardown('heartbeat-lost');
198
+ await onTeardown('heartbeat-lost');
162
199
  return { status: 'failed', exitCode: null, runtimeMs: elapsedMs, failureType: 'infrastructure' };
163
200
  }
164
201
  }
@@ -202,8 +239,9 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
202
239
  // Skip provider util lines — they're shown in the status bar instead.
203
240
  if (/\b(gpu_util|cpu_util|provider_status|uptime)=/.test(line)) continue;
204
241
 
242
+ const isErrorLine = /^error\b/i.test(line) || /Error response from daemon/i.test(line);
205
243
  stopTicker();
206
- console.log(` ${chalk.dim(line)}`);
244
+ console.log(` ${isErrorLine ? chalk.red(line) : chalk.dim(line)}`);
207
245
  startTicker();
208
246
  }
209
247
  } catch {
@@ -347,6 +385,7 @@ export async function runCommand(config, args, chalk) {
347
385
  ...(effectiveRegion ? { region: effectiveRegion } : {}),
348
386
  max_price_per_hour: flags.maxPrice,
349
387
  name: flags.name,
388
+ ...(flags.tier ? { tier: flags.tier } : {}),
350
389
  };
351
390
  }
352
391
 
@@ -399,7 +438,7 @@ export async function runCommand(config, args, chalk) {
399
438
  const d2 = err2.errorData;
400
439
  if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
401
440
  if (d2?.low_cost_provider_failed) {
402
- console.error(chalk.red(`\n ✗ Low-cost provider failed. Primary provider also unavailable.\n`));
441
+ console.error(chalk.red(`\n ✗ Tier 2 unavailable. Tier 1 also unavailable. Try again shortly.\n`));
403
442
  } else {
404
443
  console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity but could not start the machine. Please try again.\n`));
405
444
  }
@@ -416,7 +455,7 @@ export async function runCommand(config, args, chalk) {
416
455
  }
417
456
  } else if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
418
457
  if (d?.low_cost_provider_failed) {
419
- console.error(chalk.red(`\n ✗ Low-cost provider failed. Primary provider also unavailable.\n`));
458
+ console.error(chalk.red(`\n ✗ Tier 2 unavailable. Tier 1 also unavailable. Try again shortly.\n`));
420
459
  } else {
421
460
  console.error(chalk.red(`\n ✗ Badgr found ${gpu} capacity but could not start the machine. Please try again.\n`));
422
461
  }
@@ -434,8 +473,8 @@ export async function runCommand(config, args, chalk) {
434
473
  }
435
474
  }
436
475
 
437
- if (dep.provider_fallback_note === 'low_cost_provider_failed') {
438
- console.log(chalk.yellow(' ℹ Low-cost provider failed. Using primary provider instead.\n'));
476
+ if (dep.provider_fallback_note === 'tier2_failed_using_tier1') {
477
+ console.log(chalk.yellow(' ℹ Tier 2 unavailable. Running on Tier 1 instead.\n'));
439
478
  }
440
479
 
441
480
  const rcptId = dep.receipt_id || generateReceiptId();
@@ -451,6 +490,7 @@ export async function runCommand(config, args, chalk) {
451
490
  console.log();
452
491
  console.log(chalk.bold(` Job ID: ${chalk.cyan(dep.deployment_id)}`));
453
492
  console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
493
+ if (dep.tier) console.log(` ${chalk.bold('Tier:')} ${dep.tier}`);
454
494
  if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
455
495
  console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
456
496
 
@@ -460,6 +500,20 @@ export async function runCommand(config, args, chalk) {
460
500
  return;
461
501
  }
462
502
 
503
+ // If the backend is still running the smoke check, wait for it to finish.
504
+ if (dep.status === 'starting') {
505
+ console.log();
506
+ dep = await waitForRunning(config, dep.deployment_id, chalk);
507
+ }
508
+
509
+ if (dep.status === 'failed') {
510
+ console.error(chalk.red('\n ✗ Container failed to start (infrastructure error).\n'));
511
+ console.error(chalk.dim(' The backend retried automatically. All attempts failed.'));
512
+ console.error(chalk.dim(` Contact support with receipt ID: ${rcptId}`));
513
+ console.log();
514
+ process.exit(1);
515
+ }
516
+
463
517
  const ratePerHour = dep.cost_per_hour || 0;
464
518
  console.log(chalk.dim('\n ── Live status (Ctrl+C to stop) ─────────────────────────────────\n'));
465
519
 
@@ -523,7 +577,7 @@ export async function runCommand(config, args, chalk) {
523
577
  }
524
578
  console.log();
525
579
  process.exit(exitCode ?? 1);
526
- } else {
580
+ } else if (finalStatus === 'completed' && (exitCode === 0 || exitCode === null)) {
527
581
  console.log(chalk.green(`\n ✓ Complete\n`));
528
582
  }
529
583
  }
@@ -17,6 +17,7 @@ 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; }
20
21
  if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
21
22
  if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
22
23
  if (args[i] === '--no-wait') { flags.noWait = true; i++; continue; }
@@ -79,6 +80,7 @@ export async function serveCommand(config, args, chalk) {
79
80
  ...(regionOverride || effectiveRegion ? { region: regionOverride || effectiveRegion } : {}),
80
81
  max_price_per_hour: flags.maxPrice,
81
82
  name: flags.name,
83
+ ...(flags.tier ? { tier: flags.tier } : {}),
82
84
  };
83
85
  }
84
86
 
@@ -125,7 +127,7 @@ export async function serveCommand(config, args, chalk) {
125
127
  const d2 = err2.errorData;
126
128
  if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
127
129
  if (d2?.low_cost_provider_failed) {
128
- console.error(chalk.red(`\n ✗ Low-cost provider failed. Primary provider also unavailable.\n`));
130
+ console.error(chalk.red(`\n ✗ Tier 2 unavailable. Tier 1 also unavailable. Try again shortly.\n`));
129
131
  } else {
130
132
  console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity but could not start the endpoint. Please try again.\n`));
131
133
  }
@@ -137,7 +139,7 @@ export async function serveCommand(config, args, chalk) {
137
139
  }
138
140
  } else if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
139
141
  if (d?.low_cost_provider_failed) {
140
- console.error(chalk.red(`\n ✗ Low-cost provider failed. Primary provider also unavailable.\n`));
142
+ console.error(chalk.red(`\n ✗ Tier 2 unavailable. Tier 1 also unavailable. Try again shortly.\n`));
141
143
  } else {
142
144
  console.error(chalk.red(`\n ✗ Badgr found capacity but could not start the endpoint. Please try again.\n`));
143
145
  }
@@ -161,8 +163,8 @@ export async function serveCommand(config, args, chalk) {
161
163
  }
162
164
  }
163
165
 
164
- if (dep.provider_fallback_note === 'low_cost_provider_failed') {
165
- console.log(chalk.yellow(' ℹ Low-cost provider failed. Using primary provider instead.\n'));
166
+ if (dep.provider_fallback_note === 'tier2_failed_using_tier1') {
167
+ console.log(chalk.yellow(' ℹ Tier 2 unavailable. Running on Tier 1 instead.\n'));
166
168
  }
167
169
 
168
170
  addDeployment({
@@ -215,6 +217,7 @@ export async function serveCommand(config, args, chalk) {
215
217
  console.log(` ${chalk.bold('Base URL:')} ${chalk.cyan(endpointUrl)}`);
216
218
  console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
217
219
  console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
220
+ if (dep.tier) console.log(` ${chalk.bold('Tier:')} ${dep.tier}`);
218
221
  if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
219
222
  console.log(` ${chalk.bold('Logs:')} ${chalk.dim(`badgr logs ${dep.deployment_id}`)}`);
220
223
  console.log(` ${chalk.bold('Stop billing:')} ${chalk.dim(`badgr down ${dep.deployment_id}`)}`);