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.
- package/README.md +1 -1
- package/package.json +5 -7
- package/src/badgr.js +23 -1
- package/src/catalog.js +479 -0
- package/src/commands/comfyui.js +1 -1
- package/src/commands/receipts.js +2 -1
- package/src/commands/run.js +134 -25
- package/src/commands/serve.js +100 -10
- package/src/commands/template.js +119 -0
- package/src/commands/test-run.js +4 -4
- package/src/commands/workload.js +197 -0
- package/src/commands/workspace.js +136 -0
- package/tests/commands.test.js +48 -0
- package/tests/run-lifecycle.test.js +55 -18
- package/tests/serve-lifecycle.test.js +165 -0
- package/tests/template.test.js +551 -0
- package/tests/workload-rerun.test.js +56 -0
- package/tests/workload-templates.test.js +1 -1
- package/tests/workload-workspace-paths.test.js +46 -0
package/src/commands/run.js
CHANGED
|
@@ -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 <
|
|
18
|
-
if (
|
|
19
|
-
if (
|
|
20
|
-
if (
|
|
21
|
-
if (
|
|
22
|
-
if (
|
|
23
|
-
if (
|
|
24
|
-
if (
|
|
25
|
-
if (
|
|
26
|
-
if (
|
|
27
|
-
if (
|
|
28
|
-
if (
|
|
29
|
-
if (
|
|
30
|
-
if (
|
|
31
|
-
if (
|
|
32
|
-
if (
|
|
33
|
-
if (
|
|
34
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/commands/serve.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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 (
|
|
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(
|
|
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 >
|
|
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 (
|
|
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="${
|
|
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
|
+
}
|
package/src/commands/test-run.js
CHANGED
|
@@ -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
|
|
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
|
|
16
|
-
const PROVIDER_TO_TIER = { tier1: '1', tier2: '2'
|
|
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.
|
|
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'));
|