badgr-cli 1.0.27 → 1.0.28

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.27",
3
+ "version": "1.0.28",
4
4
  "description": "Badgr, run or serve GPU workloads from one command",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -59,6 +59,24 @@ export async function callApi(path, { method = 'GET', apiKey, baseUrl, body } =
59
59
  detail = rawBody || res.statusText || '';
60
60
  }
61
61
  const isCapacityError = errorData?.code === 'NO_CAPACITY_MATCH';
62
+ if (res.status === 402) {
63
+ // Payment required — format a clear, actionable error
64
+ const d = errorData?.detail ?? errorData ?? {};
65
+ const detailObj = typeof d === 'object' ? d : {};
66
+ const balanceUsd = typeof detailObj.balance_usd === 'number' ? detailObj.balance_usd : null;
67
+ const requiredUsd = typeof detailObj.required_usd === 'number' ? detailObj.required_usd : null;
68
+ const topupUrl = detailObj.topup_url || 'https://aibadgr.com/dashboard#billing';
69
+
70
+ let msg = '\nPayment required.\n';
71
+ if (balanceUsd !== null) msg += `Your balance is $${balanceUsd.toFixed(2)}.`;
72
+ if (requiredUsd !== null) msg += ` This job needs a $${requiredUsd.toFixed(2)} reserve.`;
73
+ msg += '\n\nAdd balance:\n ' + topupUrl + '\n';
74
+ const err = new Error(msg);
75
+ err.errorData = errorData;
76
+ err.httpStatus = 402;
77
+ err.isPaymentRequired = true;
78
+ throw err;
79
+ }
62
80
  const hint =
63
81
  res.status === 401 ? '\n Hint: Invalid or missing API key — run: badgr login' :
64
82
  res.status === 403 ? '\n Hint: Access denied — check your API key permissions' :
package/src/badgr.js CHANGED
@@ -12,6 +12,7 @@ import { serveCommand } from './commands/serve.js';
12
12
  import { modelsCommand } from './commands/models.js';
13
13
  import { capacityCommand } from './commands/capacity.js';
14
14
  import { testCommand } from './commands/test-run.js';
15
+ import { billingCommand } from './commands/billing.js';
15
16
 
