badgr-cli 1.0.31 → 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,15 @@
1
1
  import { requireApiKey } from '../config.js';
2
2
  import { callApi } from '../api.js';
3
- import { addDeployment, addReceipt, generateReceiptId } from '../store.js';
3
+ import { addDeployment, addReceipt, updateReceipt, generateReceiptId } from '../store.js';
4
+ import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD } from '../fallback.js';
4
5
 
5
6
  /**
7
+ * badgr serve meta-llama/Llama-3.1-8B-Instruct
6
8
  * badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
9
+ * badgr serve BAAI/bge-large-en-v1.5 --task embed
10
+ * badgr serve --image ghcr.io/my-org/diffusers-api:latest --gpu L40S --env MODEL_ID=flux
7
11
  *
8
- * Provisions a persistent vLLM endpoint, health-checks it before printing
9
- * "Endpoint ready", and returns an OpenAI-compatible base URL.
10
- * Stop billing with `badgr down <id>`.
12
+ * GPU defaults to "AUTO" backend infers from model size.
11
13
  */
12
14
  export function parseServeArgs(args) {
13
15
  const flags = {};
@@ -15,82 +17,220 @@ export function parseServeArgs(args) {
15
17
  let i = 0;
16
18
  while (i < args.length) {
17
19
  if (args[i] === '--gpu') { flags.gpu = args[++i]; i++; continue; }
20
+ if (args[i] === '--image') { flags.image = args[++i]; i++; continue; }
21
+ if (args[i] === '--task') { flags.task = args[++i]; i++; continue; }
18
22
  if (args[i] === '--count') { flags.count = parseInt(args[++i], 10); i++; continue; }
19
23
  if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
24
+ if (args[i] === '--tier') { flags.tier = args[++i]; i++; continue; }
20
25
  if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
21
26
  if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
22
- if (args[i] === '--no-wait') { flags.noWait = true; i++; continue; }
27
+ if (args[i] === '--no-wait') { flags.noWait = true; i++; continue; }
28
+ if (args[i] === '--max-cost') { flags.maxCost = parseFloat(args[++i]); i++; continue; }
29
+ if (args[i] === '--health-path') { flags.healthPath = args[++i]; i++; continue; }
30
+ // All three aliases map to noMarketplaceFallback
31
+ if (args[i] === '--no-fallback' ||
32
+ args[i] === '--strict-capacity' ||
33
+ args[i] === '--no-expanded-search') { flags.noMarketplaceFallback = true; i++; continue; }
34
+ if (args[i] === '--env') {
35
+ const kv = args[++i]; i++;
36
+ if (!flags.env) flags.env = [];
37
+ flags.env.push(kv);
38
+ continue;
39
+ }
23
40
  positional.push(args[i++]);
24
41
  }
25
42
  const model = positional[0] || null;
26
43
  return { model, flags };
27
44
  }
28
45
 
29
- // Poll GET <endpointUrl>/models until it returns 200 or timeout expires.
30
- // Returns true if healthy, false if timed out.
31
- async function waitForEndpoint(endpointUrl, timeoutMs = 5 * 60 * 1000, chalk) {
32
- const deadline = Date.now() + timeoutMs;
33
- const modelsUrl = `${endpointUrl}/models`;
34
- let attempt = 0;
46
+ function parseEnvFlag(envList) {
47
+ const obj = {};
48
+ for (const kv of (envList || [])) {
49
+ const idx = kv.indexOf('=');
50
+ if (idx > 0) obj[kv.slice(0, idx)] = kv.slice(idx + 1);
51
+ }
52
+ return obj;
53
+ }
54
+
55
+ // Mirror of backend workload_profile.py infer_profile_from_model — for pre-flight display.
56
+ function _inferServeProfile(modelName) {
57
+ const s = modelName.toLowerCase();
58
+ const moe = s.match(/(\d+)x(\d+)b/);
59
+ let paramsB;
60
+ if (moe) {
61
+ paramsB = parseInt(moe[1]) * parseInt(moe[2]);
62
+ } else {
63
+ const m = s.match(/(\d+)b/);
64
+ paramsB = m ? parseInt(m[1]) : null;
65
+ }
66
+ if (paramsB === null) return { label: 'inference (7B–8B model)', vram: '24+ GB', gpus: ['RTX 4090', 'A6000', 'L40S'] };
67
+ if (paramsB <= 9) return { label: 'inference (7B–8B model)', vram: '24+ GB', gpus: ['RTX 4090', 'A6000', 'L40S'] };
68
+ if (paramsB <= 35) return { label: 'inference (30B–34B model)', vram: '40+ GB', gpus: ['A6000', 'L40S', 'A100'] };
69
+ return { label: 'inference (70B+ model)', vram: '80+ GB', gpus: ['H100', 'A100'] };
70
+ }
71
+
72
+ function _serveStageLabel(elapsedSec, healthPath = '/models') {
73
+ if (healthPath === '/models') {
74
+ if (elapsedSec < 45) return 'Starting vLLM…';
75
+ if (elapsedSec < 150) return 'Downloading model…';
76
+ return 'Waiting for /v1/models…';
77
+ }
78
+ if (elapsedSec < 30) return 'Starting container…';
79
+ if (elapsedSec < 120) return 'Container starting…';
80
+ return `Waiting for ${healthPath}…`;
81
+ }
82
+
83
+ function _detectHealthPath(image) {
84
+ if (!image) return null;
85
+ return image.toLowerCase().includes('comfyui') ? '/system_stats' : null;
86
+ }
87
+
88
+ /**
89
+ * Poll the model endpoint until /models returns 200, or timeout.
90
+ * Also checks deployment status on each iteration — fails fast if the dep is dead.
91
+ * Returns { ready: boolean, timedOut: boolean, depFailed: boolean, failReason?: string }
92
+ */
93
+ async function waitForEndpoint(endpointUrl, deploymentId, config, timeoutMs = 5 * 60 * 1000, chalk, healthPath = '/models') {
94
+ const startMs = Date.now();
95
+ const deadline = startMs + timeoutMs;
35
96
 
36
97
  while (Date.now() < deadline) {
37
- attempt++;
98
+ // Check deployment status first — fail fast on OOM/crash before waiting more
38
99
  try {
39
- const res = await fetch(modelsUrl, { signal: AbortSignal.timeout(8000) });
40
- if (res.ok) return true;
100
+ const dep = await callApi(`/deployments/${deploymentId}`, {
101
+ apiKey: config.apiKey,
102
+ baseUrl: config.baseUrl,
103
+ timeoutMs: 10_000,
104
+ });
105
+ if (['failed', 'terminated', 'error', 'stopped'].includes(dep.status)) {
106
+ process.stdout.write('\n');
107
+ return { ready: false, timedOut: false, depFailed: true, failReason: dep.error || dep.status };
108
+ }
41
109
  } catch {
42
- // network not up yet keep polling
110
+ // status check failedcontinue to endpoint check
43
111
  }
44
- const elapsed = Math.round((Date.now() - (deadline - timeoutMs)) / 1000);
112
+
113
+ try {
114
+ const res = await fetch(`${endpointUrl}${healthPath}`, { signal: AbortSignal.timeout(8000) });
115
+ if (res.ok) { process.stdout.write('\n'); return { ready: true, timedOut: false, depFailed: false }; }
116
+ } catch {
117
+ // still starting
118
+ }
119
+
120
+ const elapsed = Math.round((Date.now() - startMs) / 1000);
45
121
  process.stdout.write(
46
- `\r ${chalk.dim(`Waiting for endpoint… ${elapsed}s (attempt ${attempt})`)} `
122
+ `\r ${chalk.dim(_serveStageLabel(elapsed, healthPath) + ` (${elapsed}s)`)} `
47
123
  );
48
124
  await new Promise(r => setTimeout(r, 8000));
49
125
  }
126
+
50
127
  process.stdout.write('\n');
51
- return false;
128
+ return { ready: false, timedOut: true, depFailed: false };
52
129
  }
53
130
 
54
131
  export async function serveCommand(config, args, chalk) {
55
132
  const { model, flags } = parseServeArgs(args);
133
+ const customImage = flags.image || null;
56
134
 
57
- if (!model) {
58
- console.error(chalk.red('Usage: badgr serve <model> --gpu <type>'));
59
- console.error(chalk.red(' badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S'));
135
+ if (!model && !customImage) {
136
+ console.error(chalk.red('Usage: badgr serve <model>'));
137
+ console.error(chalk.red(' badgr serve meta-llama/Llama-3.1-8B-Instruct'));
138
+ console.error(chalk.red(' badgr serve --image ghcr.io/my-org/api:latest --gpu L40S'));
60
139
  return;
61
140
  }
62
141
 
63
142
  requireApiKey(config);
64
143
 
65
- const gpu = flags.gpu || 'L40S';
144
+ // ── Validate flags early ───────────────────────────────────────────────────
145
+ if (flags.count !== undefined && (!Number.isFinite(flags.count) || flags.count < 1)) {
146
+ console.error(chalk.red(' ✗ --count must be an integer greater than 0'));
147
+ process.exitCode = 1;
148
+ return;
149
+ }
150
+ if (flags.maxCost !== undefined && (!Number.isFinite(flags.maxCost) || flags.maxCost <= 0)) {
151
+ console.error(chalk.red(' ✗ --max-cost must be a number greater than 0'));
152
+ process.exitCode = 1;
153
+ return;
154
+ }
155
+ if (flags.region !== undefined && !['US', 'EU', 'AU'].includes(flags.region.toUpperCase())) {
156
+ console.error(chalk.red(' ✗ --region must be US, EU, or AU'));
157
+ process.exitCode = 1;
158
+ return;
159
+ }
160
+
161
+ const envObj = parseEnvFlag(flags.env);
162
+
163
+ const gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : (customImage ? 'L40S' : 'AUTO');
164
+ const gpuLabel = gpu === 'AUTO' ? 'auto' : gpu;
165
+
166
+ const effectiveTier = normalizeTier(flags.tier);
167
+
168
+ if (customImage) {
169
+ console.log(chalk.bold('\n⚡ Serving custom container\n'));
170
+ console.log(` ${chalk.bold('Image:')} ${customImage}`);
171
+ console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
172
+ if (flags.task) console.log(` ${chalk.bold('Task:')} ${flags.task}`);
173
+ if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
174
+ } else {
175
+ console.log(chalk.bold('\n⚡ Serving model\n'));
176
+ console.log(` ${chalk.bold('Model:')} ${model}`);
177
+ console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
178
+ if (flags.task) console.log(` ${chalk.bold('Task:')} ${flags.task}`);
179
+ if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
66
180
 
67
- console.log(chalk.bold('\n🚀 Serving model\n'));
68
- console.log(` ${chalk.bold('Model:')} ${model}`);
69
- console.log(` ${chalk.bold('GPU:')} ${gpu}`);
181
+ if (gpu === 'AUTO') {
182
+ const prof = _inferServeProfile(model);
183
+ console.log();
184
+ console.log(` ${chalk.bold('Estimated workload:')} ${prof.label}`);
185
+ console.log(` ${chalk.bold('Estimated minimum VRAM:')} ${prof.vram}`);
186
+ if (flags.maxCost) console.log(` ${chalk.bold('Max cost:')} $${flags.maxCost.toFixed(2)}`);
187
+ }
188
+ }
70
189
  console.log();
71
- console.log(chalk.dim(' Finding best available GPU capacity...'));
190
+ process.stdout.write(chalk.dim(' Finding suitable capacity...\n'));
191
+
192
+ const effectiveRegion = flags.region ? flags.region.toUpperCase() : undefined;
193
+
194
+ function buildBody(gpuOverride, tierOverride) {
195
+ return {
196
+ ...(model ? { model } : {}),
197
+ ...(customImage ? { image: customImage } : {}),
198
+ ...(flags.task ? { task: flags.task } : {}),
199
+ gpu: gpuOverride || gpu,
200
+ gpu_count: flags.count || 1,
201
+ ...(effectiveRegion ? { region: effectiveRegion } : {}),
202
+ max_price_per_hour: flags.maxPrice,
203
+ name: flags.name,
204
+ tier: tierOverride || effectiveTier,
205
+ ...(Object.keys(envObj).length > 0 ? { env: envObj } : {}),
206
+ };
207
+ }
72
208
 
73
209
  let dep;
74
210
  try {
75
- dep = await callApi('/serve', {
76
- method: 'POST',
77
- apiKey: config.apiKey,
78
- baseUrl: config.baseUrl,
79
- body: {
80
- model,
81
- gpu: gpu.toUpperCase().replace('-', '_'),
82
- gpu_count: flags.count || 1,
83
- region: flags.region || 'US',
84
- max_price_per_hour: flags.maxPrice,
85
- name: flags.name,
86
- },
87
- });
211
+ dep = await callWithFallback(
212
+ '/serve',
213
+ { apiKey: config.apiKey, baseUrl: config.baseUrl },
214
+ (tierOverride) => buildBody(undefined, tierOverride),
215
+ effectiveTier,
216
+ chalk,
217
+ { thing: 'endpoint', cmd: 'badgr serve' },
218
+ { allowTier2Fallback: !flags.noMarketplaceFallback },
219
+ );
88
220
  } catch (err) {
89
- console.error(chalk.red(`\n ✗ Serve failed: ${err.message}\n`));
221
+ if (err.isPaymentRequired) {
222
+ console.error(chalk.yellow(err.message));
223
+ const rerun = `badgr serve ${args.join(' ')}`;
224
+ console.error(chalk.dim(`After payment, rerun:\n ${rerun}\n`));
225
+ process.exitCode = 1;
226
+ return;
227
+ }
228
+ // CapacityError message is pre-formatted with chalk
229
+ console.error(err.message);
230
+ process.exitCode = 1;
90
231
  return;
91
232
  }
92
233
 
93
- // Mirror to local store so badgr down/status/receipts work offline
94
234
  addDeployment({
95
235
  id: dep.deployment_id,
96
236
  name: dep.name,
@@ -98,12 +238,13 @@ export async function serveCommand(config, args, chalk) {
98
238
  model: dep.model || model,
99
239
  gpu: dep.gpu_type,
100
240
  count: dep.gpu_count,
101
- provider: dep.provider,
102
241
  status: dep.status,
103
242
  endpointUrl: dep.endpoint_url || dep.openai_base_url,
104
243
  receiptId: dep.receipt_id,
105
244
  createdAt: new Date().toISOString(),
106
245
  costPerHour: dep.cost_per_hour || 0,
246
+ providerRoute: dep.provider ?? null,
247
+ tier: dep.tier ?? null,
107
248
  });
108
249
 
109
250
  const rcptId = dep.receipt_id || generateReceiptId();
@@ -111,49 +252,126 @@ export async function serveCommand(config, args, chalk) {
111
252
  receiptId: rcptId,
112
253
  action: 'badgr serve',
113
254
  deploymentId: dep.deployment_id,
114
- provider: dep.provider,
115
255
  gpu: dep.gpu_type,
256
+ providerRoute: dep.provider ?? null,
257
+ tier: dep.tier ?? null,
116
258
  status: dep.status,
117
259
  createdAt: new Date().toISOString(),
118
260
  });
119
261
 
120
- const endpointUrl = dep.endpoint_url || dep.openai_base_url || config.baseUrl;
262
+ // ── Fix 7: never fall back to config.baseUrl for endpoint health check ─────
263
+ const endpointUrl = dep.endpoint_url || dep.openai_base_url;
264
+ if (!endpointUrl) {
265
+ console.error(chalk.red(
266
+ `\n ✗ Deployment ${dep.deployment_id} has no endpoint URL yet.\n` +
267
+ ` Run: badgr status ${dep.deployment_id}\n`
268
+ ));
269
+ updateReceipt(rcptId, { status: 'no_endpoint_url' });
270
+ process.exitCode = 1;
271
+ return;
272
+ }
121
273
 
122
- // ── Health check before declaring "ready" ─────────────────────────────────
123
- let endpointReady = false;
274
+ // ── Determine health check path ───────────────────────────────────────────
275
+ // Priority: explicit --health-path > auto-detect comfyui > /models for vLLM > null (skip)
276
+ let resolvedHealthPath;
277
+ if (flags.healthPath) {
278
+ resolvedHealthPath = flags.healthPath;
279
+ } else if (!customImage) {
280
+ resolvedHealthPath = '/models';
281
+ } else {
282
+ resolvedHealthPath = _detectHealthPath(customImage); // '/system_stats' for comfyui, null otherwise
283
+ }
124
284
 
285
+ // ── Health check ──────────────────────────────────────────────────────────
286
+ let endpointReady = false;
125
287
  if (flags.noWait) {
126
- console.log(chalk.yellow('\n Endpoint provisioning (skipped health check — use --no-wait)\n'));
288
+ console.log(chalk.yellow('\n Skipped health check (--no-wait)\n'));
289
+ } else if (resolvedHealthPath === null) {
290
+ console.log(chalk.yellow(
291
+ '\n Skipping health check (custom image — add --health-path /your-readiness-path to enable)\n'
292
+ ));
127
293
  } else {
128
- console.log(chalk.dim('\n Health-checking endpoint (up to 5 min)...'));
129
- endpointReady = await waitForEndpoint(endpointUrl, 5 * 60 * 1000, chalk);
294
+ if (resolvedHealthPath !== '/models') {
295
+ process.stdout.write(chalk.dim(` Checking ${resolvedHealthPath} for readiness…\n`));
296
+ }
297
+ // Check deployment status once before starting the 5-min wait
298
+ try {
299
+ const latest = await callApi(`/deployments/${dep.deployment_id}`, {
300
+ apiKey: config.apiKey,
301
+ baseUrl: config.baseUrl,
302
+ timeoutMs: 10_000,
303
+ });
304
+ if (['failed', 'terminated', 'error'].includes(latest.status)) {
305
+ console.error(chalk.red(
306
+ `\n ✗ Deployment failed before endpoint became ready: ${latest.error || latest.status}\n`
307
+ ));
308
+ updateReceipt(rcptId, { status: 'failed', failReason: latest.error || latest.status });
309
+ process.exitCode = 1;
310
+ return;
311
+ }
312
+ } catch {
313
+ // status check failed — proceed with endpoint poll anyway
314
+ }
315
+
316
+ const healthResult = await waitForEndpoint(endpointUrl, dep.deployment_id, config, 5 * 60 * 1000, chalk, resolvedHealthPath);
130
317
  process.stdout.write('\n');
318
+
319
+ if (healthResult.depFailed) {
320
+ console.error(chalk.red(
321
+ `\n ✗ Deployment failed during startup: ${healthResult.failReason || 'unknown error'}\n` +
322
+ ` Check logs: badgr logs ${dep.deployment_id}\n`
323
+ ));
324
+ updateReceipt(rcptId, { status: 'failed', failReason: healthResult.failReason });
325
+ process.exitCode = 1;
326
+ return;
327
+ }
328
+
329
+ endpointReady = healthResult.ready;
330
+ if (!endpointReady) updateReceipt(rcptId, { status: 'health_check_timeout' });
131
331
  }
132
332
 
133
- // ── Print result ──────────────────────────────────────────────────────────
333
+ // ── Update receipt with final state ───────────────────────────────────────
334
+ updateReceipt(rcptId, {
335
+ status: endpointReady ? 'ready' : 'starting',
336
+ endpointUrl,
337
+ updatedAt: new Date().toISOString(),
338
+ });
339
+
340
+ // ── Result ────────────────────────────────────────────────────────────────
134
341
  if (endpointReady) {
135
- console.log(chalk.green('✓ Endpoint ready\n'));
342
+ console.log(chalk.green('\n ✓ Endpoint ready\n'));
136
343
  } else {
137
- console.log(chalk.yellow('⏳ Endpoint starting\n'));
138
- console.log(chalk.dim(` Not yet responding. Check status:`));
139
- console.log(chalk.dim(` badgr status`));
140
- console.log(chalk.dim(` curl ${endpointUrl}/models`));
344
+ console.log(chalk.yellow('\n ⏳ Endpoint still starting — model download may still be in progress.\n'));
345
+ console.log(` ${chalk.bold('Stop billing now:')} ${chalk.dim(`badgr down ${dep.deployment_id}`)}`);
346
+ console.log(` ${chalk.bold('Continue watching:')} ${chalk.dim(`badgr logs ${dep.deployment_id}`)}`);
141
347
  console.log();
142
348
  }
143
349
 
144
- console.log(` ${chalk.bold('Deployment:')} ${chalk.cyan(dep.deployment_id)}`);
145
- console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
146
- console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
147
- console.log(` ${chalk.bold('Endpoint:')} ${chalk.cyan(endpointUrl)}`);
148
- if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
149
- console.log(`\n ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
350
+ const serveRate = dep.cost_per_hour || 0;
150
351
 
151
- if (endpointReady) {
152
- console.log(`\n ${chalk.bold('Use with OpenAI SDK:')}`);
352
+ console.log(` ${chalk.bold('Base URL:')} ${chalk.cyan(endpointUrl)}`);
353
+ if (dep.model || model) console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
354
+ if (customImage) console.log(` ${chalk.bold('Image:')} ${customImage}`);
355
+ console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
356
+ if (serveRate > 0) console.log(` ${chalk.bold('Rate:')} $${serveRate.toFixed(2)}/hr`);
357
+ if (flags.maxCost) console.log(` ${chalk.bold('Max cost:')} $${flags.maxCost.toFixed(2)}`);
358
+ console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
359
+ console.log(` ${chalk.bold('Logs:')} ${chalk.dim(`badgr logs ${dep.deployment_id}`)}`);
360
+ console.log(` ${chalk.bold('Stop billing:')} ${chalk.cyan(`badgr down ${dep.deployment_id}`)}`);
361
+ console.log();
362
+
363
+ if (serveRate > HIGH_RATE_THRESHOLD && !flags.maxCost) {
364
+ console.log(chalk.yellow(` Selected capacity rate: $${serveRate.toFixed(2)}/hr`));
365
+ console.log(chalk.dim(' Tip: use --max-cost to enforce a hard ceiling.\n'));
366
+ }
367
+ console.log(chalk.dim(' Billing continues until you run: ') + chalk.cyan(`badgr down ${dep.deployment_id}`));
368
+
369
+ if (endpointReady && !customImage) {
370
+ const keySnip = config.apiKey?.slice(0, 4) || 'sk-...';
371
+ console.log(` ${chalk.bold('Use with OpenAI SDK:')}`);
153
372
  console.log(chalk.dim(` from openai import OpenAI`));
154
- console.log(chalk.dim(` client = OpenAI(api_key="${config.apiKey?.slice(0, 8) || 'sk-...'}...", base_url="${endpointUrl}")`));
373
+ console.log(chalk.dim(` client = OpenAI(base_url="${endpointUrl}", api_key="${keySnip}...")`));
155
374
  console.log(chalk.dim(` resp = client.chat.completions.create(model="${dep.model || model}", messages=[...])`));
375
+ console.log();
156
376
  }
157
-
158
- console.log(`\n ${chalk.dim(`Stop billing: badgr down ${dep.deployment_id}`)}\n`);
159
377
  }
@@ -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.deployment_id || d.name;
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.deployment_id || d.name;
78
+ console.log(` ${chalk.cyan(`badgr down ${id}`)}`);
93
79
  }
80
+ console.log();
94
81
  }