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.
@@ -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
+ }
@@ -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 workload_profile.py — kept in sync for pre-flight display.
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
- // images/badgr-job-runner/entrypoint.py). flags.cmd stays the
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 deployment_service.py's
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 = [];
@@ -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/workload_profile.py's own `_extract_params_b` --
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 workload_profile.py infer_profile_from_model — for pre-flight display.
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 workload_profile.py's real preferred_gpus for each profile
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 overflow_providers.py's GPU catalog), so it never matched anything
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 workload_profile.py infer_profile_from_gguf.
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 (see backend/service_proxy.py) --
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
- // model_capabilities.py): a live call against this exact endpoint is
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 ===
@@ -1,19 +1,27 @@
1
1
  import { listDeployments as localDeployments } from '../store.js';
2
- import { listDeployments as apiDeployments, listAllActiveDeploymentsAdmin } from '../api.js';
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 (michaelhireitem@gmail.com): every user's active
10
- // deployments, not just the caller's own -- see
11
- // deployment_routes.py's GET /v1/admin/deployments/active. A non-admin
12
- // account gets a plain 403 here, same as any other access-denied call.
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 ?? [];
@@ -116,7 +116,7 @@ export function parseTrainLoraArgs(args) {
116
116
  return flags;
117
117
  }
118
118
 
119
- // Mirror of backend LORA_PRESETS (jobs_routes.py) — display only, server is authoritative.
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;
@@ -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 deployment_service.py's 180s routing-search
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
- // (reliability_engine.py / deployment_service.py's legacy provisioning
149
- // loop) already tries every viable provider/offer for a request within
150
- // the ONE deployment it creates before ever returning a failure -- a
151
- // PROVISIONING_FAILED/PROVIDER_ADAPTER_ERROR response means that whole
152
- // internal search already ran and a real, billable resource was very
153
- // likely created and destroyed along the way (see deployment_service.
154
- // _reliability_result_failure_reason and the legacy loop's own
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