badgr-cli 1.0.29 → 1.0.31

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/HOW_IT_WORKS.md CHANGED
@@ -11,7 +11,7 @@ npm install -g badgr-cli
11
11
  badgr login
12
12
  ```
13
13
 
14
- `badgr login` prompts for your API key and base URL, then writes them to `~/.badgr/config.json`. Every subsequent command reads that file — no env vars required.
14
+ `badgr login` prompts for your API key and base URL, then writes them to `~/.gpu/config.json`. Every subsequent command reads that file — no env vars required.
15
15
 
16
16
  ---
17
17
 
@@ -142,7 +142,7 @@ badgr receipts [n] # default: last 10
142
142
 
143
143
  Shows two sets of receipts:
144
144
 
145
- 1. **CLI action receipts** — every `badgr serve` / `badgr down` recorded locally in `~/.badgr/deployments.json`, with provider, retries, latency, and cost.
145
+ 1. **CLI action receipts** — every `badgr serve` / `badgr down` recorded locally in `~/.gpu/deployments.json`, with provider, retries, latency, and cost.
146
146
  2. **Inference receipts** — per-request records fetched from `GET /v1/receipts` on the API (requires `badgr login`).
147
147
 
148
148
  Every action — including failures — generates a receipt. Receipt IDs are printed on every command output so you can look them up later.
@@ -202,10 +202,10 @@ GPU type aliases are normalized automatically: `rtx-4090`, `rtx4090`, `4090`, `R
202
202
 
203
203
  ## Local State
204
204
 
205
- All CLI state lives in `~/.badgr/`:
205
+ All CLI state lives in `~/.gpu/`:
206
206
 
207
207
  ```
208
- ~/.badgr/
208
+ ~/.gpu/
209
209
  config.json API key + base URL
210
210
  deployments.json Active deployments + receipt log (last 200)
211
211
  ```
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "badgr-cli",
3
- "version": "1.0.29",
4
- "description": "Badgr, run or serve GPU workloads from one command",
3
+ "version": "1.0.31",
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,14 +21,6 @@
21
21
  "engines": {
22
22
  "node": ">=18.0.0"
23
23
  },
24
- "keywords": [
25
- "gpu",
26
- "cli",
27
- "ai",
28
- "compute",
29
- "modal",
30
- "gateway",
31
- "openai"
32
- ],
24
+ "keywords": ["gpu", "cli", "ai", "compute", "modal", "gateway", "openai"],
33
25
  "license": "MIT"
34
26
  }
