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 CHANGED
@@ -167,15 +167,17 @@ secrets, and runs free static checks as part of a Badgr Smoke Test.
167
167
  Nothing runs on a GPU without explicit `--approve`.
168
168
 
169
169
  ```bash
170
- # badgr diagnose IS the Badgr Smoke Test — free, no login, no GPU.
171
- # Applicable free structural checks run automatically, every time.
170
+ # Free Badgr Smoke Test — no login required
172
171
  badgr diagnose "https://github.com/org/repo/issues/123"
173
172
  badgr diagnose ajayrajtp/vllm_gemma412b:latest
174
173
  badgr diagnose ./vllm-error.log
175
174
  badgr diagnose "https://github.com/org/repo"
176
175
  badgr diagnose workflow.json
177
176
 
178
- # Start a real Badgr Run (paid GPU execution) after diagnosis (login required)
177
+ # Free mechanical validation of the produced command still no GPU, no login
178
+ badgr diagnose "https://github.com/org/repo/issues/123" --smoke
179
+
180
+ # Approve a capped smoke test after diagnosis (login required)
179
181
  badgr diagnose "https://github.com/org/repo/issues/123" --approve
180
182
 
181
183
  # Resume an existing case, e.g. one shared via a case link
@@ -188,8 +190,8 @@ badgr diagnose "https://github.com/org/repo/issues/123" --json
188
190
 
189
191
  | Flag | Description |
190
192
  |------|-------------|
191
- | `--approve` | Start a real Badgr Run after diagnosis actual GPU execution, capped at a hard maximum spend (opens browser sign-in automatically if not logged in) |
192
- | `--smoke` | Deprecated, no-op. The free mechanical checks this used to gate (syntax, CLI-entrypoint corroboration, referenced-file presence, Docker `ENTRYPOINT`/`CMD` consistency, required env vars, plus a client-side local-file check) now always run as part of plain `badgr diagnose`. Kept only so existing scripts that pass it don't break. Not the same as `badgr run --smoke`, which launches a real, billable GPU job |
193
+ | `--smoke` | Free. Runs real, cheap mechanical validation of the produced command (syntax, CLI-entrypoint corroboration, referenced-file presence, Docker `ENTRYPOINT`/`CMD` consistency, required env vars, plus a client-side check of any local file path you pasted). Never starts a GPU, never requires login. Combine with `--approve` — the smoke checks print first, then the normal approve flow runs. Not the same as `badgr run --smoke`, which launches a real, billable GPU job |
194
+ | `--approve` | Approve the capped smoke test after diagnosis (opens browser sign-in automatically if not logged in) |
193
195
  | `--docker <image>` | Force Docker-image intake (override auto-detect) |
194
196
  | `--repo <url>` | Force repository intake (override auto-detect) |
195
197
  | `--comfyui <path>` | Force ComfyUI workflow intake (override auto-detect) |
@@ -197,13 +199,13 @@ badgr diagnose "https://github.com/org/repo/issues/123" --json
197
199
  | `--json` | Machine-readable JSON output |
198
200
 
199
201
  Every run prints one status: `NEEDS INFO` (fields still missing) →
200
- `READY` (complete evidence-backed command, no applicable checks to run) →
201
- `SMOKE CHECKED` (applicable checks ran, all passed or were skipped) /
202
- `INVALID` (an applicable check actually failed) → `VERIFIED` (reserved for
203
- an actual successful Badgr Run — never assigned from diagnosis alone).
204
- `READY`/`SMOKE CHECKED` results also print the canonical `badgr
205
- run`/`badgr serve` command `--approve` would run, plus a shareable case
206
- link — pass that case ID or URL back into `badgr diagnose
202
+ `READY` (complete evidence-backed command, no smoke run) → `SMOKE CHECKED`
203
+ (`--smoke` ran, every applicable check passed or was skipped) / `INVALID`
204
+ (`--smoke` ran and a check actually failed) → `VERIFIED` (reserved for an
205
+ actual successful GPU-provisioned run — never assigned from static
206
+ resolution or `--smoke`). `READY`/`SMOKE CHECKED` results also print the
207
+ canonical `badgr run`/`badgr serve` command `--approve` would run, plus a
208
+ shareable case link — pass that case ID or URL back into `badgr diagnose
207
209
  <case_id_or_url> --approve` to resume the same case without re-diagnosing.
208
210
 
209
211
  `badgr run-issue` is an alias for `badgr diagnose`, matching the
