badgr-cli 1.0.37 → 1.0.39

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.
@@ -3,6 +3,7 @@ import { callApi, terminateDeployment } from '../api.js';
3
3
  import { addReceipt, updateReceipt, generateReceiptId } from '../store.js';
4
4
  import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD } from '../fallback.js';
5
5
  import { formatCliError } from '../errors.js';
6
+ import { TEMPLATE_MAP, buildTemplateFlags, parseTemplateOverrides } from '../catalog.js';
6
7
 
7
8
  /**
8
9
  * badgr run python train.py # gpu=auto, attached
@@ -13,32 +14,42 @@ import { formatCliError } from '../errors.js';
13
14
  export function parseRunArgs(args) {
14
15
  const flags = {};
15
16
  const positional = [];
17
+
18
+ // Split at -- so badgr flags and the user command don't collide.
19
+ // Everything before -- is parsed for flags; everything after becomes commandArgv.
20
+ const sepIdx = args.indexOf('--');
21
+ const flagArgs = sepIdx === -1 ? args : args.slice(0, sepIdx);
22
+ const commandArgv = sepIdx === -1 ? null : args.slice(sepIdx + 1);
23
+
16
24
  let i = 0;
17
- while (i < args.length) {
18
- if (args[i] === '--gpu') { flags.gpu = args[++i]; i++; continue; }
19
- if (args[i] === '--image') { flags.image = args[++i]; i++; continue; }
20
- if (args[i] === '--count') { flags.count = parseInt(args[++i], 10); i++; continue; }
21
- if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
22
- if (args[i] === '--tier') { flags.tier = args[++i]; i++; continue; }
23
- if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
24
- if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
25
- if (args[i] === '--detach') { flags.detach = true; i++; continue; }
26
- if (args[i] === '--fallback') { flags.fallback = args[++i]; i++; continue; }
27
- if (args[i] === '--no-fallback') { flags.noFallback = true; i++; continue; }
28
- if (args[i] === '--strict-capacity') { flags.noFallback = true; i++; continue; }
29
- if (args[i] === '--no-expanded-search') { flags.noFallback = true; i++; continue; }
30
- if (args[i] === '--max-runtime') { flags.maxRuntime = parseFloat(args[++i]); i++; continue; }
31
- if (args[i] === '--max-cost') { flags.maxCost = parseFloat(args[++i]); i++; continue; }
32
- if (args[i] === '--min-vram') { flags.minVram = parseFloat(args[++i]); i++; continue; }
33
- if (args[i] === '--env') {
34
- const kv = args[++i]; i++;
25
+ while (i < flagArgs.length) {
26
+ if (flagArgs[i] === '--gpu') { flags.gpu = flagArgs[++i]; i++; continue; }
27
+ if (flagArgs[i] === '--image') { flags.image = flagArgs[++i]; i++; continue; }
28
+ if (flagArgs[i] === '--count') { flags.count = parseInt(flagArgs[++i], 10); i++; continue; }
29
+ if (flagArgs[i] === '--region') { flags.region = flagArgs[++i]; i++; continue; }
30
+ if (flagArgs[i] === '--tier') { flags.tier = flagArgs[++i]; i++; continue; }
31
+ if (flagArgs[i] === '--max-price') { flags.maxPrice = parseFloat(flagArgs[++i]); i++; continue; }
32
+ if (flagArgs[i] === '--name') { flags.name = flagArgs[++i]; i++; continue; }
33
+ if (flagArgs[i] === '--detach') { flags.detach = true; i++; continue; }
34
+ if (flagArgs[i] === '--fallback') { flags.fallback = flagArgs[++i]; i++; continue; }
35
+ if (flagArgs[i] === '--no-fallback') { flags.noFallback = true; i++; continue; }
36
+ if (flagArgs[i] === '--strict-capacity') { flags.noFallback = true; i++; continue; }
37
+ if (flagArgs[i] === '--no-expanded-search') { flags.noFallback = true; i++; continue; }
38
+ if (flagArgs[i] === '--max-runtime') { flags.maxRuntime = parseFloat(flagArgs[++i]); i++; continue; }
39
+ if (flagArgs[i] === '--max-cost') { flags.maxCost = parseFloat(flagArgs[++i]); i++; continue; }
40
+ if (flagArgs[i] === '--min-vram') { flags.minVram = parseFloat(flagArgs[++i]); i++; continue; }
41
+ if (flagArgs[i] === '--dry-run') { flags.dryRun = true; i++; continue; }
42
+ if (flagArgs[i] === '--save') { flags.save = flagArgs[++i]; i++; continue; }
43
+ if (flagArgs[i] === '--workspace') { flags.workspace = flagArgs[++i]; i++; continue; }
44
+ if (flagArgs[i] === '--env') {
45
+ const kv = flagArgs[++i]; i++;
35
46
  if (!flags.env) flags.env = [];
36
47
  flags.env.push(kv);
37
48
  continue;
38
49
  }
39
- positional.push(args[i++]);
50
+ positional.push(flagArgs[i++]);
40
51
  }
41
- return { flags, positional };
52
+ return { flags, positional, commandArgv };
42
53
  }
43
54
 
44
55
  function parseEnvFlag(envList) {
@@ -331,10 +342,33 @@ const _KNOWN_RUN_FLAGS = new Set([
331
342
  '--gpu', '--image', '--count', '--region', '--tier', '--max-price', '--name',
332
343
  '--detach', '--fallback', '--no-fallback', '--strict-capacity',
333
344
  '--no-expanded-search', '--max-runtime', '--max-cost', '--min-vram', '--env',
345
+ '--dry-run',
334
346
  ]);
335
347
 
336
348
  export async function runCommand(config, args, chalk) {
337
- const { flags, positional } = parseRunArgs(args);
349
+ // `badgr run template <name> [flags]` expand template defaults then re-dispatch
350
+ if (args[0] === 'template') {
351
+ const name = args[1];
352
+ const t = name && TEMPLATE_MAP[name];
353
+ if (!t) {
354
+ console.error(chalk.red(`\n Unknown template: ${name || '(none)'}\n`));
355
+ console.error(chalk.dim(' Run `badgr template list` to see available templates.'));
356
+ process.exitCode = 1;
357
+ return;
358
+ }
359
+ if (t.type !== 'job') {
360
+ console.error(chalk.red(`\n "${name}" is an endpoint template — use: badgr serve template ${name}\n`));
361
+ process.exitCode = 1;
362
+ return;
363
+ }
364
+ const overrides = parseTemplateOverrides(args.slice(2));
365
+ const expandedArgs = buildTemplateFlags(t, overrides);
366
+ if (overrides.maxRuntime != null) expandedArgs.push('--max-runtime', String(overrides.maxRuntime));
367
+ console.log(chalk.dim(` Template: ${t.title} → badgr run ${expandedArgs.join(' ')}\n`));
368
+ return runCommand(config, expandedArgs, chalk);
369
+ }
370
+
371
+ const { flags, positional, commandArgv } = parseRunArgs(args);
338
372
 
339
373
  // Detect flags that ended up in the command because of broken shell line continuation
340
374
  // (e.g. `\ ` with trailing space instead of `\<newline>`).
@@ -353,12 +387,17 @@ export async function runCommand(config, args, chalk) {
353
387
  return;
354
388
  }
355
389
 
356
- if (positional.length === 0 && !flags.image) {
357
- console.error(chalk.red('Usage: badgr run <command...>'));
390
+ if (positional.length === 0 && commandArgv === null && !flags.image) {
391
+ console.error(chalk.red('Usage: badgr run --gpu <GPU> --image <image> --max-cost <n> -- <command...>'));
358
392
  console.error(chalk.red(' badgr run --image my/image:latest'));
359
393
  return;
360
394
  }
361
-
395
+ if (commandArgv !== null && commandArgv.length === 0 && !flags.image) {
396
+ console.error(chalk.red(' ✗ No command after --. Provide a command or --image.'));
397
+ console.error(chalk.dim(' Example: badgr run --gpu RTX_4090 --max-cost 1 -- node script.js'));
398
+ process.exitCode = 1;
399
+ return;
400
+ }
362
401
  requireApiKey(config);
363
402
 
364
403
  // ── Validate flags early ───────────────────────────────────────────────────
@@ -382,7 +421,18 @@ export async function runCommand(config, args, chalk) {
382
421
  process.exitCode = 1;
383
422
  return;
384
423
  }
385
- const command = positional.length > 0 ? positional : undefined;
424
+
425
+ if (!flags.maxCost && !flags.dryRun) {
426
+ console.error(chalk.red('\n ✗ --max-cost is required for run workloads.\n'));
427
+ console.error(chalk.dim(' Example: badgr run --gpu RTX_4090 --image node:20 --max-cost 5 -- node script.js\n'));
428
+ process.exitCode = 1;
429
+ return;
430
+ }
431
+
432
+ // commandArgv is set when -- separator was used; fall back to positional for legacy syntax.
433
+ const command = commandArgv !== null
434
+ ? (commandArgv.length > 0 ? commandArgv : undefined)
435
+ : (positional.length > 0 ? positional : undefined);
386
436
  const cmdStr = command ? command.join(' ') : '';
387
437
  const isSmoke = cmdStr.length < 80 && /print\s*\(|['"]hello/i.test(cmdStr);
388
438
  const inferredImage = isSmoke ? 'python:3.11-alpine' : 'python:3.11-slim';
@@ -401,6 +451,19 @@ export async function runCommand(config, args, chalk) {
401
451
 
402
452
  const gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : undefined;
403
453
 
454
+ if (flags.dryRun) {
455
+ console.log(chalk.bold('\n⚡ Dry run — no GPU will be provisioned\n'));
456
+ if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
457
+ if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
458
+ console.log(` ${chalk.bold('GPU:')} ${gpu || chalk.dim('auto')}`);
459
+ console.log(` ${chalk.bold('Max runtime:')} ${effectiveMaxRuntime}min`);
460
+ if (maxCost) console.log(` ${chalk.bold('Max cost:')} $${maxCost}`);
461
+ if (flags.maxPrice) console.log(` ${chalk.bold('Max price:')} $${flags.maxPrice}/hr`);
462
+ if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
463
+ console.log(chalk.dim('\n Remove --dry-run to provision.\n'));
464
+ return;
465
+ }
466
+
404
467
  console.log(chalk.bold('\n⚡ Running GPU job\n'));
405
468
  if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
406
469
  if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
@@ -418,6 +481,23 @@ export async function runCommand(config, args, chalk) {
418
481
  console.log();
419
482
 
420
483
 
484
+ // Resolve --workspace name → ws_… ID before submitting
485
+ let resolvedWorkspaceId = flags.workspace ?? null;
486
+ if (resolvedWorkspaceId && !resolvedWorkspaceId.startsWith('ws_')) {
487
+ try {
488
+ const wsData = await callApi(`/workspaces?limit=100`, { apiKey: config.apiKey, baseUrl: config.baseUrl });
489
+ const match = (wsData?.workspaces ?? []).find(w => w.name === resolvedWorkspaceId);
490
+ if (!match) {
491
+ console.error(chalk.red(`Workspace not found: ${resolvedWorkspaceId}`));
492
+ process.exitCode = 1; return;
493
+ }
494
+ resolvedWorkspaceId = match.workspace_id;
495
+ } catch (err) {
496
+ console.error(chalk.red(`Could not resolve workspace: ${err.message}`));
497
+ process.exitCode = 1; return;
498
+ }
499
+ }
500
+
421
501
  console.log(chalk.dim(' Finding suitable capacity...'));
422
502
  if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
423
503
  console.log(chalk.dim(` API: ${config.baseUrl}`));
@@ -438,6 +518,7 @@ export async function runCommand(config, args, chalk) {
438
518
  ...(Object.keys(envObj).length > 0 ? { env: envObj } : {}),
439
519
  max_runtime_seconds: effectiveMaxRuntime * 60,
440
520
  ...(maxCost ? { max_cost_usd: maxCost } : {}),
521
+ ...(resolvedWorkspaceId ? { workspace_id: resolvedWorkspaceId } : {}),
441
522
  };
442
523
  }
443
524
 
@@ -628,6 +709,34 @@ export async function runCommand(config, args, chalk) {
628
709
  } catch { /* already stopped */ }