16
17
  const HELP = `
17
18
  ${chalk.bold('badgr')} — run or serve GPU workloads from one command
@@ -26,6 +27,7 @@ ${chalk.bold('COMMANDS')}
26
27
  ${chalk.cyan('badgr receipts')} Show cost history
27
28
  ${chalk.cyan('badgr test')} Run an end-to-end test (provision → run → teardown)
28
29
  ${chalk.cyan('badgr capacity')} Check what GPU capacity is available right now
30
+ ${chalk.cyan('badgr billing')} Show balance and add funds
29
31
 
30
32
  ${chalk.bold('EXAMPLES')}
31
33
  ${chalk.dim('# Verify the stack works end-to-end:')}
@@ -104,6 +106,7 @@ async function main() {
104
106
  case 'models': return modelsCommand(config, chalk);
105
107
  case 'capacity': return capacityCommand(config, rest, chalk);
106
108
  case 'test': return testCommand(config, rest, chalk);
109
+ case 'billing': return billingCommand(config, rest, chalk);
107
110
  // legacy aliases kept for compatibility
108
111
  case 'up': return upCommand(config, rest, chalk);
109
112
  case 'config': {
@@ -0,0 +1,93 @@
1
+ import { execSync } from 'child_process';
2
+ import { requireApiKey } from '../config.js';
3
+ import { callApi } from '../api.js';
4
+
5
+ const BILLING_HELP = `
6
+ badgr billing — manage your AI Badgr balance
7
+
8
+ COMMANDS
9
+ badgr billing status Show current balance
10
+ badgr billing add <amount> Open checkout to add balance (minimum $10)
11
+
12
+ EXAMPLES
13
+ badgr billing status
14
+ badgr billing add 10
15
+ badgr billing add 20
16
+ badgr billing add 50
17
+ `;
18
+
19
+ function openBrowser(url) {
20
+ const platform = process.platform;
21
+ try {
22
+ if (platform === 'darwin') execSync(`open "${url}"`);
23
+ else if (platform === 'win32') execSync(`start "" "${url}"`);
24
+ else execSync(`xdg-open "${url}"`);
25
+ } catch {
26
+ // Silently ignore — we print the URL anyway
27
+ }
28
+ }
29
+
30
+ async function billingStatus(config, chalk) {
31
+ requireApiKey(config);
32
+ try {
33
+ const apiUrl = config.baseUrl.replace('/v1', '').replace('/api/v1', '');
34
+ const data = await callApi('/api/me', {
35
+ apiKey: config.apiKey,
36
+ baseUrl: apiUrl,
37
+ });
38
+ const credits = data.credits ?? 0;
39
+ const balanceUsd = (credits / 10000).toFixed(2);
40
+ console.log();
41
+ console.log(chalk.bold(' Balance'));
42
+ console.log(` ${chalk.bold(chalk.blue(`$${balanceUsd}`))}`);
43
+ console.log();
44
+ if (credits === 0) {
45
+ console.log(chalk.yellow(' Balance is $0.00. Add balance before making API calls or running GPU jobs.'));
46
+ console.log(chalk.dim(' Add balance: badgr billing add 10'));
47
+ console.log(chalk.dim(' Or visit: https://aibadgr.com/billing/top-up'));
48
+ }
49
+ console.log();
50
+ } catch (err) {
51
+ if (err.isPaymentRequired) {
52
+ console.error(err.message);
53
+ process.exit(1);
54
+ }
55
+ console.error(chalk.red(' Could not fetch balance: ' + err.message));
56
+ process.exit(1);
57
+ }
58
+ }
59
+
60
+ async function billingAdd(config, amount, chalk) {
61
+ requireApiKey(config);
62
+ const amountInt = parseInt(amount, 10);
63
+ if (!amountInt || amountInt < 10) {
64
+ console.error(chalk.red(' Minimum top-up is $10. Example: badgr billing add 10'));
65
+ process.exit(1);
66
+ }
67
+
68
+ const url = `https://aibadgr.com/dashboard#billing`;
69
+ console.log();
70
+ console.log(chalk.bold(` Opening dashboard billing to add $${amountInt}...`));
71
+ console.log();
72
+ console.log(` ${chalk.dim(url)}`);
73
+ console.log();
74
+ openBrowser(url);
75
+ console.log(chalk.dim(' Complete payment in your browser, then rerun your command.'));
76
+ console.log();
77
+ }
78
+
79
+ export async function billingCommand(config, args, chalk) {
80
+ const [sub, ...rest] = args;
81
+
82
+ if (!sub || sub === '--help' || sub === '-h') {
83
+ console.log(BILLING_HELP);
84
+ return;
85
+ }
86
+
87
+ if (sub === 'status') return billingStatus(config, chalk);
88
+ if (sub === 'add') return billingAdd(config, rest[0], chalk);
89
+
90
+ console.error(chalk.red(` Unknown billing command: ${sub}`));
91
+ console.log(chalk.dim(' Run: badgr billing --help'));
92
+ process.exit(1);
93
+ }
@@ -431,6 +431,11 @@ export async function runCommand(config, args, chalk) {
431
431
  console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.\n`));
432
432
  }
433
433
  process.exit(1);
434
+ } else if (err.isPaymentRequired) {
435
+ console.error(chalk.yellow(err.message));
436
+ const rerun = ['badgr run', ...args].join(' ');
437
+ console.error(chalk.dim(`After payment, rerun:\n ${rerun}\n`));
438
+ process.exit(1);
434
439
  } else {
435
440
  console.error(chalk.red(`\n ✗ Could not start job: ${err.message}\n`));
436
441
  console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.`));
@@ -171,6 +171,12 @@ export async function serveCommand(config, args, chalk) {
171
171
  failureType: 'infrastructure',
172
172
  createdAt: new Date().toISOString(),
173
173
  });
174
+ if (err.isPaymentRequired) {
175
+ console.error(chalk.yellow(err.message));
176
+ const rerun = `badgr serve ${args.join(' ')}`;
177
+ console.error(chalk.dim(`After payment, rerun:\n ${rerun}\n`));
178
+ return;
179
+ }
174
180
  console.error(chalk.red(`\n ✗ Could not start endpoint: ${err.message}`));
175
181
  console.error(chalk.dim(`\n Receipt: ${failRcptId}`));
176
182
  console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
@@ -175,7 +175,8 @@ export async function testCommand(config, args, chalk) {
175
175
  process.stdout.write(chalk.dim(' Waiting for container to start...'));
176
176
  const started = await pollStatus(
177
177
  config, depId,
178
- new Set(['running', 'failed', 'stopped', 'completed']),
178
+ // Modal serverless jobs may skip straight to succeeded when the callback fires.
179
+ new Set(['running', 'starting', 'succeeded', 'failed', 'stopped', 'completed']),
179
180
  TEST_MAX_RUNTIME_MS,
180
181
  );
181
182
  process.stdout.write('\n');