badgr-cli 1.0.38 → 1.0.40
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 +64 -1
- package/package.json +12 -8
- package/src/badgr.js +8 -0
- package/src/catalog.js +193 -1
- package/src/commands/comfyui.js +1 -1
- package/src/commands/receipts.js +2 -1
- package/src/commands/run.js +111 -25
- package/src/commands/test-run.js +4 -4
- package/src/commands/workload.js +197 -0
- package/src/commands/workspace.js +136 -0
- package/tests/commands.test.js +48 -0
- package/tests/run-lifecycle.test.js +55 -18
- package/tests/template.test.js +6 -4
- package/tests/workload-rerun.test.js +56 -0
- package/tests/workload-templates.test.js +1 -1
- package/tests/workload-workspace-paths.test.js +46 -0
package/README.md
CHANGED
|
@@ -46,6 +46,14 @@ badgr down <deployment-id>
|
|
|
46
46
|
| `badgr capacity` | Check available GPU capacity right now |
|
|
47
47
|
| `badgr billing` | Show balance and add funds |
|
|
48
48
|
| `badgr test` | Run an end-to-end test (provision → run → teardown) |
|
|
49
|
+
| `badgr workload list` | List saved workloads with run stats |
|
|
50
|
+
| `badgr workload run <name>` | Rerun a saved workload by name or ID |
|
|
51
|
+
| `badgr workload info <name>` | Show stats, route history, and recent jobs for a workload |
|
|
52
|
+
| `badgr workload delete <name>` | Delete a saved workload |
|
|
53
|
+
| `badgr workspace list` | List workspace trackers (job history + cost per context) |
|
|
54
|
+
| `badgr workspace create <name>` | Create a workspace tracker, optionally linked to a storage path |
|
|
55
|
+
| `badgr workspace info <name>` | Show jobs, cost, and files for a workspace |
|
|
56
|
+
| `badgr workspace delete <name>` | Archive a workspace tracker |
|
|
49
57
|
|
|
50
58
|
`badgr serve` — for anything that needs a persistent endpoint: LLM serving, embeddings, image generation APIs, transcription APIs.
|
|
51
59
|
|
|
@@ -101,6 +109,8 @@ badgr run python train.py --gpu A100 --env HF_TOKEN=$HF_TOKEN --max-runtime 60
|
|
|
101
109
|
| `--max-runtime <min>` | — | Auto-stop after N minutes (recommended) |
|
|
102
110
|
| `--max-cost <$>` | — | Auto-stop when total spend reaches this amount |
|
|
103
111
|
| `--detach` | — | Launch and return immediately, don't stream logs |
|
|
112
|
+
| `--save <name>` | — | Save this job as a named workload after it completes |
|
|
113
|
+
| `--workspace <name\|id>` | — | Link this job to a workspace tracker (name or `ws_…` ID) |
|
|
104
114
|
|
|
105
115
|
---
|
|
106
116
|
|
|
@@ -199,6 +209,59 @@ Accepts a public URL, S3/GCS URI, or a local text file under 10 MB. Outputs JSON
|
|
|
199
209
|
|
|
200
210
|
---
|
|
201
211
|
|
|
212
|
+
## Workloads
|
|
213
|
+
|
|
214
|
+
A workload is a saved job configuration. Once saved, you can rerun it by name instead of retyping all the flags. Badgr tracks success rate, average cost, average runtime, and the last known-good route so reruns are faster and cheaper over time.
|
|
215
|
+
|
|
216
|
+
**Save a workload** by adding `--save <name>` to any `badgr run` call:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
badgr run python train.py --gpu A100 --env HF_TOKEN=$HF_TOKEN --max-runtime 120 --save my-training-job
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Rerun a saved workload:**
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
badgr workload run my-training-job
|
|
226
|
+
badgr workload run my-training-job --max-cost 5 # override spend cap
|
|
227
|
+
badgr workload run my-training-job --set HF_TOKEN=newval # override an env var
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Inspect and manage workloads:**
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
badgr workload list # list all saved workloads with stats
|
|
234
|
+
badgr workload info my-training-job # stats, route history, recent jobs
|
|
235
|
+
badgr workload delete my-training-job
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
| `badgr workload run` flag | Description |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `--max-cost <$>` | Override the saved spend cap for this run |
|
|
241
|
+
| `--max-runtime <min>` | Override the saved runtime limit for this run |
|
|
242
|
+
| `--set KEY=VALUE` | Override a saved config value for this run (repeatable) |
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Workspaces
|
|
247
|
+
|
|
248
|
+
A workspace tracker groups jobs by a named context — useful for tracking cost and job history across a project or storage path.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
badgr workspace create my-project --storage s3://my-bucket/runs --desc "nightly evals"
|
|
252
|
+
badgr run python eval.py --workspace my-project --max-cost 5
|
|
253
|
+
badgr workspace info my-project # jobs, total cost, files
|
|
254
|
+
badgr workspace list
|
|
255
|
+
badgr workspace delete my-project
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
| `badgr workspace create` flag | Description |
|
|
259
|
+
|---|---|
|
|
260
|
+
| `--storage <path>` | S3/GCS path to associate with this workspace |
|
|
261
|
+
| `--desc <text>` | Optional description |
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
202
265
|
## Routing
|
|
203
266
|
|
|
204
267
|
Badgr Auto selects the best eligible route based on GPU type, VRAM, availability, region, workload requirements, and reliability. Advanced users can optionally choose an execution tier or hardware constraint.
|
|
@@ -275,7 +338,7 @@ Additional GPU types may be routable depending on current capacity — check wit
|
|
|
275
338
|
|
|
276
339
|
Pricing is confirmed before provisioning. Use `--dry-run` to see pricing before committing.
|
|
277
340
|
|
|
278
|
-
Full GPU support details: see [
|
|
341
|
+
Full GPU support details: see [NOTES.md](../../NOTES.md#gpu-support) in the repo root.
|
|
279
342
|
|
|
280
343
|
---
|
|
281
344
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "badgr-cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Badgr
|
|
3
|
+
"version": "1.0.40",
|
|
4
|
+
"description": "Badgr — run or serve GPU workloads from one command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"badgr": "src/badgr.js"
|
|
@@ -12,23 +12,27 @@
|
|
|
12
12
|
"test:watch": "vitest"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"
|
|
16
|
-
"
|
|
15
|
+
"chalk": "^5.3.0",
|
|
16
|
+
"@inquirer/prompts": "^8.5.2"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"vitest": "^4.1.8"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
|
-
"node": ">=
|
|
22
|
+
"node": ">=20.10.0"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|
|
25
25
|
"gpu",
|
|
26
26
|
"cli",
|
|
27
27
|
"ai",
|
|
28
28
|
"compute",
|
|
29
|
-
"modal",
|
|
30
29
|
"gateway",
|
|
31
|
-
"openai"
|
|
30
|
+
"openai",
|
|
31
|
+
"workload",
|
|
32
|
+
"workspace",
|
|
33
|
+
"llm",
|
|
34
|
+
"inference",
|
|
35
|
+
"fine-tuning"
|
|
32
36
|
],
|
|
33
|
-
"license": "
|
|
37
|
+
"license": "Apache-2.0"
|
|
34
38
|
}
|
package/src/badgr.js
CHANGED
|
@@ -18,6 +18,8 @@ import { trainCommand } from './commands/train.js';
|
|
|
18
18
|
import { transcribeCommand } from './commands/transcribe.js';
|
|
19
19
|
import { embedCommand } from './commands/embed.js';
|
|
20
20
|
import { templateCommand } from './commands/template.js';
|
|
21
|
+
import { workloadCommand } from './commands/workload.js';
|
|
22
|
+
import { workspaceCommand } from './commands/workspace.js';
|
|
21
23
|
|
|
22
24
|
const HELP = `
|
|
23
25
|
${chalk.bold('badgr')} — run or serve GPU workloads from one command
|
|
@@ -33,6 +35,10 @@ ${chalk.bold('COMMANDS')}
|
|
|
33
35
|
${chalk.cyan('badgr test')} Run an end-to-end test (provision → run → teardown)
|
|
34
36
|
${chalk.cyan('badgr capacity')} Check what GPU capacity is available right now
|
|
35
37
|
${chalk.cyan('badgr billing')} Show balance and add funds
|
|
38
|
+
${chalk.cyan('badgr workload list')} List saved workloads
|
|
39
|
+
${chalk.cyan('badgr workload run <name>')} Rerun a saved workload
|
|
40
|
+
${chalk.cyan('badgr workspace list')} List workspace trackers (job history + cost per named context)
|
|
41
|
+
${chalk.cyan('badgr workspace create')} Create a workspace tracker (link jobs to a named storage path)
|
|
36
42
|
|
|
37
43
|
${chalk.bold('SHORTCUTS')} ${chalk.dim('(wrappers around run / serve for common workloads)')}
|
|
38
44
|
${chalk.cyan('badgr comfyui run <workflow.json>')} Launch ComfyUI, return endpoint URL
|
|
@@ -147,6 +153,8 @@ async function main() {
|
|
|
147
153
|
case 'transcribe': return transcribeCommand(config, rest, chalk);
|
|
148
154
|
case 'embed': return embedCommand(config, rest, chalk);
|
|
149
155
|
case 'template': return templateCommand(config, rest, chalk);
|
|
156
|
+
case 'workload': return workloadCommand(config, rest, chalk);
|
|
157
|
+
case 'workspace': return workspaceCommand(config, rest, chalk);
|
|
150
158
|
// legacy aliases kept for compatibility
|
|
151
159
|
case 'up': return upCommand(config, rest, chalk);
|
|
152
160
|
case 'config': {
|
package/src/catalog.js
CHANGED
|
@@ -9,7 +9,7 @@ export const TEMPLATES = [
|
|
|
9
9
|
title: 'ComfyUI',
|
|
10
10
|
description: 'Stable Diffusion image generation with ComfyUI node editor',
|
|
11
11
|
type: 'endpoint',
|
|
12
|
-
image: 'yanwk/comfyui-boot:
|
|
12
|
+
image: process.env.COMFYUI_IMAGE || 'yanwk/comfyui-boot:cu126-megapak',
|
|
13
13
|
gpu: 'RTX_4090',
|
|
14
14
|
gpu_count: 1,
|
|
15
15
|
port: 8188,
|
|
@@ -222,6 +222,198 @@ export const TEMPLATES = [
|
|
|
222
222
|
'HF_TOKEN required for Llama, Gemma, and other gated models.',
|
|
223
223
|
],
|
|
224
224
|
},
|
|
225
|
+
{
|
|
226
|
+
name: 'auto1111',
|
|
227
|
+
title: 'AUTOMATIC1111',
|
|
228
|
+
description: 'Stable Diffusion web UI — the largest SD ecosystem with ControlNet, LoRA, img2img, inpainting, and upscaling',
|
|
229
|
+
type: 'endpoint',
|
|
230
|
+
image: 'aidockorg/stable-diffusion-webui-cuda:latest',
|
|
231
|
+
gpu: 'RTX_4090',
|
|
232
|
+
gpu_count: 1,
|
|
233
|
+
port: 7860,
|
|
234
|
+
health_path: '/',
|
|
235
|
+
min_vram_gb: 6,
|
|
236
|
+
env: {
|
|
237
|
+
WEBUI_FLAGS: '--xformers',
|
|
238
|
+
},
|
|
239
|
+
notes: [
|
|
240
|
+
'Web UI at the returned endpoint URL (port 7860).',
|
|
241
|
+
'Place model checkpoints under /opt/dl-ui/repositories/stable-diffusion-webui/models/Stable-diffusion/.',
|
|
242
|
+
'Override WEBUI_FLAGS to pass extra launch args (e.g. --medvram for lower VRAM).',
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'forge',
|
|
247
|
+
title: 'Stable Diffusion WebUI Forge',
|
|
248
|
+
description: 'Optimised fork of A1111 with GPU memory improvements — runs models that OOM on standard A1111',
|
|
249
|
+
type: 'endpoint',
|
|
250
|
+
image: 'aidockorg/stable-diffusion-webui-forge-cuda:latest',
|
|
251
|
+
gpu: 'RTX_4090',
|
|
252
|
+
gpu_count: 1,
|
|
253
|
+
port: 7860,
|
|
254
|
+
health_path: '/',
|
|
255
|
+
min_vram_gb: 4,
|
|
256
|
+
env: {
|
|
257
|
+
WEBUI_FLAGS: '--xformers',
|
|
258
|
+
},
|
|
259
|
+
notes: [
|
|
260
|
+
'Web UI at the returned endpoint URL (port 7860).',
|
|
261
|
+
'Forge uses less VRAM than A1111 for the same model — good for 8–12 GB cards.',
|
|
262
|
+
'Compatible with most A1111 extensions.',
|
|
263
|
+
],
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'nerfstudio',
|
|
267
|
+
title: 'Nerfstudio',
|
|
268
|
+
description: 'NeRF and 3D Gaussian Splatting training and reconstruction from image captures',
|
|
269
|
+
type: 'job',
|
|
270
|
+
image: 'ghcr.io/nerfstudio-project/nerfstudio:latest',
|
|
271
|
+
gpu: 'RTX_4090',
|
|
272
|
+
gpu_count: 1,
|
|
273
|
+
min_vram_gb: 16,
|
|
274
|
+
env: {
|
|
275
|
+
METHOD: 'nerfacto',
|
|
276
|
+
},
|
|
277
|
+
notes: [
|
|
278
|
+
'Mount your images to /workspace/data and your output dir to /workspace/outputs.',
|
|
279
|
+
'Override METHOD to switch between nerfacto, splatfacto, instant-ngp, etc.',
|
|
280
|
+
'Training a typical scene takes 15–45 min depending on method and image count.',
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: 'openfold',
|
|
285
|
+
title: 'OpenFold',
|
|
286
|
+
description: 'Trainable, memory-efficient GPU-friendly PyTorch reproduction of AlphaFold 2 for protein structure prediction',
|
|
287
|
+
type: 'job',
|
|
288
|
+
image: 'nvidia/cuda:12.1.0-cudnn8-devel-ubuntu22.04',
|
|
289
|
+
gpu: 'A100',
|
|
290
|
+
gpu_count: 1,
|
|
291
|
+
min_vram_gb: 40,
|
|
292
|
+
env: {
|
|
293
|
+
OPENFOLD_REPO: 'https://github.com/aqlaboratory/openfold',
|
|
294
|
+
},
|
|
295
|
+
notes: [
|
|
296
|
+
'No pre-built public image exists — this uses a CUDA base; clone and install OpenFold at boot.',
|
|
297
|
+
'See https://github.com/aqlaboratory/openfold for full install steps.',
|
|
298
|
+
'Full inference on a single sequence: ~5–20 min on an A100.',
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'blender-render',
|
|
303
|
+
title: 'Blender GPU Render',
|
|
304
|
+
description: 'Headless Blender GPU rendering via CUDA — render .blend files to image sequences or video',
|
|
305
|
+
type: 'job',
|
|
306
|
+
image: 'blenderkit/headless-blender:blender-4.4',
|
|
307
|
+
gpu: 'RTX_4090',
|
|
308
|
+
gpu_count: 1,
|
|
309
|
+
min_vram_gb: 8,
|
|
310
|
+
env: {
|
|
311
|
+
BLEND_FILE: '/workspace/scene.blend',
|
|
312
|
+
FRAME_START: '1',
|
|
313
|
+
FRAME_END: '250',
|
|
314
|
+
},
|
|
315
|
+
notes: [
|
|
316
|
+
'Mount your .blend file to /workspace/scene.blend.',
|
|
317
|
+
'Set FRAME_START and FRAME_END to the frame range to render.',
|
|
318
|
+
'Output frames are written to /workspace/output/ by default.',
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: 'openmm',
|
|
323
|
+
title: 'OpenMM',
|
|
324
|
+
description: 'GPU-accelerated molecular dynamics simulation using the OpenMM toolkit',
|
|
325
|
+
type: 'job',
|
|
326
|
+
image: 'saladtechnologies/openmm:latest',
|
|
327
|
+
gpu: 'RTX_4090',
|
|
328
|
+
gpu_count: 1,
|
|
329
|
+
min_vram_gb: 8,
|
|
330
|
+
env: {
|
|
331
|
+
SIMULATION_SCRIPT: '/workspace/simulate.py',
|
|
332
|
+
},
|
|
333
|
+
notes: [
|
|
334
|
+
'Mount your Python simulation script to /workspace/simulate.py.',
|
|
335
|
+
'OpenMM auto-selects CUDA platform when a GPU is present.',
|
|
336
|
+
'Typical MD simulation runs: minutes to hours depending on system size and steps.',
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: 'gromacs',
|
|
341
|
+
title: 'GROMACS',
|
|
342
|
+
description: 'High-performance molecular dynamics package with GPU-accelerated bonded and non-bonded calculations',
|
|
343
|
+
type: 'job',
|
|
344
|
+
image: 'scientiflow/gromacs-gpu:2024.3',
|
|
345
|
+
gpu: 'RTX_4090',
|
|
346
|
+
gpu_count: 1,
|
|
347
|
+
min_vram_gb: 8,
|
|
348
|
+
env: {
|
|
349
|
+
GMX_GPU_DD_COMMS: 'true',
|
|
350
|
+
GMX_GPU_PME_PP_COMMS: 'true',
|
|
351
|
+
},
|
|
352
|
+
notes: [
|
|
353
|
+
'Mount your .tpr input file and output directory to /workspace.',
|
|
354
|
+
'GROMACS auto-detects CUDA GPU — no extra flags needed.',
|
|
355
|
+
'For multi-GPU: increase gpu_count and pass -ntmpi / -ntomp flags via command override.',
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: 'lammps',
|
|
360
|
+
title: 'LAMMPS',
|
|
361
|
+
description: 'Large-scale Atomic/Molecular Massively Parallel Simulator with GPU acceleration via Kokkos/CUDA',
|
|
362
|
+
type: 'job',
|
|
363
|
+
image: 'nvcr.io/hpc/lammps:release-29Sep2021',
|
|
364
|
+
gpu: 'RTX_4090',
|
|
365
|
+
gpu_count: 1,
|
|
366
|
+
min_vram_gb: 8,
|
|
367
|
+
env: {
|
|
368
|
+
LAMMPS_INPUT: '/workspace/in.lammps',
|
|
369
|
+
},
|
|
370
|
+
notes: [
|
|
371
|
+
'Mount your LAMMPS input script to /workspace/in.lammps.',
|
|
372
|
+
'GPU acceleration uses the Kokkos or GPU package — enable via -pk gpu 1 in your input.',
|
|
373
|
+
'Simulation runtime scales with system size; use --max-runtime to cap spend.',
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'diffusers',
|
|
378
|
+
title: 'Hugging Face Diffusers',
|
|
379
|
+
description: 'State-of-the-art diffusion model library for image, video, and audio generation',
|
|
380
|
+
type: 'job',
|
|
381
|
+
image: 'diffusers/diffusers-pytorch-cuda:latest',
|
|
382
|
+
gpu: 'RTX_4090',
|
|
383
|
+
gpu_count: 1,
|
|
384
|
+
min_vram_gb: 16,
|
|
385
|
+
env: {
|
|
386
|
+
HF_TOKEN: '<for-gated-models>',
|
|
387
|
+
MODEL_ID: 'stabilityai/stable-diffusion-xl-base-1.0',
|
|
388
|
+
SCRIPT: '/workspace/run.py',
|
|
389
|
+
},
|
|
390
|
+
notes: [
|
|
391
|
+
'Mount your generation or training script to /workspace/run.py.',
|
|
392
|
+
'HF_TOKEN required for gated models.',
|
|
393
|
+
'Supports SDXL, FLUX, Stable Diffusion 3, ControlNet, IP-Adapter, and more.',
|
|
394
|
+
],
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: 'torchtune',
|
|
398
|
+
title: 'torchtune',
|
|
399
|
+
description: 'PyTorch-native post-training library for LLM fine-tuning via config-driven YAML recipes',
|
|
400
|
+
type: 'job',
|
|
401
|
+
image: 'pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel',
|
|
402
|
+
gpu: 'A100',
|
|
403
|
+
gpu_count: 1,
|
|
404
|
+
min_vram_gb: 40,
|
|
405
|
+
env: {
|
|
406
|
+
HF_TOKEN: '<for-gated-models>',
|
|
407
|
+
RECIPE: 'lora_finetune_single_device',
|
|
408
|
+
CONFIG: '/workspace/config.yaml',
|
|
409
|
+
},
|
|
410
|
+
notes: [
|
|
411
|
+
'Install torchtune at boot: pip install torchtune.',
|
|
412
|
+
'Mount your YAML recipe config to /workspace/config.yaml.',
|
|
413
|
+
'HF_TOKEN required for Llama and other gated base models.',
|
|
414
|
+
'Supports LoRA, QLoRA, DPO, full fine-tune, and quantisation-aware training.',
|
|
415
|
+
],
|
|
416
|
+
},
|
|
225
417
|
];
|
|
226
418
|
|
|
227
419
|
export const TEMPLATE_MAP = Object.fromEntries(TEMPLATES.map(t => [t.name, t]));
|
package/src/commands/comfyui.js
CHANGED
|
@@ -12,7 +12,7 @@ import { addDeployment, addReceipt, updateReceipt, generateReceiptId } from '../
|
|
|
12
12
|
import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD } from '../fallback.js';
|
|
13
13
|
import { formatCliError } from '../errors.js';
|
|
14
14
|
|
|
15
|
-
const COMFYUI_IMAGE = 'yanwk/comfyui-boot:
|
|
15
|
+
const COMFYUI_IMAGE = process.env.COMFYUI_IMAGE || 'yanwk/comfyui-boot:cu126-megapak';
|
|
16
16
|
const HEALTH_PATH = '/system_stats';
|
|
17
17
|
const WAIT_TIMEOUT_MS = 10 * 60 * 1000; // 10 min — model downloads on first boot
|
|
18
18
|
const MAX_WORKFLOW_B = 1 * 1024 * 1024; // 1 MB workflow limit
|
package/src/commands/receipts.js
CHANGED
package/src/commands/run.js
CHANGED
|
@@ -14,32 +14,42 @@ import { TEMPLATE_MAP, buildTemplateFlags, parseTemplateOverrides } from '../cat
|
|
|
14
14
|
export function parseRunArgs(args) {
|
|
15
15
|
const flags = {};
|
|
16
16
|
const positional = [];
|
|
17
|
+
|
|
18
|
+
// Split at -- so badgr flags and the user command don't collide.
|
|
19
|
+
// Everything before -- is parsed for flags; everything after becomes commandArgv.
|
|
20
|
+
const sepIdx = args.indexOf('--');
|
|
21
|
+
const flagArgs = sepIdx === -1 ? args : args.slice(0, sepIdx);
|
|
22
|
+
const commandArgv = sepIdx === -1 ? null : args.slice(sepIdx + 1);
|
|
23
|
+
|
|
17
24
|
let i = 0;
|
|
18
|
-
while (i <
|
|
19
|
-
if (
|
|
20
|
-
if (
|
|
21
|
-
if (
|
|
22
|
-
if (
|
|
23
|
-
if (
|
|
24
|
-
if (
|
|
25
|
-
if (
|
|
26
|
-
if (
|
|
27
|
-
if (
|
|
28
|
-
if (
|
|
29
|
-
if (
|
|
30
|
-
if (
|
|
31
|
-
if (
|
|
32
|
-
if (
|
|
33
|
-
if (
|
|
34
|
-
if (
|
|
35
|
-
|
|
25
|
+
while (i < flagArgs.length) {
|
|
26
|
+
if (flagArgs[i] === '--gpu') { flags.gpu = flagArgs[++i]; i++; continue; }
|
|
27
|
+
if (flagArgs[i] === '--image') { flags.image = flagArgs[++i]; i++; continue; }
|
|
28
|
+
if (flagArgs[i] === '--count') { flags.count = parseInt(flagArgs[++i], 10); i++; continue; }
|
|
29
|
+
if (flagArgs[i] === '--region') { flags.region = flagArgs[++i]; i++; continue; }
|
|
30
|
+
if (flagArgs[i] === '--tier') { flags.tier = flagArgs[++i]; i++; continue; }
|
|
31
|
+
if (flagArgs[i] === '--max-price') { flags.maxPrice = parseFloat(flagArgs[++i]); i++; continue; }
|
|
32
|
+
if (flagArgs[i] === '--name') { flags.name = flagArgs[++i]; i++; continue; }
|
|
33
|
+
if (flagArgs[i] === '--detach') { flags.detach = true; i++; continue; }
|
|
34
|
+
if (flagArgs[i] === '--fallback') { flags.fallback = flagArgs[++i]; i++; continue; }
|
|
35
|
+
if (flagArgs[i] === '--no-fallback') { flags.noFallback = true; i++; continue; }
|
|
36
|
+
if (flagArgs[i] === '--strict-capacity') { flags.noFallback = true; i++; continue; }
|
|
37
|
+
if (flagArgs[i] === '--no-expanded-search') { flags.noFallback = true; i++; continue; }
|
|
38
|
+
if (flagArgs[i] === '--max-runtime') { flags.maxRuntime = parseFloat(flagArgs[++i]); i++; continue; }
|
|
39
|
+
if (flagArgs[i] === '--max-cost') { flags.maxCost = parseFloat(flagArgs[++i]); i++; continue; }
|
|
40
|
+
if (flagArgs[i] === '--min-vram') { flags.minVram = parseFloat(flagArgs[++i]); i++; continue; }
|
|
41
|
+
if (flagArgs[i] === '--dry-run') { flags.dryRun = true; i++; continue; }
|
|
42
|
+
if (flagArgs[i] === '--save') { flags.save = flagArgs[++i]; i++; continue; }
|
|
43
|
+
if (flagArgs[i] === '--workspace') { flags.workspace = flagArgs[++i]; i++; continue; }
|
|
44
|
+
if (flagArgs[i] === '--env') {
|
|
45
|
+
const kv = flagArgs[++i]; i++;
|
|
36
46
|
if (!flags.env) flags.env = [];
|
|
37
47
|
flags.env.push(kv);
|
|
38
48
|
continue;
|
|
39
49
|
}
|
|
40
|
-
positional.push(
|
|
50
|
+
positional.push(flagArgs[i++]);
|
|
41
51
|
}
|
|
42
|
-
return { flags, positional };
|
|
52
|
+
return { flags, positional, commandArgv };
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
function parseEnvFlag(envList) {
|
|
@@ -332,6 +342,7 @@ const _KNOWN_RUN_FLAGS = new Set([
|
|
|
332
342
|
'--gpu', '--image', '--count', '--region', '--tier', '--max-price', '--name',
|
|
333
343
|
'--detach', '--fallback', '--no-fallback', '--strict-capacity',
|
|
334
344
|
'--no-expanded-search', '--max-runtime', '--max-cost', '--min-vram', '--env',
|
|
345
|
+
'--dry-run',
|
|
335
346
|
]);
|
|
336
347
|
|
|
337
348
|
export async function runCommand(config, args, chalk) {
|
|
@@ -357,7 +368,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
357
368
|
return runCommand(config, expandedArgs, chalk);
|
|
358
369
|
}
|
|
359
370
|
|
|
360
|
-
const { flags, positional } = parseRunArgs(args);
|
|
371
|
+
const { flags, positional, commandArgv } = parseRunArgs(args);
|
|
361
372
|
|
|
362
373
|
// Detect flags that ended up in the command because of broken shell line continuation
|
|
363
374
|
// (e.g. `\ ` with trailing space instead of `\<newline>`).
|
|
@@ -376,12 +387,17 @@ export async function runCommand(config, args, chalk) {
|
|
|
376
387
|
return;
|
|
377
388
|
}
|
|
378
389
|
|
|
379
|
-
if (positional.length === 0 && !flags.image) {
|
|
380
|
-
console.error(chalk.red('Usage: badgr run <command...>'));
|
|
390
|
+
if (positional.length === 0 && commandArgv === null && !flags.image) {
|
|
391
|
+
console.error(chalk.red('Usage: badgr run --gpu <GPU> --image <image> --max-cost <n> -- <command...>'));
|
|
381
392
|
console.error(chalk.red(' badgr run --image my/image:latest'));
|
|
382
393
|
return;
|
|
383
394
|
}
|
|
384
|
-
|
|
395
|
+
if (commandArgv !== null && commandArgv.length === 0 && !flags.image) {
|
|
396
|
+
console.error(chalk.red(' ✗ No command after --. Provide a command or --image.'));
|
|
397
|
+
console.error(chalk.dim(' Example: badgr run --gpu RTX_4090 --max-cost 1 -- node script.js'));
|
|
398
|
+
process.exitCode = 1;
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
385
401
|
requireApiKey(config);
|
|
386
402
|
|
|
387
403
|
// ── Validate flags early ───────────────────────────────────────────────────
|
|
@@ -405,7 +421,18 @@ export async function runCommand(config, args, chalk) {
|
|
|
405
421
|
process.exitCode = 1;
|
|
406
422
|
return;
|
|
407
423
|
}
|
|
408
|
-
|
|
424
|
+
|
|
425
|
+
if (!flags.maxCost && !flags.dryRun) {
|
|
426
|
+
console.error(chalk.red('\n ✗ --max-cost is required for run workloads.\n'));
|
|
427
|
+
console.error(chalk.dim(' Example: badgr run --gpu RTX_4090 --image node:20 --max-cost 5 -- node script.js\n'));
|
|
428
|
+
process.exitCode = 1;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// commandArgv is set when -- separator was used; fall back to positional for legacy syntax.
|
|
433
|
+
const command = commandArgv !== null
|
|
434
|
+
? (commandArgv.length > 0 ? commandArgv : undefined)
|
|
435
|
+
: (positional.length > 0 ? positional : undefined);
|
|
409
436
|
const cmdStr = command ? command.join(' ') : '';
|
|
410
437
|
const isSmoke = cmdStr.length < 80 && /print\s*\(|['"]hello/i.test(cmdStr);
|
|
411
438
|
const inferredImage = isSmoke ? 'python:3.11-alpine' : 'python:3.11-slim';
|
|
@@ -424,6 +451,19 @@ export async function runCommand(config, args, chalk) {
|
|
|
424
451
|
|
|
425
452
|
const gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : undefined;
|
|
426
453
|
|
|
454
|
+
if (flags.dryRun) {
|
|
455
|
+
console.log(chalk.bold('\n⚡ Dry run — no GPU will be provisioned\n'));
|
|
456
|
+
if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
|
|
457
|
+
if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
|
|
458
|
+
console.log(` ${chalk.bold('GPU:')} ${gpu || chalk.dim('auto')}`);
|
|
459
|
+
console.log(` ${chalk.bold('Max runtime:')} ${effectiveMaxRuntime}min`);
|
|
460
|
+
if (maxCost) console.log(` ${chalk.bold('Max cost:')} $${maxCost}`);
|
|
461
|
+
if (flags.maxPrice) console.log(` ${chalk.bold('Max price:')} $${flags.maxPrice}/hr`);
|
|
462
|
+
if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
|
|
463
|
+
console.log(chalk.dim('\n Remove --dry-run to provision.\n'));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
427
467
|
console.log(chalk.bold('\n⚡ Running GPU job\n'));
|
|
428
468
|
if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
|
|
429
469
|
if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
|
|
@@ -441,6 +481,23 @@ export async function runCommand(config, args, chalk) {
|
|
|
441
481
|
console.log();
|
|
442
482
|
|
|
443
483
|
|
|
484
|
+
// Resolve --workspace name → ws_… ID before submitting
|
|
485
|
+
let resolvedWorkspaceId = flags.workspace ?? null;
|
|
486
|
+
if (resolvedWorkspaceId && !resolvedWorkspaceId.startsWith('ws_')) {
|
|
487
|
+
try {
|
|
488
|
+
const wsData = await callApi(`/workspaces?limit=100`, { apiKey: config.apiKey, baseUrl: config.baseUrl });
|
|
489
|
+
const match = (wsData?.workspaces ?? []).find(w => w.name === resolvedWorkspaceId);
|
|
490
|
+
if (!match) {
|
|
491
|
+
console.error(chalk.red(`Workspace not found: ${resolvedWorkspaceId}`));
|
|
492
|
+
process.exitCode = 1; return;
|
|
493
|
+
}
|
|
494
|
+
resolvedWorkspaceId = match.workspace_id;
|
|
495
|
+
} catch (err) {
|
|
496
|
+
console.error(chalk.red(`Could not resolve workspace: ${err.message}`));
|
|
497
|
+
process.exitCode = 1; return;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
444
501
|
console.log(chalk.dim(' Finding suitable capacity...'));
|
|
445
502
|
if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
|
|
446
503
|
console.log(chalk.dim(` API: ${config.baseUrl}`));
|
|
@@ -461,6 +518,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
461
518
|
...(Object.keys(envObj).length > 0 ? { env: envObj } : {}),
|
|
462
519
|
max_runtime_seconds: effectiveMaxRuntime * 60,
|
|
463
520
|
...(maxCost ? { max_cost_usd: maxCost } : {}),
|
|
521
|
+
...(resolvedWorkspaceId ? { workspace_id: resolvedWorkspaceId } : {}),
|
|
464
522
|
};
|
|
465
523
|
}
|
|
466
524
|
|
|
@@ -651,6 +709,34 @@ export async function runCommand(config, args, chalk) {
|
|
|
651
709
|
} catch { /* already stopped */ }
|
|
652
710
|
console.log(chalk.green(`\n ✓ Complete`));
|
|
653
711
|
console.log(chalk.dim(` Billing ended`));
|
|
712
|
+
|
|
713
|
+
if (flags.save && config.apiKey) {
|
|
714
|
+
try {
|
|
715
|
+
const saved = await callApi('/workloads', {
|
|
716
|
+
method: 'POST',
|
|
717
|
+
apiKey: config.apiKey,
|
|
718
|
+
baseUrl: config.baseUrl,
|
|
719
|
+
body: {
|
|
720
|
+
name: flags.save,
|
|
721
|
+
job_type: 'custom.run',
|
|
722
|
+
config: {
|
|
723
|
+
command: command || [],
|
|
724
|
+
image: image || 'python:3.11-slim',
|
|
725
|
+
gpu: gpu || 'auto',
|
|
726
|
+
...(flags.minVram ? { min_vram: flags.minVram } : {}),
|
|
727
|
+
gpu_count: flags.count || 1,
|
|
728
|
+
...(flags.maxPrice ? { max_price_per_hour: flags.maxPrice } : {}),
|
|
729
|
+
...(Object.keys(envObj).length > 0 ? { env: envObj } : {}),
|
|
730
|
+
},
|
|
731
|
+
default_max_cost: maxCost || 10.0,
|
|
732
|
+
default_max_runtime_minutes: effectiveMaxRuntime,
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
console.log(chalk.cyan(` Saved as workload: ${saved.name} (${saved.workload_id})`));
|
|
736
|
+
} catch (err) {
|
|
737
|
+
console.log(chalk.yellow(` Could not save workload: ${err.message}`));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
654
740
|
console.log();
|
|
655
741
|
}
|
|
656
742
|
}
|
package/src/commands/test-run.js
CHANGED
|
@@ -2,7 +2,7 @@ import { requireApiKey } from '../config.js';
|
|
|
2
2
|
import { callApi, terminateDeployment } from '../api.js';
|
|
3
3
|
import { addReceipt, generateReceiptId } from '../store.js';
|
|
4
4
|
|
|
5
|
-
// max $0.80/hr × 2 min ≈ $0.027 total spend cap
|
|
5
|
+
// max $0.80/hr × 2 min ≈ $0.027 total spend cap
|
|
6
6
|
const TEST_MAX_PRICE = 0.80;
|
|
7
7
|
const TEST_MAX_RUNTIME_MS = 2 * 60 * 1000;
|
|
8
8
|
const TEST_COMMAND = ['python', '-c', "print('hello from badgr')"];
|
|
@@ -12,8 +12,8 @@ const TEST_IMAGE = 'python:3.11-alpine';
|
|
|
12
12
|
const EXPECTED_OUTPUT = 'hello from badgr';
|
|
13
13
|
|
|
14
14
|
// --provider flag resolves to a backend tier value.
|
|
15
|
-
// 'tier1' → managed routing (default), 'tier2' → marketplace routing
|
|
16
|
-
const PROVIDER_TO_TIER = { tier1: '1', tier2: '2'
|
|
15
|
+
// 'tier1' → managed routing (default), 'tier2' → marketplace routing.
|
|
16
|
+
const PROVIDER_TO_TIER = { tier1: '1', tier2: '2' };
|
|
17
17
|
|
|
18
18
|
export function parseTestArgs(args) {
|
|
19
19
|
const flags = {};
|
|
@@ -95,7 +95,7 @@ export async function testCommand(config, args, chalk) {
|
|
|
95
95
|
} catch {
|
|
96
96
|
routes = null;
|
|
97
97
|
}
|
|
98
|
-
const secondaryRoute = Array.isArray(routes) ? routes.find(r => r.
|
|
98
|
+
const secondaryRoute = Array.isArray(routes) ? routes.find(r => r.tier === '2') : null;
|
|
99
99
|
if (secondaryRoute?.available) {
|
|
100
100
|
step(chalk, true, 'Secondary provider configured');
|
|
101
101
|
console.log(chalk.green('\n ✓ Secondary dispatch provider is ready\n'));
|