629
710
  console.log(chalk.green(`\n ✓ Complete`));
630
711
  console.log(chalk.dim(` Billing ended`));
712
+
713
+ if (flags.save && config.apiKey) {
714
+ try {
715
+ const saved = await callApi('/workloads', {
716
+ method: 'POST',
717
+ apiKey: config.apiKey,
718
+ baseUrl: config.baseUrl,
719
+ body: {
720
+ name: flags.save,
721
+ job_type: 'custom.run',
722
+ config: {
723
+ command: command || [],
724
+ image: image || 'python:3.11-slim',
725
+ gpu: gpu || 'auto',
726
+ ...(flags.minVram ? { min_vram: flags.minVram } : {}),
727
+ gpu_count: flags.count || 1,
728
+ ...(flags.maxPrice ? { max_price_per_hour: flags.maxPrice } : {}),
729
+ ...(Object.keys(envObj).length > 0 ? { env: envObj } : {}),
730
+ },
731
+ default_max_cost: maxCost || 10.0,
732
+ default_max_runtime_minutes: effectiveMaxRuntime,
733
+ },
734
+ });
735
+ console.log(chalk.cyan(` Saved as workload: ${saved.name} (${saved.workload_id})`));
736
+ } catch (err) {
737
+ console.log(chalk.yellow(` Could not save workload: ${err.message}`));
738
+ }
739
+ }
631
740
  console.log();
