badgr-cli 1.0.27 → 1.0.29
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/logs.js +57 -5
- package/src/commands/run.js +17 -8
- 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/logs.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { findDeployment, listDeployments } from '../store.js';
|
|
2
2
|
import { requireApiKey } from '../config.js';
|
|
3
|
-
import { getDeploymentLogs } from '../api.js';
|
|
3
|
+
import { getDeploymentLogs, callApi } from '../api.js';
|
|
4
|
+
|
|
5
|
+
const TERMINAL_STATUSES = new Set(['succeeded', 'completed', 'failed', 'stopped', 'terminated']);
|
|
6
|
+
const FOLLOW_POLL_MS = 3000;
|
|
7
|
+
|
|
8
|
+
// Lines that carry structured metadata shown elsewhere (status bar in `badgr run`).
|
|
9
|
+
const LOG_META_RE = /^\[dep-[^\]]+\] (status|gpu|region|endpoint|cost|receipt|provider_status|uptime)=/;
|
|
4
10
|
|
|
5
11
|
export async function logsCommand(config, args, chalk) {
|
|
6
12
|
const idOrName = args.find(a => !a.startsWith('--'));
|
|
@@ -26,13 +32,18 @@ export async function logsCommand(config, args, chalk) {
|
|
|
26
32
|
|
|
27
33
|
console.log(chalk.bold(`\n📋 Logs: ${localDep?.name ?? deploymentId}\n`));
|
|
28
34
|
|
|
35
|
+
// Fetch and print initial batch of logs.
|
|
36
|
+
const seen = new Set();
|
|
29
37
|
try {
|
|
30
38
|
const data = await getDeploymentLogs(config, deploymentId);
|
|
31
39
|
const lines = data?.logs ?? [];
|
|
32
|
-
if (lines.length === 0) {
|
|
40
|
+
if (lines.length === 0 && !follow) {
|
|
33
41
|
console.log(chalk.dim(' No log lines available yet.\n'));
|
|
34
42
|
} else {
|
|
35
|
-
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
seen.add(line);
|
|
45
|
+
if (!LOG_META_RE.test(line)) console.log(` ${chalk.dim(line)}`);
|
|
46
|
+
}
|
|
36
47
|
}
|
|
37
48
|
} catch (err) {
|
|
38
49
|
console.log(chalk.yellow(` Could not fetch logs: ${err.message}`));
|
|
@@ -40,10 +51,51 @@ export async function logsCommand(config, args, chalk) {
|
|
|
40
51
|
console.log(chalk.dim(`\n GPU: ${localDep.gpu} Type: ${localDep.type}`));
|
|
41
52
|
console.log(chalk.dim(` Endpoint: ${localDep.endpointUrl}`));
|
|
42
53
|
}
|
|
54
|
+
if (!follow) { console.log(); return; }
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
if (follow) {
|
|
46
|
-
|
|
57
|
+
if (!follow) { console.log(); return; }
|
|
58
|
+
|
|
59
|
+
// --follow: poll until the deployment reaches a terminal state.
|
|
60
|
+
console.log(chalk.dim(' (following — Ctrl+C to stop)\n'));
|
|
61
|
+
|
|
62
|
+
let stopping = false;
|
|
63
|
+
process.once('SIGINT', () => { stopping = true; });
|
|
64
|
+
|
|
65
|
+
while (!stopping) {
|
|
66
|
+
await new Promise(r => setTimeout(r, FOLLOW_POLL_MS));
|
|
67
|
+
if (stopping) break;
|
|
68
|
+
|
|
69
|
+
let status = null;
|
|
70
|
+
try {
|
|
71
|
+
const dep = await callApi(`/deployments/${deploymentId}`, {
|
|
72
|
+
apiKey: config.apiKey,
|
|
73
|
+
baseUrl: config.baseUrl,
|
|
74
|
+
});
|
|
75
|
+
status = dep?.status ?? null;
|
|
76
|
+
} catch {
|
|
77
|
+
// network blip — keep following
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const data = await getDeploymentLogs(config, deploymentId);
|
|
82
|
+
for (const line of (data?.logs ?? [])) {
|
|
83
|
+
if (seen.has(line)) continue;
|
|
84
|
+
seen.add(line);
|
|
85
|
+
if (!LOG_META_RE.test(line)) {
|
|
86
|
+
const isErr = /^error\b/i.test(line) || /Error response from daemon/i.test(line);
|
|
87
|
+
console.log(` ${isErr ? chalk.red(line) : chalk.dim(line)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// logs endpoint temporarily unavailable
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (status && TERMINAL_STATUSES.has(status)) {
|
|
95
|
+
console.log(chalk.dim(`\n Job ${status}. No more logs.\n`));
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
47
98
|
}
|
|
99
|
+
|
|
48
100
|
console.log();
|
|
49
101
|
}
|
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.`));
|
|
@@ -445,12 +450,15 @@ export async function runCommand(config, args, chalk) {
|
|
|
445
450
|
|
|
446
451
|
const rcptId = dep.receipt_id || generateReceiptId();
|
|
447
452
|
addReceipt({
|
|
448
|
-
receiptId:
|
|
449
|
-
action:
|
|
450
|
-
deploymentId:
|
|
451
|
-
gpu:
|
|
452
|
-
|
|
453
|
-
|
|
453
|
+
receiptId: rcptId,
|
|
454
|
+
action: 'badgr run',
|
|
455
|
+
deploymentId: dep.deployment_id,
|
|
456
|
+
gpu: dep.gpu_type,
|
|
457
|
+
providerRoute: dep.provider ?? null,
|
|
458
|
+
maxCost: maxCost ?? null,
|
|
459
|
+
maxRuntime: flags.maxRuntime ?? null,
|
|
460
|
+
status: dep.status,
|
|
461
|
+
createdAt: new Date().toISOString(),
|
|
454
462
|
});
|
|
455
463
|
|
|
456
464
|
console.log();
|
|
@@ -501,7 +509,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
501
509
|
}
|
|
502
510
|
const runtimeMs = Date.now() - attachStart;
|
|
503
511
|
const finalCost = ratePerHour * (runtimeMs / 3_600_000);
|
|
504
|
-
updateReceipt(rcptId, { status: reason, runtimeSeconds: Math.round(runtimeMs / 1000), finalCost });
|
|
512
|
+
updateReceipt(rcptId, { status: reason, teardownStatus: 'terminated', runtimeSeconds: Math.round(runtimeMs / 1000), finalCost });
|
|
505
513
|
console.log(chalk.dim(` Runtime: ${fmtRuntime(runtimeMs)} • Est. cost: $${finalCost.toFixed(4)}`));
|
|
506
514
|
console.log(chalk.dim(' Job stopped. Billing ended.\n'));
|
|
507
515
|
process.exit(reason === 'interrupted' ? 0 : 1);
|
|
@@ -520,11 +528,12 @@ export async function runCommand(config, args, chalk) {
|
|
|
520
528
|
|
|
521
529
|
const finalCost = ratePerHour * (runtimeMs / 3_600_000);
|
|
522
530
|
updateReceipt(rcptId, {
|
|
523
|
-
status:
|
|
531
|
+
status: finalStatus,
|
|
524
532
|
exitCode,
|
|
525
533
|
runtimeSeconds: Math.round(runtimeMs / 1000),
|
|
526
534
|
finalCost,
|
|
527
535
|
failureType,
|
|
536
|
+
teardownStatus: (finalStatus === 'completed' || finalStatus === 'succeeded') ? 'terminated' : 'failed',
|
|
528
537
|
});
|
|
529
538
|
|
|
530
539
|
console.log(` ${chalk.bold('Runtime:')} ${fmtRuntime(runtimeMs)}`);
|
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');
|