badgr-cli 1.1.6 → 1.1.8
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 +65 -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 +277 -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 +380 -0
- package/src/nodeWorker/test_node_worker.py +243 -0
- package/src/progress.js +2 -2
- package/src/spec.js +2 -2
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
| `--
|
|
192
|
-
| `--
|
|
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
|
|
201
|
-
`
|
|
202
|
-
`
|
|
203
|
-
|
|
204
|
-
`READY`/`SMOKE CHECKED` results also print the
|
|
205
|
-
run`/`badgr serve` command `--approve` would run, plus a
|
|
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,53 @@ 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
|
+
### Host-bound dependencies (venvs, patched source, model/cache dirs)
|
|
551
|
+
|
|
552
|
+
Jobs run inside a Docker container, which starts with none of this machine's filesystem visible except what's explicitly mounted. If your launch command depends on paths that already exist on this host — a Python venv, a patched source checkout (e.g. a custom-built vLLM), a model directory, a pip/HuggingFace cache — those paths will **not** exist inside the container unless you mount them:
|
|
553
|
+
|
|
554
|
+
```bash
|
|
555
|
+
badgr node connect \
|
|
556
|
+
--mount /home/you/venvs/vllm:/opt/venv \
|
|
557
|
+
--mount /home/you/vllm-src:/opt/vllm-src \
|
|
558
|
+
--mount /sync/Models:/models:ro \
|
|
559
|
+
--mount /home/you/.cache:/root/.cache
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Each `--mount` is `HOST:CONTAINER` or `HOST:CONTAINER:ro` (same shape as `docker run -v`), repeatable, and both sides must be absolute paths. Mounts are configured once per node (not per job) and applied to every job that node runs. If you connected without `--mount` and the worker is already running, stop it and restart with the printed `python3 .../node_worker.py ...` command plus your `--mount` flags — there is no separate command to add a mount to an already-running worker yet.
|
|
563
|
+
|
|
564
|
+
An exact command that already works when you run it directly on this machine can still fail through `--target` if it references host paths you haven't mounted — that's a mounting gap, not a vendor/architecture/capacity one (see the BYO Preflight vendor/architecture checks above, which catch a different class of mismatch and pass independently of whether paths are mounted).
|
|
565
|
+
|
|
566
|
+
| Command | What it does |
|
|
567
|
+
|---------|-------------|
|
|
568
|
+
| `badgr node connect [--name <name>] [--mount HOST:CONTAINER[:ro] ...]` | Register this machine and start its worker |
|
|
569
|
+
| `badgr node list` | List your connected nodes |
|
|
570
|
+
| `badgr node inspect <node-id>` | Show one node's full detail as JSON |
|
|
571
|
+
| `badgr node disable <node-id>` | Stop a node from receiving new jobs |
|
|
572
|
+
| `badgr node remove <node-id>` | Disconnect and forget a node |
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
524
576
|
## Routing
|
|
525
577
|
|
|
526
578
|
`--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
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
|
|
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
|
|
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
|
-
//
|
|
232
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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('#
|
|
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;
|
package/src/commands/capacity.js
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/connect.js
CHANGED
|
@@ -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
|
-
:
|
|
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
|
}
|