632
741
  }
633
742
  }
@@ -3,12 +3,16 @@ import { callApi, listDeployments } from '../api.js';
3
3
  import { addDeployment, addReceipt, updateReceipt, generateReceiptId } from '../store.js';
4
4
  import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD } from '../fallback.js';
5
5
  import { formatCliError } from '../errors.js';
6
+ import { TEMPLATE_MAP, buildTemplateFlags, parseTemplateOverrides } from '../catalog.js';
7
+
8
+ const LLAMA_CPP_IMAGE = 'michaelmanleyx/llama-cpp:server-cuda';
6
9
 
7
10
  /**
8
11
  * badgr serve meta-llama/Llama-3.1-8B-Instruct
9
12
  * badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
10
13
  * badgr serve BAAI/bge-large-en-v1.5 --task embed
11
14
  * badgr serve --image ghcr.io/my-org/diffusers-api:latest --gpu L40S --env MODEL_ID=flux
15
+ * badgr serve --runtime llama.cpp --hf-repo org/repo --hf-file model.gguf --max-cost 10
12
16
  *
13
17
  * GPU defaults to "AUTO" — backend infers from model size.
14
18
  */
@@ -35,6 +39,9 @@ export function parseServeArgs(args) {
35
39
  args[i] === '--no-expanded-search') { flags.noMarketplaceFallback = true; i++; continue; }
