badgr-cli 1.0.11 → 1.0.14

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.11",
4
- "description": "Badgr run or serve GPU workloads from one command",
3
+ "version": "1.0.14",
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
  }
@@ -1,14 +1,13 @@
1
+ import readline from 'readline';
1
2
  import { requireApiKey } from '../config.js';
2
3
  import { callApi, terminateDeployment } from '../api.js';
3
4
  import { addReceipt, updateReceipt, generateReceiptId } from '../store.js';
4
5
  import { rankAlternatives, promptFallback } from '../fallback.js';
5
6
 
6
7
  /**
7
- * badgr run python train.py --gpu A100 # attached (default)
8
+ * badgr run python train.py # gpu=auto, attached
9
+ * badgr run python train.py --gpu A100 # specific GPU
8
10
  * badgr run --image my/image:latest --gpu L40S --detach
9
- *
10
- * Attaches by default: polls status + streams logs until the job finishes,
11
- * then exits with the job's exit code. Pass --detach to return immediately.
12
11
  */
13
12
  export function parseRunArgs(args) {
14
13
  const flags = {};
@@ -38,26 +37,15 @@ function fmtRuntime(ms) {
38
37
  return `${m}m ${s % 60}s`;
39
38
  }
40
39
 
41
- // Polls above this count without a response while 'running' → heartbeat lost.
42
- const HEARTBEAT_WARN_POLLS = 3; // ~12s → warn
43
- const HEARTBEAT_KILL_POLLS = 15; // ~60s → treat as infrastructure failure
40
+ const HEARTBEAT_WARN_POLLS = 3;
41
+ const HEARTBEAT_KILL_POLLS = 15;
44
42
 
45
- /**
46
- * Classify a failure into 'customer_code' or 'infrastructure'.
47
- * customer_code: process ran and exited non-zero.
48
- * infrastructure: provider issue — job never completed normally.
49
- */
50
43
  export function classifyFailure(finalStatus, exitCode) {
51
44
  if (finalStatus === 'failed' && (exitCode === null || exitCode === undefined)) return 'infrastructure';
52
45
  if (exitCode !== null && exitCode !== undefined && exitCode !== 0) return 'customer_code';
53
46
  return null;
54
47
  }
55
48
 
56
- /**
57
- * Poll until terminal status, streaming new log lines.
58
- * Returns { status, exitCode, runtimeMs, failureType }.
59
- * Calls onTeardown(reason) when SIGINT, maxRuntime, maxCost, or heartbeat loss hits.
60
- */
61
49
  async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost = null, ratePerHour = 0, onTeardown }) {
62
50
  const TERMINAL = new Set(['stopped', 'failed', 'completed']);
63
51
  const POLL_MS = 4000;
@@ -81,14 +69,12 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
81
69
  const elapsedMs = Date.now() - startMs;
82
70
  const spentSoFar = ratePerHour * (elapsedMs / 3_600_000);
83
71
 
84
- // Hard spend cap
85
72
  if (maxCost !== null && spentSoFar >= maxCost) {
86
73
  tearing = true;
87
74
  onTeardown('max-cost');
88
75
  return { status: 'timeout', exitCode: null, runtimeMs: elapsedMs, failureType: null };
89
76
  }
90
77
 
91
- // Hard runtime cap
92
78
  if (maxRuntimeMs !== null && elapsedMs >= maxRuntimeMs) {
93
79
  tearing = true;
94
80
  onTeardown('max-runtime');
@@ -107,7 +93,7 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
107
93
  if (lastStatus === 'running') {
108
94
  const lostSec = Math.round((consecutiveErrs * POLL_MS) / 1000);
109
95
  if (consecutiveErrs === HEARTBEAT_WARN_POLLS) {
110
- console.log(chalk.yellow(`\n ⚠ No heartbeat for ${lostSec}s — Vast machine may be unresponsive`));
96
+ console.log(chalk.yellow(`\n ⚠ No heartbeat for ${lostSec}s — cloud machine may be unresponsive`));
111
97
  } else if (consecutiveErrs >= HEARTBEAT_KILL_POLLS) {
112
98
  tearing = true;
113
99
  onTeardown('heartbeat-lost');
@@ -155,41 +141,103 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
155
141
  }
156
142
  }
157
143
 
144
+ // Ask the backend for the cheapest available GPU right now.
145
+ // Returns { gpu, region, price } or null when nothing is available.
146
+ async function findAutoGpu(config, chalk) {
147
+ try {
148
+ const params = new URLSearchParams({ max_price: '10' });
149
+ return await callApi(`/capacity/auto?${params}`, {
150
+ apiKey: config.apiKey,
151
+ baseUrl: config.baseUrl,
152
+ });
153
+ } catch (err) {
154
+ if (err.errorData?.code === 'NO_CAPACITY_MATCH') return null;
155
+ throw err;
156
+ }
157
+ }
158
+
159
+ function askConfirm(prompt) {
160
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
161
+ return new Promise(resolve => rl.question(prompt, ans => { rl.close(); resolve(ans.trim()); }));
162
+ }
163
+
158
164
  export async function runCommand(config, args, chalk) {
159
165
  const { flags, positional } = parseRunArgs(args);
160
166
 
161
167
  if (positional.length === 0 && !flags.image) {
162
- console.error(chalk.red('Usage: badgr run <command...> --gpu <type>'));
163
- console.error(chalk.red(' badgr run --image my/image:latest --gpu A100'));
168
+ console.error(chalk.red('Usage: badgr run <command...>'));
169
+ console.error(chalk.red(' badgr run --image my/image:latest'));
164
170
  return;
165
171
  }
166
172
 
167
173
  requireApiKey(config);
168
174
 
169
- const command = positional.length > 0 ? positional : undefined;
170
- const gpu = flags.gpu || 'RTX_4090';
171
- const image = flags.image || (command ? 'python:3.11-slim' : undefined);
172
- const detach = flags.detach || false;
173
- const fallbackMode = flags.noFallback ? 'none' : (flags.fallback || 'closest');
174
- const maxRuntimeMs = flags.maxRuntime ? flags.maxRuntime * 60 * 1000 : null;
175
- const maxCost = flags.maxCost ?? null;
176
-
177
- console.log(chalk.bold('\n⚡ Running GPU job\n'));
178
- if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
179
- if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
180
- console.log(` ${chalk.bold('GPU:')} ${gpu}`);
181
- if (flags.maxRuntime) console.log(` ${chalk.bold('Max runtime:')} ${flags.maxRuntime}min`);
182
- if (maxCost) console.log(` ${chalk.bold('Max cost:')} $${maxCost.toFixed(2)}`);
183
- if (flags.maxPrice) console.log(` ${chalk.bold('Max price:')} $${flags.maxPrice.toFixed(2)}/hr`);
184
- if (detach) console.log(` ${chalk.dim('(detached — returns immediately)')}`);
185
- console.log();
175
+ const command = positional.length > 0 ? positional : undefined;
176
+ const image = flags.image || (command ? 'python:3.11-slim' : undefined);
177
+ const detach = flags.detach || false;
178
+ const fallbackMode = flags.noFallback ? 'none' : (flags.fallback || 'closest');
179
+ const maxRuntimeMs = flags.maxRuntime ? flags.maxRuntime * 60 * 1000 : null;
180
+ const maxCost = flags.maxCost ?? null;
181
+
182
+ // ── Auto GPU selection (no --gpu specified) ────────────────────────────────
183
+ let gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : null;
184
+ let autoRegion = null;
185
+
186
+ if (!gpu) {
187
+ console.log(chalk.bold('\n⚡ Running GPU job\n'));
188
+ if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
189
+ if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
190
+ console.log();
191
+ process.stdout.write(chalk.dim(' Finding GPU...'));
192
+
193
+ let best;
194
+ try {
195
+ best = await findAutoGpu(config, chalk);
196
+ } catch (err) {
197
+ process.stdout.write('\n');
198
+ console.error(chalk.red(`\n ✗ Could not find GPU capacity: ${err.message}\n`));
199
+ process.exit(1);
200
+ }
186
201
 
187
- // Safety guidance for jobs without a spend cap
188
- if (!detach && !flags.maxRuntime && !maxCost) {
189
- console.log(chalk.dim(' Tip: add --max-runtime 60 or --max-cost 5.00 to cap spend automatically'));
190
- }
191
- if (flags.maxRuntime > 60) {
192
- console.log(chalk.yellow(' Note: jobs over 60min should checkpoint progress to recover from failures'));
202
+ process.stdout.write('\n');
203
+
204
+ if (!best) {
205
+ console.error(chalk.red('\n ✗ No GPU capacity available right now.\n'));
206
+ console.error(chalk.dim(' Run `badgr capacity` for details, or try again in a few minutes.'));
207
+ process.exit(1);
208
+ }
209
+
210
+ console.log(`\n ${chalk.bold('Best match:')} ${chalk.cyan(best.gpu)} in ${best.region} — ${chalk.green('$' + best.price.toFixed(2) + '/hr')} estimated`);
211
+ console.log();
212
+
213
+ if (process.stdin.isTTY) {
214
+ const answer = await askConfirm(` Press ${chalk.bold('Enter')} to run, or ${chalk.bold('q')} to cancel: `);
215
+ if (answer.toLowerCase() === 'q') {
216
+ console.log(chalk.dim('\n Cancelled.\n'));
217
+ process.exit(0);
218
+ }
219
+ } else {
220
+ console.log(chalk.dim(` Auto-selecting ${best.gpu} (non-interactive).`));
221
+ }
222
+
223
+ gpu = best.gpu;
224
+ autoRegion = best.region;
225
+ console.log();
226
+ } else {
227
+ // Specific GPU requested — show header
228
+ console.log(chalk.bold('\n⚡ Running GPU job\n'));
229
+ if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
230
+ if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
231
+ console.log(` ${chalk.bold('GPU:')} ${gpu}`);
232
+ if (flags.maxRuntime) console.log(` ${chalk.bold('Max runtime:')} ${flags.maxRuntime}min`);
233
+ if (maxCost) console.log(` ${chalk.bold('Max cost:')} $${maxCost.toFixed(2)}`);
234
+ if (flags.maxPrice) console.log(` ${chalk.bold('Max price:')} $${flags.maxPrice.toFixed(2)}/hr`);
235
+ if (detach) console.log(` ${chalk.dim('(detached — returns immediately)')}`);
236
+ console.log();
237
+
238
+ if (!detach && !flags.maxRuntime && !maxCost) {
239
+ console.log(chalk.dim(' Tip: add --max-runtime 60 or --max-cost 5.00 to cap spend automatically'));
240
+ }
193
241
  }
194
242
 
195
243
  console.log(chalk.dim(' Finding best available GPU capacity...'));
@@ -198,11 +246,13 @@ export async function runCommand(config, args, chalk) {
198
246
  }
199
247
 
200
248
  function buildBody(gpuOverride, regionOverride) {
201
- const effectiveRegion = regionOverride ?? (flags.region ? flags.region.toUpperCase() : undefined);
249
+ const effectiveRegion = regionOverride
250
+ ?? autoRegion
251
+ ?? (flags.region ? flags.region.toUpperCase() : undefined);
202
252
  return {
203
253
  command,
204
254
  image,
205
- gpu: (gpuOverride || gpu).toUpperCase().replace('-', '_'),
255
+ gpu: (gpuOverride || gpu),
206
256
  gpu_count: flags.count || 1,
207
257
  ...(effectiveRegion ? { region: effectiveRegion } : {}),
208
258
  max_price_per_hour: flags.maxPrice,
@@ -227,15 +277,14 @@ export async function runCommand(config, args, chalk) {
227
277
  process.exit(1);
228
278
  }
229
279
 
230
- // Merge same-GPU-other-regions and cross-GPU alternatives into one ranked list.
231
280
  const pool = [
232
281
  ...(Array.isArray(d.same_gpu_other_regions) ? d.same_gpu_other_regions : []),
233
282
  ...(Array.isArray(d.alternatives) ? d.alternatives : []),
234
283
  ];
235
284
 
236
285
  if (pool.length === 0) {
237
- console.error(chalk.red(`\n ✗ ${gpu} isn't available right now and no alternatives found.\n`));
238
- console.error(chalk.dim(' Try increasing --max-price or contact support for a manual quote.'));
286
+ console.error(chalk.red(`\n ✗ ${gpu} isn't available right now and no alternatives were found.\n`));
287
+ console.error(chalk.dim(' Try `badgr capacity` to see what\'s available, or try again shortly.'));
239
288
  process.exit(1);
240
289
  }
241
290
 
@@ -247,7 +296,7 @@ export async function runCommand(config, args, chalk) {
247
296
  process.exit(0);
248
297
  }
249
298
 
250
- console.log(chalk.dim(`\n Running on ${chosen.gpu} in ${chosen.region}...\n`));
299
+ console.log(chalk.dim(`\n Trying ${chosen.gpu} in ${chosen.region}...\n`));
251
300
 
252
301
  try {
253
302
  dep = await callApi('/run', {
@@ -257,15 +306,23 @@ export async function runCommand(config, args, chalk) {
257
306
  body: buildBody(chosen.gpu, chosen.region),
258
307
  });
259
308
  } catch (err2) {
260
- console.error(chalk.red(`\n ✗ Job failed to start on ${chosen.gpu}: ${err2.message}`));
261
- console.error(chalk.dim(`\n Debug: BADGR_DEBUG=1 badgr run shows full request/response\n`));
309
+ const d2 = err2.errorData;
310
+ if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
311
+ console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity but could not start the machine. Please try again.\n`));
312
+ } else {
313
+ console.error(chalk.red(`\n ✗ Job failed to start on ${chosen.gpu}: ${err2.message}\n`));
314
+ }
315
+ console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.\n`));
262
316
  process.exit(1);
263
317
  }
318
+ } else if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
319
+ console.error(chalk.red(`\n ✗ Badgr found ${gpu} capacity but could not start the machine. Please try again.\n`));
320
+ console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.\n`));
321
+ process.exit(1);
264
322
  } else {
265
- console.error(chalk.red(`\n ✗ Job failed to start: ${err.message}`));
266
- console.error(chalk.dim(`\n Debug: BADGR_DEBUG=1 badgr run … shows full request/response`));
267
- console.error(chalk.dim(` Config: badgr config`));
268
- console.error(chalk.dim(` Docs: badgr --help\n`));
323
+ console.error(chalk.red(`\n ✗ Could not start job: ${err.message}\n`));
324
+ console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.`));
325
+ console.error(chalk.dim(` Check config: badgr config\n`));
269
326
  process.exit(1);
270
327
  }
271
328
  }
