badgr-cli 1.1.5 → 1.1.7
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 +49 -13
- package/package.json +1 -1
- package/src/admin.js +29 -0
- package/src/api.js +35 -29
- package/src/badgr.js +18 -6
- package/src/catalog.js +12 -0
- package/src/commands/capacity.js +38 -1
- package/src/commands/check.js +127 -0
- package/src/commands/connect.js +3 -2
- package/src/commands/diagnose.js +89 -41
- package/src/commands/down.js +16 -7
- package/src/commands/job.js +5 -5
- package/src/commands/launch.js +37 -12
- package/src/commands/node.js +250 -0
- package/src/commands/run.js +14 -4
- package/src/commands/serve.js +103 -15
- package/src/commands/status.js +13 -5
- package/src/commands/train.js +1 -1
- package/src/config.js +12 -19
- package/src/credentials.js +6 -0
- package/src/fallback.js +9 -11
- package/src/gpuDoctor/gpuInfo.js +144 -5
- package/src/nodeWorker/node_worker.py +326 -0
- package/src/nodeWorker/test_node_worker.py +188 -0
- package/src/progress.js +2 -2
- package/src/spec.js +2 -2
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { execFileSync, spawn } from 'child_process';
|
|
6
|
+
import { ensureLoggedInReady } from '../onboarding.js';
|
|
7
|
+
import { connectNode, listNodes, inspectNode, disableNode, removeNode } from '../api.js';
|
|
8
|
+
import { detectGpus } from '../gpuDoctor/gpuInfo.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* badgr node connect [--name <name>]
|
|
12
|
+
* badgr node list
|
|
13
|
+
* badgr node inspect <node>
|
|
14
|
+
* badgr node disable <node>
|
|
15
|
+
* badgr node remove <node>
|
|
16
|
+
*
|
|
17
|
+
* BYO GPU (Phase 1). `connect` registers this machine as a private capacity
|
|
18
|
+
* source (server-side) AND
|
|
19
|
+
* starts the badgr-node worker daemon (src/nodeWorker/node_worker.py,
|
|
20
|
+
* shipped inside this npm package) so the promised self-serve flow —
|
|
21
|
+
* `npm install -g badgr-cli && badgr node connect` — actually leaves a
|
|
22
|
+
* running worker, not just a database row. It never becomes
|
|
23
|
+
* marketplace-listed on its own.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const WORKER_SCRIPT = path.join(__dirname, '..', 'nodeWorker', 'node_worker.py');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Shared `--target <node>` resolution for `badgr run`/`badgr serve` — CLI
|
|
31
|
+
* sugar over the explicit customer_node target the API takes. Resolves a node name to its
|
|
32
|
+
* node_id; a bare `node_…` id is used as-is. Returns `{ ok: true, nodeId }`
|
|
33
|
+
* on success (`nodeId` is `undefined` when `target` is falsy — leaves the
|
|
34
|
+
* caller's body.target unset, i.e. today's cloud-provider search,
|
|
35
|
+
* unchanged) or `{ ok: false }` after printing a user-facing error, in
|
|
36
|
+
* which case the caller should set `process.exitCode = 1` and return.
|
|
37
|
+
*/
|
|
38
|
+
export async function resolveTargetNodeId(config, target, chalk) {
|
|
39
|
+
if (!target) return { ok: true, nodeId: undefined };
|
|
40
|
+
if (target.startsWith('node_')) return { ok: true, nodeId: target };
|
|
41
|
+
let nodes;
|
|
42
|
+
try {
|
|
43
|
+
nodes = await listNodes(config);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(chalk.red(`\n ✗ Could not resolve --target ${target}: ${err.message}\n`));
|
|
46
|
+
return { ok: false };
|
|
47
|
+
}
|
|
48
|
+
const match = nodes.find((n) => n.name === target || n.node_id === target);
|
|
49
|
+
if (!match) {
|
|
50
|
+
console.error(chalk.red(`\n ✗ No connected node named "${target}". Run \`badgr node list\`.\n`));
|
|
51
|
+
return { ok: false };
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, nodeId: match.node_id };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function dockerAvailable() {
|
|
57
|
+
try {
|
|
58
|
+
execFileSync('docker', ['info'], { stdio: 'ignore', timeout: 5000 });
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function python3Available() {
|
|
66
|
+
try {
|
|
67
|
+
execFileSync('python3', ['--version'], { stdio: 'ignore', timeout: 5000 });
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function startWorker(config, node) {
|
|
75
|
+
const logDir = path.join(os.homedir(), '.badgr', 'logs');
|
|
76
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
77
|
+
const logPath = path.join(logDir, `node-${node.node_id}.log`);
|
|
78
|
+
const workerArgs = [
|
|
79
|
+
WORKER_SCRIPT, '--node-id', node.node_id, '--api-key', config.apiKey, '--base-url', config.baseUrl,
|
|
80
|
+
];
|
|
81
|
+
// Header line makes it obvious in the log file which `connect` invocation
|
|
82
|
+
// spawned this worker run (helpful when a node is connected more than
|
|
83
|
+
// once, or the worker is restarted) without needing to inspect argv.
|
|
84
|
+
fs.appendFileSync(
|
|
85
|
+
logPath,
|
|
86
|
+
`\n=== ${new Date().toISOString()} badgr node connect: spawning worker for ${node.node_id} (base_url=${config.baseUrl}) ===\n`,
|
|
87
|
+
);
|
|
88
|
+
const out = fs.openSync(logPath, 'a');
|
|
89
|
+
const child = spawn('python3', workerArgs, { detached: true, stdio: ['ignore', out, out] });
|
|
90
|
+
child.unref();
|
|
91
|
+
return { pid: child.pid, logPath };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function detectHardware() {
|
|
95
|
+
const gpuProbe = detectGpus();
|
|
96
|
+
const primary = gpuProbe.gpus[0];
|
|
97
|
+
return {
|
|
98
|
+
gpu_vendor: gpuProbe.vendor,
|
|
99
|
+
gpu_model: primary ? primary.name : null,
|
|
100
|
+
gpu_count: gpuProbe.gpus.length,
|
|
101
|
+
vram_gb: primary ? primary.vramTotalGb : null,
|
|
102
|
+
// Feeds byo_preflight.run_preflight()'s required_gpu_architecture check
|
|
103
|
+
// (backend/byo_preflight.py) -- AMD reports a gfx#### string (e.g.
|
|
104
|
+
// "gfx1030"), NVIDIA reports its compute capability (e.g. "8.9"), both
|
|
105
|
+
// read straight off this machine's own GPU(s), never guessed.
|
|
106
|
+
gpu_architecture: primary ? primary.architecture : null,
|
|
107
|
+
driver_version: gpuProbe.driverVersion,
|
|
108
|
+
cuda_version: gpuProbe.cudaVersion,
|
|
109
|
+
cpu_count: os.cpus().length,
|
|
110
|
+
memory_gb: Math.round(os.totalmem() / (1024 ** 3)),
|
|
111
|
+
platform: os.platform(),
|
|
112
|
+
docker: dockerAvailable(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseFlags(args) {
|
|
117
|
+
const flags = {};
|
|
118
|
+
let i = 0;
|
|
119
|
+
while (i < args.length) {
|
|
120
|
+
if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
return flags;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveNodeIdArg(config, args) {
|
|
127
|
+
return args.find((a) => !a.startsWith('--'));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function nodeCommand(config, args, chalk) {
|
|
131
|
+
const sub = args[0];
|
|
132
|
+
const rest = args.slice(1);
|
|
133
|
+
config = await ensureLoggedInReady(config, chalk);
|
|
134
|
+
|
|
135
|
+
if (sub === 'connect') {
|
|
136
|
+
const flags = parseFlags(rest);
|
|
137
|
+
const hardware = detectHardware();
|
|
138
|
+
console.log(chalk.dim(
|
|
139
|
+
` Detected: vendor=${hardware.gpu_vendor || 'none'} gpu_count=${hardware.gpu_count} gpu_model=${hardware.gpu_model || 'none'} `
|
|
140
|
+
+ `arch=${hardware.gpu_architecture || 'none'} driver=${hardware.driver_version || 'none'} `
|
|
141
|
+
+ `docker=${hardware.docker} platform=${hardware.platform}`,
|
|
142
|
+
));
|
|
143
|
+
if (!hardware.gpu_count) {
|
|
144
|
+
console.log(chalk.yellow('\n No GPU detected (nvidia-smi and rocm-smi both not found or returned nothing).'));
|
|
145
|
+
console.log(chalk.dim(' BYO GPU currently supports Linux + NVIDIA or AMD/ROCm.\n'));
|
|
146
|
+
}
|
|
147
|
+
if (!hardware.docker) {
|
|
148
|
+
console.log(chalk.yellow(' Docker was not detected — badgr-node needs Docker to run workloads.\n'));
|
|
149
|
+
}
|
|
150
|
+
let node;
|
|
151
|
+
try {
|
|
152
|
+
node = await connectNode(config, { name: flags.name, hardware });
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error(chalk.red(`\n ✗ Failed to connect node: ${err.message}\n`));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
console.log(chalk.green(`\n ✓ Connected ${chalk.bold(node.name)} (${node.node_id})`));
|
|
158
|
+
console.log(chalk.dim(` ${hardware.gpu_count} × ${hardware.gpu_model || 'unknown GPU'} — private, not listed on the marketplace\n`));
|
|
159
|
+
|
|
160
|
+
const hasPython3 = python3Available();
|
|
161
|
+
if (!hardware.gpu_count || !hardware.docker || !hasPython3) {
|
|
162
|
+
const missing = [
|
|
163
|
+
!hardware.gpu_count && 'GPU (nvidia-smi/rocm-smi)',
|
|
164
|
+
!hardware.docker && 'Docker',
|
|
165
|
+
!hasPython3 && 'python3',
|
|
166
|
+
].filter(Boolean).join(', ');
|
|
167
|
+
console.log(chalk.yellow(` Not starting the worker automatically — missing: ${missing}.`));
|
|
168
|
+
console.log(` Once fixed, start it with:`);
|
|
169
|
+
console.log(chalk.cyan(` python3 "${WORKER_SCRIPT}" --node-id ${node.node_id} --api-key <your-api-key>\n`));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { pid, logPath } = startWorker(config, node);
|
|
174
|
+
console.log(` Worker started (pid ${pid}) — this node is now receiving jobs.`);
|
|
175
|
+
console.log(chalk.dim(` Logs: ${logPath}\n`));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (sub === 'list') {
|
|
180
|
+
let nodes;
|
|
181
|
+
try {
|
|
182
|
+
nodes = await listNodes(config);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error(chalk.red(`\n ✗ Failed to list nodes: ${err.message}\n`));
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
if (!nodes.length) {
|
|
188
|
+
console.log(chalk.dim('\n No connected nodes. Run `badgr node connect` on a machine with an NVIDIA or AMD/ROCm GPU.\n'));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.log(chalk.bold('\nMy GPUs\n'));
|
|
192
|
+
for (const n of nodes) {
|
|
193
|
+
const status = n.online ? chalk.green('online') : chalk.dim('offline');
|
|
194
|
+
const hw = n.hardware || {};
|
|
195
|
+
console.log(` ${chalk.bold(n.name)} ${chalk.dim(n.node_id)}`);
|
|
196
|
+
console.log(` ${hw.gpu_count || 0} × ${hw.gpu_model || 'unknown'} ${status} ${n.visibility}`);
|
|
197
|
+
}
|
|
198
|
+
console.log();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (sub === 'inspect') {
|
|
203
|
+
const nodeId = resolveNodeIdArg(config, rest);
|
|
204
|
+
if (!nodeId) { console.error(chalk.red('\n Usage: badgr node inspect <node-id>\n')); process.exit(1); }
|
|
205
|
+
let node;
|
|
206
|
+
try {
|
|
207
|
+
node = await inspectNode(config, nodeId);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.error(chalk.red(`\n ✗ ${err.message}\n`));
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
console.log(chalk.bold(`\n${node.name} ${chalk.dim(node.node_id)}\n`));
|
|
213
|
+
console.log(JSON.stringify(node, null, 2));
|
|
214
|
+
console.log();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (sub === 'disable') {
|
|
219
|
+
const nodeId = resolveNodeIdArg(config, rest);
|
|
220
|
+
if (!nodeId) { console.error(chalk.red('\n Usage: badgr node disable <node-id>\n')); process.exit(1); }
|
|
221
|
+
try {
|
|
222
|
+
await disableNode(config, nodeId);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.error(chalk.red(`\n ✗ ${err.message}\n`));
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
console.log(chalk.green(`\n ✓ Disabled ${nodeId} — it will no longer receive new jobs.\n`));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (sub === 'remove') {
|
|
232
|
+
const nodeId = resolveNodeIdArg(config, rest);
|
|
233
|
+
if (!nodeId) { console.error(chalk.red('\n Usage: badgr node remove <node-id>\n')); process.exit(1); }
|
|
234
|
+
try {
|
|
235
|
+
await removeNode(config, nodeId);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.error(chalk.red(`\n ✗ ${err.message}\n`));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
console.log(chalk.green(`\n ✓ Removed ${nodeId}\n`));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.log(chalk.bold('\nbadgr node — manage your own connected GPUs\n'));
|
|
245
|
+
console.log(' badgr node connect [--name <name>] Connect this machine\'s GPU (private)');
|
|
246
|
+
console.log(' badgr node list List your connected nodes');
|
|
247
|
+
console.log(' badgr node inspect <node-id> Show one node\'s details');
|
|
248
|
+
console.log(' badgr node disable <node-id> Stop a node from receiving new jobs');
|
|
249
|
+
console.log(' badgr node remove <node-id> Disconnect and forget a node\n');
|
|
250
|
+
}
|
package/src/commands/run.js
CHANGED
|
@@ -14,6 +14,7 @@ import { ensureBadgrReady } from '../onboarding.js';
|
|
|
14
14
|
import { VM_CLASSES, parseGbSize } from '../spec.js';
|
|
15
15
|
import { parseEnvFlag } from '../envFlag.js';
|
|
16
16
|
import { isMetaLogLine, isErrorLogLine, parseProviderStatusLine } from '../deploymentLog.js';
|
|
17
|
+
import { resolveTargetNodeId } from './node.js';
|
|
17
18
|
|
|
18
19
|
function vmClassLine(sizeKey) {
|
|
19
20
|
const vmClass = VM_CLASSES[sizeKey];
|
|
@@ -54,6 +55,7 @@ export function parseRunArgs(args) {
|
|
|
54
55
|
let i = 0;
|
|
55
56
|
while (i < flagArgs.length) {
|
|
56
57
|
if (flagArgs[i] === '--gpu') { flags.gpu = flagArgs[++i]; i++; continue; }
|
|
58
|
+
if (flagArgs[i] === '--target') { flags.target = flagArgs[++i]; i++; continue; }
|
|
57
59
|
if (flagArgs[i] === '--image') { flags.image = flagArgs[++i]; i++; continue; }
|
|
58
60
|
if (flagArgs[i] === '--count') { flags.count = parseInt(flagArgs[++i], 10); i++; continue; }
|
|
59
61
|
if (flagArgs[i] === '--region') { flags.region = flagArgs[++i]; i++; continue; }
|
|
@@ -128,7 +130,7 @@ function redactEnvForDisplay(envList) {
|
|
|
128
130
|
}).join(', ');
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
// Mirror of backend
|
|
133
|
+
// Mirror of the backend's own workload-profile logic — kept in sync for pre-flight display.
|
|
132
134
|
const _PROFILES = {
|
|
133
135
|
smoke_test: { label: 'smoke test', vram: '4 GB', gpus: ['RTX 3080', 'RTX 3090', 'RTX 4090'] },
|
|
134
136
|
lora_finetune: { label: 'fine-tuning (LoRA)', vram: '40+ GB', gpus: ['A6000', 'L40S', 'A100'] },
|
|
@@ -392,7 +394,7 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
|
|
|
392
394
|
|
|
393
395
|
// Known badgr run flags — used to detect broken shell line continuation.
|
|
394
396
|
const _KNOWN_RUN_FLAGS = new Set([
|
|
395
|
-
'--gpu', '--image', '--count', '--region', '--tier', '--smoke', '--max-price', '--name',
|
|
397
|
+
'--gpu', '--target', '--image', '--count', '--region', '--tier', '--smoke', '--max-price', '--name',
|
|
396
398
|
'--detach', '--no-detach', '--fallback', '--no-fallback', '--strict-capacity',
|
|
397
399
|
'--no-expanded-search', '--max-runtime', '--max-cost', '--min-vram', '--gpu-memory',
|
|
398
400
|
'--cpu', '--memory', '--no-gpu', '--env',
|
|
@@ -495,7 +497,7 @@ export async function runCommand(config, args, chalk, opts = {}) {
|
|
|
495
497
|
// callers (badgr launch's agent workloads), never parsed from a shell
|
|
496
498
|
// string. Passed to the backend as command_argv, which the worker always
|
|
497
499
|
// prefers over the legacy cmd string + shlex.split() path (see
|
|
498
|
-
//
|
|
500
|
+
// the worker image's own entrypoint). flags.cmd stays the
|
|
499
501
|
// human-readable display string only.
|
|
500
502
|
const cmdArgv = Array.isArray(opts.cmdArgv) ? opts.cmdArgv : undefined;
|
|
501
503
|
const cmdName = isLaunch ? 'badgr launch' : 'badgr run';
|
|
@@ -903,6 +905,13 @@ export async function runCommand(config, args, chalk, opts = {}) {
|
|
|
903
905
|
console.log(chalk.dim(` API: ${config.baseUrl}`));
|
|
904
906
|
}
|
|
905
907
|
|
|
908
|
+
// --target <node> — see node.js's resolveTargetNodeId for the shared
|
|
909
|
+
// resolution (also used by serve.js). Omitting --target leaves
|
|
910
|
+
// body.target unset — today's cloud-provider search, unchanged.
|
|
911
|
+
const _targetResolved = await resolveTargetNodeId(config, flags.target, chalk);
|
|
912
|
+
if (!_targetResolved.ok) { process.exitCode = 1; return; }
|
|
913
|
+
const targetNodeId = _targetResolved.nodeId;
|
|
914
|
+
|
|
906
915
|
function buildBody(gpuOverride, tierOverride) {
|
|
907
916
|
const effectiveRegion = flags.region ? flags.region.toUpperCase() : undefined;
|
|
908
917
|
return {
|
|
@@ -932,6 +941,7 @@ export async function runCommand(config, args, chalk, opts = {}) {
|
|
|
932
941
|
...(flags.artifacts?.length ? { output_paths: flags.artifacts } : {}),
|
|
933
942
|
...(flags.agentName ? { agent: flags.agentName } : {}),
|
|
934
943
|
...(flags.size ? { size: flags.size } : {}),
|
|
944
|
+
...(targetNodeId ? { target: { type: 'customer_node', node_id: targetNodeId } } : {}),
|
|
935
945
|
};
|
|
936
946
|
}
|
|
937
947
|
|
|
@@ -999,7 +1009,7 @@ export async function runCommand(config, args, chalk, opts = {}) {
|
|
|
999
1009
|
// Show what was actually provisioned, not just what was requested — only
|
|
1000
1010
|
// when the caller asked for a resource floor and the provider reported
|
|
1001
1011
|
// enough to say something concrete (best-effort; not every provider
|
|
1002
|
-
// exposes vcpu/ram/vram per-offer, see
|
|
1012
|
+
// exposes vcpu/ram/vram per-offer, see the backend's own
|
|
1003
1013
|
// chosen_instance.extra enrichment).
|
|
1004
1014
|
if (flags.cpu || flags.memory || flags.minVram) {
|
|
1005
1015
|
const parts = [];
|
package/src/commands/serve.js
CHANGED
|
@@ -7,6 +7,7 @@ import { requireApiKey, webBaseUrl } from '../config.js';
|
|
|
7
7
|
import { parseEnvFlag } from '../envFlag.js';
|
|
8
8
|
import { TEMPLATE_MAP, buildTemplateFlags, parseTemplateOverrides, BLESSED_VLLM_MODELS, isLikelyGatedModel } from '../catalog.js';
|
|
9
9
|
import { stage as _stage, stageDone as _stageDone, writeBlock as _writeBlock, clearBlock as _clearBlock, renderLiveBlock as _renderLiveBlock, printFailureClass as _printFailureClass, printCapacityPreview, formatTierLabel } from '../progress.js';
|
|
10
|
+
import { resolveTargetNodeId } from './node.js';
|
|
10
11
|
|
|
11
12
|
const LLAMA_CPP_IMAGE = 'michaelmanleyx/llama-cpp:server-cuda';
|
|
12
13
|
|
|
@@ -16,15 +17,38 @@ const LLAMA_CPP_IMAGE = 'michaelmanleyx/llama-cpp:server-cuda';
|
|
|
16
17
|
* badgr serve BAAI/bge-large-en-v1.5 --task embed
|
|
17
18
|
* badgr serve --image ghcr.io/my-org/diffusers-api:latest --gpu L40S --env MODEL_ID=flux
|
|
18
19
|
* badgr serve --runtime llama.cpp --hf-repo org/repo --hf-file model.gguf --max-cost 10
|
|
20
|
+
* badgr serve org/Qwen3-27B-NVFP4 --vllm-arg --quantization=nvfp4 --vllm-arg --max-model-len=32768
|
|
21
|
+
* badgr serve org/Qwen3-27B-NVFP4 -- --quantization nvfp4 --max-model-len 32768
|
|
19
22
|
*
|
|
20
23
|
* GPU defaults to "AUTO" — backend infers from model size.
|
|
24
|
+
*
|
|
25
|
+
* --vllm-arg and the `--` passthrough form both feed the same extra_args
|
|
26
|
+
* list sent to POST /v1/serve, which appends them verbatim to vLLM's launch
|
|
27
|
+
* command after --model/--served-model-name (see
|
|
28
|
+
* the backend's own ServeBody.extra_args and
|
|
29
|
+
* cache_layer.build_vllm_launch). Only meaningful for the vLLM path (a
|
|
30
|
+
* HuggingFace --model, with or without --image) — ignored for --runtime
|
|
31
|
+
* llama.cpp and other managed runtimes, which don't build a vLLM command.
|
|
21
32
|
*/
|
|
22
33
|
export function parseServeArgs(args) {
|
|
23
34
|
const flags = {};
|
|
24
35
|
const positional = [];
|
|
25
36
|
let i = 0;
|
|
26
37
|
while (i < args.length) {
|
|
38
|
+
if (args[i] === '--') {
|
|
39
|
+
// Everything after a bare `--` is passed through verbatim as vLLM
|
|
40
|
+
// launch args — same destination as repeated --vllm-arg, just
|
|
41
|
+
// without needing to prefix each one.
|
|
42
|
+
if (!flags.vllmArgs) flags.vllmArgs = [];
|
|
43
|
+
flags.vllmArgs.push(...args.slice(i + 1));
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (args[i] === '--vllm-arg') {
|
|
47
|
+
if (!flags.vllmArgs) flags.vllmArgs = [];
|
|
48
|
+
flags.vllmArgs.push(args[++i]); i++; continue;
|
|
49
|
+
}
|
|
27
50
|
if (args[i] === '--gpu') { flags.gpu = args[++i]; i++; continue; }
|
|
51
|
+
if (args[i] === '--target') { flags.target = args[++i]; i++; continue; }
|
|
28
52
|
if (args[i] === '--image') { flags.image = args[++i]; i++; continue; }
|
|
29
53
|
if (args[i] === '--task') { flags.task = args[++i]; i++; continue; }
|
|
30
54
|
if (args[i] === '--count') { flags.count = parseInt(args[++i], 10); i++; continue; }
|
|
@@ -73,7 +97,7 @@ function envObjHasHfToken(envList) {
|
|
|
73
97
|
// Splits into delimiter-bounded segments first so a version number like "2.5"
|
|
74
98
|
// in "Qwen2.5-0.5B" is never mistaken for the param count — only a segment that
|
|
75
99
|
// IS entirely "<digits>b"/"<digits>m" or "<digits>x<digits>b" counts as a size
|
|
76
|
-
// hint. Mirrors backend
|
|
100
|
+
// hint. Mirrors the backend's own parameter-extraction logic --
|
|
77
101
|
// keep the two in sync; a drift between them means this CLI preview and the
|
|
78
102
|
// backend's real GPU selection could show a different workload class for
|
|
79
103
|
// the same model.
|
|
@@ -95,13 +119,13 @@ export function _extractParamsB(name) {
|
|
|
95
119
|
return null;
|
|
96
120
|
}
|
|
97
121
|
|
|
98
|
-
// Mirror of backend
|
|
122
|
+
// Mirror of the backend's own model-profile inference — for pre-flight display.
|
|
99
123
|
// Sizing is only asserted when a param-count hint is found in the name; unknown
|
|
100
124
|
// sizing falls back to the 7B–8B/24GB+ default rather than guessing small or large.
|
|
101
|
-
// GPU lists match
|
|
125
|
+
// GPU lists match the backend's own preferred_gpus for each profile
|
|
102
126
|
// (not just a cosmetic display choice) -- "L4" previously appeared in the
|
|
103
127
|
// ≤3B bucket here but is not an actual GPU type this codebase ever routes to
|
|
104
|
-
// (see
|
|
128
|
+
// (see the backend's own GPU catalog), so it never matched anything
|
|
105
129
|
// real; the cheapest-available-first list below is the one the backend's
|
|
106
130
|
// own "inference_tiny" profile actually searches.
|
|
107
131
|
export function _inferServeProfile(modelName) {
|
|
@@ -113,7 +137,7 @@ export function _inferServeProfile(modelName) {
|
|
|
113
137
|
return { label: 'inference (70B+ model)', vram: '80+ GB', gpus: ['H100', 'A100'] };
|
|
114
138
|
}
|
|
115
139
|
|
|
116
|
-
// Mirror of backend
|
|
140
|
+
// Mirror of the backend's own GGUF-profile inference.
|
|
117
141
|
// Accepts the --hf-file filename; looks for param-count hints like "35B" or "8x7B".
|
|
118
142
|
function _inferGgufProfile(ggufPath) {
|
|
119
143
|
const paramsB = _extractParamsB(ggufPath);
|
|
@@ -151,7 +175,12 @@ function _resolveHealthPath({ healthPath, isLlamaCpp, customImage, task }) {
|
|
|
151
175
|
* directly. The backend does the real health_path probing (see
|
|
152
176
|
* DeploymentService.check_endpoint_readiness); a pod reporting RUNNING only
|
|
153
177
|
* means infrastructure is up, not that the app inside is serving.
|
|
154
|
-
* Returns { ready: boolean, timedOut: boolean, depFailed: boolean, failReason?: string
|
|
178
|
+
* Returns { ready: boolean, timedOut: boolean, depFailed: boolean, failReason?: string,
|
|
179
|
+
* dep? }. `dep` (only present when ready) is the last polled
|
|
180
|
+
* /deployments/{id} response — carries supports_completions/
|
|
181
|
+
* supports_chat_completions, probed server-side once the endpoint first
|
|
182
|
+
* reports ready, so the caller's final usage example can match what the
|
|
183
|
+
* model actually serves instead of guessing.
|
|
155
184
|
*/
|
|
156
185
|
// vLLM cold start (model download + load) often exceeds 5 min on first boot.
|
|
157
186
|
const VLLM_SERVE_WAIT_MS = 15 * 60 * 1000;
|
|
@@ -183,7 +212,7 @@ async function waitForEndpoint(deploymentId, config, timeoutMs = VLLM_SERVE_WAIT
|
|
|
183
212
|
}
|
|
184
213
|
if (dep.endpoint_ready) {
|
|
185
214
|
if (blockLines > 0) { _clearBlock(blockLines); blockLines = 0; }
|
|
186
|
-
return { ready: true, timedOut: false, depFailed: false };
|
|
215
|
+
return { ready: true, timedOut: false, depFailed: false, dep };
|
|
187
216
|
}
|
|
188
217
|
|
|
189
218
|
const now = Date.now();
|
|
@@ -231,7 +260,7 @@ async function validateComfyNodes(baseUrl, nodeList, chalk) {
|
|
|
231
260
|
|
|
232
261
|
// Known badgr serve flags — used to detect broken shell line continuation.
|
|
233
262
|
const _KNOWN_SERVE_FLAGS = new Set([
|
|
234
|
-
'--gpu', '--image', '--task', '--count', '--region', '--tier', '--max-price',
|
|
263
|
+
'--gpu', '--target', '--image', '--task', '--count', '--region', '--tier', '--max-price',
|
|
235
264
|
'--name', '--no-wait', '--max-cost', '--idle-timeout', '--health-path', '--check-nodes',
|
|
236
265
|
'--no-fallback', '--strict-capacity', '--no-expanded-search', '--env',
|
|
237
266
|
'--persistent', '--yes', '-y', '--runtime', '--hf-repo', '--hf-file', '--dry-run',
|
|
@@ -536,6 +565,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
536
565
|
if (flags.gpu) headerLines.push(['GPU', gpuLabel]);
|
|
537
566
|
if (flags.task) headerLines.push(['Task', flags.task]);
|
|
538
567
|
if (flags.env?.length) headerLines.push(['Env', flags.env.join(', ')]);
|
|
568
|
+
if (flags.vllmArgs?.length) headerLines.push(['vLLM args', flags.vllmArgs.join(' ')]);
|
|
539
569
|
if (flags.dryRun) {
|
|
540
570
|
headerLines.push(['Tier', formatTierLabel(effectiveTier)]);
|
|
541
571
|
const previewHealthPath = _resolveHealthPath({ healthPath: flags.healthPath, isLlamaCpp, customImage, task: flags.task });
|
|
@@ -612,6 +642,13 @@ export async function serveCommand(config, args, chalk) {
|
|
|
612
642
|
|
|
613
643
|
const effectiveRegion = flags.region ? flags.region.toUpperCase() : undefined;
|
|
614
644
|
|
|
645
|
+
// --target <node> — see node.js's resolveTargetNodeId for the shared
|
|
646
|
+
// resolution (also used by run.js). Omitting --target leaves body.target
|
|
647
|
+
// unset — today's cloud-provider search, unchanged.
|
|
648
|
+
const _targetResolved = await resolveTargetNodeId(config, flags.target, chalk);
|
|
649
|
+
if (!_targetResolved.ok) { process.exitCode = 1; return; }
|
|
650
|
+
const targetNodeId = _targetResolved.nodeId;
|
|
651
|
+
|
|
615
652
|
function buildBody(gpuOverride, tierOverride) {
|
|
616
653
|
const effectiveEnv = isLlamaCpp
|
|
617
654
|
? { LLAMA_ARG_HF_REPO: flags.hfRepo, LLAMA_ARG_HF_FILE: flags.hfFile, ...envObj }
|
|
@@ -631,6 +668,8 @@ export async function serveCommand(config, args, chalk) {
|
|
|
631
668
|
...(flags.maxCost ? { max_cost_usd: flags.maxCost } : {}),
|
|
632
669
|
...(flags.idleTimeout ? { idle_timeout_minutes: flags.idleTimeout } : {}),
|
|
633
670
|
...(flags.healthPath ? { health_path: flags.healthPath } : {}),
|
|
671
|
+
...(flags.vllmArgs?.length ? { extra_args: flags.vllmArgs } : {}),
|
|
672
|
+
...(targetNodeId ? { target: { type: 'customer_node', node_id: targetNodeId } } : {}),
|
|
634
673
|
};
|
|
635
674
|
}
|
|
636
675
|
|
|
@@ -793,6 +832,11 @@ export async function serveCommand(config, args, chalk) {
|
|
|
793
832
|
if (endpointReady) {
|
|
794
833
|
console.log(chalk.dim(_stage(healthStageN, STAGE_TOTAL, 'Checking endpoint health...')));
|
|
795
834
|
console.log(chalk.green(_stage(readyStageN, STAGE_TOTAL, 'Ready')));
|
|
835
|
+
if (healthResult.dep) {
|
|
836
|
+
dep.supports_completions = healthResult.dep.supports_completions;
|
|
837
|
+
dep.supports_chat_completions = healthResult.dep.supports_chat_completions;
|
|
838
|
+
dep.capability_source = healthResult.dep.capability_source;
|
|
839
|
+
}
|
|
796
840
|
} else {
|
|
797
841
|
updateReceipt(rcptId, { status: 'health_check_timeout' });
|
|
798
842
|
}
|
|
@@ -820,7 +864,14 @@ export async function serveCommand(config, args, chalk) {
|
|
|
820
864
|
console.log();
|
|
821
865
|
}
|
|
822
866
|
|
|
823
|
-
|
|
867
|
+
// dep.service_url is the Badgr-owned https://<id>.serve.aibadgr.com URL
|
|
868
|
+
// once that proxy is actually live (server-side) --
|
|
869
|
+
// until then it's absent/equal to endpointUrl, so this falls back to the
|
|
870
|
+
// raw provider URL exactly as before. Only the customer-facing display
|
|
871
|
+
// and usage examples switch to it; polling/comfy-node validation above
|
|
872
|
+
// still talk to the real internal endpointUrl directly.
|
|
873
|
+
const displayUrl = dep.service_url || endpointUrl;
|
|
874
|
+
console.log(` ${chalk.bold('Base URL:')} ${chalk.cyan(displayUrl)}`);
|
|
824
875
|
if (isLlamaCpp) {
|
|
825
876
|
console.log(` ${chalk.bold('HF Repo:')} ${flags.hfRepo}`);
|
|
826
877
|
console.log(` ${chalk.bold('HF File:')} ${flags.hfFile}`);
|
|
@@ -843,7 +894,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
843
894
|
|
|
844
895
|
if (endpointReady && isOllama) {
|
|
845
896
|
console.log(` ${chalk.bold('Test with curl:')}`);
|
|
846
|
-
console.log(chalk.dim(` curl ${
|
|
897
|
+
console.log(chalk.dim(` curl ${displayUrl}/api/generate \\`));
|
|
847
898
|
console.log(chalk.dim(` -H "Content-Type: application/json" \\`));
|
|
848
899
|
console.log(chalk.dim(` -d '{"model":"${dep.model || effectiveModel}","prompt":"Hello","stream":false}'`));
|
|
849
900
|
console.log();
|
|
@@ -860,13 +911,45 @@ export async function serveCommand(config, args, chalk) {
|
|
|
860
911
|
console.log(` ${chalk.bold('API key:')} ${chalk.yellow(dep.endpoint_api_key)}`);
|
|
861
912
|
console.log(chalk.dim(' Shown once — copy it now. This key is scoped to this endpoint only.'));
|
|
862
913
|
}
|
|
914
|
+
// dep.supports_chat_completions is probed server-side (see backend
|
|
915
|
+
// the backend's own model-capabilities lookup): a live call against this exact endpoint is
|
|
916
|
+
// authoritative when it's reachable (dep.capability_source ===
|
|
917
|
+
// "runtime"); otherwise it falls back to a best-effort hint from the
|
|
918
|
+
// model's public HF chat-template config (capability_source ===
|
|
919
|
+
// "hf_hint"). `true`/`false` here always mean one of those two actually
|
|
920
|
+
// produced a real signal -- a model without a chat template (e.g.
|
|
921
|
+
// facebook/opt-125m) answers /v1/completions but 400s on
|
|
922
|
+
// /v1/chat/completions, so a generic chat example would be wrong.
|
|
923
|
+
// `undefined`/`null` means NEITHER produced a signal (auth-protected
|
|
924
|
+
// endpoint + HF lookup failed, or a non-vLLM/task image) -- unlike this
|
|
925
|
+
// command's old behavior, that must never silently default to showing
|
|
926
|
+
// a chat example that might not work; show neither example instead.
|
|
927
|
+
const noTask = !flags.task;
|
|
928
|
+
const chatKnown = dep.supports_chat_completions === true || dep.supports_chat_completions === false;
|
|
929
|
+
const showChatExample = noTask && chatKnown && dep.supports_chat_completions === true;
|
|
930
|
+
const showCompletionsExample = noTask && chatKnown && dep.supports_chat_completions === false;
|
|
931
|
+
const capabilityUnverified = noTask && !chatKnown;
|
|
932
|
+
const unconfirmedNote = dep.capability_source === 'hf_hint'
|
|
933
|
+
? chalk.dim(' (based on the model\'s public Hugging Face config — not confirmed against this live endpoint)')
|
|
934
|
+
: null;
|
|
863
935
|
console.log(` ${chalk.bold('Test with curl:')}`);
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
936
|
+
if (showChatExample) {
|
|
937
|
+
console.log(chalk.dim(` curl ${displayUrl}/chat/completions \\`));
|
|
938
|
+
console.log(chalk.dim(` -H "Authorization: Bearer ${authKey}" -H "Content-Type: application/json" \\`));
|
|
939
|
+
console.log(chalk.dim(` -d '{"model":"${sdkModel}","messages":[{"role":"user","content":"Hello"}]}'`));
|
|
940
|
+
if (unconfirmedNote) console.log(unconfirmedNote);
|
|
941
|
+
} else if (showCompletionsExample) {
|
|
942
|
+
console.log(chalk.dim(` curl ${displayUrl}/completions \\`));
|
|
943
|
+
console.log(chalk.dim(` -H "Authorization: Bearer ${authKey}" -H "Content-Type: application/json" \\`));
|
|
944
|
+
console.log(chalk.dim(` -d '{"model":"${sdkModel}","prompt":"Hello","max_tokens":32}'`));
|
|
945
|
+
if (unconfirmedNote) console.log(unconfirmedNote);
|
|
946
|
+
} else if (capabilityUnverified) {
|
|
947
|
+
console.log(chalk.yellow(' Capability not verified yet — check available models first:'));
|
|
948
|
+
console.log(chalk.dim(` curl ${displayUrl}/models -H "Authorization: Bearer ${authKey}"`));
|
|
949
|
+
}
|
|
867
950
|
console.log(` ${chalk.bold('Use with OpenAI SDK:')}`);
|
|
868
951
|
console.log(chalk.dim(` from openai import OpenAI`));
|
|
869
|
-
console.log(chalk.dim(` client = OpenAI(base_url="${
|
|
952
|
+
console.log(chalk.dim(` client = OpenAI(base_url="${displayUrl}", api_key="${authKey}")`));
|
|
870
953
|
if (flags.task === 'transcribe') {
|
|
871
954
|
console.log(chalk.dim(` with open("audio.mp3", "rb") as f:`));
|
|
872
955
|
console.log(chalk.dim(` t = client.audio.transcriptions.create(model="${sdkModel}", file=f, response_format="text")`));
|
|
@@ -875,8 +958,13 @@ export async function serveCommand(config, args, chalk) {
|
|
|
875
958
|
console.log(chalk.dim(` # resp.data[0].b64_json contains the base64-encoded PNG`));
|
|
876
959
|
} else if (flags.task === 'embed') {
|
|
877
960
|
console.log(chalk.dim(` resp = client.embeddings.create(model="${sdkModel}", input=["hello world"])`));
|
|
878
|
-
} else {
|
|
961
|
+
} else if (showChatExample) {
|
|
879
962
|
console.log(chalk.dim(` resp = client.chat.completions.create(model="${sdkModel}", messages=[{"role": "user", "content": "Hello"}])`));
|
|
963
|
+
} else if (showCompletionsExample) {
|
|
964
|
+
console.log(chalk.dim(` resp = client.completions.create(model="${sdkModel}", prompt="Hello", max_tokens=32)`));
|
|
965
|
+
console.log(chalk.dim(` # this model has no chat template — /v1/chat/completions returns 400 for it`));
|
|
966
|
+
} else {
|
|
967
|
+
console.log(chalk.dim(` resp = client.models.list() # check capability_unverified — chat vs completions support isn't confirmed yet`));
|
|
880
968
|
}
|
|
881
969
|
console.log();
|
|
882
970
|
if (flags.idleTimeout) {
|
package/src/commands/status.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
import { listDeployments as localDeployments } from '../store.js';
|
|
2
|
-
import { listDeployments as apiDeployments
|
|
2
|
+
import { listDeployments as apiDeployments } from '../api.js';
|
|
3
3
|
|
|
4
4
|
export async function statusCommand(config, args, chalk) {
|
|
5
5
|
const isAdmin = args.includes('--admin');
|
|
6
6
|
let deployments = [];
|
|
7
7
|
|
|
8
8
|
if (isAdmin) {
|
|
9
|
-
// Admin-only
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// Admin-only: every user's active deployments, not just the caller's
|
|
10
|
+
// own. Gated server-side by an internal admin allowlist -- a
|
|
11
|
+
// non-admin account gets a plain 403 here, same as any other
|
|
12
|
+
// access-denied call. See ../admin.js for why this is a separate,
|
|
13
|
+
// never-publicly-exported module.
|
|
13
14
|
if (!config.apiKey) {
|
|
14
15
|
console.log(chalk.red('\n Sign in required: badgr login\n'));
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
18
|
+
let listAllActiveDeploymentsAdmin;
|
|
19
|
+
try {
|
|
20
|
+
({ listAllActiveDeploymentsAdmin } = await import('../admin.js')); // oss-safe: guarded by try/catch, no-ops when admin.js is absent from a public export
|
|
21
|
+
} catch {
|
|
22
|
+
console.log(chalk.red('\n Admin mode is not available in this build.\n'));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
17
25
|
try {
|
|
18
26
|
const data = await listAllActiveDeploymentsAdmin(config);
|
|
19
27
|
deployments = data?.deployments ?? [];
|
package/src/commands/train.js
CHANGED
|
@@ -116,7 +116,7 @@ export function parseTrainLoraArgs(args) {
|
|
|
116
116
|
return flags;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
// Mirror of backend LORA_PRESETS
|
|
119
|
+
// Mirror of the backend's own LORA_PRESETS — display only, server is authoritative.
|
|
120
120
|
export const LORA_PRESET_INFO = {
|
|
121
121
|
small: { gpu_type: 'RTX_4090', rank: 16, epochs: 3, description: 'Fast, low-cost — good default for most datasets' },
|
|
122
122
|
medium: { gpu_type: 'A100', rank: 32, epochs: 5, description: 'Larger rank/more epochs — bigger datasets or higher quality' },
|
package/src/config.js
CHANGED
|
@@ -29,25 +29,6 @@ export function normalizeBaseUrl(url) {
|
|
|
29
29
|
return trimmed.endsWith('/v1') ? trimmed : `${trimmed}/v1`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const DEFAULT_WEB_URL = 'https://aibadgr.com';
|
|
33
|
-
const LOCAL_WEB_URL = 'http://localhost:3000';
|
|
34
|
-
|
|
35
|
-
// The frontend origin to link out to (Run page, evidence/repro page, etc).
|
|
36
|
-
// start-local.sh points config.baseUrl at http://localhost:8000/v1 so the
|
|
37
|
-
// CLI's API calls hit the local backend; when it does, links the CLI prints
|
|
38
|
-
// should point at the local frontend (localhost:3000) too, not production.
|
|
39
|
-
export function webBaseUrl(config) {
|
|
40
|
-
const envWeb = process.env.BADGR_WEB_URL?.trim().replace(/\/+$/, '');
|
|
41
|
-
if (envWeb) return envWeb;
|
|
42
|
-
try {
|
|
43
|
-
const { hostname } = new URL(config?.baseUrl || DEFAULTS.baseUrl);
|
|
44
|
-
if (hostname === 'localhost' || hostname === '127.0.0.1') return LOCAL_WEB_URL;
|
|
45
|
-
} catch {
|
|
46
|
-
// fall through to the default below
|
|
47
|
-
}
|
|
48
|
-
return DEFAULT_WEB_URL;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
32
|
function applyEnvOverrides(config) {
|
|
52
33
|
const envBase = process.env.BADGR_API_URL?.trim();
|
|
53
34
|
if (envBase) config.baseUrl = normalizeBaseUrl(envBase);
|
|
@@ -91,6 +72,18 @@ export function saveConfig(updates, configFile = CONFIG_FILE) {
|
|
|
91
72
|
return merged;
|
|
92
73
|
}
|
|
93
74
|
|
|
75
|
+
/**
|
|
76
|
+
* The Badgr dashboard's web origin. A localhost API base (local dev
|
|
77
|
+
* backend) maps to the local frontend dev server; every other API base
|
|
78
|
+
* (including a self-hosted/custom one) still points at the production
|
|
79
|
+
* dashboard, since that's the only place the dashboard is hosted today.
|
|
80
|
+
*/
|
|
81
|
+
export function webBaseUrl(config) {
|
|
82
|
+
const base = normalizeBaseUrl(config?.baseUrl);
|
|
83
|
+
if (/^https?:\/\/localhost(?::\d+)?\//.test(base)) return 'http://localhost:3000';
|
|
84
|
+
return DEFAULTS.baseUrl.replace(/\/v1$/, '');
|
|
85
|
+
}
|
|
86
|
+
|
|
94
87
|
export function requireApiKey(config) {
|
|
95
88
|
if (!config.apiKey) throw new Error('No API key configured. Run: badgr login');
|
|
96
89
|
return config.apiKey;
|