badgr-cli 1.0.12 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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', {
@@ -259,19 +308,30 @@ export async function runCommand(config, args, chalk) {
259
308
  } catch (err2) {
260
309
  const d2 = err2.errorData;
261
310
  if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
262
- console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity, but the provider adapter failed during provisioning.`));
263
- if (d2.message) console.error(chalk.dim(` Detail: ${d2.message}`));
311
+ console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity but could not start the machine. Please try again.\n`));
312
+ if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
313
+ if (d2?.debug_error) console.error(chalk.dim(` Provider detail: ${d2.debug_error}`));
314
+ }
264
315
  } else {
265
- console.error(chalk.red(`\n ✗ Job failed to start on ${chosen.gpu}: ${err2.message}`));
316
+ console.error(chalk.red(`\n ✗ Job failed to start on ${chosen.gpu}: ${err2.message}\n`));
317
+ }
318
+ if (!(process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true')) {
319
+ console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.\n`));
266
320
  }
267
- console.error(chalk.dim(`\n Debug: BADGR_DEBUG=1 badgr run … — shows full request/response\n`));
268
321
  process.exit(1);
269
322
  }
323
+ } else if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
324
+ console.error(chalk.red(`\n ✗ Badgr found ${gpu} capacity but could not start the machine. Please try again.\n`));
325
+ if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
326
+ if (d?.debug_error) console.error(chalk.dim(` Provider detail: ${d.debug_error}`));
327
+ } else {
328
+ console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.\n`));
329
+ }
330
+ process.exit(1);
270
331
  } else {
271
- console.error(chalk.red(`\n ✗ Job failed to start: ${err.message}`));
272
- console.error(chalk.dim(`\n Debug: BADGR_DEBUG=1 badgr run … shows full request/response`));
273
- console.error(chalk.dim(` Config: badgr config`));
274
- console.error(chalk.dim(` Docs: badgr --help\n`));
332
+ console.error(chalk.red(`\n ✗ Could not start job: ${err.message}\n`));
333
+ console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.`));
334
+ console.error(chalk.dim(` Check config: badgr config\n`));
275
335
  process.exit(1);
276
336
  }
277
337
  }
@@ -281,13 +341,13 @@ export async function runCommand(config, args, chalk) {
281
341
  receiptId: rcptId,
282
342
  action: 'badgr run',
283
343
  deploymentId: dep.deployment_id,
284
- provider: dep.provider,
285
344
  gpu: dep.gpu_type,
286
345
  status: dep.status,
287
346
  createdAt: new Date().toISOString(),
288
347
  });
289
348
 
290
- console.log(chalk.bold(`\n Job ID: ${chalk.cyan(dep.deployment_id)}`));
349
+ console.log();
350
+ console.log(chalk.bold(` Job ID: ${chalk.cyan(dep.deployment_id)}`));
291
351
  console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
292
352
  if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
293
353
  console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
@@ -298,16 +358,14 @@ export async function runCommand(config, args, chalk) {
298
358
  return;
299
359
  }
300
360
 
301
- // ── Attached mode: stream logs until job completes ────────────────────────
302
361
  const ratePerHour = dep.cost_per_hour || 0;
303
- console.log(chalk.dim('\n ── Attaching (Ctrl+C tears down job) ─────────────────────────\n'));
362
+ console.log(chalk.dim('\n ── Streaming logs (Ctrl+C to stop job) ─────────────────────────\n'));
304
363
 
305
- // Teardown helper — called on SIGINT, max-runtime, max-cost, or heartbeat loss.
306
364
  async function teardown(reason) {
307
365
  const labels = {
308
366
  'max-runtime': chalk.yellow('\n ⏱ Max runtime reached — stopping job...'),
309
367
  'max-cost': chalk.yellow('\n 💰 Spend cap reached — stopping job...'),
310
- 'heartbeat-lost': chalk.red('\n ✗ Heartbeat lost infrastructure failure, stopping job...'),
368
+ 'heartbeat-lost': chalk.red('\n ✗ No response from machine stopping job...'),
311
369
  'interrupted': chalk.yellow('\n Stopping job...'),
312
370
  };
313
371
  console.log(labels[reason] ?? chalk.yellow('\n Stopping job...'));
@@ -318,8 +376,7 @@ export async function runCommand(config, args, chalk) {
318
376
  }
319
377
  const runtimeMs = Date.now() - attachStart;
320
378
  const finalCost = ratePerHour * (runtimeMs / 3_600_000);
321
- const failureType = reason === 'heartbeat-lost' ? 'infrastructure' : null;
322
- updateReceipt(rcptId, { status: reason, runtimeSeconds: Math.round(runtimeMs / 1000), finalCost, failureType });
379
+ updateReceipt(rcptId, { status: reason, runtimeSeconds: Math.round(runtimeMs / 1000), finalCost });
323
380
  console.log(chalk.dim(` Runtime: ${fmtRuntime(runtimeMs)} • Est. cost: $${finalCost.toFixed(4)}`));
324
381
  console.log(chalk.dim(' Job stopped. Billing ended.\n'));
325
382
  process.exit(reason === 'interrupted' ? 0 : 1);
@@ -345,7 +402,6 @@ export async function runCommand(config, args, chalk) {
345
402
  failureType,
346
403
  });
347
404
 
348
- // ── Job summary ──────────────────────────────────────────────────────────
349
405
  console.log(` ${chalk.bold('Runtime:')} ${fmtRuntime(runtimeMs)}`);
350
406
  if (ratePerHour > 0) {
351
407
  console.log(` ${chalk.bold('Cost:')} $${finalCost.toFixed(4)} (${chalk.dim(`$${ratePerHour.toFixed(2)}/hr`)})`);
@@ -353,18 +409,19 @@ export async function runCommand(config, args, chalk) {
353
409
  if (exitCode !== null && exitCode !== undefined) {
354
410
  console.log(` ${chalk.bold('Exit code:')} ${exitCode !== 0 ? chalk.red(exitCode) : chalk.green(exitCode)}`);
355
411
  }
412
+ console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
356
413
 
357
414
  if (finalStatus === 'failed' || (exitCode !== null && exitCode !== 0)) {
358
- const label = failureType === 'infrastructure'
359
- ? chalk.red(`\n ✗ Infrastructure failure (${dep.deployment_id}) — not your code\n`)
360
- : chalk.red(`\n ✗ Job failed (${dep.deployment_id})\n`);
361
- console.error(label);
362
415
  if (failureType === 'infrastructure') {
363
- console.error(chalk.dim(' This is a provider-side failure. Contact support with your receipt ID.'));
416
+ console.error(chalk.red(`\n Machine failure this is not your code.\n`));
417
+ console.error(chalk.dim(' Contact support with your receipt ID for a refund.'));
418
+ } else {
419
+ console.error(chalk.red(`\n ✗ Job failed (exit ${exitCode ?? 'unknown'})\n`));
420
+ console.error(chalk.dim(` Check logs: badgr logs ${dep.deployment_id}`));
364
421
  }
365
- console.error(chalk.dim(` Logs: badgr logs ${dep.deployment_id}\n`));
422
+ console.log();
366
423
  process.exit(exitCode ?? 1);
367
424
  } else {
368
- console.log(chalk.green(`\n ✓ Job complete\n`));
425
+ console.log(chalk.green(`\n ✓ Complete\n`));
369
426
  }
370
427
  }
@@ -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,68 +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
125
  const d2 = err2.errorData;
134
126
  if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
135
- console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity, but the provider adapter failed during provisioning.`));
136
- if (d2.message) console.error(chalk.dim(` Detail: ${d2.message}`));
127
+ console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity but could not start the endpoint. Please try again.\n`));
137
128
  } else {
138
- console.error(chalk.red(`\n ✗ Serve failed on ${chosen.gpu}: ${err2.message}`));
129
+ console.error(chalk.red(`\n ✗ Failed to start endpoint on ${chosen.gpu}: ${err2.message}\n`));
139
130
  }
140
- console.error(chalk.dim(`\n Debug: BADGR_DEBUG=1 badgr serve … shows full request/response\n`));
131
+ console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
141
132
  process.exit(1);
142
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);
143
138
  } else {
144
- // Write a failure receipt so every serve attempt is auditable.
145
139
  const failRcptId = generateReceiptId();
146
140
  addReceipt({
147
141
  receiptId: failRcptId,
148
142
  action: 'badgr serve',
149
143
  model,
150
- gpu,
144
+ gpu: gpuLabel,
151
145
  status: 'failed',
152
146
  failureType: 'infrastructure',
153
147
  createdAt: new Date().toISOString(),
154
148
  });
155
- console.error(chalk.red(`\n ✗ Serve failed: ${err.message}`));
149
+ console.error(chalk.red(`\n ✗ Could not start endpoint: ${err.message}`));
156
150
  console.error(chalk.dim(`\n Receipt: ${failRcptId}`));
157
- console.error(chalk.dim(` Debug: BADGR_DEBUG=1 badgr serve … shows full request/response`));
158
- console.error(chalk.dim(` Config: badgr config`));
159
- console.error(chalk.dim(` Docs: badgr --help\n`));
151
+ console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
160
152
  return;
161
153
  }
162
154
  }
163
155
 
164
- // Mirror to local store so badgr down/status/receipts work offline
165
156
  addDeployment({
166
157
  id: dep.deployment_id,
167
158
  name: dep.name,
@@ -169,7 +160,6 @@ export async function serveCommand(config, args, chalk) {
169
160
  model: dep.model || model,
170
161
  gpu: dep.gpu_type,
171
162
  count: dep.gpu_count,
172
- provider: dep.provider,
173
163
  status: dep.status,
174
164
  endpointUrl: dep.endpoint_url || dep.openai_base_url,
175
165
  receiptId: dep.receipt_id,
@@ -182,7 +172,6 @@ export async function serveCommand(config, args, chalk) {
182
172
  receiptId: rcptId,
183
173
  action: 'badgr serve',
184
174
  deploymentId: dep.deployment_id,
185
- provider: dep.provider,
186
175
  gpu: dep.gpu_type,
187
176
  status: dep.status,
188
177
  createdAt: new Date().toISOString(),
@@ -190,48 +179,41 @@ export async function serveCommand(config, args, chalk) {
190
179
 
191
180
  const endpointUrl = dep.endpoint_url || dep.openai_base_url || config.baseUrl;
192
181
 
193
- // ── Health check before declaring "ready" ─────────────────────────────────
182
+ // ── Health check ──────────────────────────────────────────────────────────
194
183
  let endpointReady = false;
195
-
196
184
  if (flags.noWait) {
197
- console.log(chalk.yellow('\nSkipped health check (--no-wait)\n'));
185
+ console.log(chalk.yellow('\n Skipped health check (--no-wait)\n'));
198
186
  } else {
199
- console.log(chalk.dim('\n Health-checking endpoint (up to 5 min)...'));
200
187
  endpointReady = await waitForEndpoint(endpointUrl, 5 * 60 * 1000, chalk);
201
188
  process.stdout.write('\n');
202
- if (!endpointReady) {
203
- updateReceipt(rcptId, { status: 'health_check_timeout' });
204
- }
189
+ if (!endpointReady) updateReceipt(rcptId, { status: 'health_check_timeout' });
205
190
  }
206
191
 
207
- // ── Print result ──────────────────────────────────────────────────────────
192
+ // ── Result ────────────────────────────────────────────────────────────────
208
193
  if (endpointReady) {
209
- console.log(chalk.green('✓ Endpoint ready\n'));
194
+ console.log(chalk.green('\n✓ Endpoint ready\n'));
210
195
  } else {
211
- console.log(chalk.yellow('⏳ Endpoint still starting (health check timed out)\n'));
212
- console.log(chalk.dim(' The deployment was provisioned but did not respond within 5 minutes.'));
213
- console.log(chalk.dim(' It may still be loading the model. Check:'));
214
- console.log(chalk.dim(` badgr status`));
215
- console.log(chalk.dim(` badgr logs ${dep.deployment_id}`));
216
- console.log(chalk.dim(` curl ${endpointUrl}/models`));
217
- 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}`));
218
200
  console.log();
219
201
  }
220
202
 
221
- console.log(` ${chalk.bold('Deployment:')} ${chalk.cyan(dep.deployment_id)}`);
203
+ console.log(` ${chalk.bold('Base URL:')} ${chalk.cyan(endpointUrl)}`);
222
204
  console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
223
205
  console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
224
- console.log(` ${chalk.bold('Endpoint:')} ${chalk.cyan(endpointUrl)}`);
225
206
  if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
226
207
  console.log(` ${chalk.bold('Logs:')} ${chalk.dim(`badgr logs ${dep.deployment_id}`)}`);
227
- 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();
228
210
 
229
211
  if (endpointReady) {
230
- 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:')}`);
231
214
  console.log(chalk.dim(` from openai import OpenAI`));
232
- 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}...")`));
233
216
  console.log(chalk.dim(` resp = client.chat.completions.create(model="${dep.model || model}", messages=[...])`));
217
+ console.log();
234
218
  }
235
-
236
- console.log(`\n ${chalk.dim(`Stop billing: badgr down ${dep.deployment_id}`)}\n`);
237
219
  }