badgr-cli 1.0.12 → 1.0.15

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,10 +1,10 @@
1
1
  {
2
2
  "name": "badgr-cli",
3
- "version": "1.0.12",
4
- "description": "Badgr run or serve GPU workloads from one command",
3
+ "version": "1.0.15",
4
+ "description": "Badgr, run or serve GPU workloads from one command",
5
5
  "type": "module",
6
6
  "bin": {
7
- "badgr": "./src/badgr.js"
7
+ "badgr": "src/badgr.js"
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node src/badgr.js",
@@ -21,6 +21,14 @@
21
21
  "engines": {
22
22
  "node": ">=18.0.0"
23
23
  },
24
- "keywords": ["gpu", "cli", "ai", "compute", "modal", "gateway", "openai"],
24
+ "keywords": [
25
+ "gpu",
26
+ "cli",
27
+ "ai",
28
+ "compute",
29
+ "modal",
30
+ "gateway",
31
+ "openai"
32
+ ],
25
33
  "license": "MIT"
26
34
  }
package/src/badgr.js CHANGED
@@ -15,64 +15,60 @@ import { capacityCommand } from './commands/capacity.js';
15
15
  const HELP = `
16
16
  ${chalk.bold('badgr')} — run or serve GPU workloads from one command
17
17
 
18
- ${chalk.bold('CORE COMMANDS')}
19
- ${chalk.cyan('badgr login')} Authenticate (save API key to ~/.badgr/config.json)
20
- ${chalk.cyan('badgr run <cmd...> --gpu <type>')} Run a one-off GPU job
21
- ${chalk.cyan('badgr serve <model> --gpu <type>')} Serve a model (OpenAI-compatible endpoint)
22
- ${chalk.cyan('badgr status')} Show active deployments + endpoint URLs
23
- ${chalk.cyan('badgr logs <id>')} Stream logs for a deployment
24
- ${chalk.cyan('badgr down <id>')} Terminate a deployment (stop billing)
25
- ${chalk.cyan('badgr receipts [<id>|<n>]')} Show receipts — pass ID for single, number for list
26
- ${chalk.cyan('badgr capacity [--gpu <type>]')} Check live GPU availability and alternatives
27
-
28
- ${chalk.bold('badgr run OPTIONS')}
29
- --gpu <type> GPU: RTX_4090, A100, L40S, H100 (default: RTX_4090)
30
- --image <image> Docker image (default: python:3.11-slim)
31
- --count <n> GPU count (default: 1)
32
- --region US|EU|AU Region preference (default: US)
33
- --max-price <$/hr> Hard spend cap per GPU-hour
34
- --name <name> Job name (auto-generated if omitted)
35
- --fallback closest|cheapest If requested GPU is unavailable, interactively pick the
36
- closest (default) or cheapest alternative
37
- --no-fallback Exit immediately when requested GPU is unavailable
38
- --max-runtime <minutes> Auto-teardown job after N minutes (prevents runaway billing)
39
- --max-cost <$> Auto-teardown when total spend reaches this amount
40
-
41
- ${chalk.bold('badgr serve OPTIONS')}
42
- --gpu <type> GPU: L40S, A100, H100, RTX_4090 (default: L40S)
43
- --count <n> GPU count (default: 1)
44
- --region US|EU|AU Region preference (default: US)
45
- --max-price <$/hr> Hard spend cap per GPU-hour
46
- --name <name> Deployment name (auto-generated if omitted)
18
+ ${chalk.bold('COMMANDS')}
19
+ ${chalk.cyan('badgr login')} Authenticate with your API key
20
+ ${chalk.cyan('badgr run <command>')} Run a one-off GPU job
21
+ ${chalk.cyan('badgr serve <model>')} Serve a model with an OpenAI-compatible endpoint
22
+ ${chalk.cyan('badgr status')} Show what's running and what's billing
23
+ ${chalk.cyan('badgr logs <id>')} Stream logs for a running job or endpoint
24
+ ${chalk.cyan('badgr down <id>')} Stop a deployment and end billing
25
+ ${chalk.cyan('badgr receipts')} Show cost history
26
+ ${chalk.cyan('badgr capacity')} Check what GPU capacity is available right now
47
27
 
48
28
  ${chalk.bold('EXAMPLES')}
49
- badgr login
29
+ ${chalk.dim('# Simplest — Badgr picks the GPU:')}
30
+ badgr run python train.py
31
+ badgr serve meta-llama/Llama-3.1-8B-Instruct
32
+
33
+ ${chalk.dim('# Pin a specific GPU:')}
50
34
  badgr run python train.py --gpu A100
51
- badgr run python train.py --gpu A100 --fallback closest
52
- badgr run python train.py --gpu A100 --fallback cheapest
53
- badgr run python train.py --gpu A100 --no-fallback
54
- badgr run --image my/image:latest --gpu L40S
55
35
  badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
56
- badgr serve mistralai/Mistral-7B-v0.1 --gpu RTX_4090
36
+
37
+ ${chalk.dim('# Add safety caps:')}
38
+ badgr run python train.py --max-runtime 60 --max-cost 5
39
+
40
+ ${chalk.dim('# Manage a running deployment:')}
57
41
  badgr status
58
- badgr logs dep_abc123
59
- badgr down dep_abc123
60
- badgr receipts
61
- badgr receipts dep_abc123
42
+ badgr logs dep-abc123
43
+ badgr down dep-abc123
44
+ badgr receipts dep-abc123
62
45
 
63
- ${chalk.bold('OPENAI-COMPATIBLE SERVING')}
64
- ${chalk.dim('After `badgr serve`, point any OpenAI client at the returned URL:')}
65
- ${chalk.dim(' client = OpenAI(api_key="sk-...", base_url="https://aibadgr.com/v1")')}
66
- ${chalk.dim(' client.chat.completions.create(model="dep_xxx", messages=[...])')}
46
+ ${chalk.bold('badgr run OPTIONS')}
47
+ --gpu <type> GPU type (default: auto Badgr picks cheapest available)
48
+ --image <image> Docker image (default: python:3.11-slim)
49
+ --count <n> Number of GPUs (default: 1)
50
+ --region US|EU|AU Region preference
51
+ --max-price <$/hr> Hard spend cap per GPU-hour
52
+ --max-runtime <min> Auto-stop after N minutes (recommended)
53
+ --max-cost <$> Auto-stop when spend reaches this amount
54
+ --detach Return immediately, don't stream logs
55
+
56
+ ${chalk.bold('badgr serve OPTIONS')}
57
+ --gpu <type> GPU type (default: auto — inferred from model size)
58
+ --count <n> Number of GPUs (default: 1)
59
+ --region US|EU|AU Region preference
60
+ --max-price <$/hr> Hard spend cap per GPU-hour
61
+ --no-wait Skip endpoint health check
67
62
 
68
- ${chalk.bold('ROUTING')}
69
- ${chalk.dim('Badgr automatically selects best available GPU capacity for your request.')}
63
+ ${chalk.bold('AFTER SERVING')}
64
+ ${chalk.dim('Point any OpenAI client at the returned URL:')}
65
+ ${chalk.dim(' from openai import OpenAI')}
66
+ ${chalk.dim(' client = OpenAI(base_url="<endpoint url>", api_key="<your key>")')}
67
+ ${chalk.dim(' client.chat.completions.create(model="<model>", messages=[...])')}
70
68
 
71
- ${chalk.bold('DEBUGGING')}
72
- ${chalk.dim('Set BADGR_DEBUG=1 to print the full URL, API key prefix, request body, and response status:')}
73
- ${chalk.dim(' BADGR_DEBUG=1 badgr run python train.py --gpu A100')}
74
- ${chalk.dim(' BADGR_DEBUG=1 badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S')}
75
- ${chalk.dim(' badgr config # show current baseUrl and key')}
69
+ ${chalk.bold('DEBUG')}
70
+ ${chalk.dim('BADGR_DEBUG=1 badgr run python train.py # full request/response trace')}
71
+ ${chalk.dim('badgr config # show current API config')}
76
72
  `;
77
73
 
78
74
  async function main() {
@@ -2,15 +2,9 @@ import { requireApiKey } from '../config.js';
2
2
  import { callApi } from '../api.js';
3
3
 
4
4
  /**
5
- * badgr capacity # check RTX_4090 at default $10/hr cap
6
- * badgr capacity --gpu L40S # check a specific GPU type
7
- * badgr capacity --gpu A100 --region EU
8
- * badgr capacity --max-price 5
9
- *
10
- * Internal diagnostic command: shows per-provider availability breakdown.
11
- * Provider display names come from the backend — none are hardcoded here.
5
+ * badgr capacity # show what's available right now (auto)
6
+ * badgr capacity --gpu A100 # check a specific GPU type
12
7
  */
13
-
14
8
  function parseCapacityArgs(args) {
15
9
  const flags = {};
16
10
  let i = 0;
@@ -27,17 +21,50 @@ export async function capacityCommand(config, args, chalk) {
27
21
  const flags = parseCapacityArgs(args);
28
22
  requireApiKey(config);
29
23
 
30
- const gpu = flags.gpu || 'RTX_4090';
31
24
  const maxPrice = flags.maxPrice ?? 10;
32
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('-', '_');
33
63
  const params = new URLSearchParams({ gpu, max_price: String(maxPrice) });
34
64
  if (flags.region) params.set('region', flags.region.toUpperCase());
35
65
 
36
- console.log(chalk.bold(`\n⚡ GPU Capacity Check\n`));
37
- console.log(` ${chalk.bold('GPU:')} ${gpu}`);
38
- if (flags.region) console.log(` ${chalk.bold('Region:')} ${flags.region.toUpperCase()}`);
39
- console.log(` ${chalk.bold('Max price:')} $${maxPrice.toFixed(2)}/hr`);
40
- console.log();
66
+ console.log(chalk.bold(`\nCapacity: ${gpu}\n`));
67
+ process.stdout.write(chalk.dim(' Checking...\n'));
41
68
 
42
69
  let data;
43
70
  try {
@@ -50,55 +77,35 @@ export async function capacityCommand(config, args, chalk) {
50
77
  process.exit(1);
51
78
  }
52
79
 
53
- // Per-provider breakdown
54
- const diags = data.diagnostics ?? [];
55
- if (diags.length > 0) {
56
- console.log(chalk.bold(' Provider breakdown:'));
57
- for (const d of diags) {
58
- const padded = (d.display_name ?? 'Unknown').padEnd(12);
59
- if (d.status === 'no_key') {
60
- console.log(` ${chalk.dim(padded)} ${chalk.dim('disabled — ' + d.reason)}`);
61
- } else if (d.status === 'error') {
62
- console.log(` ${chalk.dim(padded)} ${chalk.red('error — ' + d.reason)}`);
63
- } else if (d.status === 'ok') {
64
- const price = d.cheapest_price != null ? chalk.green(`cheapest $${d.cheapest_price.toFixed(2)}/hr`) : '';
65
- console.log(` ${chalk.bold(padded)} ${d.eligible_count} eligible ${price}`);
66
- } else {
67
- const reason = d.reason ? chalk.dim(` — ${d.reason}`) : '';
68
- console.log(` ${chalk.dim(padded)} 0 eligible${reason}`);
69
- }
70
- }
71
- console.log();
72
- }
73
-
74
- // Direct matches
75
80
  const matches = data.matches ?? [];
76
81
  if (matches.length > 0) {
77
- console.log(chalk.bold(' Available now:'));
82
+ console.log(chalk.bold('\n Available now:\n'));
78
83
  for (const m of matches) {
79
- console.log(` ${chalk.green('')} ${m.gpu} in ${m.region} $${m.price.toFixed(2)}/hr`);
84
+ console.log(` ${chalk.green('')} ${m.gpu} in ${m.region} ${chalk.green('$' + m.price.toFixed(2) + '/hr')}`);
80
85
  }
81
86
  console.log();
82
- console.log(chalk.bold(' Run:'));
83
- console.log(chalk.cyan(` badgr run ... --gpu ${gpu}${flags.region ? ' --region ' + flags.region.toUpperCase() : ''}`));
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();
84
91
  } else {
85
- console.log(chalk.dim(` No ${gpu} found under $${maxPrice.toFixed(2)}/hr${flags.region ? ' in ' + flags.region.toUpperCase() : ' globally'}.`));
86
- }
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`));
87
94
 
88
- // Alternatives
89
- const alternatives = data.alternatives ?? [];
90
- if (alternatives.length > 0 && matches.length === 0) {
91
- console.log();
92
- console.log(chalk.bold(' Alternatives:'));
93
- for (const a of alternatives) {
94
- console.log(` ${chalk.dim('•')} ${a.gpu} in ${a.region} $${a.price.toFixed(2)}/hr`);
95
- }
96
- console.log();
97
- console.log(chalk.bold(' Try:'));
98
- for (const a of alternatives) {
99
- console.log(chalk.cyan(` badgr run ... --gpu ${a.gpu}`));
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'));
100
109
  }
101
110
  }
102
-
103
- console.log();
104
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,8 +1,9 @@
1
1
  import { input } from '@inquirer/prompts';
2
2
  import { DEFAULTS } from '../config.js';
3
+ import { callApi } from '../api.js';
3
4
 
4
5
  export async function loginCommand(chalk, saveConfigFn) {
5
- console.log(chalk.bold('\n🔑 Badgr Login\n'));
6
+ console.log(chalk.bold('\nBadgr Login\n'));
6
7
 
7
8
  const apiKey = await input({
8
9
  message: 'Enter your Badgr API key:',
@@ -13,7 +14,23 @@ export async function loginCommand(chalk, saveConfigFn) {
13
14
  apiKey: apiKey.trim(),
14
15
  baseUrl: DEFAULTS.baseUrl,
15
16
  });
16
- console.log(chalk.green('\n✓ Logged in — config saved to ~/.badgr/config.json\n'));
17
- console.log(chalk.dim(` Base URL: ${config.baseUrl}\n`));
17
+
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`));
18
35
  return config;
19
36
  }