@@ -275,13 +332,13 @@ export async function runCommand(config, args, chalk) {
275
332
  receiptId: rcptId,
276
333
  action: 'badgr run',
277
334
  deploymentId: dep.deployment_id,
278
- provider: dep.provider,
279
335
  gpu: dep.gpu_type,
280
336
  status: dep.status,
281
337
  createdAt: new Date().toISOString(),
282
338
  });
283
339
 
284
- console.log(chalk.bold(`\n Job ID: ${chalk.cyan(dep.deployment_id)}`));
340
+ console.log();
341
+ console.log(chalk.bold(` Job ID: ${chalk.cyan(dep.deployment_id)}`));
285
342
  console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
286
343
  if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
287
344
  console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
@@ -292,16 +349,14 @@ export async function runCommand(config, args, chalk) {
292
349
  return;
293
350
  }
294
351
 
295
- // ── Attached mode: stream logs until job completes ────────────────────────
296
352
  const ratePerHour = dep.cost_per_hour || 0;
297
- console.log(chalk.dim('\n ── Attaching (Ctrl+C tears down job) ─────────────────────────\n'));
353
+ console.log(chalk.dim('\n ── Streaming logs (Ctrl+C to stop job) ─────────────────────────\n'));
298
354
 
299
- // Teardown helper — called on SIGINT, max-runtime, max-cost, or heartbeat loss.
300
355
  async function teardown(reason) {
301
356
  const labels = {
302
357
  'max-runtime': chalk.yellow('\n ⏱ Max runtime reached — stopping job...'),
303
358
  'max-cost': chalk.yellow('\n 💰 Spend cap reached — stopping job...'),
304
- 'heartbeat-lost': chalk.red('\n ✗ Heartbeat lost infrastructure failure, stopping job...'),
359
+ 'heartbeat-lost': chalk.red('\n ✗ No response from machine stopping job...'),
305
360
  'interrupted': chalk.yellow('\n Stopping job...'),
306
361
  };
307
362
  console.log(labels[reason] ?? chalk.yellow('\n Stopping job...'));
@@ -312,8 +367,7 @@ export async function runCommand(config, args, chalk) {
312
367
  }
313
368
  const runtimeMs = Date.now() - attachStart;
314
369
  const finalCost = ratePerHour * (runtimeMs / 3_600_000);
315
- const failureType = reason === 'heartbeat-lost' ? 'infrastructure' : null;
316
- updateReceipt(rcptId, { status: reason, runtimeSeconds: Math.round(runtimeMs / 1000), finalCost, failureType });
370
+ updateReceipt(rcptId, { status: reason, runtimeSeconds: Math.round(runtimeMs / 1000), finalCost });
317
371
  console.log(chalk.dim(` Runtime: ${fmtRuntime(runtimeMs)} • Est. cost: $${finalCost.toFixed(4)}`));
318
372
  console.log(chalk.dim(' Job stopped. Billing ended.\n'));
319
373
  process.exit(reason === 'interrupted' ? 0 : 1);
@@ -339,7 +393,6 @@ export async function runCommand(config, args, chalk) {
339
393
  failureType,
340
394
  });
