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 +1 -1
- package/src/api.js +18 -0
- package/src/badgr.js +3 -0
- package/src/commands/billing.js +93 -0
- package/src/commands/run.js +5 -0
- package/src/commands/serve.js +6 -0
- package/src/commands/test-run.js +2 -1
package/package.json
CHANGED
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
|
+
}
|
package/src/commands/run.js
CHANGED
|
@@ -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.`));
|
package/src/commands/serve.js
CHANGED
|
@@ -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`));
|
package/src/commands/test-run.js
CHANGED
|
@@ -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
|
-
|
|
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');
|