@@ -217,6 +219,7 @@ web flow.
217
219
  ```text
218
220
  login
219
221
  connect
222
+ node
220
223
  doctor
221
224
  diagnose
222
225
  run
@@ -238,7 +241,8 @@ test
238
241
  | Command | What it does |
239
242
  |---------|-------------|
240
243
  | `badgr login` | Save API key to `~/.badgr/config.json` |
241
- | `badgr connect <provider>` | Store a provider credential (`anthropic`, `openai`) for `badgr launch` |
244
+ | `badgr connect <provider>` | Store a provider credential (`anthropic`, `openai`, `openrouter`, `deepseek`, `glm`, `custom`) for `badgr launch` |
245
+ | `badgr node connect\|list\|inspect\|disable\|remove` | BYO GPU — connect your own Linux/NVIDIA or Linux/AMD-ROCm machine and run `run`/`serve` on it (see [BYO GPU](#byo-gpu-badgr-node)) |
242
246
  | `badgr doctor` | Diagnose a GPU workload failure — read-only, no login needed |
243
247
  | `badgr diagnose "<input>"` | Run a Badgr Smoke Test on a GitHub issue, Docker image, repo, log, or workflow — free, no GPU until `--approve` |
244
248
  | `badgr run <command>` | Run a one-off GPU job (any container command) |
@@ -271,6 +275,7 @@ These appear on most commands (`run`, `serve`, `comfyui`, `train`, `transcribe`,
271
275
  | Flag | Default | Description |
272
276
  |------|---------|-------------|
273
277
  | `--gpu <type>` | auto | GPU type override — see [GPU options](#gpu-options) |
278
+ | `--target <node>` | — | Run on one of your own connected machines instead of Badgr's cloud capacity — a node name or `node_…` id from `badgr node list`. See [BYO GPU](#byo-gpu-badgr-node) |
274
279
  | `--tier 1\|2` | 1 | `1` = reliable managed routing (default); `2` = lower-cost marketplace routing |
275
280
  | `--region US\|EU\|AU` | — | Region preference. If omitted, Badgr chooses best available capacity |
276
281
  | `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
@@ -521,6 +526,37 @@ Additional GPU types may be routable depending on current capacity — check wit
521
526
 
522
527
  ---
523
528
 
529
+ ## BYO GPU: `badgr node`
530
+
531
+ Connect a machine you already own (Linux + NVIDIA or AMD/ROCm, Docker required) and run `badgr run`/`badgr serve` workloads on it instead of Badgr's cloud capacity — no separate execution path, just a `--target` on the commands you already use.
532
+
533
+ ```bash
534
+ badgr node connect # register this machine and start the badgr-node worker
535
+ badgr node connect --name gpu-box-1 # optional human-readable name
536
+
537
+ badgr node list # your connected nodes, online/offline
538
+ badgr node inspect <node-id> # full JSON detail for one node
539
+ badgr node disable <node-id> # stop it from receiving new jobs (keeps the row)
540
+ badgr node remove <node-id> # disconnect and forget it
541
+
542
+ badgr run "python train.py" --target gpu-box-1 # run on that node instead of the cloud
543
+ badgr serve meta-llama/Llama-3.1-8B-Instruct --target gpu-box-1
544
+ ```
545
+
546
+ `badgr node connect` detects your GPU(s) via `nvidia-smi` (falling back to `rocm-smi` for AMD/ROCm) and Docker availability, registers the node (`POST /v1/nodes`), and — if a GPU, Docker, and `python3` are all present — spawns the `badgr-node` worker daemon in the background so the node starts accepting jobs immediately. If anything's missing, it prints the exact command to start the worker manually once fixed. The worker itself re-probes vendor locally (`nvidia-smi`/`rocm-smi`) at startup and picks the matching `docker run` GPU flags — `--gpus all` on NVIDIA, or `--device=/dev/kfd --device=/dev/dri --group-add video --group-add render --security-opt seccomp=unconfined` on AMD.
547
+
548
+ A connected node is **private by default** — it is never marketplace-listed or shared; only you can target it, and only with `--target`. Marketplace listing, payout, and reputation scoring are not built yet (explicit follow-up phases).
549
+
550
+ | Command | What it does |
551
+ |---------|-------------|
552
+ | `badgr node connect [--name <name>]` | Register this machine and start its worker |
553
+ | `badgr node list` | List your connected nodes |
554
+ | `badgr node inspect <node-id>` | Show one node's full detail as JSON |
555
+ | `badgr node disable <node-id>` | Stop a node from receiving new jobs |
556
+ | `badgr node remove <node-id>` | Disconnect and forget a node |
557
+
558
+ ---
559
+
524
560
  ## Routing
525
561
 
526
562
  `--tier 1` (default) uses managed provider routing — reliable, consistent performance. `--tier 2` uses marketplace routing for lower-cost options. Most users should stick with the default.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badgr-cli",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "Badgr — run or serve GPU workloads from one command",
5
5
  "type": "module",
