badgr-cli 1.1.3 → 1.1.4
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 +178 -13
- package/package.json +2 -1
- package/src/badgr.js +2 -2
- package/src/commands/diagnose.js +357 -52
- package/src/commands/serve.js +13 -3
- package/src/errors.js +5 -0
- package/src/fallback.js +4 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Badgr supports many GPU workloads through two commands: `serve` for persistent e
|
|
|
13
13
|
npm install -g badgr-cli
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
**Jump to:** [Quick start](#quick-start) · [Coding agents (`badgr launch`)](#coding--testing-agents-badgr-launch) · [Image generation](#also-try-image-generation) · [`badgr doctor`](#something-not-working-badgr-doctor) · [Commands](#commands) · [`serve` options](#badgr-serve-options) · [`run` options](#badgr-run-options) · [Receipts](#receipts) · [OpenAI compatibility](#openai-compatibility) · [GPU options](#gpu-options) · [Advanced](#advanced) · [Requirements](#requirements)
|
|
16
|
+
**Jump to:** [Quick start](#quick-start) · [Coding agents (`badgr launch`)](#coding--testing-agents-badgr-launch) · [Image generation](#also-try-image-generation) · [`badgr doctor`](#something-not-working-badgr-doctor) · [`badgr diagnose`](#diagnose-a-gpu-issue-badgr-diagnose) · [Commands](#commands) · [`serve` options](#badgr-serve-options) · [`run` options](#badgr-run-options) · [Receipts](#receipts) · [OpenAI compatibility](#openai-compatibility) · [GPU options](#gpu-options) · [Advanced](#advanced) · [Requirements](#requirements)
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
@@ -159,21 +159,80 @@ Run `badgr doctor --help` for the full flag list. Details in
|
|
|
159
159
|
|
|
160
160
|
---
|
|
161
161
|
|
|
162
|
+
## Badgr Smoke Test: `badgr diagnose`
|
|
163
|
+
|
|
164
|
+
Paste a GitHub issue, a Docker image, a repo URL, a log file, a ComfyUI
|
|
165
|
+
workflow, or a raw conversation — Badgr auto-detects the input, redacts
|
|
166
|
+
secrets, and runs free static checks as part of a Badgr Smoke Test.
|
|
167
|
+
Nothing runs on a GPU without explicit `--approve`.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Free Badgr Smoke Test — no login required
|
|
171
|
+
badgr diagnose "https://github.com/org/repo/issues/123"
|
|
172
|
+
badgr diagnose ajayrajtp/vllm_gemma412b:latest
|
|
173
|
+
badgr diagnose ./vllm-error.log
|
|
174
|
+
badgr diagnose "https://github.com/org/repo"
|
|
175
|
+
badgr diagnose workflow.json
|
|
176
|
+
|
|
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)
|
|
181
|
+
badgr diagnose "https://github.com/org/repo/issues/123" --approve
|
|
182
|
+
|
|
183
|
+
# Resume an existing case, e.g. one shared via a case link
|
|
184
|
+
badgr diagnose repro_xxxxxxxx --approve
|
|
185
|
+
badgr diagnose "https://aibadgr.com/repro/repro_xxxxxxxx" --approve
|
|
186
|
+
|
|
187
|
+
# Machine-readable output
|
|
188
|
+
badgr diagnose "https://github.com/org/repo/issues/123" --json
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
| Flag | Description |
|
|
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) |
|
|
195
|
+
| `--docker <image>` | Force Docker-image intake (override auto-detect) |
|
|
196
|
+
| `--repo <url>` | Force repository intake (override auto-detect) |
|
|
197
|
+
| `--comfyui <path>` | Force ComfyUI workflow intake (override auto-detect) |
|
|
198
|
+
| `--github <url>` | Include a GitHub issue URL found inside pasted text as additional context (opt-in, never fetched automatically) |
|
|
199
|
+
| `--json` | Machine-readable JSON output |
|
|
200
|
+
|
|
201
|
+
Every run prints one status: `NEEDS INFO` (fields still missing) →
|
|
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
|
|
209
|
+
<case_id_or_url> --approve` to resume the same case without re-diagnosing.
|
|
210
|
+
|
|
211
|
+
`badgr run-issue` is an alias for `badgr diagnose`, matching the
|
|
212
|
+
[aibadgr.com/run-issue](https://aibadgr.com/run-issue) Badgr Smoke Test
|
|
213
|
+
web flow.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
162
217
|
## Commands
|
|
163
218
|
|
|
164
219
|
```text
|
|
165
220
|
login
|
|
166
221
|
connect
|
|
167
222
|
doctor
|
|
223
|
+
diagnose
|
|
168
224
|
run
|
|
169
225
|
launch
|
|
170
|
-
|
|
226
|
+
job
|
|
171
227
|
serve
|
|
172
228
|
status
|
|
173
229
|
logs
|
|
174
230
|
pull
|
|
175
231
|
artifacts
|
|
176
232
|
down
|
|
233
|
+
restart
|
|
234
|
+
rerun
|
|
235
|
+
heartbeat
|
|
177
236
|
receipts
|
|
178
237
|
test
|
|
179
238
|
```
|
|
@@ -183,6 +242,7 @@ test
|
|
|
183
242
|
| `badgr login` | Save API key to `~/.badgr/config.json` |
|
|
184
243
|
| `badgr connect <provider>` | Store a provider credential (`anthropic`, `openai`) for `badgr launch` |
|
|
185
244
|
| `badgr doctor` | Diagnose a GPU workload failure — read-only, no login needed |
|
|
245
|
+
| `badgr diagnose "<input>"` | Run a Badgr Smoke Test on a GitHub issue, Docker image, repo, log, or workflow — free, no GPU until `--approve` |
|
|
186
246
|
| `badgr run <command>` | Run a one-off GPU job (any container command) |
|
|
187
247
|
| `badgr launch cline\|claude\|codex\|playwright "<task>"` | Run a coding/testing agent on a CPU VM — image + command auto-selected |
|
|
188
248
|
| `badgr job <agent> "<instruction>" --check "<cmd>"` | Tracked coding-agent job via `POST /v1/jobs` (type: agent) |
|
|
@@ -192,10 +252,13 @@ test
|
|
|
192
252
|
| `badgr pull <id>` | Pull a code-editing agent's patch as a local git diff/branch |
|
|
193
253
|
| `badgr artifacts <id>` | Download non-patch outputs (test reports, screenshots, traces) |
|
|
194
254
|
| `badgr down <id>` | Terminate a deployment — stops billing immediately |
|
|
255
|
+
| `badgr restart <id>` | Relaunch an endpoint with the same config, on a new deployment ID, keeping its API key |
|
|
256
|
+
| `badgr rerun <id>` | Replay a past job or endpoint with its exact original spec, on a new deployment ID |
|
|
257
|
+
| `badgr heartbeat <id>` | Reset an endpoint's idle-timeout clock (see `--idle-timeout` under [`badgr serve` options](#badgr-serve-options)) |
|
|
195
258
|
| `badgr receipts [n]` | Cost, route, and retry receipts (default 10) |
|
|
196
259
|
| `badgr test` | Run an end-to-end test (provision → run → teardown) |
|
|
197
260
|
|
|
198
|
-
More commands below, under [Advanced](#advanced): `comfyui`, `train`, `transcribe`, `embed`, `workload`, `workspace`, `batch`, `sbatch`, `capacity`, `billing`.
|
|
261
|
+
More commands below, under [Advanced](#advanced): `comfyui`, `train`, `transcribe`, `embed`, `workload`, `workspace`, `batch`, `sbatch`, `capacity`, `billing`, `models`, `template`.
|
|
199
262
|
|
|
200
263
|
`badgr serve` — for anything that needs a persistent endpoint: LLM serving, embeddings, image generation APIs, transcription APIs.
|
|
201
264
|
|
|
@@ -228,16 +291,38 @@ Each command section below lists only its own extra flags.
|
|
|
228
291
|
badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S --region EU
|
|
229
292
|
```
|
|
230
293
|
|
|
294
|
+
Endpoints bill continuously, so `serve` requires either `--max-cost` or `--persistent` (or `--dry-run` to just preview) — it refuses to start otherwise.
|
|
295
|
+
|
|
231
296
|
| Flag | Default | Description |
|
|
232
297
|
|------|---------|-------------|
|
|
233
298
|
| `--image <img>` | — | Serve a custom container instead of a HuggingFace model |
|
|
234
299
|
| `--task <task>` | — | vLLM task override, e.g. `embed` for embedding models |
|
|
235
|
-
| `--
|
|
300
|
+
| `--runtime llama.cpp\|ollama` | vLLM | Serve via a different runtime instead of vLLM |
|
|
301
|
+
| `--hf-repo <repo>` | — | HuggingFace repo for a GGUF file (with `--runtime llama.cpp`), e.g. `org/model-repo` |
|
|
302
|
+
| `--hf-file <file>` | — | GGUF filename within that repo, e.g. `model.gguf` (required with `--runtime llama.cpp`) |
|
|
303
|
+
| `--idle-timeout <min>` | — | Auto-stop after N minutes with no `badgr heartbeat` call — see [`badgr heartbeat`](#badgr-heartbeat) |
|
|
304
|
+
| `--persistent` | off | Run until manually stopped — satisfies the cost-control requirement in place of `--max-cost` |
|
|
305
|
+
| `--check-nodes <n1,n2>` | — | For ComfyUI-shaped images: verify custom nodes are installed after startup |
|
|
306
|
+
| `--health-path <path>` | auto | Readiness path to poll (auto-detected: ComfyUI → `/system_stats`, llama.cpp → `/health`) |
|
|
236
307
|
| `--no-wait` | off | Skip endpoint health check and return immediately |
|
|
308
|
+
| `--yes` / `-y` | off | Skip duplicate-deployment warning |
|
|
309
|
+
| `--dry-run` | — | Preview the plan (GPU, price) without provisioning |
|
|
237
310
|
| `--list-aliases` | — | List blessed vLLM model aliases (`qwen-7b`, `llama-8b`, `qwen-coder-7b`) and exit — no provisioning, no API key required |
|
|
238
311
|
|
|
239
312
|
Blessed aliases expand to a full model ID + preset GPU, e.g. `badgr serve qwen-7b` → `Qwen/Qwen2.5-7B-Instruct` on an RTX 4090. Run `badgr serve --list-aliases` to see the current list.
|
|
240
313
|
|
|
314
|
+
```bash
|
|
315
|
+
# Serve a Hugging Face GGUF file via llama.cpp instead of vLLM
|
|
316
|
+
badgr serve --runtime llama.cpp --hf-repo org/model-repo --hf-file model.gguf --max-cost 10
|
|
317
|
+
|
|
318
|
+
# Serve Open WebUI, a chat UI, pointed at a model endpoint
|
|
319
|
+
badgr serve openwebui --model qwen-7b --max-cost 10
|
|
320
|
+
badgr serve openwebui --connect <existing-endpoint-url> # connect to an endpoint you already have
|
|
321
|
+
|
|
322
|
+
# Auto-stop only when idle — requires periodic badgr heartbeat calls to stay up
|
|
323
|
+
badgr serve qwen-7b --idle-timeout 30 --max-cost 10
|
|
324
|
+
```
|
|
325
|
+
|
|
241
326
|
### Model support levels
|
|
242
327
|
|
|
243
328
|
`badgr serve qwen-7b` is the happy path — a tested route with no extra setup. `badgr serve` also accepts any other model ID or a custom container:
|
|
@@ -269,10 +354,21 @@ badgr run . --image mycompany/custom:latest --cmd "python train.py" --max-cost 5
|
|
|
269
354
|
|
|
270
355
|
Badgr zips and uploads the folder (Flow 1) or clones the repo (Flow 2), picks a generic runner, installs deps, runs the command, stores outputs for 48 hours, and tears down the GPU. `--max-cost` is required.
|
|
271
356
|
|
|
357
|
+
```bash
|
|
358
|
+
# No GPU needed — run on a plain CPU VM instead
|
|
359
|
+
badgr run . --cmd "npm test" --no-gpu --max-cost 1
|
|
360
|
+
|
|
361
|
+
# Describe basic compute needs instead of a GPU model — Badgr finds a compatible machine
|
|
362
|
+
badgr run . --cpu 16 --memory 64GB --gpu-memory 24GB --max-cost 5
|
|
363
|
+
```
|
|
364
|
+
|
|
272
365
|
| Flag | Default | Description |
|
|
273
366
|
|------|---------|-------------|
|
|
274
367
|
| `--cmd <command>` | — | Command to run inside the uploaded project or cloned repo (required for folder/GitHub flows) |
|
|
275
|
-
| `--min-vram <GB>` | — | Minimum VRAM in GB — optional constraint for Auto routing |
|
|
368
|
+
| `--min-vram <GB>` | — | Minimum VRAM in GB — optional constraint for Auto routing (alias: `--gpu-memory`) |
|
|
369
|
+
| `--cpu <cores>` | — | Minimum CPU cores (for a CPU-only run) |
|
|
370
|
+
| `--memory <size>` | — | Minimum RAM, e.g. `64GB` |
|
|
371
|
+
| `--no-gpu` | off | Run on a CPU-only VM — no GPU is provisioned (conflicts with `--gpu`/`--min-vram`) |
|
|
276
372
|
| `--image <img>` | — | Custom Docker image — bypasses the runner |
|
|
277
373
|
| `--max-runtime <min>` | — | Auto-stop after N minutes |
|
|
278
374
|
| `--save <name>` | — | Save this job as a named workload after it completes |
|
|
@@ -320,13 +416,14 @@ Productized batch image generation — runs a list of prompts through a **blesse
|
|
|
320
416
|
```bash
|
|
321
417
|
badgr comfyui batch --workflow sdxl-basic --prompts prompts.txt --max-cost 10
|
|
322
418
|
badgr comfyui batch --workflow sdxl-basic --prompt "a cat on a beach" --prompt "a dog in the park" --max-cost 5
|
|
419
|
+
badgr comfyui batch --workflow flux-basic --prompt "a neon city at night" --max-cost 5
|
|
323
420
|
```
|
|
324
421
|
|
|
325
|
-
Blessed workflows: `sdxl-basic` (SDXL text-to-image, default sampler settings). Max 20 prompts per batch.
|
|
422
|
+
Blessed workflows: `sdxl-basic` (SDXL text-to-image, default sampler settings), `flux-basic` (FLUX.1-schnell text-to-image). Max 20 prompts per batch.
|
|
326
423
|
|
|
327
424
|
| Flag | Default | Description |
|
|
328
425
|
|------|---------|-------------|
|
|
329
|
-
| `--workflow <name>` | — | Blessed workflow ID (required) —
|
|
426
|
+
| `--workflow <name>` | — | Blessed workflow ID (required) — `sdxl-basic` or `flux-basic` |
|
|
330
427
|
| `--prompts <file>` | — | Text file, one prompt per line |
|
|
331
428
|
| `--prompt <text>` | — | Inline prompt (repeatable) — combine with `--prompts` if needed |
|
|
332
429
|
| `--max-runtime <min>` | 60 | Auto-stop after N minutes |
|
|
@@ -350,6 +447,37 @@ Each receipt includes runtime, estimated/settled cost, status, retries, teardown
|
|
|
350
447
|
|
|
351
448
|
---
|
|
352
449
|
|
|
450
|
+
## Managing a deployment
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
badgr status # what's running and billing
|
|
454
|
+
badgr logs dep-abc123 # fetch current log output
|
|
455
|
+
badgr logs dep-abc123 --follow # stream logs until the deployment reaches a terminal state
|
|
456
|
+
badgr pull dep-abc123 # pull a coding agent's patch as a local git diff
|
|
457
|
+
badgr pull dep-abc123 --branch # ...as a new local branch instead of a diff
|
|
458
|
+
badgr pull dep-abc123 --diff-only # print the diff, don't touch the working tree
|
|
459
|
+
badgr artifacts dep-abc123 # download non-patch outputs (test reports, screenshots, traces)
|
|
460
|
+
badgr down dep-abc123 # terminate one deployment, stop billing
|
|
461
|
+
badgr down --all # terminate everything running, with a confirmation prompt
|
|
462
|
+
badgr down --all --yes # ...skip the confirmation prompt
|
|
463
|
+
badgr restart dep-abc123 # relaunch an endpoint with the same config — new ID, same API key
|
|
464
|
+
badgr rerun dep-abc123 # replay a past job/endpoint with its exact original spec — new ID
|
|
465
|
+
badgr heartbeat dep-abc123 # reset an endpoint's idle-timeout clock (see --idle-timeout on badgr serve)
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
| Command | Flag | Description |
|
|
469
|
+
|---------|------|-------------|
|
|
470
|
+
| `badgr logs <id>` | `--follow` / `-f` | Stream/poll logs until the deployment reaches a terminal state instead of a one-shot fetch |
|
|
471
|
+
| `badgr pull <id>` | `--branch` | Create a new local git branch from the patch instead of leaving it as an unstaged diff |
|
|
472
|
+
| `badgr pull <id>` | `--diff-only` | Print the raw diff, don't touch the local working tree at all |
|
|
473
|
+
| `badgr pull <id>` | `--yes` / `-y` | Skip the confirmation prompt before applying |
|
|
474
|
+
| `badgr down <id\|--all>` | `--all` | Terminate every running deployment instead of one by ID |
|
|
475
|
+
| `badgr down <id\|--all>` | `--yes` / `-y` | Skip the confirmation prompt |
|
|
476
|
+
|
|
477
|
+
`badgr restart` is endpoint-only (it tears down the current pod and relaunches with the same GPU/model/price/runtime caps and endpoint API key, so existing clients keep working against a new URL). `badgr rerun` works for both one-off jobs and endpoints, replaying the exact original image/command/env/GPU/caps, and never tears down the source deployment.
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
353
481
|
## OpenAI compatibility
|
|
354
482
|
|
|
355
483
|
`badgr serve` provisions a vLLM endpoint that is fully OpenAI-compatible:
|
|
@@ -417,22 +545,26 @@ Less common commands — training, transcription, embeddings, and the workload/w
|
|
|
417
545
|
badgr train config.yaml --gpu A100 --max-runtime 240 --env HF_TOKEN=$HF_TOKEN
|
|
418
546
|
```
|
|
419
547
|
|
|
420
|
-
Detects framework (axolotl, unsloth, trl) from the config file
|
|
548
|
+
Detects framework (axolotl, unsloth, trl) from the config file. **Axolotl and TRL configs run today** — `unsloth`/unrecognized configs are blocked before provisioning rather than billing a GPU that's guaranteed to fail. Default max-runtime is 120 min.
|
|
421
549
|
|
|
422
550
|
```bash
|
|
423
551
|
badgr train lora --base-model mistralai/Mistral-7B-v0.1 --dataset ./train.jsonl --preset small --max-cost 20
|
|
552
|
+
|
|
553
|
+
# Resume from a prior job's checkpoint instead of starting over
|
|
554
|
+
badgr train lora --base-model mistralai/Mistral-7B-v0.1 --dataset ./train.jsonl --resume https://.../checkpoint --max-cost 20
|
|
424
555
|
```
|
|
425
556
|
|
|
426
557
|
Productized LoRA training — pass a base model and dataset, no Axolotl config file needed. Badgr generates the config from a preset and returns a downloadable adapter.
|
|
427
558
|
|
|
428
559
|
| Flag | Default | Description |
|
|
429
560
|
|------|---------|-------------|
|
|
430
|
-
| `--framework <name>` | auto-detect | Force framework: `axolotl`, `unsloth`, `trl` (
|
|
561
|
+
| `--framework <name>` | auto-detect | Force framework: `axolotl`, `unsloth`, `trl` (`axolotl`/`trl` currently run; `unsloth` is blocked pre-provisioning) |
|
|
431
562
|
| `--base-model <id>` | — | HuggingFace model ID (required for `train lora`) — validated to exist before provisioning |
|
|
432
563
|
| `--dataset <path\|url>` | — | Local file, direct URL, or `s3://` URI |
|
|
433
564
|
| `--file-id <id>` | — | Badgr upload ID instead of `--dataset` |
|
|
434
565
|
| `--preset small\|medium` | `small` | `small` = RTX 4090, rank 16, 3 epochs. `medium` = A100, rank 32, 5 epochs |
|
|
435
566
|
| `--gpu-type <type>` | preset default | GPU type override for `train lora` |
|
|
567
|
+
| `--resume <checkpoint-url>` | — | Continue training from a prior job's checkpoint instead of starting fresh |
|
|
436
568
|
| `--dry-run` | — | Preview the job without provisioning |
|
|
437
569
|
|
|
438
570
|
On completion, `train lora` prints an `adapter_url` — download with `GET /v1/jobs/{job_id}/adapter`, or via `badgr workload info` if saved.
|
|
@@ -502,8 +634,11 @@ badgr batch status dep-abc123
|
|
|
502
634
|
badgr batch artifacts dep-abc123
|
|
503
635
|
badgr batch receipt dep-abc123
|
|
504
636
|
badgr batch compare dep-abc123 dep-def456
|
|
637
|
+
badgr batch compare dep-abc123 dep-def456 --key accuracy --higher-is-better
|
|
505
638
|
```
|
|
506
639
|
|
|
640
|
+
`batch compare` reads each run's `success_metric` by default; `--key <metric>` compares a different field from the receipt instead, and `--higher-is-better` (default) / `--higher-is-better false` controls which run is reported as the winner.
|
|
641
|
+
|
|
507
642
|
For CV/video/scientific batch, simulation, and physical-AI eval workloads — runs a container from a `workload.yml` spec and captures output artifacts automatically.
|
|
508
643
|
|
|
509
644
|
**Fan-out** — run the same program once per file in a directory, one deployment per input, in parallel:
|
|
@@ -539,14 +674,44 @@ Translates `--cpus-per-task`/`--mem`/`--gres`/`--time`/`--export` from a real `.
|
|
|
539
674
|
| `--max-concurrency <n>` | 5 | Cap in-flight array tasks |
|
|
540
675
|
| `--dry-run` | — | Preview the translated job without provisioning |
|
|
541
676
|
|
|
542
|
-
###
|
|
677
|
+
### `badgr models` — GPU catalog and pricing
|
|
678
|
+
|
|
679
|
+
```bash
|
|
680
|
+
badgr models
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
Lists available GPU types cheapest-first, with VRAM and hourly rate — pulled live from your account when logged in, falling back to the local catalog otherwise. No flags.
|
|
543
684
|
|
|
544
|
-
|
|
545
|
-
|
|
685
|
+
### `badgr template` — pre-built workload templates
|
|
686
|
+
|
|
687
|
+
```bash
|
|
688
|
+
badgr template list
|
|
689
|
+
badgr template info axolotl
|
|
690
|
+
badgr serve template vllm --model meta-llama/Llama-3.1-8B-Instruct --max-cost 10
|
|
691
|
+
badgr run template axolotl --config ./config.yaml --max-cost 10
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Provider-neutral templates for common frameworks (`vllm`, `invokeai`, `comfyui`, `axolotl`, `unsloth`, …). `template list`/`template info <name>` just browse the catalog; launching always goes through `badgr serve template <name>` or `badgr run template <name>`, which apply the template's default flags before handing off to the normal `serve`/`run` path.
|
|
695
|
+
|
|
696
|
+
### `badgr capacity` — check live availability
|
|
697
|
+
|
|
698
|
+
```bash
|
|
699
|
+
badgr capacity # cheapest runnable GPU across all types
|
|
700
|
+
badgr capacity --gpu A100 # a specific GPU type
|
|
701
|
+
badgr capacity --gpu A100 --region EU # region-filtered
|
|
702
|
+
badgr capacity --gpu A100 --max-price 2.50 # price-capped
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### `badgr billing`
|
|
706
|
+
|
|
707
|
+
```bash
|
|
708
|
+
badgr billing status # current balance
|
|
709
|
+
badgr billing add 20 # add funds — $5 minimum top-up
|
|
710
|
+
```
|
|
546
711
|
|
|
547
712
|
---
|
|
548
713
|
|
|
549
714
|
## Requirements
|
|
550
715
|
|
|
551
|
-
- Node.js
|
|
716
|
+
- Node.js 20.10+
|
|
552
717
|
- A Badgr account — sign up at [aibadgr.com](https://aibadgr.com)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "badgr-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Badgr — run or serve GPU workloads from one command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@inquirer/prompts": "^8.5.2",
|
|
16
16
|
"archiver": "^7.0.1",
|
|
17
|
+
"badgr-shared": "0.1.1",
|
|
17
18
|
"chalk": "^5.3.0",
|
|
18
19
|
"js-yaml": "^4.1.0",
|
|
19
20
|
"tar": "^7.4.3"
|
package/src/badgr.js
CHANGED
|
@@ -47,7 +47,7 @@ ${chalk.bold('COMMANDS')}
|
|
|
47
47
|
${chalk.cyan('badgr job <agent> "<instruction>" --check "<command>"')} Bounded coding-agent job, tracked at /jobs (POST /v1/jobs, type: agent)
|
|
48
48
|
${chalk.cyan('badgr serve <model>')} Serve a model with an OpenAI-compatible endpoint
|
|
49
49
|
${chalk.cyan('badgr serve openwebui')} Serve Open WebUI — chat UI, connects to a model endpoint
|
|
50
|
-
${chalk.cyan('badgr diagnose "<input>"')}
|
|
50
|
+
${chalk.cyan('badgr diagnose "<input>"')} Run a free Badgr Smoke Test on any GPU issue — GitHub issue, Docker image, log, repo, or text
|
|
51
51
|
${chalk.cyan('badgr doctor')} Local GPU / model-fit diagnosis (read-only, no login needed)
|
|
52
52
|
${chalk.cyan('badgr status')} Show what's running and what's billing
|
|
53
53
|
${chalk.cyan('badgr logs <id>')} Stream logs for a running job or endpoint
|
|
@@ -113,7 +113,7 @@ ${chalk.bold('EXAMPLES')}
|
|
|
113
113
|
${chalk.dim('# badgr job — bounded coding-agent job with a pass/fail check, tracked at /jobs:')}
|
|
114
114
|
badgr job cline "Run the Chromium tests and tell me what failed" --check "npm run test:chromium" --max-cost 1
|
|
115
115
|
|
|
116
|
-
${chalk.dim('#
|
|
116
|
+
${chalk.dim('# Run a free Badgr Smoke Test on any GPU issue (no login needed):')}
|
|
117
117
|
badgr diagnose "https://github.com/org/repo/issues/123"
|
|
118
118
|
badgr diagnose ./error.log
|
|
119
119
|
badgr diagnose --docker ajayrajtp/vllm_gemma412b:latest
|
package/src/commands/diagnose.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import { classifyPastedInput } from 'badgr-shared';
|
|
2
3
|
import { callApi } from '../api.js';
|
|
3
4
|
import { ensureLoggedIn } from '../onboarding.js';
|
|
4
5
|
|
|
@@ -9,6 +10,7 @@ smoke-test plan. No GPU launches without explicit --approve.
|
|
|
9
10
|
|
|
10
11
|
Usage:
|
|
11
12
|
badgr diagnose "<input>"
|
|
13
|
+
badgr diagnose "<input>" --smoke
|
|
12
14
|
badgr diagnose "<input>" --approve
|
|
13
15
|
badgr diagnose "<input>" --json
|
|
14
16
|
badgr diagnose <case_id_or_url> --approve Resume an existing case (e.g.
|
|
@@ -24,8 +26,24 @@ Input (auto-detected):
|
|
|
24
26
|
Existing case repro_xxxxxxxx or https://aibadgr.com/repro/repro_xxxxxxxx
|
|
25
27
|
|
|
26
28
|
Flags:
|
|
29
|
+
--smoke Free. Runs real, cheap mechanical validation of the
|
|
30
|
+
produced command on top of the normal diagnosis --
|
|
31
|
+
command syntax, CLI-entrypoint corroboration,
|
|
32
|
+
referenced-file presence, Docker ENTRYPOINT/CMD
|
|
33
|
+
consistency, required env vars -- plus a client-side
|
|
34
|
+
check of any local file path you pasted (e.g. a
|
|
35
|
+
.gguf path), which only this machine can see. Never
|
|
36
|
+
starts a GPU, never requires login, never prints
|
|
37
|
+
"Verified" -- only "Smoke Checked" (AGENTS.md proof
|
|
38
|
+
levels). Can be combined with --approve: the smoke
|
|
39
|
+
checks print first, then the normal approve flow
|
|
40
|
+
runs (they are independent, non-blocking steps).
|
|
41
|
+
Unrelated to "badgr run --smoke", which launches a
|
|
42
|
+
real, billable cheapest-GPU job.
|
|
27
43
|
--approve Approve the capped smoke test after diagnosis (opens
|
|
28
|
-
browser sign-in automatically if not logged in)
|
|
44
|
+
browser sign-in automatically if not logged in).
|
|
45
|
+
This is a real, billable GPU-provisioned run --
|
|
46
|
+
distinct from the free --smoke flag above.
|
|
29
47
|
--docker <image> Force Docker-image intake (override auto-detect)
|
|
30
48
|
--repo <url> Force repository intake (override auto-detect)
|
|
31
49
|
--comfyui <path> Force ComfyUI workflow intake (override auto-detect)
|
|
@@ -35,8 +53,27 @@ Flags:
|
|
|
35
53
|
--json Machine-readable JSON output
|
|
36
54
|
--help, -h Show this help
|
|
37
55
|
|
|
56
|
+
Status: every run prints one of NEEDS INFO / READY / SMOKE CHECKED / INVALID / VERIFIED.
|
|
57
|
+
NEEDS INFO -- cannot yet produce a complete command
|
|
58
|
+
READY -- complete evidence-backed command, no smoke run
|
|
59
|
+
SMOKE CHECKED -- --smoke ran; all applicable checks PASS or SKIPPED
|
|
60
|
+
INVALID -- --smoke ran and an applicable check actually FAILED
|
|
61
|
+
VERIFIED -- only after a real successful run (never from --smoke)
|
|
62
|
+
|
|
63
|
+
Run page: READY and SMOKE CHECKED results also create a free, anonymous
|
|
64
|
+
"Run" link (the same prepared case /run-issue's own confirm form
|
|
65
|
+
creates) -- a saved, runnable configuration you or anyone with the
|
|
66
|
+
link can open and click Run on. NEEDS INFO and INVALID never get
|
|
67
|
+
one (nothing runnable to save, or a check just proved it broken)
|
|
68
|
+
-- both print the correction instead. The page shows the exact
|
|
69
|
+
canonical "badgr ..." command --approve would run, not merely the
|
|
70
|
+
source command extraction found. Still no GPU starts until that
|
|
71
|
+
click. --approve reuses this same case rather than creating a
|
|
72
|
+
second one.
|
|
73
|
+
|
|
38
74
|
Safety: nothing runs from AI-extracted data without --approve.
|
|
39
75
|
No GPU launches without explicit approval and a credit check.
|
|
76
|
+
--smoke never provisions a GPU and never requires login.
|
|
40
77
|
|
|
41
78
|
Full interactive flow: https://aibadgr.com/run-issue`;
|
|
42
79
|
|
|
@@ -66,32 +103,27 @@ function detectInput(raw, flags) {
|
|
|
66
103
|
|
|
67
104
|
if (!raw) return null;
|
|
68
105
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
106
|
+
// Case-id / GitHub-URL-shape classification is shared with the web page
|
|
107
|
+
// (`classifyPastedInput`, packages/shared/src/index.ts) so the CLI and
|
|
108
|
+
// /run-issue never disagree about what a given paste is. A case_id (bare,
|
|
109
|
+
// or embedded in a /repro/<id> or /run-issue?case_id=<id> link — the two
|
|
110
|
+
// shapes an admin-prepared or self-created case gets shared as) resumes
|
|
111
|
+
// that existing case instead of diagnosing new input.
|
|
112
|
+
const classified = classifyPastedInput(raw);
|
|
113
|
+
if (classified.kind === 'existing_case') {
|
|
114
|
+
return { kind: 'existing_case', label: `Existing case: ${classified.body.case_id}`, caseId: classified.body.case_id };
|
|
77
115
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
kind: 'github_issue',
|
|
82
|
-
label: `GitHub issue: ${raw}`,
|
|
83
|
-
body: { github_url: raw },
|
|
84
|
-
};
|
|
116
|
+
if (classified.kind === 'github_issue') {
|
|
117
|
+
return { kind: 'github_issue', label: `GitHub issue: ${raw}`, body: classified.body };
|
|
85
118
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
kind: 'repo_url',
|
|
90
|
-
label: `GitHub repository: ${raw}`,
|
|
91
|
-
body: { repo_url: raw },
|
|
92
|
-
};
|
|
119
|
+
if (classified.kind === 'repo_url') {
|
|
120
|
+
return { kind: 'repo_url', label: `GitHub repository: ${raw}`, body: classified.body };
|
|
93
121
|
}
|
|
94
122
|
|
|
123
|
+
// Local filesystem paths -- only the CLI has a filesystem to check
|
|
124
|
+
// against, so this stays CLI-only, sitting between the shared URL checks
|
|
125
|
+
// above and the shared Docker-image/text fallback below (matches
|
|
126
|
+
// classifyPastedInput's own precedence for everything except this).
|
|
95
127
|
if (fs.existsSync(raw)) {
|
|
96
128
|
const content = fs.readFileSync(raw, 'utf8');
|
|
97
129
|
if (raw.endsWith('.json') || raw.endsWith('.JSON')) {
|
|
@@ -112,23 +144,11 @@ function detectInput(raw, flags) {
|
|
|
112
144
|
};
|
|
113
145
|
}
|
|
114
146
|
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
/^[a-z0-9][a-z0-9._\-]*(?:\/[a-z0-9._\-]+)*(?::[a-zA-Z0-9._\-]+)?$/.test(raw) &&
|
|
118
|
-
raw.length < 200
|
|
119
|
-
) {
|
|
120
|
-
return {
|
|
121
|
-
kind: 'docker_image',
|
|
122
|
-
label: `Docker image: ${raw}`,
|
|
123
|
-
body: { docker_image: raw },
|
|
124
|
-
};
|
|
147
|
+
if (classified.kind === 'docker_image') {
|
|
148
|
+
return { kind: 'docker_image', label: `Docker image: ${raw}`, body: classified.body };
|
|
125
149
|
}
|
|
126
150
|
|
|
127
|
-
return {
|
|
128
|
-
kind: 'text',
|
|
129
|
-
label: 'Text / conversation',
|
|
130
|
-
body: { text: raw.slice(0, 50_000) },
|
|
131
|
-
};
|
|
151
|
+
return { kind: 'text', label: 'Text / conversation', body: classified.body };
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
function _printResult(result, chalk) {
|
|
@@ -204,11 +224,187 @@ function _printResult(result, chalk) {
|
|
|
204
224
|
}
|
|
205
225
|
}
|
|
206
226
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
227
|
+
// Local-machine file paths (e.g. a pasted .gguf path) can only be checked
|
|
228
|
+
// client-side -- the backend never sees this machine's filesystem, so it
|
|
229
|
+
// must not (and does not) claim to validate them. Only paths that look like
|
|
230
|
+
// a real filesystem reference (absolute, `./`, `../`, or `~/`) are checked;
|
|
231
|
+
// bare words, URLs, and Docker image references are left alone.
|
|
232
|
+
//
|
|
233
|
+
// Path checks must respect context (locked Free Command Check spec, problem
|
|
234
|
+
// #3): a repo path is checked against the repo tree, a container path
|
|
235
|
+
// against Dockerfile/image evidence -- both server-side, inside
|
|
236
|
+
// `mechanical_checks` -- and only a genuinely local path belongs to this
|
|
237
|
+
// machine. Once a repository or Docker image is in evidence for this
|
|
238
|
+
// workload, an absolute path *inside the resolved command* (e.g. "python3
|
|
239
|
+
// /src/main.py") is a repo/container path, not a claim about this laptop,
|
|
240
|
+
// so it must never be re-checked against the local filesystem here -- doing
|
|
241
|
+
// so is exactly the "/src/main.py NOT FOUND on this machine" false failure
|
|
242
|
+
// the spec calls out. `model_artifact` is different: it is always this
|
|
243
|
+
// machine's own claim about a locally-downloaded checkpoint, never repo or
|
|
244
|
+
// container evidence, so it stays checked regardless.
|
|
245
|
+
function _localPathCandidates(e) {
|
|
246
|
+
const candidates = new Set();
|
|
247
|
+
const hasRepoOrContainerContext = Boolean(
|
|
248
|
+
e.repositories?.length || e.docker_images?.length || e.dockerfile
|
|
249
|
+
);
|
|
250
|
+
const maybeAdd = (value) => {
|
|
251
|
+
if (typeof value !== 'string') return;
|
|
252
|
+
const s = value.trim().replace(/^['"]|['"]$/g, '');
|
|
253
|
+
if (!s || /^https?:\/\//i.test(s)) return;
|
|
254
|
+
if (s.startsWith('/') || s.startsWith('./') || s.startsWith('../') || s.startsWith('~/')) {
|
|
255
|
+
candidates.add(s);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
maybeAdd(e.model_artifact);
|
|
259
|
+
if (!hasRepoOrContainerContext) {
|
|
260
|
+
for (const tok of (e.commands?.[0] || '').split(/\s+/)) maybeAdd(tok);
|
|
261
|
+
}
|
|
262
|
+
return [...candidates];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Locked status ladder (product-level labels, not the backend's internal
|
|
266
|
+
// proof_level enum): NEEDS INFO / READY / SMOKE CHECKED / INVALID / VERIFIED.
|
|
267
|
+
// Pure presentation over data /run-issue/extract already returns -- no new
|
|
268
|
+
// endpoint, no new GPU behaviour. --smoke can reach SMOKE CHECKED or INVALID
|
|
269
|
+
// (never VERIFIED, see AGENTS.md §12 and _printCopySummary below).
|
|
270
|
+
//
|
|
271
|
+
// The evidence-only judgment (needs_info/invalid/smoke_checked) is computed
|
|
272
|
+
// once, server-side, in `_compute_smoke_status` (backend/run_issue_routes.py)
|
|
273
|
+
// and read here via `e.smoke_status` -- this and /run-issue's `smokeStatus()`
|
|
274
|
+
// (frontend/app/run-issue/page.tsx) both just render that one backend
|
|
275
|
+
// verdict rather than each re-deriving pass/fail from mechanical_checks, so
|
|
276
|
+
// the CLI and the web page can never disagree on what a given result means.
|
|
277
|
+
// A `smoke_status` fallback derivation is kept only for a backend response
|
|
278
|
+
// that predates this field (defensive, not the normal path).
|
|
279
|
+
//
|
|
280
|
+
// READY means "no smoke run" -- a command that's merely complete is never
|
|
281
|
+
// itself proof of validity, so mechanical/local-file results only ever
|
|
282
|
+
// change the printed word when --smoke actually ran them (locked spec
|
|
283
|
+
// problem #6: a real FAIL must demote SMOKE CHECKED to INVALID; SKIPPED
|
|
284
|
+
// checks never do). This part stays CLI-side: it depends on the --smoke
|
|
285
|
+
// flag and this machine's own filesystem, neither of which the backend can
|
|
286
|
+
// see.
|
|
287
|
+
function _statusWord(result, smokeRequested) {
|
|
288
|
+
const e = result.extraction || {};
|
|
289
|
+
if (result.static_incompatibilities?.length || e.missing_information?.length) return 'NEEDS INFO';
|
|
290
|
+
if (!smokeRequested) return 'READY';
|
|
291
|
+
const backendStatus = e.smoke_status
|
|
292
|
+
|| ((e.mechanical_checks || []).some((check) => check.status === 'fail') ? 'invalid' : 'smoke_checked');
|
|
293
|
+
const localFail = _localPathCandidates(e).some((p) => !fs.existsSync(p));
|
|
294
|
+
if (backendStatus === 'invalid' || localFail) return 'INVALID';
|
|
295
|
+
return backendStatus === 'ready' ? 'READY' : 'SMOKE CHECKED';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const _CHECK_LABELS = {
|
|
299
|
+
command_syntax: 'command syntax',
|
|
300
|
+
cli_entrypoint: 'CLI entrypoint',
|
|
301
|
+
referenced_files: 'referenced repo files',
|
|
302
|
+
docker_entrypoint: 'Docker metadata',
|
|
303
|
+
required_env_vars: 'required env vars',
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Compact, copy-pasteable result -- the literal STATUS word plus just enough
|
|
307
|
+
// to forward to someone else. Additive to the detailed report above it;
|
|
308
|
+
// does not replace it. Never prints "Verified" -- SMOKE CHECKED is the
|
|
309
|
+
// ceiling for this free path.
|
|
310
|
+
function _printCopySummary(result, flags, chalk, caseData) {
|
|
311
|
+
const e = result.extraction || {};
|
|
312
|
+
const status = _statusWord(result, flags.smoke);
|
|
313
|
+
|
|
314
|
+
console.log(` ${chalk.bold(`STATUS: ${status}`)}`);
|
|
315
|
+
console.log();
|
|
316
|
+
|
|
317
|
+
if (status === 'NEEDS INFO') {
|
|
318
|
+
// Static incompatibilities and missing-information both mean "no
|
|
319
|
+
// command yet" -- whichever fired is the reason to surface here.
|
|
320
|
+
const reasons = result.static_incompatibilities?.length
|
|
321
|
+
? result.static_incompatibilities
|
|
322
|
+
: (e.missing_information || []);
|
|
323
|
+
console.log(` ${chalk.bold('Missing:')}`);
|
|
324
|
+
for (const r of reasons) console.log(` - ${r}`);
|
|
325
|
+
console.log();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// The prepared case's canonical `badgr ...` command (the exact line
|
|
330
|
+
// --approve would run) is preferred over the raw source command --
|
|
331
|
+
// /confirm can only compute it once a case exists (READY/SMOKE CHECKED),
|
|
332
|
+
// and only when there's enough evidence to convert (see
|
|
333
|
+
// _canonical_badgr_command's docstring, backend/run_issue_routes.py, for
|
|
334
|
+
// what it can't yet convert, e.g. training/ComfyUI). Falls back to the
|
|
335
|
+
// raw extracted command otherwise -- still true and still runnable input,
|
|
336
|
+
// just not guaranteed to already be `badgr`-shaped.
|
|
337
|
+
const badgrJob = caseData?.findings?.canonical_command || e.commands?.[0] || '(no command produced)';
|
|
338
|
+
console.log(` ${chalk.bold('Badgr job:')}`);
|
|
339
|
+
console.log(` ${badgrJob}`);
|
|
340
|
+
console.log();
|
|
341
|
+
|
|
342
|
+
if (caseData?.case_id) {
|
|
343
|
+
console.log(` ${chalk.bold('Run:')}`);
|
|
344
|
+
console.log(` ${chalk.cyan(_runPageUrl(caseData.case_id))}`);
|
|
345
|
+
console.log();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (status === 'SMOKE CHECKED' || status === 'INVALID') {
|
|
349
|
+
console.log(` ${chalk.bold('Checks:')}`);
|
|
350
|
+
const iconFor = (s) => (s === 'pass' ? '✓' : s === 'fail' ? '✗' : '-');
|
|
351
|
+
for (const check of (e.mechanical_checks || [])) {
|
|
352
|
+
console.log(` ${iconFor(check.status)} ${_CHECK_LABELS[check.name] || check.name}`);
|
|
353
|
+
}
|
|
354
|
+
const localPaths = _localPathCandidates(e);
|
|
355
|
+
if (!localPaths.length) {
|
|
356
|
+
console.log(' - local file check skipped');
|
|
357
|
+
} else {
|
|
358
|
+
const allExist = localPaths.every((p) => fs.existsSync(p));
|
|
359
|
+
console.log(` ${allExist ? '✓' : '✗'} local file check${allExist ? '' : ' (not found)'}`);
|
|
360
|
+
}
|
|
361
|
+
console.log();
|
|
362
|
+
if (status === 'INVALID') {
|
|
363
|
+
console.log(' A check above actually failed -- this command is not runnable as-is.');
|
|
364
|
+
} else {
|
|
365
|
+
console.log(' Not GPU-verified.');
|
|
366
|
+
}
|
|
367
|
+
console.log();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// --smoke's mechanical checks: prints the backend's evidence-backed
|
|
372
|
+
// pass/fail/skip results (`extraction.mechanical_checks`, computed for
|
|
373
|
+
// free from data already gathered by /run-issue/extract -- no extra
|
|
374
|
+
// network call), plus this machine's own local-file existence check.
|
|
375
|
+
// Deliberately never prints "Verified" -- see AGENTS.md §12; a genuinely
|
|
376
|
+
// passing smoke check still only earns "Smoke Checked".
|
|
377
|
+
function _printSmokeChecks(result, chalk) {
|
|
378
|
+
const e = result.extraction || {};
|
|
379
|
+
const mech = e.mechanical_checks || [];
|
|
380
|
+
const localPaths = _localPathCandidates(e);
|
|
381
|
+
|
|
382
|
+
console.log();
|
|
383
|
+
console.log(` ${chalk.bold('Smoke Checked (free, no GPU, no login):')}`);
|
|
384
|
+
|
|
385
|
+
const icon = (status) => (
|
|
386
|
+
status === 'pass' ? chalk.green('✓') : status === 'fail' ? chalk.red('✗') : chalk.dim('-')
|
|
387
|
+
);
|
|
388
|
+
for (const check of mech) {
|
|
389
|
+
console.log(` ${icon(check.status)} ${chalk.dim(`[${check.name}]`)} ${check.detail}`);
|
|
390
|
+
}
|
|
391
|
+
for (const p of localPaths) {
|
|
392
|
+
const exists = fs.existsSync(p);
|
|
393
|
+
console.log(` ${exists ? chalk.green('✓') : chalk.red('✗')} ${chalk.dim('[local_file]')} ${p} ${exists ? 'exists on this machine' : 'NOT FOUND on this machine'}`);
|
|
394
|
+
}
|
|
395
|
+
if (!mech.length && !localPaths.length) {
|
|
396
|
+
console.log(` ${chalk.dim('No mechanical checks applicable to this input.')}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const proofLevel = e.smoke_check?.proof_level;
|
|
400
|
+
if (proofLevel === 'smoke_checked') {
|
|
401
|
+
console.log(` ${chalk.green('✓')} ${chalk.dim('[command_scope]')} Command is the repository/image/service's own documented default.`);
|
|
402
|
+
}
|
|
403
|
+
console.log();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function _confirmBodyFromExtraction(e) {
|
|
407
|
+
return {
|
|
212
408
|
workload_type: e.workload_type || 'generic',
|
|
213
409
|
docker_image: e.docker_images?.[0] || null,
|
|
214
410
|
model_id: e.models?.[0] || null,
|
|
@@ -221,23 +417,39 @@ async function _doApprove(config, result, chalk) {
|
|
|
221
417
|
: e.gpu_requirements || null,
|
|
222
418
|
source_summary: e.summary || null,
|
|
223
419
|
};
|
|
420
|
+
}
|
|
224
421
|
|
|
225
|
-
|
|
422
|
+
function _runPageUrl(caseId) {
|
|
423
|
+
return `https://aibadgr.com/run-issue?case_id=${caseId}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Creates the free, anonymous "prepared run" case that backs a shareable
|
|
427
|
+
// Run Link -- the exact same /run-issue/confirm call and case object
|
|
428
|
+
// /run-issue's own confirm step creates, so the CLI and the web page never
|
|
429
|
+
// diverge on what a given diagnosis resolves to. No GPU, no login, no
|
|
430
|
+
// payment -- confirm only ever inspects and plans (see its own docstring).
|
|
431
|
+
// Called once per diagnosis (main flow), then reused by --approve below
|
|
432
|
+
// instead of confirming a second time.
|
|
433
|
+
//
|
|
434
|
+
// Returns `{ caseData }` on success, `{ error }` on a network/API failure --
|
|
435
|
+
// never throws, since a plain diagnosis must still succeed even if the
|
|
436
|
+
// free run-page creation itself fails.
|
|
437
|
+
async function _createRunPage(config, e, chalk) {
|
|
226
438
|
try {
|
|
227
|
-
|
|
228
|
-
caseData = await callApi('/run-issue/confirm', {
|
|
439
|
+
const caseData = await callApi('/run-issue/confirm', {
|
|
229
440
|
method: 'POST',
|
|
230
441
|
apiKey: config.apiKey || '',
|
|
231
442
|
baseUrl: config.baseUrl,
|
|
232
|
-
body:
|
|
443
|
+
body: _confirmBodyFromExtraction(e),
|
|
233
444
|
timeoutMs: 20_000,
|
|
234
445
|
});
|
|
446
|
+
return caseData?.case_id ? { caseData } : { caseData: null };
|
|
235
447
|
} catch (err) {
|
|
236
|
-
|
|
237
|
-
process.exitCode = 1;
|
|
238
|
-
return;
|
|
448
|
+
return { error: err };
|
|
239
449
|
}
|
|
450
|
+
}
|
|
240
451
|
|
|
452
|
+
async function _doApprove(config, caseData, chalk) {
|
|
241
453
|
const { case_id: caseId, status, test_plan: plan, missing_information: missing } = caseData;
|
|
242
454
|
|
|
243
455
|
if (status === 'incompatible') {
|
|
@@ -386,6 +598,31 @@ async function _diagnoseExistingCase(config, caseId, flags, chalk) {
|
|
|
386
598
|
for (const m of caseData.missing_information) console.log(` ? ${m}`);
|
|
387
599
|
}
|
|
388
600
|
|
|
601
|
+
// "Verified" is only ever earned here -- a case whose real GPU run
|
|
602
|
+
// actually completed (`lead_summary.status`, computed server-side by
|
|
603
|
+
// `_build_lead_summary` from `ReproCase.status`/`final_command`). Never
|
|
604
|
+
// reachable from --smoke, which never provisions anything.
|
|
605
|
+
console.log();
|
|
606
|
+
const existingStatus = caseData.lead_summary?.status === 'verified'
|
|
607
|
+
|| caseData.status === 'verified' || caseData.status === 'completed'
|
|
608
|
+
? 'VERIFIED'
|
|
609
|
+
: (caseData.missing_information || []).length ? 'NEEDS INFO' : 'READY';
|
|
610
|
+
console.log(` ${chalk.bold(`STATUS: ${existingStatus}`)}`);
|
|
611
|
+
if (existingStatus === 'VERIFIED') {
|
|
612
|
+
console.log();
|
|
613
|
+
console.log(` ${chalk.bold('Command:')}`);
|
|
614
|
+
console.log(` ${caseData.final_command || caseData.known_command}`);
|
|
615
|
+
console.log();
|
|
616
|
+
console.log(` ${chalk.bold('Result:')}`);
|
|
617
|
+
const resultLines = caseData.lead_summary?.test_checklist?.length
|
|
618
|
+
? caseData.lead_summary.test_checklist
|
|
619
|
+
: ['Verified'];
|
|
620
|
+
for (const line of resultLines) console.log(` - ${line}`);
|
|
621
|
+
if (caseData.actual_cost_aud != null) {
|
|
622
|
+
console.log(` Verified on Badgr for AUD $${Number(caseData.actual_cost_aud).toFixed(2)}. GPU torn down after the test.`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
389
626
|
console.log();
|
|
390
627
|
if (caseData.free_verification_consumed) {
|
|
391
628
|
console.log(` ${chalk.dim('This case\'s free run has already been used — further verification is normal billing.')}`);
|
|
@@ -420,6 +657,7 @@ export async function diagnoseCommand(config, args, chalk) {
|
|
|
420
657
|
if (a === '--comfyui') { flags.comfyui = args[++i]; i++; continue; }
|
|
421
658
|
if (a === '--github') { flags.github = args[++i]; i++; continue; }
|
|
422
659
|
if (a === '--approve') { flags.approve = true; i++; continue; }
|
|
660
|
+
if (a === '--smoke') { flags.smoke = true; i++; continue; }
|
|
423
661
|
if (a === '--json') { flags.json = true; i++; continue; }
|
|
424
662
|
if (!a.startsWith('-') && positional === null) positional = a;
|
|
425
663
|
i++;
|
|
@@ -474,20 +712,87 @@ export async function diagnoseCommand(config, args, chalk) {
|
|
|
474
712
|
return;
|
|
475
713
|
}
|
|
476
714
|
|
|
715
|
+
const status = _statusWord(result, flags.smoke);
|
|
716
|
+
|
|
717
|
+
// A command that's actually runnable also gets a free prepared Run
|
|
718
|
+
// page -- the same case /run-issue's own confirm step would create --
|
|
719
|
+
// so diagnosis ends at a page someone can click Run on, not just a
|
|
720
|
+
// printed command. Only READY and SMOKE CHECKED qualify: NEEDS INFO has
|
|
721
|
+
// nothing runnable to save yet, and INVALID is a command a real check
|
|
722
|
+
// just proved broken -- turning that into a clickable "Run" page would
|
|
723
|
+
// contradict the label. Both print their own guidance instead (Missing:
|
|
724
|
+
// / the failed check) and stop there.
|
|
725
|
+
let caseData = null;
|
|
726
|
+
let caseError = null;
|
|
727
|
+
if (status === 'READY' || status === 'SMOKE CHECKED') {
|
|
728
|
+
const created = await _createRunPage(config, result.extraction || {}, chalk);
|
|
729
|
+
if (created.error) caseError = created.error;
|
|
730
|
+
else caseData = created.caseData;
|
|
731
|
+
}
|
|
732
|
+
|
|
477
733
|
if (flags.json) {
|
|
478
|
-
|
|
734
|
+
const output = { detected: detected.kind, ...result, status };
|
|
735
|
+
if (flags.smoke) {
|
|
736
|
+
const e = result.extraction || {};
|
|
737
|
+
output.smoke = {
|
|
738
|
+
mechanical_checks: e.mechanical_checks || [],
|
|
739
|
+
local_file_checks: _localPathCandidates(e).map(p => ({ path: p, exists: fs.existsSync(p) })),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
if (caseData?.case_id) {
|
|
743
|
+
output.run = {
|
|
744
|
+
case_id: caseData.case_id,
|
|
745
|
+
url: _runPageUrl(caseData.case_id),
|
|
746
|
+
canonical_command: caseData.findings?.canonical_command || null,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
console.log(JSON.stringify(output, null, 2));
|
|
750
|
+
|
|
751
|
+
// --json must not silently skip --approve -- the JSON branch used to
|
|
752
|
+
// return here unconditionally, so a scripted `--approve --json` call
|
|
753
|
+
// never claimed/approved/launched anything (bug: approval was silently
|
|
754
|
+
// dropped in the one output mode automation actually uses). Approval
|
|
755
|
+
// output itself stays human-readable (chalk console lines), matching
|
|
756
|
+
// _diagnoseExistingCase's existing --json + --approve behaviour below.
|
|
757
|
+
if (flags.approve) {
|
|
758
|
+
if (caseError) {
|
|
759
|
+
console.error(chalk.red(`\n ✗ Could not create case: ${caseError.message}\n`));
|
|
760
|
+
process.exitCode = 1;
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (!caseData) {
|
|
764
|
+
console.error(chalk.yellow('\n Cannot approve — missing information above.\n'));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
await _doApprove(config, caseData, chalk);
|
|
768
|
+
}
|
|
479
769
|
return;
|
|
480
770
|
}
|
|
481
771
|
|
|
482
772
|
console.log();
|
|
483
773
|
_printResult(result, chalk);
|
|
484
774
|
|
|
775
|
+
if (flags.smoke) {
|
|
776
|
+
_printSmokeChecks(result, chalk);
|
|
777
|
+
}
|
|
778
|
+
|
|
485
779
|
console.log();
|
|
780
|
+
_printCopySummary(result, flags, chalk, caseData);
|
|
781
|
+
|
|
486
782
|
console.log(chalk.dim(' Free diagnosis. No GPU was provisioned.'));
|
|
487
783
|
console.log(chalk.dim(' Full interactive flow: https://aibadgr.com/run-issue'));
|
|
488
784
|
console.log();
|
|
489
785
|
|
|
490
786
|
if (flags.approve) {
|
|
491
|
-
|
|
787
|
+
if (caseError) {
|
|
788
|
+
console.error(chalk.red(`\n ✗ Could not create case: ${caseError.message}\n`));
|
|
789
|
+
process.exitCode = 1;
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (!caseData) {
|
|
793
|
+
console.log(chalk.yellow('\n Cannot approve — missing information above.\n'));
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
await _doApprove(config, caseData, chalk);
|
|
492
797
|
}
|
|
493
798
|
}
|
package/src/commands/serve.js
CHANGED
|
@@ -357,9 +357,10 @@ export async function serveCommand(config, args, chalk) {
|
|
|
357
357
|
const { model, flags } = parseServeArgs(args);
|
|
358
358
|
const customImage = flags.image || null;
|
|
359
359
|
const isLlamaCpp = flags.runtime === 'llama.cpp';
|
|
360
|
+
const isOllama = flags.runtime === 'ollama';
|
|
360
361
|
|
|
361
362
|
// Expand blessed alias (qwen-7b, llama-8b, qwen-coder-7b) to full model ID + GPU.
|
|
362
|
-
const vllmAlias = model && !customImage && !isLlamaCpp ? BLESSED_VLLM_MODELS[model] : null;
|
|
363
|
+
const vllmAlias = model && !customImage && !isLlamaCpp && !isOllama ? BLESSED_VLLM_MODELS[model] : null;
|
|
363
364
|
const effectiveModel = vllmAlias ? vllmAlias.model_id : model;
|
|
364
365
|
|
|
365
366
|
// Detect flags that ended up as positional args due to broken shell line continuation
|
|
@@ -564,6 +565,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
564
565
|
return {
|
|
565
566
|
...(effectiveModel ? { model: effectiveModel } : {}),
|
|
566
567
|
...(isLlamaCpp ? { image: LLAMA_CPP_IMAGE } : customImage ? { image: customImage } : {}),
|
|
568
|
+
...(isOllama ? { runtime: 'ollama' } : {}),
|
|
567
569
|
...(flags.task ? { task: flags.task } : {}),
|
|
568
570
|
gpu: gpuOverride || gpu,
|
|
569
571
|
gpu_count: flags.count || 1,
|
|
@@ -649,7 +651,9 @@ export async function serveCommand(config, args, chalk) {
|
|
|
649
651
|
}
|
|
650
652
|
|
|
651
653
|
// ── Determine health check path ───────────────────────────────────────────
|
|
652
|
-
const resolvedHealthPath =
|
|
654
|
+
const resolvedHealthPath = isOllama
|
|
655
|
+
? (flags.healthPath || '/api/tags')
|
|
656
|
+
: _resolveHealthPath({ healthPath: flags.healthPath, isLlamaCpp, customImage, task: flags.task });
|
|
653
657
|
|
|
654
658
|
// Gated-model guidance is only shown when it's actually needed — on failure —
|
|
655
659
|
// not up front, so common launches stay short and uncluttered.
|
|
@@ -770,7 +774,13 @@ export async function serveCommand(config, args, chalk) {
|
|
|
770
774
|
console.log();
|
|
771
775
|
console.log(chalk.dim(' Billing continues until you run: ') + chalk.cyan(`badgr down ${dep.deployment_id}`));
|
|
772
776
|
|
|
773
|
-
if (endpointReady &&
|
|
777
|
+
if (endpointReady && isOllama) {
|
|
778
|
+
console.log(` ${chalk.bold('Test with curl:')}`);
|
|
779
|
+
console.log(chalk.dim(` curl ${endpointUrl}/api/generate \\`));
|
|
780
|
+
console.log(chalk.dim(` -H "Content-Type: application/json" \\`));
|
|
781
|
+
console.log(chalk.dim(` -d '{"model":"${dep.model || effectiveModel}","prompt":"Hello","stream":false}'`));
|
|
782
|
+
console.log();
|
|
783
|
+
} else if (endpointReady && !customImage) {
|
|
774
784
|
// dep.endpoint_api_key is a per-endpoint key generated for this deployment
|
|
775
785
|
// (vLLM model serves only) — shown exactly once, here. Falls back to the
|
|
776
786
|
// account-wide key (truncated) for serves that don't get one yet
|
package/src/errors.js
CHANGED
|
@@ -17,7 +17,12 @@ export const CATALOG = {
|
|
|
17
17
|
// ── Capacity ──────────────────────────────────────────────────────────────
|
|
18
18
|
|
|
19
19
|
NO_CAPACITY: {
|
|
20
|
+
// Prefer the backend's specific detail (e.g. "CPU launch is not
|
|
21
|
+
// configured on this backend (missing HETZNER_API_TOKEN)") over the
|
|
22
|
+
// generic template — a real config/provider reason is far more
|
|
23
|
+
// actionable than "not available right now" when that's not why.
|
|
20
24
|
message: (ctx) =>
|
|
25
|
+
ctx.server_message ||
|
|
21
26
|
`No ${ctx.gpu || 'GPU'} available${ctx.region ? ` in ${ctx.region}` : ''} right now.`,
|
|
22
27
|
billing: 'never_started',
|
|
23
28
|
retried: false,
|
package/src/fallback.js
CHANGED
|
@@ -93,7 +93,10 @@ export async function callWithFallback(endpoint, callOpts, buildBody, effectiveT
|
|
|
93
93
|
if (key) {
|
|
94
94
|
const ctx = {
|
|
95
95
|
gpu: d.filters?.gpu ?? d.gpu,
|
|
96
|
-
|
|
96
|
+
// Server uses the literal string "any" as a filters.region placeholder
|
|
97
|
+
// when no region was requested — don't surface that as if the user
|
|
98
|
+
// had actually asked for a region named "any" (e.g. "in any right now").
|
|
99
|
+
region: d.filters?.region && d.filters.region !== 'any' ? d.filters.region : undefined,
|
|
97
100
|
failure_category: d.failure_category,
|
|
98
101
|
low_cost_failed: d.low_cost_provider_failed,
|
|
99
102
|
server_message: d.message,
|