badgr-cli 1.1.6 → 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 +42 -8
- 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);
|
|
@@ -236,7 +260,7 @@ async function validateComfyNodes(baseUrl, nodeList, chalk) {
|
|
|
236
260
|
|
|
237
261
|
// Known badgr serve flags — used to detect broken shell line continuation.
|
|
238
262
|
const _KNOWN_SERVE_FLAGS = new Set([
|
|
239
|
-
'--gpu', '--image', '--task', '--count', '--region', '--tier', '--max-price',
|
|
263
|
+
'--gpu', '--target', '--image', '--task', '--count', '--region', '--tier', '--max-price',
|
|
240
264
|
'--name', '--no-wait', '--max-cost', '--idle-timeout', '--health-path', '--check-nodes',
|
|
241
265
|
'--no-fallback', '--strict-capacity', '--no-expanded-search', '--env',
|
|
242
266
|
'--persistent', '--yes', '-y', '--runtime', '--hf-repo', '--hf-file', '--dry-run',
|
|
@@ -541,6 +565,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
541
565
|
if (flags.gpu) headerLines.push(['GPU', gpuLabel]);
|
|
542
566
|
if (flags.task) headerLines.push(['Task', flags.task]);
|
|
543
567
|
if (flags.env?.length) headerLines.push(['Env', flags.env.join(', ')]);
|
|
568
|
+
if (flags.vllmArgs?.length) headerLines.push(['vLLM args', flags.vllmArgs.join(' ')]);
|
|
544
569
|
if (flags.dryRun) {
|
|
545
570
|
headerLines.push(['Tier', formatTierLabel(effectiveTier)]);
|
|
546
571
|
const previewHealthPath = _resolveHealthPath({ healthPath: flags.healthPath, isLlamaCpp, customImage, task: flags.task });
|
|
@@ -617,6 +642,13 @@ export async function serveCommand(config, args, chalk) {
|
|
|
617
642
|
|
|
618
643
|
const effectiveRegion = flags.region ? flags.region.toUpperCase() : undefined;
|
|
619
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
|
+
|
|
620
652
|
function buildBody(gpuOverride, tierOverride) {
|
|
621
653
|
const effectiveEnv = isLlamaCpp
|
|
622
654
|
? { LLAMA_ARG_HF_REPO: flags.hfRepo, LLAMA_ARG_HF_FILE: flags.hfFile, ...envObj }
|
|
@@ -636,6 +668,8 @@ export async function serveCommand(config, args, chalk) {
|
|
|
636
668
|
...(flags.maxCost ? { max_cost_usd: flags.maxCost } : {}),
|
|
637
669
|
...(flags.idleTimeout ? { idle_timeout_minutes: flags.idleTimeout } : {}),
|
|
638
670
|
...(flags.healthPath ? { health_path: flags.healthPath } : {}),
|
|
671
|
+
...(flags.vllmArgs?.length ? { extra_args: flags.vllmArgs } : {}),
|
|
672
|
+
...(targetNodeId ? { target: { type: 'customer_node', node_id: targetNodeId } } : {}),
|
|
639
673
|
};
|
|
640
674
|
}
|
|
641
675
|
|
|
@@ -831,7 +865,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
831
865
|
}
|
|
832
866
|
|
|
833
867
|
// dep.service_url is the Badgr-owned https://<id>.serve.aibadgr.com URL
|
|
834
|
-
// once that proxy is actually live (
|
|
868
|
+
// once that proxy is actually live (server-side) --
|
|
835
869
|
// until then it's absent/equal to endpointUrl, so this falls back to the
|
|
836
870
|
// raw provider URL exactly as before. Only the customer-facing display
|
|
837
871
|
// and usage examples switch to it; polling/comfy-node validation above
|
|
@@ -878,7 +912,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
878
912
|
console.log(chalk.dim(' Shown once — copy it now. This key is scoped to this endpoint only.'));
|
|
879
913
|
}
|
|
880
914
|
// dep.supports_chat_completions is probed server-side (see backend
|
|
881
|
-
//
|
|
915
|
+
// the backend's own model-capabilities lookup): a live call against this exact endpoint is
|
|
882
916
|
// authoritative when it's reachable (dep.capability_source ===
|
|
883
917
|
// "runtime"); otherwise it falls back to a best-effort hint from the
|
|
884
918
|
// model's public HF chat-template config (capability_source ===
|
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;
|
package/src/credentials.js
CHANGED
|
@@ -8,6 +8,12 @@ export const CREDENTIALS_FILE = join(CONFIG_DIR, 'credentials.json');
|
|
|
8
8
|
export const PROVIDER_ENV_KEYS = {
|
|
9
9
|
anthropic: 'ANTHROPIC_API_KEY',
|
|
10
10
|
openai: 'OPENAI_API_KEY',
|
|
11
|
+
// Command Code is a single hosted account (bundled model catalog), not an
|
|
12
|
+
// OpenAI-compatible swap target — verified live: `COMMAND_CODE_API_KEY`
|
|
13
|
+
// read directly from env, no login step (see
|
|
14
|
+
// images/badgr-agent-commandcode/badgr-commandcode-run). Fixed credential
|
|
15
|
+
// like anthropic/openai, not part of the BYOK lane below.
|
|
16
|
+
commandcode: 'COMMAND_CODE_API_KEY',
|
|
11
17
|
// OpenAI-compatible BYOK lanes for `badgr launch cline --provider <name>`
|
|
12
18
|
// (see MODEL_PROVIDERS below) — the cline agent image only ever reads
|
|
13
19
|
// OPENAI_API_KEY/OPENAI_BASE_URL/MODEL (images/badgr-agent-cline/
|
package/src/fallback.js
CHANGED
|
@@ -49,7 +49,7 @@ export async function callWithFallback(endpoint, callOpts, buildBody, effectiveT
|
|
|
49
49
|
const cmd = labels?.cmd ?? 'badgr run';
|
|
50
50
|
|
|
51
51
|
// 220s: comfortably above backend's BADGR_PROVISION_TIMEOUT_SECONDS (default
|
|
52
|
-
// 200s, itself set above
|
|
52
|
+
// 200s, itself set above the backend's own 180s routing-search
|
|
53
53
|
// deadline). Found live: the previous 130s value fired before the server's
|
|
54
54
|
// own (then-120s) deadline could, so the CLI reported "failed to submit"
|
|
55
55
|
// for jobs that had actually been created and were already billing —
|
|
@@ -144,16 +144,14 @@ export async function callWithFallback(endpoint, callOpts, buildBody, effectiveT
|
|
|
144
144
|
|
|
145
145
|
const d = firstErr.errorData;
|
|
146
146
|
|
|
147
|
-
// No client-side "provider retry" here on purpose. The backend
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
// smoke-check-failure path, both of which only report these codes once
|
|
156
|
-
// a resource actually existed). A second /run or /serve call here would
|
|
147
|
+
// No client-side "provider retry" here on purpose. The backend's own
|
|
148
|
+
// provisioning logic already tries every viable provider/offer for a
|
|
149
|
+
// request within the ONE deployment it creates before ever returning a
|
|
150
|
+
// failure -- a PROVISIONING_FAILED/PROVIDER_ADAPTER_ERROR response means
|
|
151
|
+
// that whole internal search already ran and a real, billable resource
|
|
152
|
+
// was very likely created and destroyed along the way (the backend only
|
|
153
|
+
// reports these codes once a resource actually existed). A second /run
|
|
154
|
+
// or /serve call here would
|
|
157
155
|
// be an entirely new deployment repeating that same internal search from
|
|
158
156
|
// scratch -- duplicate billable exposure for a workload that was never a
|
|
159
157
|
// capacity problem in the first place. This CLI previously did retry
|