badgr-cli 1.0.31 → 1.0.32

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,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,34 @@ 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
+
21
36
  if (r.status) console.log(` ${chalk.bold('status:')} ${r.status}`);
22
37
  console.log(` ${chalk.dim(ts)}`);
23
38
  console.log();