36
40
  if (args[i] === '--persistent') { flags.persistent = true; i++; continue; }
37
41
  if (args[i] === '--yes' || args[i] === '-y') { flags.yes = true; i++; continue; }
42
+ if (args[i] === '--runtime') { flags.runtime = args[++i]; i++; continue; }
43
+ if (args[i] === '--hf-repo') { flags.hfRepo = args[++i]; i++; continue; }
44
+ if (args[i] === '--hf-file') { flags.hfFile = args[++i]; i++; continue; }
38
45
  if (args[i] === '--env') {
39
46
  const kv = args[++i]; i++;
40
47
  if (!flags.env) flags.env = [];
@@ -73,12 +80,34 @@ function _inferServeProfile(modelName) {
73
80
  return { label: 'inference (70B+ model)', vram: '80+ GB', gpus: ['H100', 'A100'] };
74
81
  }
75
82
 
83
+ // Mirror of backend workload_profile.py infer_profile_from_gguf.
84
+ // Accepts the --hf-file filename; looks for param-count hints like "35B" or "8x7B".
85
+ function _inferGgufProfile(ggufPath) {
86
+ const s = ggufPath.toLowerCase();
87
+ const moe = s.match(/(\d+)x(\d+)b/);
88
+ let paramsB;
89
+ if (moe) {
90
+ paramsB = parseInt(moe[1]) * parseInt(moe[2]);
91
+ } else {
92
+ const m = s.match(/(\d+)b/);
93
+ paramsB = m ? parseInt(m[1]) : null;
94
+ }
95
+ if (paramsB === null || paramsB <= 9) return { label: 'GGUF inference (≤9B, llama.cpp)', vram: '8+ GB', gpus: ['RTX 4090', 'RTX 3090', 'A6000'] };
96
+ if (paramsB <= 35) return { label: 'GGUF inference (10B–35B, llama.cpp)', vram: '24+ GB', gpus: ['RTX 4090', 'A6000', 'L40S'] };
97
+ return { label: 'GGUF inference (36B+, llama.cpp)', vram: '48+ GB', gpus: ['A6000', 'L40S', 'A100'] };
98
+ }
99
+
76
100
  function _serveStageLabel(elapsedSec, healthPath = '/models') {
77
101
  if (healthPath === '/models') {
78
102
  if (elapsedSec < 45) return 'Starting vLLM…';
79
103
  if (elapsedSec < 150) return 'Downloading model…';
80
104
  return 'Waiting for /v1/models…';
81
105
  }
106
+ if (healthPath === '/health') {
107
+ if (elapsedSec < 60) return 'Starting llama-server…';
108
+ if (elapsedSec < 180) return 'Downloading from Hugging Face…';
109
+ return 'Waiting for /health…';
110
+ }
82
111
  if (elapsedSec < 30) return 'Starting container…';
83
112
  if (elapsedSec < 120) return 'Container starting…';
84
113
  return `Waiting for ${healthPath}…`;
@@ -86,7 +115,9 @@ function _serveStageLabel(elapsedSec, healthPath = '/models') {
86
115
 
87
116
  function _detectHealthPath(image) {
88
117
  if (!image) return null;
89
- return image.toLowerCase().includes('comfyui') ? '/system_stats' : null;
118
+ if (image.toLowerCase().includes('comfyui')) return '/system_stats';
119
+ if (image.toLowerCase().includes('llama.cpp')) return '/health';
120
+ return null;
90
121
  }
91
122
 
92
123
  /**
@@ -161,12 +192,34 @@ const _KNOWN_SERVE_FLAGS = new Set([
161
192
  '--gpu', '--image', '--task', '--count', '--region', '--tier', '--max-price',
162
193
  '--name', '--no-wait', '--max-cost', '--health-path', '--check-nodes',
163
194
  '--no-fallback', '--strict-capacity', '--no-expanded-search', '--env',
164
- '--persistent', '--yes', '-y',
195
+ '--persistent', '--yes', '-y', '--runtime', '--hf-repo', '--hf-file',
165
196
  ]);
166
197
 
167
198
  export async function serveCommand(config, args, chalk) {
199
+ // `badgr serve template <name> [flags]` — expand template defaults then re-dispatch
200
+ if (args[0] === 'template') {
201
+ const name = args[1];
202
+ const t = name && TEMPLATE_MAP[name];
203
+ if (!t) {
204
+ console.error(chalk.red(`\n Unknown template: ${name || '(none)'}\n`));
205
+ console.error(chalk.dim(' Run `badgr template list` to see available templates.'));
206
+ process.exitCode = 1;
207
+ return;
208
+ }
209
+ if (t.type !== 'endpoint') {
210
+ console.error(chalk.red(`\n "${name}" is a job template — use: badgr run template ${name}\n`));
211
+ process.exitCode = 1;
212
+ return;
213
+ }
214
+ const overrides = parseTemplateOverrides(args.slice(2));
215
+ const expandedArgs = buildTemplateFlags(t, overrides);
216
+ console.log(chalk.dim(` Template: ${t.title} → badgr serve ${expandedArgs.join(' ')}\n`));
217
+ return serveCommand(config, expandedArgs, chalk);
218
+ }
219
+
168
220
  const { model, flags } = parseServeArgs(args);
169
221
  const customImage = flags.image || null;
222
+ const isLlamaCpp = flags.runtime === 'llama.cpp';
170
223
 
171
224
  // Detect flags that ended up as positional args due to broken shell line continuation
172
225
  // (e.g. `\ ` with a trailing space instead of `\<newline>`).
@@ -188,10 +241,22 @@ export async function serveCommand(config, args, chalk) {
188
241
  return;
189
242
  }
190
243
 
191
- if (!model && !customImage) {
244
+ if (isLlamaCpp && (!flags.hfRepo || !flags.hfFile)) {
245
+ console.error(chalk.red('\n ✗ --runtime llama.cpp requires --hf-repo and --hf-file\n'));
246
+ console.error(chalk.dim(' Example:'));
247
+ console.error(chalk.dim(' badgr serve --runtime llama.cpp \\'));
248
+ console.error(chalk.dim(' --hf-repo org/model-repo \\'));
249
+ console.error(chalk.dim(' --hf-file model.gguf \\'));
250
+ console.error(chalk.dim(' --max-cost 10\n'));
251
+ process.exitCode = 1;
252
+ return;
253
+ }
254
+
255
+ if (!model && !customImage && !isLlamaCpp) {
192
256
  console.error(chalk.red('Usage: badgr serve <model>'));
193
257
  console.error(chalk.red(' badgr serve meta-llama/Llama-3.1-8B-Instruct'));
194
258
  console.error(chalk.red(' badgr serve --image ghcr.io/my-org/api:latest --gpu L40S'));
259
+ console.error(chalk.red(' badgr serve --runtime llama.cpp --hf-repo org/repo --hf-file model.gguf'));
195
260
  return;
196
261
  }
197
262
 
@@ -233,7 +298,20 @@ export async function serveCommand(config, args, chalk) {
233
298
 
234
299
  const effectiveTier = normalizeTier(flags.tier);
235
300
 
236
- if (customImage) {
301
+ if (isLlamaCpp) {
302
+ console.log(chalk.bold('\n⚡ Serving HF GGUF (llama.cpp)\n'));
303
+ console.log(` ${chalk.bold('HF Repo:')} ${flags.hfRepo}`);
304
+ console.log(` ${chalk.bold('HF File:')} ${flags.hfFile}`);
305
+ console.log(` ${chalk.bold('Runtime:')} llama.cpp`);
306
+ console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
307
+ if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
308
+ if (gpu === 'AUTO') {
309
+ const prof = _inferGgufProfile(flags.hfFile);
310
+ console.log();
311
+ console.log(` ${chalk.bold('Estimated workload:')} ${prof.label}`);
312
+ console.log(` ${chalk.bold('Estimated minimum VRAM:')} ${prof.vram}`);
313
+ }
314
+ } else if (customImage) {
237
315
  console.log(chalk.bold('\n⚡ Serving custom container\n'));
238
316
  console.log(` ${chalk.bold('Image:')} ${customImage}`);
239
317
  console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
@@ -271,7 +349,9 @@ export async function serveCommand(config, args, chalk) {
271
349
  d.workload_type === 'endpoint' &&
272
350
  (
273
351
  (model && d.model === model) ||
274
- (customImage && d.image === customImage)
352
+ (customImage && d.image === customImage) ||
353
+ (isLlamaCpp && d.image === LLAMA_CPP_IMAGE &&
354
+ d.env?.LLAMA_ARG_HF_REPO === flags.hfRepo && d.env?.LLAMA_ARG_HF_FILE === flags.hfFile)
275
355
  )
276
356
  );
277
357
  if (duplicate) {
@@ -297,9 +377,12 @@ export async function serveCommand(config, args, chalk) {
297
377
  const effectiveRegion = flags.region ? flags.region.toUpperCase() : undefined;
298
378
 
299
379
  function buildBody(gpuOverride, tierOverride) {
380
+ const effectiveEnv = isLlamaCpp
381
+ ? { LLAMA_ARG_HF_REPO: flags.hfRepo, LLAMA_ARG_HF_FILE: flags.hfFile, ...envObj }
382
+ : envObj;
300
383
  return {
301
384
  ...(model ? { model } : {}),
302
- ...(customImage ? { image: customImage } : {}),
385
+ ...(isLlamaCpp ? { image: LLAMA_CPP_IMAGE } : customImage ? { image: customImage } : {}),
303
386
  ...(flags.task ? { task: flags.task } : {}),
304
387
  gpu: gpuOverride || gpu,
305
388
  gpu_count: flags.count || 1,
@@ -307,7 +390,7 @@ export async function serveCommand(config, args, chalk) {
307
390
  max_price_per_hour: flags.maxPrice,
308
391
  name: flags.name,
309
392
  tier: tierOverride || effectiveTier,
310
- ...(Object.keys(envObj).length > 0 ? { env: envObj } : {}),
393
+ ...(Object.keys(effectiveEnv).length > 0 ? { env: effectiveEnv } : {}),
311
394
  ...(flags.maxCost ? { max_cost_usd: flags.maxCost } : {}),
312
395
  };
313
396
  }
@@ -378,10 +461,12 @@ export async function serveCommand(config, args, chalk) {
378
461
  }
379
462
 
380
463
  // ── Determine health check path ───────────────────────────────────────────
381
- // Priority: explicit --health-path > auto-detect comfyui > /models for vLLM > null (skip)
464
+ // Priority: explicit --health-path > llama.cpp /health > vLLM → /models > auto-detect custom image > null
382
465
  let resolvedHealthPath;
383
466
  if (flags.healthPath) {
384
467
  resolvedHealthPath = flags.healthPath;
468
+ } else if (isLlamaCpp) {
469
+ resolvedHealthPath = '/health';
385
470
  } else if (!customImage) {
386
471
  resolvedHealthPath = '/models';
387
472
  } else {
@@ -462,7 +547,11 @@ export async function serveCommand(config, args, chalk) {
462
547
  const serveRate = dep.cost_per_hour || 0;
463
548
 
464
549
  console.log(` ${chalk.bold('Base URL:')} ${chalk.cyan(endpointUrl)}`);
465
- if (dep.model || model) console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
550
+ if (isLlamaCpp) {
551
+ console.log(` ${chalk.bold('HF Repo:')} ${flags.hfRepo}`);
552
+ console.log(` ${chalk.bold('HF File:')} ${flags.hfFile}`);
553
+ }
554
+ else if (dep.model || model) console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
466
555
  if (customImage) console.log(` ${chalk.bold('Image:')} ${customImage}`);
467
556
  console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
468
557
  if (serveRate > 0) console.log(` ${chalk.bold('Rate:')} $${serveRate.toFixed(2)}/hr`);
@@ -480,10 +569,11 @@ export async function serveCommand(config, args, chalk) {
480
569
 
481
570
  if (endpointReady && !customImage) {
482
571
  const keySnip = config.apiKey?.slice(0, 4) || 'sk-...';
572
+ const sdkModel = isLlamaCpp ? 'default' : (dep.model || model);
483
573
  console.log(` ${chalk.bold('Use with OpenAI SDK:')}`);
484
574
  console.log(chalk.dim(` from openai import OpenAI`));
485
575
  console.log(chalk.dim(` client = OpenAI(base_url="${endpointUrl}", api_key="${keySnip}...")`));
486
- console.log(chalk.dim(` resp = client.chat.completions.create(model="${dep.model || model}", messages=[...])`));
576
+ console.log(chalk.dim(` resp = client.chat.completions.create(model="${sdkModel}", messages=[...])`));
487
577
  console.log();
488
578
  }
489
579
  }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * badgr template list
3
+ * badgr template info <name>
4
+ *
5
+ * Provider-neutral workload templates for common ML frameworks.
6
+ * Launching routes through `badgr serve template <name>` / `badgr run template <name>`.
7
+ */
8
+ import { TEMPLATES, TEMPLATE_MAP, buildTemplateFlags, parseTemplateOverrides } from '../catalog.js';
9
+ import { serveCommand } from './serve.js';
10
+ import { runCommand } from './run.js';
11
+
12
+ // Re-export for tests
13
+ export { TEMPLATES };
14
+
15
+ // ── Helpers ────────────────────────────────────────────────────────────────────
16
+
17
+ function col(s, w) {
18
+ return String(s ?? '').padEnd(w).slice(0, w);
19
+ }
20
+
21
+ function printList(chalk) {
22
+ console.log(chalk.bold('\nAvailable templates\n'));
23
+ console.log(` ${'NAME'.padEnd(18)} ${'TYPE'.padEnd(8)} ${'GPU'.padEnd(10)} DESCRIPTION`);
24
+ console.log(` ${'─'.repeat(18)} ${'─'.repeat(8)} ${'─'.repeat(10)} ${'─'.repeat(44)}`);
25
+ for (const t of TEMPLATES) {
26
+ const type = t.type === 'endpoint' ? chalk.cyan(col(t.type, 8)) : chalk.yellow(col(t.type, 8));
27
+ console.log(` ${chalk.bold(col(t.name, 18))} ${type} ${col(t.gpu, 10)} ${t.description}`);
28
+ }
29
+ console.log();
30
+ console.log(chalk.dim(' badgr template info <name> Show full template details'));
31
+ console.log(chalk.dim(' badgr serve template <name> [flags] Launch an endpoint template'));
32
+ console.log(chalk.dim(' badgr run template <name> [flags] Launch a job template'));
33
+ console.log();
34
+ }
35
+
36
+ function printInfo(t, chalk) {
37
+ console.log(chalk.bold(`\n${t.title}\n`));
38
+ console.log(` ${chalk.bold('Name:')} ${t.name}`);
39
+ console.log(` ${chalk.bold('Description:')} ${t.description}`);
40
+ console.log(` ${chalk.bold('Type:')} ${t.type}`);
41
+ console.log(` ${chalk.bold('Image:')} ${t.image}`);
42
+ console.log(` ${chalk.bold('GPU:')} ${t.gpu} × ${t.gpu_count} (${t.min_vram_gb}+ GB VRAM)`);
43
+ if (t.port) console.log(` ${chalk.bold('Port:')} ${t.port}`);
44
+ if (t.health_path) console.log(` ${chalk.bold('Health:')} ${t.health_path}`);
45
+ if (t.env && Object.keys(t.env).length > 0) {
46
+ console.log(` ${chalk.bold('Env defaults:')}`);
47
+ for (const [k, v] of Object.entries(t.env)) {
48
+ console.log(` ${chalk.cyan(k)}=${chalk.dim(v)}`);
49
+ }
50
+ }
51
+ if (t.command) {
52
+ console.log(` ${chalk.bold('Command:')} ${t.command.join(' ')}`);
53
+ }
54
+ if (t.notes?.length) {
55
+ console.log(`\n ${chalk.bold('Notes:')}`);
56
+ for (const n of t.notes) console.log(` ${chalk.dim(n)}`);
57
+ }
58
+ console.log();
59
+ const verb = t.type === 'job' ? 'run' : 'serve';
60
+ console.log(` ${chalk.bold('Launch:')}`);
61
+ console.log(chalk.dim(` badgr ${verb} template ${t.name} --max-cost <N>`));
62
+ console.log();
63
+ }
64
+
65
+ // ── Main command ───────────────────────────────────────────────────────────────
66
+
67
+ export async function templateCommand(config, args, chalk) {
68
+ const [sub, name, ...rest] = args;
69
+
70
+ if (!sub || sub === 'list' || sub === '--help' || sub === '-h') {
71
+ printList(chalk);
72
+ return;
73
+ }
74
+
75
+ if (sub === 'info') {
76
+ if (!name) {
77
+ console.error(chalk.red(' Usage: badgr template info <name>\n'));
78
+ printList(chalk);
79
+ process.exitCode = 1;
80
+ return;
81
+ }
82
+ const t = TEMPLATE_MAP[name];
83
+ if (!t) {
84
+ console.error(chalk.red(`\n Unknown template: ${name}\n`));
85
+ console.error(chalk.dim(' Run `badgr template list` to see available templates.'));
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+ printInfo(t, chalk);
90
+ return;
91
+ }
92
+
93
+ // `badgr template run <name>` — kept as a convenience alias
94
+ if (sub === 'run') {
95
+ if (!name) {
96
+ console.error(chalk.red(' Usage: badgr template run <name> [--max-cost N] [--env K=V]\n'));
97
+ printList(chalk);
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ const t = TEMPLATE_MAP[name];
102
+ if (!t) {
103
+ console.error(chalk.red(`\n Unknown template: ${name}\n`));
104
+ console.error(chalk.dim(' Run `badgr template list` to see available templates.'));
105
+ process.exitCode = 1;
106
+ return;
107
+ }
108
+ const overrides = parseTemplateOverrides(rest);
109
+ const flags = buildTemplateFlags(t, overrides);
110
+ console.log(chalk.dim(` Template: ${t.title} → badgr ${t.type === 'job' ? 'run' : 'serve'} ${flags.join(' ')}\n`));
111
+ return t.type === 'job'
112
+ ? runCommand(config, flags, chalk)
113
+ : serveCommand(config, flags, chalk);
114
+ }
115
+
116
+ console.error(chalk.red(`\n Unknown subcommand: badgr template ${sub}\n`));
117
+ console.error(chalk.dim(' Subcommands: list, info <name>'));
118
+ process.exitCode = 1;
119
+ }
@@ -2,7 +2,7 @@ import { requireApiKey } from '../config.js';
2
2
  import { callApi, terminateDeployment } from '../api.js';
3
3
  import { addReceipt, generateReceiptId } from '../store.js';
4
4
 
5
- // max $0.80/hr × 2 min ≈ $0.027 total spend cap (smoke / Modal T4 tier)
5
+ // max $0.80/hr × 2 min ≈ $0.027 total spend cap
6
6
  const TEST_MAX_PRICE = 0.80;
7
7
  const TEST_MAX_RUNTIME_MS = 2 * 60 * 1000;
8
8
  const TEST_COMMAND = ['python', '-c', "print('hello from badgr')"];
@@ -12,8 +12,8 @@ const TEST_IMAGE = 'python:3.11-alpine';
12
12
  const EXPECTED_OUTPUT = 'hello from badgr';
13
13
 
14
14
  // --provider flag resolves to a backend tier value.
15
- // 'tier1' → managed routing (default), 'tier2' → marketplace routing, 'secondary' → secondary dispatch.
16
- const PROVIDER_TO_TIER = { tier1: '1', tier2: '2', secondary: 'modal' };
15
+ // 'tier1' → managed routing (default), 'tier2' → marketplace routing.
16
+ const PROVIDER_TO_TIER = { tier1: '1', tier2: '2' };
17
17
 
18
18
  export function parseTestArgs(args) {
19
19
  const flags = {};
@@ -95,7 +95,7 @@ export async function testCommand(config, args, chalk) {
95
95
  } catch {
96
96
  routes = null;
97
97
  }
98
- const secondaryRoute = Array.isArray(routes) ? routes.find(r => r.name === 'modal') : null;
98
+ const secondaryRoute = Array.isArray(routes) ? routes.find(r => r.tier === '2') : null;
99
99
  if (secondaryRoute?.available) {
100
100
  step(chalk, true, 'Secondary provider configured');
101
101
  console.log(chalk.green('\n ✓ Secondary dispatch provider is ready\n'));