341
395
 
342
- // ── Job summary ──────────────────────────────────────────────────────────
343
396
  console.log(` ${chalk.bold('Runtime:')} ${fmtRuntime(runtimeMs)}`);
344
397
  if (ratePerHour > 0) {
345
398
  console.log(` ${chalk.bold('Cost:')} $${finalCost.toFixed(4)} (${chalk.dim(`$${ratePerHour.toFixed(2)}/hr`)})`);
@@ -347,18 +400,19 @@ export async function runCommand(config, args, chalk) {
347
400
  if (exitCode !== null && exitCode !== undefined) {
348
401
  console.log(` ${chalk.bold('Exit code:')} ${exitCode !== 0 ? chalk.red(exitCode) : chalk.green(exitCode)}`);
349
402
  }
403
+ console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
350
404
 
351
405
  if (finalStatus === 'failed' || (exitCode !== null && exitCode !== 0)) {
352
- const label = failureType === 'infrastructure'
353
- ? chalk.red(`\n ✗ Infrastructure failure (${dep.deployment_id}) — not your code\n`)
354
- : chalk.red(`\n ✗ Job failed (${dep.deployment_id})\n`);
355
- console.error(label);
356
406
  if (failureType === 'infrastructure') {
357
- console.error(chalk.dim(' This is a provider-side failure. Contact support with your receipt ID.'));
407
+ console.error(chalk.red(`\n Machine failure this is not your code.\n`));
408
+ console.error(chalk.dim(' Contact support with your receipt ID for a refund.'));
409
+ } else {
410
+ console.error(chalk.red(`\n ✗ Job failed (exit ${exitCode ?? 'unknown'})\n`));
411
+ console.error(chalk.dim(` Check logs: badgr logs ${dep.deployment_id}`));
358
412
  }
359
- console.error(chalk.dim(` Logs: badgr logs ${dep.deployment_id}\n`));
413
+ console.log();
360
414
  process.exit(exitCode ?? 1);
361
415
  } else {
362
- console.log(chalk.green(`\n ✓ Job complete\n`));
416
+ console.log(chalk.green(`\n ✓ Complete\n`));
363
417
  }
364
418
  }
@@ -4,11 +4,10 @@ import { addDeployment, addReceipt, updateReceipt, generateReceiptId } from '../
4
4
  import { rankAlternatives, promptFallback } from '../fallback.js';
5
5
 
6
6
  /**
7
+ * badgr serve meta-llama/Llama-3.1-8B-Instruct
7
8
  * badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
8
9
  *
9
- * Provisions a persistent vLLM endpoint, health-checks it before printing
10
- * "Endpoint ready", and returns an OpenAI-compatible base URL.
11
- * Stop billing with `badgr down <id>`.
10
+ * GPU defaults to "AUTO" backend infers from model size.
12
11
  */
13
12
  export function parseServeArgs(args) {
14
13
  const flags = {};
@@ -27,24 +26,21 @@ export function parseServeArgs(args) {
27
26
  return { model, flags };
28
27
  }
29
28
 
30
- // Poll GET <endpointUrl>/models until it returns 200 or timeout expires.
31
- // Returns true if healthy, false if timed out.
32
29
  async function waitForEndpoint(endpointUrl, timeoutMs = 5 * 60 * 1000, chalk) {
33
30
  const deadline = Date.now() + timeoutMs;
34
- const modelsUrl = `${endpointUrl}/models`;
35
31
  let attempt = 0;
36
32
 
37
33
  while (Date.now() < deadline) {
38
34
  attempt++;
39
35
  try {
40
- const res = await fetch(modelsUrl, { signal: AbortSignal.timeout(8000) });
36
+ const res = await fetch(`${endpointUrl}/models`, { signal: AbortSignal.timeout(8000) });
41
37
  if (res.ok) return true;
42
38
  } catch {
43
- // network not up yet — keep polling
39
+ // still starting
44
40
  }
45
41
  const elapsed = Math.round((Date.now() - (deadline - timeoutMs)) / 1000);
46
42
  process.stdout.write(
47
- `\r ${chalk.dim(`Waiting for endpoint… ${elapsed}s (attempt ${attempt})`)} `
43
+ `\r ${chalk.dim(`Health-checking /v1/models… ${elapsed}s`)} `
48
44
  );
49
45
  await new Promise(r => setTimeout(r, 8000));
50
46
  }
@@ -56,40 +52,43 @@ export async function serveCommand(config, args, chalk) {
56
52
  const { model, flags } = parseServeArgs(args);
57
53
 
58
54
  if (!model) {
59
- console.error(chalk.red('Usage: badgr serve <model> --gpu <type>'));
60
- console.error(chalk.red(' badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S'));
55
+ console.error(chalk.red('Usage: badgr serve <model>'));
56
+ console.error(chalk.red(' badgr serve meta-llama/Llama-3.1-8B-Instruct'));
61
57
  return;
62
58
  }
63
59
 
64
60
  requireApiKey(config);
65
61
 
66
- const gpu = flags.gpu || 'L40S';
62
+ // gpu=AUTO tells the backend to infer the right GPU from model size
63
+ const gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : 'AUTO';
64
+ const gpuLabel = gpu === 'AUTO' ? 'auto' : gpu;
67
65
 
68
- console.log(chalk.bold('\n🚀 Serving model\n'));
66
+ console.log(chalk.bold('\nServing model\n'));
69
67
  console.log(` ${chalk.bold('Model:')} ${model}`);
70
- console.log(` ${chalk.bold('GPU:')} ${gpu}`);
68
+ console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
71
69
  console.log();
72
- console.log(chalk.dim(' Finding best available GPU capacity...'));
73
- if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
74
- console.log(chalk.dim(` API: ${config.baseUrl}`));
75
- }
70
+ process.stdout.write(chalk.dim(' Finding GPU capacity...\n'));
76
71
 
77
72
  const effectiveRegion = flags.region ? flags.region.toUpperCase() : undefined;
78
73
 
74
+ function buildBody(gpuOverride, regionOverride) {
75
+ return {
76
+ model,
77
+ gpu: gpuOverride || gpu,
78
+ gpu_count: flags.count || 1,
79
+ ...(regionOverride || effectiveRegion ? { region: regionOverride || effectiveRegion } : {}),
80
+ max_price_per_hour: flags.maxPrice,
81
+ name: flags.name,
82
+ };
83
+ }
84
+
79
85
  let dep;
80
86
  try {
81
87
  dep = await callApi('/serve', {
82
88
  method: 'POST',
83
89
  apiKey: config.apiKey,
84
90
  baseUrl: config.baseUrl,
85
- body: {
86
- model,
87
- gpu: gpu.toUpperCase().replace('-', '_'),
88
- gpu_count: flags.count || 1,
89
- ...(effectiveRegion ? { region: effectiveRegion } : {}),
90
- max_price_per_hour: flags.maxPrice,
91
- name: flags.name,
92
- },
91
+ body: buildBody(),
93
92
  });
94
93
  } catch (err) {
95
94
  const d = err.errorData;
@@ -100,62 +99,60 @@ export async function serveCommand(config, args, chalk) {
100
99
  ];
101
100
 
102
101
  if (pool.length === 0) {
103
- console.error(chalk.red(`\n ✗ ${gpu} isn't available right now and no alternatives found.\n`));
104
- console.error(chalk.dim(' Try increasing --max-price or contact support for a manual quote.'));
102
+ console.error(chalk.red(`\n ✗ No GPU capacity available right now.\n`));
103
+ console.error(chalk.dim(' Run `badgr capacity` to see alternatives, or try again shortly.'));
105
104
  process.exit(1);
106
105
  }
107
106
 
108
- const ranked = rankAlternatives(gpu, pool, 'closest');
109
- const chosen = await promptFallback(gpu, ranked, chalk);
107
+ const ranked = rankAlternatives(gpuLabel, pool, 'closest');
108
+ const chosen = await promptFallback(gpuLabel, ranked, chalk);
110
109
 
111
110
  if (!chosen) {
112
111
  console.log(chalk.dim('\n Cancelled.\n'));
113
112
  process.exit(0);
114
113
  }
115
114
 
116
- console.log(chalk.dim(`\n Serving on ${chosen.gpu} in ${chosen.region}...\n`));
115
+ console.log(chalk.dim(`\n Trying ${chosen.gpu} in ${chosen.region}...\n`));
117
116
 
118
117
  try {
119
118
  dep = await callApi('/serve', {
120
119
  method: 'POST',
121
120
  apiKey: config.apiKey,
122
121
  baseUrl: config.baseUrl,
123
- body: {
124
- model,
125
- gpu: chosen.gpu.toUpperCase().replace('-', '_'),
126
- gpu_count: flags.count || 1,
127
- region: chosen.region,
128
- max_price_per_hour: flags.maxPrice,
129
- name: flags.name,
130
- },
122
+ body: buildBody(chosen.gpu, chosen.region),
131
123
  });
132
124
  } catch (err2) {
133
- console.error(chalk.red(`\n ✗ Serve failed on ${chosen.gpu}: ${err2.message}`));
134
- console.error(chalk.dim(`\n Debug: BADGR_DEBUG=1 badgr serve shows full request/response\n`));
125
+ const d2 = err2.errorData;
126
+ if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
127
+ console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity but could not start the endpoint. Please try again.\n`));
128
+ } else {
129
+ console.error(chalk.red(`\n ✗ Failed to start endpoint on ${chosen.gpu}: ${err2.message}\n`));
130
+ }
131
+ console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
135
132
  process.exit(1);
136
133
  }
134
+ } else if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
135
+ console.error(chalk.red(`\n ✗ Badgr found capacity but could not start the endpoint. Please try again.\n`));
136
+ console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
137
+ process.exit(1);
137
138
  } else {
138
- // Write a failure receipt so every serve attempt is auditable.
139
139
  const failRcptId = generateReceiptId();
140
140
  addReceipt({
141
141
  receiptId: failRcptId,
142
142
  action: 'badgr serve',
143
143
  model,
144
- gpu,
144
+ gpu: gpuLabel,
145
145
  status: 'failed',
146
146
  failureType: 'infrastructure',
147
147
  createdAt: new Date().toISOString(),
148
148
  });
149
- console.error(chalk.red(`\n ✗ Serve failed: ${err.message}`));
149
+ console.error(chalk.red(`\n ✗ Could not start endpoint: ${err.message}`));
150
150
  console.error(chalk.dim(`\n Receipt: ${failRcptId}`));
151
- console.error(chalk.dim(` Debug: BADGR_DEBUG=1 badgr serve … shows full request/response`));
152
- console.error(chalk.dim(` Config: badgr config`));
153
- console.error(chalk.dim(` Docs: badgr --help\n`));
151
+ console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
154
152
  return;
155
153
  }
156
154
  }
157
155
 
158
- // Mirror to local store so badgr down/status/receipts work offline
159
156
  addDeployment({
160
157
  id: dep.deployment_id,
161
158
  name: dep.name,
@@ -163,7 +160,6 @@ export async function serveCommand(config, args, chalk) {
163
160
  model: dep.model || model,
164
161
  gpu: dep.gpu_type,
165
162
  count: dep.gpu_count,
166
- provider: dep.provider,
167
163
  status: dep.status,
168
164
  endpointUrl: dep.endpoint_url || dep.openai_base_url,
169
165
  receiptId: dep.receipt_id,
@@ -176,7 +172,6 @@ export async function serveCommand(config, args, chalk) {
176
172
  receiptId: rcptId,
177
173
  action: 'badgr serve',
178
174
  deploymentId: dep.deployment_id,
179
- provider: dep.provider,
180
175
  gpu: dep.gpu_type,
181
176
  status: dep.status,
182
177
  createdAt: new Date().toISOString(),
@@ -184,48 +179,41 @@ export async function serveCommand(config, args, chalk) {
184
179
 
185
180
  const endpointUrl = dep.endpoint_url || dep.openai_base_url || config.baseUrl;
186
181
 
187
- // ── Health check before declaring "ready" ─────────────────────────────────
182
+ // ── Health check ──────────────────────────────────────────────────────────
188
183
  let endpointReady = false;
189
-
190
184
  if (flags.noWait) {
191
- console.log(chalk.yellow('\nSkipped health check (--no-wait)\n'));
185
+ console.log(chalk.yellow('\n Skipped health check (--no-wait)\n'));
192
186
  } else {
193
- console.log(chalk.dim('\n Health-checking endpoint (up to 5 min)...'));
194
187
  endpointReady = await waitForEndpoint(endpointUrl, 5 * 60 * 1000, chalk);
195
188
  process.stdout.write('\n');
196
- if (!endpointReady) {
197
- updateReceipt(rcptId, { status: 'health_check_timeout' });
198
- }
189
+ if (!endpointReady) updateReceipt(rcptId, { status: 'health_check_timeout' });
199
190
  }
200
191
 
201
- // ── Print result ──────────────────────────────────────────────────────────
192
+ // ── Result ────────────────────────────────────────────────────────────────
202
193
  if (endpointReady) {
203
- console.log(chalk.green('✓ Endpoint ready\n'));
194
+ console.log(chalk.green('\n✓ Endpoint ready\n'));
204
195
  } else {
205
- console.log(chalk.yellow('⏳ Endpoint still starting (health check timed out)\n'));
206
- console.log(chalk.dim(' The deployment was provisioned but did not respond within 5 minutes.'));
207
- console.log(chalk.dim(' It may still be loading the model. Check:'));
208
- console.log(chalk.dim(` badgr status`));
209
- console.log(chalk.dim(` badgr logs ${dep.deployment_id}`));
210
- console.log(chalk.dim(` curl ${endpointUrl}/models`));
211
- console.log(chalk.dim(` If it never starts, run: badgr down ${dep.deployment_id}`));
196
+ console.log(chalk.yellow('\n⏳ Endpoint still starting\n'));
197
+ console.log(chalk.dim(' The deployment is provisioned but not yet responding.'));
198
+ console.log(chalk.dim(` Check: badgr logs ${dep.deployment_id}`));
199
+ console.log(chalk.dim(` Stop if it never starts: badgr down ${dep.deployment_id}`));
212
200
  console.log();
213
201
  }
214
202
 
215
- console.log(` ${chalk.bold('Deployment:')} ${chalk.cyan(dep.deployment_id)}`);
203
+ console.log(` ${chalk.bold('Base URL:')} ${chalk.cyan(endpointUrl)}`);
216
204
  console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
217
205
  console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
218
- console.log(` ${chalk.bold('Endpoint:')} ${chalk.cyan(endpointUrl)}`);
219
206
  if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
220
207
  console.log(` ${chalk.bold('Logs:')} ${chalk.dim(`badgr logs ${dep.deployment_id}`)}`);
221
- console.log(`\n ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
208
+ console.log(` ${chalk.bold('Stop billing:')} ${chalk.dim(`badgr down ${dep.deployment_id}`)}`);
209
+ console.log();
222
210
 
223
211
  if (endpointReady) {
224
- console.log(`\n ${chalk.bold('Use with OpenAI SDK:')}`);
212
+ const keySnip = config.apiKey?.slice(0, 8) || 'sk-...';
213
+ console.log(` ${chalk.bold('Use with OpenAI SDK:')}`);
225
214
  console.log(chalk.dim(` from openai import OpenAI`));
226
- console.log(chalk.dim(` client = OpenAI(api_key="${config.apiKey?.slice(0, 8) || 'sk-...'}...", base_url="${endpointUrl}")`));
215
+ console.log(chalk.dim(` client = OpenAI(base_url="${endpointUrl}", api_key="${keySnip}...")`));
227
216
  console.log(chalk.dim(` resp = client.chat.completions.create(model="${dep.model || model}", messages=[...])`));
217
+ console.log();
228
218
  }
229
-
230
- console.log(`\n ${chalk.dim(`Stop billing: badgr down ${dep.deployment_id}`)}\n`);
231
219
  }
@@ -2,24 +2,20 @@ import { listDeployments as localDeployments } from '../store.js';
2
2
  import { listDeployments as apiDeployments } from '../api.js';
3
3
 
4
4
  export async function statusCommand(config, args, chalk) {
5
- console.log(chalk.bold('\n📊 GPU Status\n'));
6
-
7
5
  let deployments = [];
8
6
 
9
- // ── Live data from the backend ────────────────────────────────────────────
10
7
  if (config.apiKey) {
11
8
  try {
12
9
  const data = await apiDeployments(config);
13
10
  deployments = data?.deployments ?? [];
14
11
  } catch (err) {
15
- console.log(chalk.yellow(` Could not reach API: ${err.message}. Showing local state.\n`));
12
+ console.log(chalk.yellow(`\n Could not reach API: ${err.message}. Showing local state.\n`));
16
13
  deployments = localDeployments().map(d => ({
17
14
  deployment_id: d.id,
18
15
  name: d.name,
19
16
  workload_type: d.type,
20
17
  gpu_type: d.gpu,
21
18
  gpu_count: d.count,
22
- provider: d.provider,
23
19
  status: d.status,
24
20
  cost_per_hour: d.costPerHour,
25
21
  endpoint_url: d.endpointUrl,
@@ -33,7 +29,6 @@ export async function statusCommand(config, args, chalk) {
33
29
  workload_type: d.type,
34
30
  gpu_type: d.gpu,
35
31
  gpu_count: d.count,
36
- provider: d.provider,
37
32
  status: d.status,
38
33
  cost_per_hour: d.costPerHour,
39
34
  endpoint_url: d.endpointUrl,
@@ -41,54 +36,46 @@ export async function statusCommand(config, args, chalk) {
41
36
  }));
42
37
  }
43
38
 
44
- if (deployments.length === 0) {
45
- console.log(chalk.dim(' No active deployments.'));
46
- console.log(chalk.dim(' Run `badgr serve <model> --gpu L40S` to provision one.\n'));
39
+ const running = deployments.filter(d => d.status === 'running' || d.status === 'provisioning');
40
+
41
+ if (running.length === 0) {
42
+ console.log(chalk.dim('\n Nothing running.\n'));
43
+ console.log(chalk.dim(' badgr run python train.py'));
44
+ console.log(chalk.dim(' badgr serve meta-llama/Llama-3.1-8B-Instruct\n'));
47
45
  return;
48
46
  }
49
47
 
50
- const cols = { name: 16, type: 9, gpu: 11, status: 14, price: 9 };
48
+ console.log(chalk.bold('\nRunning now:\n'));
49
+ for (const d of running) {
50
+ const id = d.name || d.deployment_id;
51
+ const type = d.workload_type === 'endpoint' ? 'endpoint' : 'job';
52
+ const gpu = d.gpu_type || '—';
53
+ const rate = d.cost_per_hour > 0 ? chalk.yellow(`$${d.cost_per_hour.toFixed(2)}/hr`) : '';
54
+ const badge = d.status === 'provisioning'
55
+ ? chalk.yellow('● starting')
56
+ : chalk.green('● running');
51
57
 
52
- const header =
53
- 'Name'.padEnd(cols.name) +
54
- 'Type'.padEnd(cols.type) +
55
- 'GPU'.padEnd(cols.gpu) +
56
- 'Status'.padEnd(cols.status) +
57
- 'Price/hr';
58
- console.log(' ' + chalk.bold(header));
59
- console.log(' ' + '─'.repeat(Object.values(cols).reduce((a, b) => a + b, 0) + 8));
58
+ console.log(` ${badge} ${chalk.bold(id)} ${type} ${gpu} ${rate}`);
60
59
 
61
- deployments.forEach(d => {
62
- const statusColor = d.status === 'running'
63
- ? chalk.green(d.status.padEnd(cols.status))
64
- : d.status === 'provisioning'
65
- ? chalk.yellow(d.status.padEnd(cols.status))
66
- : chalk.dim(d.status.padEnd(cols.status));
67
- const price = d.cost_per_hour > 0 ? `$${d.cost_per_hour.toFixed(2)}` : '—';
68
- const name = (d.name || d.deployment_id || '').slice(0, cols.name - 1);
69
- const gpu = (d.gpu_type || '—').slice(0, cols.gpu - 1).padEnd(cols.gpu);
70
- console.log(
71
- ' ' +
72
- name.padEnd(cols.name) +
73
- (d.workload_type || '—').padEnd(cols.type) +
74
- gpu +
75
- statusColor +
76
- price
77
- );
78
- });
60
+ if (d.workload_type === 'endpoint' && d.endpoint_url) {
61
+ console.log(` ${chalk.dim('URL:')} ${d.endpoint_url}`);
62
+ }
63
+ if (d.model) {
64
+ console.log(` ${chalk.dim('Model:')} ${d.model}`);
65
+ }
66
+ console.log();
67
+ }
79
68
 
80
- console.log();
69
+ const billable = running.filter(d => (d.cost_per_hour || 0) > 0);
70
+ if (billable.length > 0) {
71
+ const totalPerHr = billable.reduce((s, d) => s + (d.cost_per_hour || 0), 0);
72
+ console.log(` ${chalk.bold('Total billing:')} ${chalk.yellow(`$${totalPerHr.toFixed(2)}/hr`)}\n`);
73
+ }
81
74
 
82
- // Show endpoint URLs for running endpoint deployments
83
- const endpoints = deployments.filter(d => d.workload_type === 'endpoint' && d.status === 'running');
84
- if (endpoints.length > 0) {
85
- console.log(chalk.bold(' Endpoints\n'));
86
- endpoints.forEach(d => {
87
- const url = d.endpoint_url || config.baseUrl;
88
- console.log(` ${chalk.cyan(d.name || d.deployment_id)}`);
89
- console.log(` ${chalk.dim('URL:')} ${url}`);
90
- if (d.model) console.log(` ${chalk.dim('model:')} ${d.model}`);
91
- });
92
- console.log();
75
+ console.log(chalk.bold('Stop billing:\n'));
76
+ for (const d of running) {
77
+ const id = d.name || d.deployment_id;
78
+ console.log(` ${chalk.cyan(`badgr down ${id}`)}`);
93
79
  }
80
+ console.log();
94
81
  }
package/src/fallback.js CHANGED
@@ -89,8 +89,9 @@ export async function promptFallback(requestedGpu, ranked, chalk) {
89
89
  console.log(chalk.yellow(`\n ${requestedGpu} isn't available right now.\n`));
90
90
  console.log(chalk.bold(' Closest match:'));
91
91
  console.log(` ${chalk.cyan(top.gpu)} in ${top.region}`);
92
- console.log(` ${chalk.green('$' + top.price.toFixed(2) + '/hr')} estimated`);
92
+ console.log(` ${chalk.green('$' + top.price.toFixed(2) + '/hr')} estimated price`);
93
93
  if (topMeta?.desc) console.log(` ${topMeta.desc}`);
94
+ console.log(chalk.dim(' (availability estimated from market data — not pre-verified)'));
94
95
  console.log();
95
96
 
96
97
  if (!process.stdin.isTTY) {