package/src/api.js CHANGED
@@ -1,93 +1,16 @@
1
- const DEBUG = process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true';
2
-
3
- function dbg(...args) {
4
- if (DEBUG) console.error('[badgr:debug]', ...args);
5
- }
6
-
7
1
  export async function callApi(path, { method = 'GET', apiKey, baseUrl, body } = {}) {
8
2
  const url = `${baseUrl}${path}`;
9
- const keyPreview = apiKey ? `${apiKey.slice(0, 8)}…` : '(not set)';
10
-
11
- dbg(`${method} ${url}`);
12
- dbg(`API key: ${keyPreview}`);
13
- if (body !== undefined) dbg('Request body:', JSON.stringify(body));
14
-
15
- let res;
16
- const startMs = Date.now();
17
- try {
18
- res = await fetch(url, {
19
- method,
20
- headers: {
21
- 'Content-Type': 'application/json',
22
- 'Authorization': `Bearer ${apiKey}`,
23
- },
24
- body: body !== undefined ? JSON.stringify(body) : undefined,
25
- });
26
- } catch (cause) {
27
- const elapsed = Date.now() - startMs;
28
- dbg(`Fetch threw after ${elapsed}ms:`, cause);
29
- // Network-level failure: DNS, connection refused, timeout, etc.
30
- const msg = cause?.message ?? String(cause);
31
- const code = cause?.cause?.code ?? cause?.code ?? '';
32
- const hint =
33
- (code === 'ECONNREFUSED' || msg.includes('ECONNREFUSED'))
34
- ? `\n Hint: Connection refused — is the server running at ${baseUrl}?` :
35
- (code === 'ENOTFOUND' || msg.includes('ENOTFOUND'))
36
- ? `\n Hint: DNS lookup failed for ${baseUrl}\n Check your internet or set BADGR_API_URL to the correct host` :
37
- (code === 'ETIMEDOUT' || msg.includes('ETIMEDOUT'))
38
- ? `\n Hint: Request timed out — server may be overloaded` :
39
- (msg.includes('fetch failed') || msg === 'fetch failed')
40
- ? `\n Hint: Network error reaching ${url}\n • Check internet connection\n • Run: badgr config (verify baseUrl)\n • Try: BADGR_DEBUG=1 badgr run … for full details\n • Test: curl -v ${baseUrl}/models` :
41
- `\n Hint: Check your network and that BADGR_API_URL is correct (${baseUrl})`;
42
- throw new Error(`Cannot reach ${url} (${msg})${hint}`);
43
- }
44
-
45
- const elapsed = Date.now() - startMs;
46
- dbg(`Response: HTTP ${res.status} in ${elapsed}ms`);
47
-
3
+ const res = await fetch(url, {
4
+ method,
5
+ headers: {
6
+ 'Content-Type': 'application/json',
7
+ 'Authorization': `Bearer ${apiKey}`,
8
+ },
9
+ body: body !== undefined ? JSON.stringify(body) : undefined,
10
+ });
48
11
  if (!res.ok) {
49
- let detail = '';
50
- let rawBody = '';
51
- let errorData = null;
52
- try {
53
- rawBody = await res.text();
54
- dbg('Error response body:', rawBody);
55
- const json = JSON.parse(rawBody);
56
- errorData = json;
57
- detail = json?.detail ?? json?.message ?? json?.error ?? rawBody;
58
- } catch {
59
- detail = rawBody || res.statusText || '';
60
- }
61
- const isCapacityError = errorData?.code === 'NO_CAPACITY_MATCH';
62
- if (res.status === 402) {
63
- // Payment required — format a clear, actionable error
64
- const d = errorData?.detail ?? errorData ?? {};
65
- const detailObj = typeof d === 'object' ? d : {};
66
- const balanceUsd = typeof detailObj.balance_usd === 'number' ? detailObj.balance_usd : null;
67
- const requiredUsd = typeof detailObj.required_usd === 'number' ? detailObj.required_usd : null;
68
- const topupUrl = detailObj.topup_url || 'https://aibadgr.com/dashboard#billing';
69
-
70
- let msg = '\nPayment required.\n';
71
- if (balanceUsd !== null) msg += `Your balance is $${balanceUsd.toFixed(2)}.`;
72
- if (requiredUsd !== null) msg += ` This job needs a $${requiredUsd.toFixed(2)} reserve.`;
73
- msg += '\n\nAdd balance:\n ' + topupUrl + '\n';
74
- const err = new Error(msg);
75
- err.errorData = errorData;
76
- err.httpStatus = 402;
77
- err.isPaymentRequired = true;
78
- throw err;
79
- }
80
- const hint =
81
- res.status === 401 ? '\n Hint: Invalid or missing API key — run: badgr login' :
82
- res.status === 403 ? '\n Hint: Access denied — check your API key permissions' :
83
- res.status === 404 ? `\n Hint: Endpoint not found — check BADGR_API_URL (currently: ${baseUrl})` :
84
- (res.status === 502 || res.status === 503) && !isCapacityError
85
- ? '\n Hint: Server error — no GPU capacity available or backend is down' :
86
- '';
87
- const err = new Error(`${method} ${path} → HTTP ${res.status}: ${detail}${hint}`);
88
- err.errorData = errorData;
89
- err.httpStatus = res.status;
90
- throw err;
12
+ const text = await res.text().catch(() => '');
13
+ throw new Error(`${method} ${path} → ${res.status}: ${text}`);
91
14
  }
92
15
  return res.json();
93
16
  }
package/src/badgr.js CHANGED
@@ -10,80 +10,53 @@ import { receiptsCommand } from './commands/receipts.js';
10
10
  import { runCommand } from './commands/run.js';
11
11
  import { serveCommand } from './commands/serve.js';
12
12
  import { modelsCommand } from './commands/models.js';
13
- import { capacityCommand } from './commands/capacity.js';
14
- import { testCommand } from './commands/test-run.js';
15
- import { billingCommand } from './commands/billing.js';
16
13
 