6
6
  "bin": {
package/src/admin.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Internal admin-only operations -- never part of the public CLI export.
3
+ * Backs `badgr status --admin` / `badgr down <id> --admin`, gated
4
+ * server-side by an internal admin allowlist. Kept in its own module,
5
+ * separate from api.js, so it can be excluded by name from
6
+ * OSS_EXPORTS.md / scripts/publish-oss.sh's forbidden-import scan without
7
+ * pulling the rest of the (public-safe) HTTP client down with it.
8
+ */
9
+ import { callApi } from './api.js';
10
+
11
+ // Every non-terminal deployment across every user (not just the caller's
12
+ // own), each carrying its owner's user_email/username so a stuck or
13
+ // forgotten GPU can be identified without logging into that customer's
14
+ // account. Backs `badgr status --admin`.
15
+ export function listAllActiveDeploymentsAdmin(config) {
16
+ return callApi('/admin/deployments/active', { apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 15_000 });
17
+ }
18
+
19
+ // Force-terminate any deployment regardless of owner -- bypasses the
20
+ // per-user ownership check DELETE /deployments/{id} enforces. Backs
21
+ // `badgr down <id> --admin`.
22
+ export function adminTerminateDeployment(config, deploymentId) {
23
+ return callApi(`/admin/deployments/${deploymentId}`, {
24
+ method: 'DELETE',
25
+ apiKey: config.apiKey,
26
+ baseUrl: config.baseUrl,
27
+ timeoutMs: 20_000,
28
+ });
29
+ }
package/src/api.js CHANGED
@@ -140,7 +140,7 @@ export function runJob(config, body) {
140
140
  }
141
141
 
142
142
  // Preview the VM class + Badgr rate for a CPU launch before provisioning
143
- // anything (see backend run_serve_routes.py's POST /v1/run/quote). Callers
143
+ // anything (see the backend's own POST /v1/run/quote). Callers
144
144
  // should treat a failure here as non-fatal — this is a best-effort preview,
145
145
  // never a precondition for launching.
146
146
  export function quoteRun(config, body) {
@@ -154,7 +154,7 @@ export function quoteRun(config, body) {
154
154
  }
155
155
 
156
156
  // Live, no-provisioning price/capacity check for a GPU dry-run preview (see
157
- // backend run_serve_routes.py's GET /v1/capacity/suggestions). Used by every
157
+ // the backend's own GET /v1/capacity/suggestions). Used by every
158
158
  // GPU-provisioning command's --dry-run to show a real observed rate instead
159
159
  // of just the requested config — callers must treat a failure here as
160
160
  // non-fatal (best-effort) and never let it block or delay the dry-run.
@@ -179,6 +179,37 @@ export function serveModel(config, body) {
179
179
  });
180
180
  }
181
181
 
182
+ // ---- BYO GPU nodes (POST/GET/DELETE /v1/nodes/*) ----------------------------
183
+ // Server-side handling. A connected node is always private until its
184
+ // owner explicitly changes that (out of scope for this CLI today).
185
+
186
+ export function connectNode(config, { name, hardware } = {}) {
187
+ return callApi('/nodes/connect', {
188
+ method: 'POST', apiKey: config.apiKey, baseUrl: config.baseUrl,
189
+ body: { name, hardware },
190
+ });
191
+ }
192
+
193
+ export function listNodes(config) {
194
+ return callApi('/nodes', { apiKey: config.apiKey, baseUrl: config.baseUrl });
195
+ }
196
+
197
+ export function inspectNode(config, nodeId) {
198
+ return callApi(`/nodes/${encodeURIComponent(nodeId)}`, { apiKey: config.apiKey, baseUrl: config.baseUrl });
199
+ }
200
+
201
+ export function disableNode(config, nodeId) {
202
+ return callApi(`/nodes/${encodeURIComponent(nodeId)}/disable`, {
203
+ method: 'POST', apiKey: config.apiKey, baseUrl: config.baseUrl,
204
+ });
205
+ }
206
+
207
+ export function removeNode(config, nodeId) {
208
+ return callApi(`/nodes/${encodeURIComponent(nodeId)}`, {
209
+ method: 'DELETE', apiKey: config.apiKey, baseUrl: config.baseUrl,
210
+ });
211
+ }
212
+
182
213
  // ---- Deployments (POST/GET/DELETE /v1/deployments) --------------------------
183
214
 
