badgr-cli 1.0.31 → 1.0.34

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.
@@ -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
+ }
@@ -0,0 +1,111 @@
1
+ import { requireApiKey } from '../config.js';
2
+ import { callApi } from '../api.js';
3
+
4
+ /**
5
+ * badgr capacity # show what's available right now (auto)
6
+ * badgr capacity --gpu A100 # check a specific GPU type
7
+ */
8
+ function parseCapacityArgs(args) {
9
+ const flags = {};
10
+ let i = 0;
11
+ while (i < args.length) {
12
+ if (args[i] === '--gpu') { flags.gpu = args[++i]; i++; continue; }
13
+ if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
14
+ if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
15
+ i++;
16
+ }
17
+ return flags;
18
+ }
19
+
20
+ export async function capacityCommand(config, args, chalk) {
21
+ const flags = parseCapacityArgs(args);
22
+ requireApiKey(config);
23
+
24
+ const maxPrice = flags.maxPrice ?? 10;
25
+
26
+ // No --gpu: show cheapest runnable GPU across all types
27
+ if (!flags.gpu) {
28
+ console.log(chalk.bold('\nAvailable GPU capacity\n'));
29
+ process.stdout.write(chalk.dim(' Checking availability...\n'));
30
+
31
+ let data;
32
+ try {
33
+ const params = new URLSearchParams({ max_price: String(maxPrice) });
34
+ if (flags.region) params.set('region', flags.region.toUpperCase());
35
+ data = await callApi(`/capacity/auto?${params}`, {
36
+ apiKey: config.apiKey,
37
+ baseUrl: config.baseUrl,
38
+ });
39
+ } catch (err) {
40
+ if (err.errorData?.code === 'NO_CAPACITY_MATCH') {
41
+ console.log(chalk.dim('\n No GPU capacity available right now.\n'));
42
+ console.log(chalk.dim(' Try again in a few minutes, or check a specific GPU with --gpu <type>.\n'));
43
+ return;
44
+ }
45
+ console.error(chalk.red(`\n ✗ Capacity check failed: ${err.message}\n`));
46
+ process.exit(1);
47
+ }
48
+
49
+ console.log();
50
+ console.log(` ${chalk.bold('Cheapest available:')} ${chalk.cyan(data.gpu)} in ${data.region} ${chalk.green('$' + data.price.toFixed(2) + '/hr')}`);
51
+ console.log();
52
+ console.log(` ${chalk.bold('Run now:')}`);
53
+ console.log(chalk.cyan(` badgr run python train.py`));
54
+ console.log(chalk.cyan(` badgr serve meta-llama/Llama-3.1-8B-Instruct`));
55
+ console.log();
56
+ console.log(chalk.dim(` Use badgr capacity --gpu A100 to check a specific GPU type.`));
57
+ console.log();
58
+ return;
59
+ }
60
+
61
+ // --gpu specified: show availability for that type
62
+ const gpu = flags.gpu.toUpperCase().replace('-', '_');
63
+ const params = new URLSearchParams({ gpu, max_price: String(maxPrice) });
64
+ if (flags.region) params.set('region', flags.region.toUpperCase());
65
+
66
+ console.log(chalk.bold(`\nCapacity: ${gpu}\n`));
67
+ process.stdout.write(chalk.dim(' Checking...\n'));
68
+
69
+ let data;
70
+ try {
71
+ data = await callApi(`/capacity/suggestions?${params}`, {
72
+ apiKey: config.apiKey,
73
+ baseUrl: config.baseUrl,
74
+ });
75
+ } catch (err) {
76
+ console.error(chalk.red(`\n ✗ Capacity check failed: ${err.message}\n`));
77
+ process.exit(1);
78
+ }
79
+
80
+ const matches = data.matches ?? [];
81
+ if (matches.length > 0) {
82
+ console.log(chalk.bold('\n Available now:\n'));
83
+ for (const m of matches) {
84
+ console.log(` ${chalk.green('●')} ${m.gpu} in ${m.region} ${chalk.green('$' + m.price.toFixed(2) + '/hr')}`);
85
+ }
86
+ console.log();
87
+ const regionFlag = flags.region ? ` --region ${flags.region.toUpperCase()}` : '';
88
+ console.log(` ${chalk.bold('Run:')}`);
89
+ console.log(chalk.cyan(` badgr run python train.py --gpu ${gpu}${regionFlag}`));
90
+ console.log();
91
+ } else {
92
+ const regionLabel = flags.region ? ` in ${flags.region.toUpperCase()}` : '';
93
+ console.log(chalk.dim(`\n No ${gpu} available right now under $${maxPrice.toFixed(2)}/hr${regionLabel}.\n`));
94
+
95
+ const alternatives = data.alternatives ?? [];
96
+ if (alternatives.length > 0) {
97
+ console.log(chalk.bold(' Available alternatives:\n'));
98
+ for (const a of alternatives) {
99
+ console.log(` ${chalk.dim('●')} ${a.gpu} in ${a.region} $${a.price.toFixed(2)}/hr`);
100
+ }
101
+ console.log();
102
+ console.log(` ${chalk.bold('Try:')}`);
103
+ for (const a of alternatives.slice(0, 3)) {
104
+ console.log(chalk.cyan(` badgr run python train.py --gpu ${a.gpu}`));
105
+ }
106
+ console.log();
107
+ } else {
108
+ console.log(chalk.dim(' No alternatives found. Try `badgr capacity` to see global availability.\n'));
109
+ }
110
+ }
111
+ }
@@ -12,43 +12,46 @@ export async function downCommand(config, args, chalk) {
12
12
 
13
13
  requireApiKey(config);
14
14
 
15
- const localDep = findDeployment(idOrName);
16
-
17
- // Resolve the deployment ID to send to the backend
15
+ const localDep = findDeployment(idOrName);
18
16
  const deploymentId = localDep?.id ?? idOrName;
19
17
 
20
- console.log(chalk.dim(` Terminating ${deploymentId}...`));
18
+ process.stdout.write(chalk.dim(` Stopping ${deploymentId}...`));
21
19
 
22
20
  let dep;
23
21
  try {
24
22
  dep = await terminateDeployment(config, deploymentId);
25
23
  } catch (err) {
26
- console.error(chalk.red(`\n ✗ Terminate failed: ${err.message}\n`));
24
+ process.stdout.write('\n');
25
+ console.error(chalk.red(`\n ✗ Could not stop deployment: ${err.message}\n`));
27
26
  return;
28
27
  }
29
28
 
29
+ process.stdout.write('\n');
30
+
31
+ // Compute final cost from the deployment timestamps
32
+ const stoppedAt = dep.stopped_at ?? (Date.now() / 1000);
33
+ const startedAt = dep.started_at ?? stoppedAt;
34
+ const runtimeMin = Math.round((stoppedAt - startedAt) / 60);
35
+ const runtimeHr = (stoppedAt - startedAt) / 3600;
36
+ const finalCost = (dep.cost_per_hour || 0) * runtimeHr;
37
+
30
38
  const rcptId = generateReceiptId();
31
39
  addReceipt({
32
- receiptId: rcptId,
33
- action: 'badgr down',
34
- deploymentId: dep.deployment_id,
35
- name: dep.name,
36
- provider: dep.provider,
37
- gpu: dep.gpu_type,
38
- latencyMs: 0,
39
- status: 'terminated',
40
- createdAt: new Date().toISOString(),
40
+ receiptId: rcptId,
41
+ action: 'badgr down',
42
+ deploymentId: dep.deployment_id,
43
+ gpu: dep.gpu_type,
44
+ runtimeSeconds: Math.round(stoppedAt - startedAt),
45
+ finalCost,
46
+ status: 'terminated',
47
+ createdAt: new Date().toISOString(),
41
48
  });
42
49
 
43
- // Remove from local store
44
50
  removeDeployment(idOrName);
45
51
 
46
- console.log(chalk.green(`\n✓ Deployment stopped: ${dep.name}\n`));
47
- console.log(` ${chalk.bold('ID:')} ${dep.deployment_id}`);
48
- console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
49
- if (dep.started_at) {
50
- const uptime = Math.round((Date.now() / 1000 - dep.started_at) / 60);
51
- console.log(` ${chalk.bold('Uptime:')} ${uptime} min`);
52
- }
53
- console.log(`\n ${chalk.bold('Receipt ID:')} ${chalk.dim(rcptId)}\n`);
52
+ console.log(chalk.green('\n✓ Stopped'));
53
+ console.log(chalk.green(' Billing ended\n'));
54
+ console.log(` ${chalk.bold('Runtime:')} ${runtimeMin}m`);
55
+ if (finalCost > 0) console.log(` ${chalk.bold('Final cost:')} $${finalCost.toFixed(4)}`);
56
+ console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}\n`);
54
57
  }
@@ -1,20 +1,36 @@
1
1
  import { input } from '@inquirer/prompts';
2
+ import { DEFAULTS } from '../config.js';
3
+ import { callApi } from '../api.js';
2
4
 
3
5
  export async function loginCommand(chalk, saveConfigFn) {
4
- console.log(chalk.bold('\n🔑 Badgr Login\n'));
6
+ console.log(chalk.bold('\nBadgr Login\n'));
5
7
 
6
8
  const apiKey = await input({
7
9
  message: 'Enter your Badgr API key:',
8
10
  validate: v => v.trim() ? true : 'API key is required',
9
11
  });
10
12
 
11
- const baseUrl = await input({
12
- message: 'API base URL:',
13
- default: 'https://api.badgr.ai/v1',
13
+ const config = saveConfigFn({
14
+ apiKey: apiKey.trim(),
15
+ baseUrl: DEFAULTS.baseUrl,
14
16
  });
15
17
 
16
- const config = saveConfigFn({ apiKey: apiKey.trim(), baseUrl: baseUrl.trim() });
17
- console.log(chalk.green('\n✓ Logged in — config saved to ~/.badgr/config.json\n'));
18
- console.log(chalk.dim(` Base URL: ${config.baseUrl}\n`));
18
+ console.log(chalk.green('\n✓ Logged in'));
19
+ console.log(chalk.dim(` Config saved to ~/.badgr/config.json`));
20
+
21
+ // Verify the key works against the live API
22
+ try {
23
+ await callApi('/models', { apiKey: config.apiKey, baseUrl: config.baseUrl });
24
+ console.log(chalk.green('✓ API reachable'));
25
+ console.log(chalk.green('✓ API key valid\n'));
26
+ } catch (err) {
27
+ if (err.httpStatus === 401 || err.httpStatus === 403) {
28
+ console.log(chalk.yellow('⚠ API key may be invalid — double-check and run badgr login again\n'));
29
+ } else {
30
+ console.log(chalk.yellow('⚠ Could not reach the API right now — check your internet connection\n'));
31
+ }
32
+ }
33
+
34
+ console.log(chalk.dim(` Run ${chalk.cyan('badgr run python train.py')} to launch your first job.\n`));
19
35
  return config;
20
36
  }
@@ -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
- lines.forEach(l => console.log(` ${chalk.dim(l)}`));
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
- console.log(chalk.yellow('\n --follow: live log streaming not yet supported. Poll with `badgr logs`.\n'));
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
  }
@@ -1,19 +1,38 @@
1
- import { listModels } from '../api.js';
1
+ import { listModels, callApi } from '../api.js';
2
2
  import { listAll } from '../router.js';
3
3
 
4
4
  export async function modelsCommand(config, chalk) {
5
- const gpus = listAll();
6
-
7
5
  console.log(chalk.bold('\n📦 GPU Options (cheapest first)\n'));
6
+
7
+ let gpus = null;
8
+ if (config.apiKey) {
9
+ try {
10
+ const data = await callApi('/gpus', { apiKey: config.apiKey, baseUrl: config.baseUrl });
11
+ if (data?.gpus?.length) {
12
+ gpus = data.gpus.map(g => ({
13
+ id: g.id,
14
+ name: g.name,
15
+ vramGb: g.vram_gb,
16
+ ratePerHour: g.rate_per_hour,
17
+ })).sort((a, b) => a.ratePerHour - b.ratePerHour);
18
+ }
19
+ } catch {
20
+ // fallback to local catalog on error
21
+ }
22
+ }
23
+
24
+ if (!gpus) gpus = listAll();
25
+
8
26
  console.log(
9
- ` ${'ID'.padEnd(14)} ${'Name'.padEnd(24)} ${'VRAM'.padEnd(8)} Rate/hr`
27
+ ` ${'ID'.padEnd(14)} ${'Name'.padEnd(24)} ${'VRAM'.padEnd(8)} Rate/hr (indicative)`
10
28
  );
11
- console.log(` ${'─'.repeat(58)}`);
29
+ console.log(` ${'─'.repeat(68)}`);
12
30
  gpus.forEach(g => {
13
31
  console.log(
14
- ` ${chalk.cyan(g.id.padEnd(14))} ${g.name.padEnd(24)} ${`${g.vramGb}GB`.padEnd(8)} $${g.ratePerHour.toFixed(2)}`
32
+ ` ${chalk.cyan(g.id.padEnd(14))} ${g.name.padEnd(24)} ${`${g.vramGb}GB`.padEnd(8)} ~$${g.ratePerHour.toFixed(2)}`
15
33
  );
16
34
  });
35
+ console.log(chalk.dim(' Actual billing is set at job start — use `badgr run` to see live rates.'));
17
36
 
18
37
  console.log(chalk.bold('\n🤖 LLM Models\n'));
19
38
  if (!config.apiKey) {
@@ -5,19 +5,38 @@ function fmtMs(ms) {
5
5
  return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
6
6
  }
7
7
 
8
+ function fmtRuntime(s) {
9
+ if (s < 60) return `${s}s`;
10
+ return `${Math.floor(s / 60)}m ${s % 60}s`;
11
+ }
12
+
8
13
  function printReceipt(r, chalk) {
9
14
  const id = r.receiptId ?? r.request_id ?? r.id ?? '—';
10
15
  const ts = r.createdAt ?? r.created_at ?? '—';
11
- const cost = r.route?.ratePerHour ?? r.cost_usd ?? 0;
12
- const prov = r.route?.provider ?? r.provider ?? r.model_provider ?? '—';
13
- const lat = r.latencyMs ?? r.latency_ms;
14
16
  const action = r.action ?? r.endpoint ?? '—';
17
+ const lat = r.latencyMs ?? r.latency_ms;
15
18
 
16
19
  console.log(` ${chalk.cyan(id)}`);
17
20
  console.log(` ${chalk.bold('action:')} ${action}`);
21
+
22
+ // GPU job fields (from badgr run / badgr down)
23
+ if (r.gpu) console.log(` ${chalk.bold('gpu:')} ${r.gpu}`);
24
+ if (r.runtimeSeconds) console.log(` ${chalk.bold('runtime:')} ${fmtRuntime(r.runtimeSeconds)}`);
25
+ if (r.finalCost) console.log(` ${chalk.bold('cost:')} $${r.finalCost.toFixed(4)}`);
26
+ else if (r.cost_usd) console.log(` ${chalk.bold('cost:')} $${r.cost_usd.toFixed(4)}`);
27
+ if (r.exitCode !== undefined && r.exitCode !== null) {
28
+ console.log(` ${chalk.bold('exit:')} ${r.exitCode !== 0 ? chalk.red(r.exitCode) : chalk.green(r.exitCode)}`);
29
+ }
30
+
31
+ // API inference receipt fields
18
32
  if (lat !== undefined) console.log(` ${chalk.bold('latency:')} ${fmtMs(lat)}`);
19
- if (cost) console.log(` ${chalk.bold('cost:')} $${typeof cost === 'number' ? cost.toFixed(4) : cost}/hr`);
20
33
  if (r.retries !== undefined) console.log(` ${chalk.bold('retries:')} ${r.retries}`);
34
+ if (r.route?.ratePerHour) console.log(` ${chalk.bold('rate:')} $${r.route.ratePerHour.toFixed(4)}/hr`);
35
+
36
+ if (r.failureCategory ?? r.failure_category ?? r.failure_reason) {
37
+ const cat = r.failureCategory ?? r.failure_category ?? r.failure_reason;
38
+ console.log(` ${chalk.bold('failure:')} ${chalk.red(cat)}`);
39
+ }
21
40
  if (r.status) console.log(` ${chalk.bold('status:')} ${r.status}`);
22
41
  console.log(` ${chalk.dim(ts)}`);
23
42
  console.log();