17
14
  const HELP = `
18
15
  ${chalk.bold('badgr')} — run or serve GPU workloads from one command
19
16
 
20
- ${chalk.bold('COMMANDS')}
21
- ${chalk.cyan('badgr login')} Authenticate with your API key
22
- ${chalk.cyan('badgr run <command>')} Run a one-off GPU job
23
- ${chalk.cyan('badgr serve <model>')} Serve a model with an OpenAI-compatible endpoint
24
- ${chalk.cyan('badgr status')} Show what's running and what's billing
25
- ${chalk.cyan('badgr logs <id>')} Stream logs for a running job or endpoint
26
- ${chalk.cyan('badgr down <id>')} Stop a deployment and end billing
27
- ${chalk.cyan('badgr receipts')} Show cost history
28
- ${chalk.cyan('badgr test')} Run an end-to-end test (provision → run → teardown)
29
- ${chalk.cyan('badgr capacity')} Check what GPU capacity is available right now
30
- ${chalk.cyan('badgr billing')} Show balance and add funds
17
+ ${chalk.bold('CORE COMMANDS')}
18
+ ${chalk.cyan('badgr login')} Authenticate (save API key to ~/.badgr/config.json)
19
+ ${chalk.cyan('badgr run <cmd...> --gpu <type>')} Run a one-off GPU job
20
+ ${chalk.cyan('badgr serve <model> --gpu <type>')} Serve a model (OpenAI-compatible endpoint)
21
+ ${chalk.cyan('badgr status')} Show active deployments + endpoint URLs
22
+ ${chalk.cyan('badgr logs <id>')} Stream logs for a deployment
23
+ ${chalk.cyan('badgr down <id>')} Terminate a deployment (stop billing)
24
+ ${chalk.cyan('badgr receipts [<id>|<n>]')} Show receipts — pass ID for single, number for list
31
25
 
32
- ${chalk.bold('EXAMPLES')}
33
- ${chalk.dim('# Verify the stack works end-to-end:')}
34
- badgr test
35
-
36
- ${chalk.dim('# Tier 1 — managed provider routing (default):')}
37
- badgr run python train.py
38
- badgr serve meta-llama/Llama-3.1-8B-Instruct
26
+ ${chalk.bold('badgr run OPTIONS')}
27
+ --gpu <type> GPU: RTX_4090, A100, L40S, H100 (default: RTX_4090)
28
+ --image <image> Docker image (default: python:3.11-slim)
29
+ --count <n> GPU count (default: 1)
30
+ --region US|EU|AU Region preference (default: US)
31
+ --max-price <$/hr> Hard spend cap per GPU-hour
32
+ --name <name> Job name (auto-generated if omitted)
39
33
 
40
- ${chalk.dim('# Tier 2 — marketplace routing, lower-cost options:')}
41
- badgr run python train.py --tier 2
42
- badgr serve meta-llama/Llama-3.1-8B-Instruct --tier 2
34
+ ${chalk.bold('badgr serve OPTIONS')}
35
+ --gpu <type> GPU: L40S, A100, H100, RTX_4090 (default: L40S)
36
+ --count <n> GPU count (default: 1)
37
+ --region US|EU|AU Region preference (default: US)
38
+ --max-price <$/hr> Hard spend cap per GPU-hour
39
+ --name <name> Deployment name (auto-generated if omitted)
43
40
 
44
- ${chalk.dim('# Pin a specific GPU:')}
41
+ ${chalk.bold('EXAMPLES')}
42
+ badgr login
45
43
  badgr run python train.py --gpu A100
44
+ badgr run --image my/image:latest --gpu L40S
46
45
  badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
47
-
48
- ${chalk.dim('# Add safety caps:')}
49
- badgr run python train.py --max-runtime 60 --max-cost 5
50
-
51
- ${chalk.dim('# Manage a running deployment:')}
46
+ badgr serve mistralai/Mistral-7B-v0.1 --gpu RTX_4090
52
47
  badgr status
53
- badgr logs dep-abc123
54
- badgr down dep-abc123
55
- badgr receipts dep-abc123
56
-
57
- ${chalk.bold('badgr run OPTIONS')}
58
- --gpu <type> GPU type (default: auto — Badgr picks best available)
59
- --tier 1 Managed provider routing (default)
60
- --tier 2 Marketplace provider routing, lower-cost options
61
- --image <image> Docker image (default: python:3.11-slim)
62
- --count <n> Number of GPUs (default: 1)
63
- --region US|EU|AU Region preference
64
- --max-price <$/hr> Hard spend cap per GPU-hour
65
- --max-runtime <min> Auto-stop after N minutes (recommended)
66
- --max-cost <$> Auto-stop when spend reaches this amount
67
- --detach Return immediately, don't stream logs
68
-
69
- ${chalk.bold('badgr serve OPTIONS')}
70
- --gpu <type> GPU type (default: auto — inferred from model size)
71
- --tier 1 Managed provider routing (default)
72
- --tier 2 Marketplace provider routing, lower-cost options
73
- --count <n> Number of GPUs (default: 1)
74
- --region US|EU|AU Region preference
75
- --max-price <$/hr> Hard spend cap per GPU-hour
76
- --no-wait Skip endpoint health check
77
-
78
- ${chalk.bold('AFTER SERVING')}
79
- ${chalk.dim('Point any OpenAI client at the returned URL:')}
80
- ${chalk.dim(' from openai import OpenAI')}
81
- ${chalk.dim(' client = OpenAI(base_url="<endpoint url>", api_key="<your key>")')}
82
- ${chalk.dim(' client.chat.completions.create(model="<model>", messages=[...])')}
83
-
84
- ${chalk.bold('DEBUG')}
85
- ${chalk.dim('BADGR_DEBUG=1 badgr run python train.py # full request/response trace')}
86
- ${chalk.dim('badgr config # show current API config')}
48
+ badgr logs dep_abc123
49
+ badgr down dep_abc123
50
+ badgr receipts
51
+ badgr receipts dep_abc123
52
+
53
+ ${chalk.bold('OPENAI-COMPATIBLE SERVING')}
54
+ ${chalk.dim('After `badgr serve`, point any OpenAI client at the returned URL:')}
55
+ ${chalk.dim(' client = OpenAI(api_key="sk-...", base_url="https://api.badgr.ai/v1")')}
56
+ ${chalk.dim(' client.chat.completions.create(model="dep_xxx", messages=[...])')}
57
+
58
+ ${chalk.bold('ROUTING')}
59
+ ${chalk.dim('Badgr automatically selects best available GPU capacity for your request.')}
87
60
  `;