184
215
  export function createDeployment(config, spec) {
@@ -228,33 +259,8 @@ export async function terminateDeployment(config, deploymentId) {
228
259
  throw lastErr;
229
260
  }
230
261
 
231
- // ---- Admin (michaelhireitem@gmail.com only) ---------------------------------
232
- // CLI counterparts to the web admin dashboard's own /admin/api/deployments/*
233
- // routes (main.py) -- those are gated by a browser session cookie the CLI's
234
- // Authorization: Bearer <api_key> auth can never satisfy, so
235
- // deployment_routes.py exposes these two under /v1/admin/deployments/*
236
- // instead, gated by the same is_admin_email() check via API key. A non-admin
237
- // caller gets a plain 403 from callApi, same as any other access-denied route.
238
-
239
- // Every non-terminal deployment across every user (not just the caller's
240
- // own), each carrying its owner's user_email/username so a stuck or
241
- // forgotten GPU can be identified without logging into that customer's
242
- // account. Backs `badgr status --admin`.
243
- export function listAllActiveDeploymentsAdmin(config) {
244
- return callApi('/admin/deployments/active', { apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 15_000 });
245
- }
246
-
247
- // Force-terminate any deployment regardless of owner -- bypasses the
248
- // per-user ownership check DELETE /deployments/{id} enforces. Backs
249
- // `badgr down <id> --admin`.
250
- export function adminTerminateDeployment(config, deploymentId) {
251
- return callApi(`/admin/deployments/${deploymentId}`, {
252
- method: 'DELETE',
253
- apiKey: config.apiKey,
254
- baseUrl: config.baseUrl,
255
- timeoutMs: 20_000,
256
- });
257
- }
262
+ // Admin-only operations (badgr status --admin / badgr down --admin) live in
263
+ // ./admin.js, not here -- see that file's header comment for why.
258
264
 
259
265
  export function restartDeployment(config, deploymentId) {
260
266
  return callApi(`/deployments/${deploymentId}/restart`, {
package/src/badgr.js CHANGED
@@ -14,6 +14,7 @@ import { artifactsCommand } from './commands/artifacts.js';
14
14
  import { serveCommand } from './commands/serve.js';
15
15
  import { modelsCommand } from './commands/models.js';
16
16
  import { capacityCommand } from './commands/capacity.js';
17
+ import { nodeCommand } from './commands/node.js';
17
18
  import { testCommand } from './commands/test-run.js';
18
19
  import { billingCommand } from './commands/billing.js';
19
20
  import { comfyuiCommand } from './commands/comfyui.js';
@@ -31,6 +32,7 @@ import { rerunCommand } from './commands/rerun.js';
31
32
  import { heartbeatCommand } from './commands/heartbeat.js';
32
33
  import { pullCommand } from './commands/pull.js';
33
34
  import { doctorCommand } from './commands/doctor.js';
35
+ import { checkCommand } from './commands/check.js';
34
36
  import { diagnoseCommand } from './commands/diagnose.js';
35
37
  import { connectCommand } from './commands/connect.js';
36
38
 
@@ -40,14 +42,15 @@ ${chalk.bold('badgr')} — run or serve GPU workloads from one command
40
42
  ${chalk.bold('COMMANDS')}
41
43
  ${chalk.cyan('badgr login')} Open a browser to log in (or: badgr login --key <value>)
42
44
  ${chalk.cyan('badgr connect <provider>')} Credential setup (badgr connect anthropic/openai) — also prompted inline by badgr launch if missing
45
+ ${chalk.cyan('badgr check [path]')} Validate a workload (project or workload.yaml) and check it against the local GPU — read-only, no login needed
43
46
  ${chalk.cyan('badgr detect <path>')} Inspect a project and report the GPU job Badgr would run
44
47
  ${chalk.cyan('badgr run <command>')} Run a one-off GPU job
45
- ${chalk.cyan('badgr launch cline|claude|codex|playwright "<task>"')} Run a coding/testing workload on a CPU VM — image + command auto-selected
48
+ ${chalk.cyan('badgr launch cline|claude|codex|opencode|commandcode|playwright "<task>"')} Run a coding/testing workload on a CPU VM — image + command auto-selected
46
49
  ${chalk.cyan('badgr launch <source> -- <command>')} Advanced escape hatch: run any other command on a CPU VM
47
50
  ${chalk.cyan('badgr job <agent> "<instruction>" --check "<command>"')} Bounded coding-agent job, tracked at /jobs (POST /v1/jobs, type: agent)
48
51
  ${chalk.cyan('badgr serve <model>')} Serve a model with an OpenAI-compatible endpoint
49
52
  ${chalk.cyan('badgr serve openwebui')} Serve Open WebUI — chat UI, connects to a model endpoint
50
- ${chalk.cyan('badgr diagnose "<input>"')} Run a free Badgr Smoke Test on any GPU issue — GitHub issue, Docker image, log, repo, or text
53
+ ${chalk.cyan('badgr diagnose "<input>"')} Run free Badgr Preflight on any GPU issue — GitHub issue, Docker image, log, repo, or text
51
54
  ${chalk.cyan('badgr doctor')} Local GPU / model-fit diagnosis (read-only, no login needed)
52
55
  ${chalk.cyan('badgr status')} Show what's running and what's billing
53
56
  ${chalk.cyan('badgr logs <id>')} Stream logs for a running job or endpoint
@@ -95,6 +98,8 @@ ${chalk.bold('EXAMPLES')}
95
98
  badgr launch cline "Fix the checkout bug"
96
99
  badgr launch claude "Fix the checkout bug"
97
100
  badgr launch codex "Write tests"
101
+ badgr launch opencode "Write tests"
102
+ badgr launch commandcode "Write tests"
98
103
  badgr launch playwright "Test the checkout flow"
99
104
  badgr launch claude --size medium "Run the complete test suite"
100
105
  badgr pull <id>
@@ -102,7 +107,7 @@ ${chalk.bold('EXAMPLES')}
102
107
  badgr logs <id>
103
108
  badgr receipts <id>
104
109
 
105
- ${chalk.dim('# Explicit form — advanced escape hatch for anything not in the four workloads above.')}
110
+ ${chalk.dim('# Explicit form — advanced escape hatch for anything not in the five workloads above.')}
106
111
  ${chalk.dim('# Badgr flags go BEFORE --; everything after -- is passed to your command unchanged.')}
107
112
  ${chalk.dim('# The default runner image is Python-only (no Node.js/npm) — a Node-based command')}
108
113
  ${chalk.dim('# must install what it needs, or use --image with a custom image (see images/badgr-agent-*):')}
@@ -113,13 +118,13 @@ ${chalk.bold('EXAMPLES')}
113
118
  ${chalk.dim('# badgr job — bounded coding-agent job with a pass/fail check, tracked at /jobs:')}
114
119
  badgr job cline "Run the Chromium tests and tell me what failed" --check "npm run test:chromium" --max-cost 1
115
120
 
116
- ${chalk.dim('# Run a free Badgr Smoke Test on any GPU issue (no login needed):')}
121
+ ${chalk.dim('# Run free Badgr Preflight on any GPU issue (no login needed):')}
117
122
  badgr diagnose "https://github.com/org/repo/issues/123"
118
123
  badgr diagnose ./error.log
119
124
  badgr diagnose --docker ajayrajtp/vllm_gemma412b:latest
120
125
  badgr diagnose --repo https://github.com/org/repo
121
126
  badgr diagnose --comfyui workflow.json
122
- badgr diagnose "https://github.com/org/repo/issues/123" --approve ${chalk.dim('# start a real Badgr Run')}
127
+ badgr diagnose "https://github.com/org/repo/issues/123" --approve ${chalk.dim('# approve a capped smoke test')}
123
128
 
124
129
  ${chalk.dim('# Local model-fit and workload diagnosis (no network):')}
125
130
  badgr doctor
@@ -237,7 +242,7 @@ ${chalk.bold('badgr launch OPTIONS')}
237
242
  --max-runtime <min> Auto-stop after N minutes (default: 60)
238
243
  --region US|EU|AU Region preference
239
244
  --size small|medium|browser VM class override. Defaults per workload: small for cline/claude/
240
- codex/explicit form, browser for playwright (Chromium preinstalled).
245
+ codex/opencode/commandcode/explicit form, browser for playwright (Chromium preinstalled).
241
246
 
242
247
  ${chalk.bold('badgr artifacts OPTIONS')}
243
248
  --output <dir> Directory to extract into (default: ~/.badgr/artifacts/<id>)
@@ -257,6 +262,11 @@ ${chalk.bold('badgr serve OPTIONS')}
257
262
  --hf-file <file> GGUF filename within that repo, e.g. model.gguf
258
263
  --health-path <path> Readiness path to poll (auto-detected: comfyui → /system_stats, llama.cpp → /health)
259
264
  --no-wait Skip endpoint health check
265
+ --vllm-arg <arg> Extra vLLM CLI arg, passed through verbatim (repeatable). e.g.
266
+ --vllm-arg --quantization=nvfp4 --vllm-arg --max-model-len=32768
267
+ -- <args...> Same as repeated --vllm-arg — everything after a bare -- is passed
268
+ through verbatim, e.g. badgr serve MODEL -- --quantization nvfp4
269
+ Only applies to the vLLM launch path (ignored for --runtime llama.cpp).
260
270
 
261
271
  ${chalk.bold('AFTER SERVING')}
262
272
  ${chalk.dim('Point any OpenAI client at the returned URL:')}
@@ -281,6 +291,7 @@ async function main() {
281
291
  switch (cmd) {
282
292
  case 'login': return loginCommand(chalk, saveConfig, rest);
283
293
  case 'connect': return connectCommand(rest, chalk);
294
+ case 'check': return checkCommand(config, rest, chalk);
284
295
  case 'detect': return detectCommand(config, rest, chalk);
285
296
  case 'run': return runCommand(config, rest, chalk);
286
297
  case 'launch': return launchCommand(config, rest, chalk);
@@ -300,6 +311,7 @@ async function main() {
300
311
  case 'receipts': return receiptsCommand(config, rest, chalk);
301
312
  case 'models': return modelsCommand(config, chalk);
302
313
  case 'capacity': return capacityCommand(config, rest, chalk);
314
+ case 'node': return nodeCommand(config, rest, chalk);
303
315
  case 'test': return testCommand(config, rest, chalk);
304
316
  case 'billing': return billingCommand(config, rest, chalk);
305
317
  case 'comfyui': return comfyuiCommand(config, rest, chalk);
package/src/catalog.js CHANGED
@@ -545,6 +545,9 @@ export function buildTemplateFlags(template, overrides) {
545
545
  if (overrides.name) args.push('--name', overrides.name);
546
546
  if (overrides.noWait) args.push('--no-wait');
547
547
  if (overrides.persistent) args.push('--persistent');
548
+ if (overrides.vllmArgs?.length) {
549
+ for (const a of overrides.vllmArgs) args.push('--vllm-arg', a);
550
+ }
548
551
 
549
552
  if (template.type === 'endpoint' && template.health_path) {
550
553
  args.push('--health-path', template.health_path);
@@ -581,6 +584,15 @@ export function parseTemplateOverrides(args) {
581
584
  if (idx > 0) overrides.env[kv.slice(0, idx)] = kv.slice(idx + 1);
582
585
  continue;
583
586
  }
587
+ if (a === '--') {
588
+ if (!overrides.vllmArgs) overrides.vllmArgs = [];
589
+ overrides.vllmArgs.push(...args.slice(i + 1));
590
+ break;
591
+ }
592
+ if (a === '--vllm-arg') {
593
+ if (!overrides.vllmArgs) overrides.vllmArgs = [];
594
+ overrides.vllmArgs.push(args[++i]); i++; continue;
595
+ }
584
596
  i++;
585
597
  }
586
598
  return overrides;
@@ -12,11 +12,42 @@ function parseCapacityArgs(args) {
12
12
  if (args[i] === '--gpu') { flags.gpu = args[++i]; i++; continue; }
13
13
  if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
14
14
  if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
15
+ if (args[i] === '--count') { flags.count = parseInt(args[++i], 10); i++; continue; }
16
+ if (args[i] === '--min-ram') { flags.minRam = parseFloat(args[++i]); i++; continue; }
17
+ if (args[i] === '--min-vram') { flags.minVram = parseFloat(args[++i]); i++; continue; }
15
18
  i++;
16
19
  }
17
20
  return flags;
18
21
  }
19
22
 
23
+ // Renders the machine-spec line for one match, e.g.:
24
+ // 8x GPU 256GB VRAM/GPU same host 128 cores 2048GB RAM 25Gbps net
25
+ // Only fields the provider actually reported are shown — no fabricated data.
26
+ function formatSpecLine(m, chalk) {
27
+ const parts = [];
28
+ if (m.gpu_count && m.gpu_count > 1) parts.push(`${m.gpu_count}x GPU`);
29
+ if (m.gpu_vram_gb) parts.push(`${m.gpu_vram_gb}GB VRAM/GPU`);
30
+ if (m.same_host && m.gpu_count > 1) parts.push('same host');
31
+ if (m.nvlink) parts.push('NVLink');
32
+ if (m.cpu_cores) parts.push(`${m.cpu_cores} vCPU`);
33
+ if (m.system_ram_gb) parts.push(`${m.system_ram_gb}GB RAM`);
34
+ if (m.local_storage_gb) parts.push(`${m.local_storage_gb}GB storage`);
35
+ if (m.network_up_gbps || m.network_gbps) parts.push(`${m.network_up_gbps ?? m.network_gbps}Gbps net`);
36
+ if (m.rdma) parts.push('RDMA');
37
+ if (m.cuda_version) parts.push(`CUDA ${m.cuda_version}`);
38
+ if (parts.length === 0) return null;
39
+ return ` ${chalk.dim(parts.join(' · '))}`;
40
+ }
41
+
42
+ // Client-side filters over fields the API doesn't filter on itself (only
43
+ // applied to a field when the match actually reports it — an offer with
44
+ // unknown RAM is never dropped just because we don't know its RAM).
45
+ function matchesSpecFilters(m, flags) {
46
+ if (flags.minRam !== undefined && m.system_ram_gb !== undefined && m.system_ram_gb < flags.minRam) return false;
47
+ if (flags.minVram !== undefined && m.gpu_vram_gb !== undefined && m.gpu_vram_gb < flags.minVram) return false;
48
+ return true;
49
+ }
50
+
20
51
  export async function capacityCommand(config, args, chalk) {
21
52
  const flags = parseCapacityArgs(args);
22
53
  config = await ensureLoggedInReady(config, chalk);
@@ -35,6 +66,7 @@ export async function capacityCommand(config, args, chalk) {
35
66
  const params = new URLSearchParams();
36
67
  if (maxPrice !== undefined) params.set('max_price', String(maxPrice));
37
68
  if (flags.region) params.set('region', flags.region.toUpperCase());
69
+ if (flags.count) params.set('count', String(flags.count));
38
70
  data = await callApi(`/capacity/auto?${params}`, {
39
71
  apiKey: config.apiKey,
40
72
  baseUrl: config.baseUrl,
@@ -51,6 +83,8 @@ export async function capacityCommand(config, args, chalk) {
51
83
 
52
84
  console.log();
53
85
  console.log(` ${chalk.bold('Cheapest available:')} ${chalk.cyan(data.gpu)} in ${data.region} ${chalk.green('$' + data.price.toFixed(2) + '/hr')}`);
86
+ const bestSpecLine = formatSpecLine(data, chalk);
87
+ if (bestSpecLine) console.log(bestSpecLine);
54
88
  console.log();
55
89
  console.log(` ${chalk.bold('Run now:')}`);
56
90
  console.log(chalk.cyan(` badgr run python train.py`));
@@ -66,6 +100,7 @@ export async function capacityCommand(config, args, chalk) {
66
100
  const params = new URLSearchParams({ gpu });
67
101
  if (maxPrice !== undefined) params.set('max_price', String(maxPrice));
68
102
  if (flags.region) params.set('region', flags.region.toUpperCase());
103
+ if (flags.count) params.set('count', String(flags.count));
69
104
 
70
105
  console.log(chalk.bold(`\nCapacity: ${gpu}\n`));
71
106
  process.stdout.write(chalk.dim(' Checking...\n'));
@@ -81,11 +116,13 @@ export async function capacityCommand(config, args, chalk) {
81
116
  process.exit(1);
82
117
  }
83
118
 
84
- const matches = data.matches ?? [];
119
+ const matches = (data.matches ?? []).filter((m) => matchesSpecFilters(m, flags));
85
120
  if (matches.length > 0) {
86
121
  console.log(chalk.bold('\n Available now:\n'));
87
122
  for (const m of matches) {
88
123
  console.log(` ${chalk.green('●')} ${m.gpu} in ${m.region} ${chalk.green('$' + m.price.toFixed(2) + '/hr')}`);
124
+ const specLine = formatSpecLine(m, chalk);
125
+ if (specLine) console.log(specLine);
89
126
  }
90
127
  console.log();
91
128
  const regionFlag = flags.region ? ` --region ${flags.region.toUpperCase()}` : '';
@@ -0,0 +1,127 @@
1
+ import { existsSync, statSync } from 'fs';
2
+ import { detectWorkload } from '../detect.js';
3
+ import { parseWorkloadYaml, WorkloadSpecError } from '../workloadSpec.js';
4
+ import { runGpuDoctor } from '../gpuDoctor/doctor.js';
5
+ import { renderDetectReport, suggestedCommandLine } from './detect.js';
6
+
7
+ const CHECK_HELP = `
8
+ Validate a workload before you run it: identify what it is, check it's
9
+ well-formed, and check it against the local GPU/runtime — all read-only,
10
+ no login needed.
11
+
12
+ Usage:
13
+ badgr check [path]
14
+ badgr check workload.yaml
15
+ badgr check --json
16
+
17
+ Flags:
18
+ --json Machine-readable JSON output
19
+
20
+ path defaults to "." — a project directory or a batch workload.yml file.
21
+ Never mutates the machine: no restarts, no driver/CUDA changes, no installs.
22
+
23
+ Full reference: docs/gpu-doctor.md, docs/badgr-batch.md`;
24
+
25
+ function isWorkloadYaml(target) {
26
+ return /\.ya?ml$/i.test(target) && existsSync(target) && statSync(target).isFile();
27
+ }
28
+
29
+ // Routes a check result to the next Badgr command family (run/serve/job/batch),
30
+ // matching the badgr-serve/-run/-agent-jobs/-ai-api conversion paths in
31
+ // docs/OPEN_SOURCE_SPLIT.md's "badgr check" hero demo.
32
+ function nextStep(kind, detectReport) {
33
+ if (kind === 'workload-yaml') {
34
+ return 'badgr batch run <workload.yaml>';
35
+ }
36
+ if (detectReport?.workloadType === 'endpoint') {
37
+ return 'badgr serve <model>';
38
+ }
39
+ return suggestedCommandLine(detectReport || {});
40
+ }
41
+
42
+ export async function checkCommand(config, args, chalk) {
43
+ if (args.includes('--help') || args.includes('-h')) {
44
+ console.log(CHECK_HELP);
45
+ return;
46
+ }
47
+ const json = args.includes('--json');
48
+ const target = args.find(a => !a.startsWith('--')) || '.';
49
+
50
+ const result = { target, kind: null, workload: null, detect: null, gpu: null, errors: [] };
51
+
52
+ if (isWorkloadYaml(target)) {
53
+ result.kind = 'workload-yaml';
54
+ try {
55
+ result.workload = parseWorkloadYaml(target);
56
+ } catch (err) {
57
+ const message = err instanceof WorkloadSpecError ? err.message : `Could not parse ${target}: ${err.message}`;
58
+ result.errors.push(message);
59
+ }
60
+ } else {
61
+ result.kind = 'project';
62
+ result.detect = detectWorkload(target);
63
+ if (result.detect.error) result.errors.push(result.detect.error);
64
+ }
65
+
66
+ try {
67
+ result.gpu = await runGpuDoctor({}, {});
68
+ } catch (err) {
69
+ // GPU probing is best-effort here — a missing/unreadable GPU shouldn't
70
+ // block workload validation, which is the more common local check.
71
+ result.gpu = null;
72
+ }
73
+
74
+ result.nextCommand = result.errors.length === 0 ? nextStep(result.kind, result.detect) : null;
75
+
76
+ if (json) {
77
+ console.log(JSON.stringify({
78
+ target: result.target,
79
+ kind: result.kind,
80
+ valid: result.errors.length === 0,
81
+ errors: result.errors,
82
+ workload: result.workload,
83
+ detect: result.detect,
84
+ gpu_verdict: result.gpu?.verdictSlug ?? null,
85
+ next_command: result.nextCommand,
86
+ }, null, 2));
87
+ if (result.errors.length > 0) process.exitCode = 1;
88
+ return result;
89
+ }
90
+
91
+ console.log();
92
+ if (result.kind === 'workload-yaml') {
93
+ if (result.errors.length > 0) {
94
+ console.log(chalk.red(`✗ ${target} is invalid:`));
95
+ for (const e of result.errors) console.log(chalk.red(` - ${e}`));
96
+ } else {
97
+ console.log(chalk.green(`✓ ${target} is a valid workload`));
98
+ console.log(` name: ${result.workload.name}`);
99
+ console.log(` image: ${result.workload.image}`);
100
+ console.log(` command: ${result.workload.command.join(' ')}`);
101
+ console.log(` max_cost: $${result.workload.maxCost}`);
102
+ console.log(` max_runtime_minutes: ${result.workload.maxRuntimeMinutes}`);
103
+ }
104
+ } else {
105
+ if (result.errors.length > 0) {
106
+ console.log(chalk.red(`✗ ${result.errors[0]}`));
107
+ } else {
108
+ console.log(renderDetectReport(result.detect, chalk));
109
+ }
110
+ }
111
+
112
+ if (result.gpu) {
113
+ console.log();
114
+ console.log(chalk.bold('Local GPU/runtime:'));
115
+ console.log(` ${result.gpu.verdict}: ${result.gpu.likelyCause}`);
116
+ }
117
+
118
+ if (result.nextCommand) {
119
+ console.log();
120
+ console.log(chalk.bold('Next step:'));
121
+ console.log(` ${chalk.cyan(result.nextCommand)}`);
122
+ }
123
+ console.log();
124
+
125
+ if (result.errors.length > 0) process.exitCode = 1;
126
+ return result;
127
+ }
@@ -80,7 +80,8 @@ export async function connectCommand(args, chalk, deps = {}) {
80
80
  console.log(chalk.green(`\n ✓ ${alreadyConnected ? 'Updated' : 'Connected'} ${provider}`));
81
81
  console.log(chalk.dim(` Stored in ${CREDENTIALS_FILE} (owner-only file permissions)`));
82
82
  const usageHint = provider === 'anthropic' ? 'badgr launch claude "<task>"'
83
- : provider === 'openai' ? 'badgr launch codex "<task>"'
84
- : `badgr launch cline/claude/codex --provider ${provider} --model <model-id> "<task>"`;
83
+ : provider === 'openai' ? 'badgr launch codex/opencode "<task>"'
84
+ : provider === 'commandcode' ? 'badgr launch commandcode "<task>"'
85
+ : `badgr launch cline/claude/codex/opencode --provider ${provider} --model <model-id> "<task>"`;
85
86
  console.log(chalk.dim(` Used automatically by: ${usageHint}\n`));
86
87
  }