88
61
 
89
62
  async function main() {
@@ -104,9 +77,6 @@ async function main() {
104
77
  case 'down': return downCommand(config, rest, chalk);
105
78
  case 'receipts': return receiptsCommand(config, rest, chalk);
106
79
  case 'models': return modelsCommand(config, chalk);
107
- case 'capacity': return capacityCommand(config, rest, chalk);
108
- case 'test': return testCommand(config, rest, chalk);
109
- case 'billing': return billingCommand(config, rest, chalk);
110
80
  // legacy aliases kept for compatibility
111
81
  case 'up': return upCommand(config, rest, chalk);
112
82
  case 'config': {
@@ -12,46 +12,43 @@ export async function downCommand(config, args, chalk) {
12
12
 
13
13
  requireApiKey(config);
14
14
 
15
- const localDep = findDeployment(idOrName);
15
+ const localDep = findDeployment(idOrName);
16
+
17
+ // Resolve the deployment ID to send to the backend
16
18
  const deploymentId = localDep?.id ?? idOrName;
17
19
 
18
- process.stdout.write(chalk.dim(` Stopping ${deploymentId}...`));
20
+ console.log(chalk.dim(` Terminating ${deploymentId}...`));
19
21
 
20
22
  let dep;
21
23
  try {
22
24
  dep = await terminateDeployment(config, deploymentId);
23
25
  } catch (err) {
24
- process.stdout.write('\n');
25
- console.error(chalk.red(`\n ✗ Could not stop deployment: ${err.message}\n`));
26
+ console.error(chalk.red(`\n ✗ Terminate failed: ${err.message}\n`));
26
27
  return;
27
28
  }
28
29
 
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
-
38
30
  const rcptId = generateReceiptId();
39
31
  addReceipt({
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(),
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(),
48
41
  });
49
42
 
43
+ // Remove from local store
50
44
  removeDeployment(idOrName);
51
45
 
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`);
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`);
57
54
  }
@@ -1,36 +1,20 @@
1
1
  import { input } from '@inquirer/prompts';
2
- import { DEFAULTS } from '../config.js';
3
- import { callApi } from '../api.js';
4
2
 
5
3
  export async function loginCommand(chalk, saveConfigFn) {
6
- console.log(chalk.bold('\nBadgr Login\n'));
4
+ console.log(chalk.bold('\n🔑 Badgr Login\n'));
7
5
 
8
6
  const apiKey = await input({
9
7
  message: 'Enter your Badgr API key:',
10
8
  validate: v => v.trim() ? true : 'API key is required',
11
9
  });
12
10
 
13
- const config = saveConfigFn({
14
- apiKey: apiKey.trim(),
15
- baseUrl: DEFAULTS.baseUrl,
11
+ const baseUrl = await input({
12
+ message: 'API base URL:',
13
+ default: 'https://api.badgr.ai/v1',
16
14
  });
17
15
 
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`));
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`));
35
19
  return config;
36
20
  }
@@ -1,12 +1,6 @@
1
1
  import { findDeployment, listDeployments } from '../store.js';
2
2
  import { requireApiKey } from '../config.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)=/;
3
+ import { getDeploymentLogs } from '../api.js';
10
4
 
11
5
  export async function logsCommand(config, args, chalk) {
12
6
  const idOrName = args.find(a => !a.startsWith('--'));
@@ -32,18 +26,13 @@ export async function logsCommand(config, args, chalk) {
32
26
 
33
27
  console.log(chalk.bold(`\n📋 Logs: ${localDep?.name ?? deploymentId}\n`));
34
28
 
35
- // Fetch and print initial batch of logs.
36
- const seen = new Set();
37
29
  try {
38
30
  const data = await getDeploymentLogs(config, deploymentId);
39
31
  const lines = data?.logs ?? [];
40
- if (lines.length === 0 && !follow) {
32
+ if (lines.length === 0) {
41
33
  console.log(chalk.dim(' No log lines available yet.\n'));
42
34
  } else {
43
- for (const line of lines) {
44
- seen.add(line);
45
- if (!LOG_META_RE.test(line)) console.log(` ${chalk.dim(line)}`);
46
- }
35
+ lines.forEach(l => console.log(` ${chalk.dim(l)}`));
47
36
  }
48
37
  } catch (err) {
49
38
  console.log(chalk.yellow(` Could not fetch logs: ${err.message}`));
@@ -51,51 +40,10 @@ export async function logsCommand(config, args, chalk) {
51
40
  console.log(chalk.dim(`\n GPU: ${localDep.gpu} Type: ${localDep.type}`));
52
41
  console.log(chalk.dim(` Endpoint: ${localDep.endpointUrl}`));
53
42
  }
54
- if (!follow) { console.log(); return; }
55
43
  }
56
44
 
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
- }
45
+ if (follow) {
46
+ console.log(chalk.yellow('\n --follow: live log streaming not yet supported. Poll with `badgr logs`.\n'));
98
47
  }
99
-
100
48
  console.log();
101
49
  }
@@ -1,38 +1,19 @@
1
- import { listModels, callApi } from '../api.js';
1
+ import { listModels } from '../api.js';
2
2
  import { listAll } from '../router.js';
3
3
 
4
4
  export async function modelsCommand(config, chalk) {
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();
5
+ const gpus = listAll();
25
6
 
7
+ console.log(chalk.bold('\n📦 GPU Options (cheapest first)\n'));
26
8
  console.log(
27
- ` ${'ID'.padEnd(14)} ${'Name'.padEnd(24)} ${'VRAM'.padEnd(8)} Rate/hr (indicative)`
9
+ ` ${'ID'.padEnd(14)} ${'Name'.padEnd(24)} ${'VRAM'.padEnd(8)} Rate/hr`
28
10
  );
29
- console.log(` ${'─'.repeat(68)}`);
11
+ console.log(` ${'─'.repeat(58)}`);
30
12
  gpus.forEach(g => {
31
13
  console.log(
32
- ` ${chalk.cyan(g.id.padEnd(14))} ${g.name.padEnd(24)} ${`${g.vramGb}GB`.padEnd(8)} ~$${g.ratePerHour.toFixed(2)}`
14
+ ` ${chalk.cyan(g.id.padEnd(14))} ${g.name.padEnd(24)} ${`${g.vramGb}GB`.padEnd(8)} $${g.ratePerHour.toFixed(2)}`
33
15
  );
34
16
  });
35
- console.log(chalk.dim(' Actual billing is set at job start — use `badgr run` to see live rates.'));
36
17
 
37
18
  console.log(chalk.bold('\n🤖 LLM Models\n'));
38
19
  if (!config.apiKey) {
@@ -5,34 +5,19 @@ 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
-
13
8
  function printReceipt(r, chalk) {
14
9
  const id = r.receiptId ?? r.request_id ?? r.id ?? '—';
15
10
  const ts = r.createdAt ?? r.created_at ?? '—';
16
- const action = r.action ?? r.endpoint ?? '—';
11
+ const cost = r.route?.ratePerHour ?? r.cost_usd ?? 0;
12
+ const prov = r.route?.provider ?? r.provider ?? r.model_provider ?? '—';
17
13
  const lat = r.latencyMs ?? r.latency_ms;
14
+ const action = r.action ?? r.endpoint ?? '—';
18
15
 
19
16
  console.log(` ${chalk.cyan(id)}`);
20
17
  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
32
18
  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`);
33
20
  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
21
  if (r.status) console.log(` ${chalk.bold('status:')} ${r.status}`);
37
22
  console.log(` ${chalk.dim(ts)}`);
38
23
  console.log();