badgr-cli 1.0.35 → 1.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,6 +51,13 @@ badgr down <deployment-id>
51
51
 
52
52
  `badgr run` — for anything that starts, runs, and exits: batch inference, fine-tuning, evals, image/video batch jobs, audio processing.
53
53
 
54
+ **Shortcuts** — wrappers around `run` / `serve` for common workloads:
55
+
56
+ | `badgr comfyui run <workflow.json>` | Launch ComfyUI, queue workflow, return endpoint URL |
57
+ | `badgr train <config.yaml>` | LoRA / fine-tuning job, stream logs |
58
+ | `badgr transcribe <audio>` | Whisper transcription, print transcript |
59
+ | `badgr embed <model> <input>` | Text embeddings, output JSONL |
60
+
54
61
  ---
55
62
 
56
63
  ## `badgr serve` options
@@ -97,6 +104,101 @@ badgr run python train.py --gpu A100 --env HF_TOKEN=$HF_TOKEN --max-runtime 60
97
104
 
98
105
  ---
99
106
 
107
+ ## `badgr comfyui run` options
108
+
109
+ ```bash
110
+ badgr comfyui run workflow.json --max-cost 10
111
+ badgr comfyui run workflow.json --gpu RTX_4090 --check-nodes KSampler,CLIPTextEncode
112
+ ```
113
+
114
+ Requires either `--max-cost` or `--persistent` to prevent runaway billing.
115
+
116
+ | Flag | Default | Description |
117
+ |------|---------|-------------|
118
+ | `--gpu <type>` | auto (16+ GB VRAM) | GPU type override |
119
+ | `--max-cost <$>` | — | Auto-stop when total spend reaches this amount |
120
+ | `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
121
+ | `--tier 1\|2` | 1 | Provider tier |
122
+ | `--region US\|EU\|AU` | — | Region preference |
123
+ | `--check-nodes <n1,n2>` | — | Verify custom nodes are installed after startup |
124
+ | `--no-wait` | — | Skip health check, return immediately |
125
+ | `--persistent` | — | Run until manually stopped (no spending cap) |
126
+ | `--env KEY=VALUE` | — | Environment variable (repeatable) |
127
+ | `--yes` / `-y` | — | Skip duplicate-deployment warning |
128
+
129
+ ---
130
+
131
+ ## `badgr train` options
132
+
133
+ ```bash
134
+ badgr train config.yaml --gpu A100 --max-runtime 240 --env HF_TOKEN=$HF_TOKEN
135
+ ```
136
+
137
+ Auto-detects framework (axolotl, unsloth, trl) from config content. Default max-runtime is 120 min.
138
+
139
+ | Flag | Default | Description |
140
+ |------|---------|-------------|
141
+ | `--gpu <type>` | auto (40+ GB VRAM preferred) | GPU type override |
142
+ | `--max-runtime <min>` | 120 | Auto-stop after N minutes |
143
+ | `--max-cost <$>` | — | Auto-stop when total spend reaches this amount |
144
+ | `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
145
+ | `--tier 1\|2` | 1 | Provider tier |
146
+ | `--region US\|EU\|AU` | — | Region preference |
147
+ | `--framework <name>` | auto-detect | Force framework: `axolotl`, `unsloth`, `trl` |
148
+ | `--env KEY=VALUE` | — | Environment variable (repeatable) |
149
+ | `--detach` | — | Launch and return immediately, don't stream logs |
150
+
151
+ ---
152
+
153
+ ## `badgr transcribe` options
154
+
155
+ ```bash
156
+ badgr transcribe recording.mp3 --max-cost 2
157
+ badgr transcribe s3://bucket/meeting.mp3 --model large-v3 --language en
158
+ ```
159
+
160
+ Accepts a public URL, S3/GCS URI, or a local file under 50 MB. Default max-runtime is 30 min.
161
+
162
+ | Flag | Default | Description |
163
+ |------|---------|-------------|
164
+ | `--model <name>` | `large-v3` | Whisper model |
165
+ | `--gpu <type>` | auto (8+ GB VRAM) | GPU type override |
166
+ | `--language <code>` | — | Language hint (e.g. `en`, `fr`) |
167
+ | `--output <format>` | — | Output format: `txt`, `srt`, `vtt` |
168
+ | `--max-runtime <min>` | 30 | Auto-stop after N minutes |
169
+ | `--max-cost <$>` | — | Auto-stop when total spend reaches this amount |
170
+ | `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
171
+ | `--tier 1\|2` | 1 | Provider tier |
172
+ | `--region US\|EU\|AU` | — | Region preference |
173
+ | `--env KEY=VALUE` | — | Environment variable (repeatable) |
174
+ | `--detach` | — | Launch and return immediately, don't stream logs |
175
+
176
+ ---
177
+
178
+ ## `badgr embed` options
179
+
180
+ ```bash
181
+ badgr embed BAAI/bge-large-en-v1.5 documents.txt --max-cost 2
182
+ badgr embed BAAI/bge-large-en-v1.5 s3://bucket/corpus.jsonl
183
+ badgr embed documents.txt # uses default model
184
+ ```
185
+
186
+ Accepts a public URL, S3/GCS URI, or a local text file under 10 MB. Outputs JSONL (`{"text": ..., "embedding": [...]}`). Default max-runtime is 30 min.
187
+
188
+ | Flag | Default | Description |
189
+ |------|---------|-------------|
190
+ | `--gpu <type>` | auto (8+ GB VRAM) | GPU type override |
191
+ | `--batch-size <n>` | — | Embedding batch size |
192
+ | `--max-runtime <min>` | 30 | Auto-stop after N minutes |
193
+ | `--max-cost <$>` | — | Auto-stop when total spend reaches this amount |
194
+ | `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
195
+ | `--tier 1\|2` | 1 | Provider tier |
196
+ | `--region US\|EU\|AU` | — | Region preference |
197
+ | `--env KEY=VALUE` | — | Environment variable (repeatable) |
198
+ | `--detach` | — | Launch and return immediately, don't stream logs |
199
+
200
+ ---
201
+
100
202
  ## Routing
101
203
 
102
204
  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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badgr-cli",
3
- "version": "1.0.35",
3
+ "version": "1.0.36",
4
4
  "description": "Badgr, run or serve GPU workloads from one command",
5
5
  "type": "module",
6
6
  "bin": {
package/src/badgr.js CHANGED
@@ -1,18 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import chalk from 'chalk';
3
3
  import { loadConfig, saveConfig } from './config.js';
4
- import { loginCommand } from './commands/login.js';
5
- import { upCommand } from './commands/up.js';
6
- import { downCommand } from './commands/down.js';
7
- import { statusCommand } from './commands/status.js';
8
- import { logsCommand } from './commands/logs.js';
9
- import { receiptsCommand } from './commands/receipts.js';
10
- import { runCommand } from './commands/run.js';
11
- import { serveCommand } from './commands/serve.js';
12
- import { modelsCommand } from './commands/models.js';
13
- import { capacityCommand } from './commands/capacity.js';
14
- import { testCommand } from './commands/test-run.js';
15
- import { billingCommand } from './commands/billing.js';
4
+ import { loginCommand } from './commands/login.js';
5
+ import { upCommand } from './commands/up.js';
6
+ import { downCommand } from './commands/down.js';
7
+ import { statusCommand } from './commands/status.js';
8
+ import { logsCommand } from './commands/logs.js';
9
+ import { receiptsCommand } from './commands/receipts.js';
10
+ import { runCommand } from './commands/run.js';
11
+ import { serveCommand } from './commands/serve.js';
12
+ import { modelsCommand } from './commands/models.js';
13
+ import { capacityCommand } from './commands/capacity.js';
14
+ import { testCommand } from './commands/test-run.js';
15
+ import { billingCommand } from './commands/billing.js';
16
+ import { comfyuiCommand } from './commands/comfyui.js';
17
+ import { trainCommand } from './commands/train.js';
18
+ import { transcribeCommand } from './commands/transcribe.js';
19
+ import { embedCommand } from './commands/embed.js';
16
20
 
17
21
  const HELP = `
18
22
  ${chalk.bold('badgr')} — run or serve GPU workloads from one command
@@ -29,33 +33,37 @@ ${chalk.bold('COMMANDS')}
29
33
  ${chalk.cyan('badgr capacity')} Check what GPU capacity is available right now
30
34
  ${chalk.cyan('badgr billing')} Show balance and add funds
31
35
 
36
+ ${chalk.bold('SHORTCUTS')} ${chalk.dim('(wrappers around run / serve for common workloads)')}
37
+ ${chalk.cyan('badgr comfyui run <workflow.json>')} Launch ComfyUI, return endpoint URL
38
+ ${chalk.cyan('badgr train <config.yaml>')} LoRA / fine-tuning job, stream logs
39
+ ${chalk.cyan('badgr transcribe <audio>')} Whisper transcription, print transcript
40
+ ${chalk.cyan('badgr embed <model> <input>')} Text embeddings, output JSONL
41
+
32
42
  ${chalk.bold('EXAMPLES')}
33
43
  ${chalk.dim('# Verify the stack works end-to-end:')}
34
44
  badgr test
35
45
 
36
- ${chalk.dim('# Tier 1 managed provider routing (default):')}
37
- badgr run python train.py
38
- badgr serve meta-llama/Llama-3.1-8B-Instruct
46
+ ${chalk.dim('# Serve a model (OpenAI-compatible):')}
47
+ badgr serve meta-llama/Llama-3.1-8B-Instruct --max-cost 10
39
48
 
40
- ${chalk.dim('# Tier 2 marketplace routing, lower-cost options:')}
41
- badgr run python train.py --tier 2
42
- badgr serve meta-llama/Llama-3.1-8B-Instruct --tier 2
49
+ ${chalk.dim('# Launch ComfyUI with a workflow:')}
50
+ badgr comfyui run workflow.json --max-cost 5
43
51
 
44
- ${chalk.dim('# Pin a specific GPU:')}
45
- badgr run python train.py --gpu A100
46
- badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
52
+ ${chalk.dim('# LoRA fine-tuning:')}
53
+ badgr train axolotl_config.yaml --max-runtime 120
47
54
 
48
- ${chalk.dim('# Pass environment variables:')}
49
- badgr run python train.py --env HF_TOKEN=$HF_TOKEN --env DATASET=my/data
55
+ ${chalk.dim('# Transcribe audio (URL or local file ≤50 MB):')}
56
+ badgr transcribe s3://my-bucket/meeting.mp3 --max-cost 2
57
+ badgr transcribe recording.mp3
50
58
 
51
- ${chalk.dim('# Serve an embedding model:')}
52
- badgr serve BAAI/bge-large-en-v1.5 --task embed
59
+ ${chalk.dim('# Generate embeddings:')}
60
+ badgr embed BAAI/bge-large-en-v1.5 documents.txt --max-cost 2
53
61
 
54
- ${chalk.dim('# Serve a custom container (Diffusers, Whisper, etc.):')}
55
- badgr serve --image ghcr.io/my-org/diffusers-api:latest --gpu L40S --env MODEL_ID=flux
62
+ ${chalk.dim('# Tier 2 marketplace routing, lower-cost options:')}
63
+ badgr serve meta-llama/Llama-3.1-8B-Instruct --tier 2 --max-cost 10
56
64
 
57
- ${chalk.dim('# Add safety caps:')}
58
- badgr run python train.py --max-runtime 60 --max-cost 5
65
+ ${chalk.dim('# Pin a specific GPU:')}
66
+ badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S --max-cost 10
59
67
 
60
68
  ${chalk.dim('# Manage a running deployment:')}
61
69
  badgr status
@@ -120,9 +128,13 @@ async function main() {
120
128
  case 'models': return modelsCommand(config, chalk);
121
129
  case 'capacity': return capacityCommand(config, rest, chalk);
122
130
  case 'test': return testCommand(config, rest, chalk);
123
- case 'billing': return billingCommand(config, rest, chalk);
131
+ case 'billing': return billingCommand(config, rest, chalk);
132
+ case 'comfyui': return comfyuiCommand(config, rest, chalk);
133
+ case 'train': return trainCommand(config, rest, chalk);
134
+ case 'transcribe': return transcribeCommand(config, rest, chalk);
135
+ case 'embed': return embedCommand(config, rest, chalk);
124
136
  // legacy aliases kept for compatibility
125
- case 'up': return upCommand(config, rest, chalk);
137
+ case 'up': return upCommand(config, rest, chalk);
126
138
  case 'config': {
127
139
  const { apiKey, ...safe } = config;
128
140
  const display = { ...safe, apiKey: apiKey ? `${apiKey.slice(0, 8)}...` : '(not set)' };
package/src/batch.js ADDED
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Shared batch job monitor.
3
+ *
4
+ * After a /run job is provisioned, this module handles:
5
+ * wait for container start → stream logs → enforce spend/runtime caps → teardown
6
+ *
7
+ * Used by: badgr train, badgr transcribe, badgr embed
8
+ */
9
+ import { callApi, terminateDeployment } from './api.js';
10
+ import { updateReceipt } from './store.js';
11
+ import { formatCliError } from './errors.js';
12
+
13
+ // Log lines that carry telemetry metadata, not user output.
14
+ const LOG_META_RE = /^\[dep-[^\]]+\] (status|gpu|region|cost|receipt|provider_status|uptime)=/;
15
+
16
+ export function fmtRuntime(ms) {
17
+ const s = Math.round(ms / 1000);
18
+ return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
19
+ }
20
+
21
+ const STARTUP = new Set(['queued', 'provisioning', 'starting']);
22
+ const TERMINAL = new Set(['stopped', 'failed', 'completed', 'succeeded']);
23
+
24
+ async function waitForRunning(config, depId, chalk) {
25
+ const POLL_MS = 3000;
26
+ const TIMEOUT_MS = 5 * 60 * 1000;
27
+ const startMs = Date.now();
28
+
29
+ process.stdout.write(chalk.dim(' Starting container'));
30
+ while (Date.now() - startMs < TIMEOUT_MS) {
31
+ await new Promise(r => setTimeout(r, POLL_MS));
32
+ let dep;
33
+ try {
34
+ dep = await callApi(`/deployments/${depId}`, {
35
+ apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 10_000,
36
+ });
37
+ } catch { process.stdout.write('.'); continue; }
38
+ if (!STARTUP.has(dep.status)) { process.stdout.write('\n'); return dep; }
39
+ process.stdout.write('.');
40
+ }
41
+ process.stdout.write('\n');
42
+ return callApi(`/deployments/${depId}`, {
43
+ apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 10_000,
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Monitor a running batch job until completion, cap, or interrupt.
49
+ *
50
+ * Installs a SIGINT handler — callers must not install their own.
51
+ *
52
+ * @param {object} config
53
+ * @param {string} depId
54
+ * @param {string} rcptId
55
+ * @param {{chalk, maxRuntimeMs, maxCost, ratePerHour}} opts
56
+ * @returns {Promise<{ status, exitCode, runtimeMs, reason }>}
57
+ */
58
+ export async function monitorBatchJob(config, depId, rcptId, opts) {
59
+ const { chalk, maxRuntimeMs, maxCost, ratePerHour } = opts;
60
+ const POLL_MS = 5000;
61
+
62
+ const startDep = await waitForRunning(config, depId, chalk);
63
+
64
+ if (startDep.status === 'failed') {
65
+ updateReceipt(rcptId, { status: 'failed', runtimeSeconds: 0, finalCost: 0 });
66
+ console.error(formatCliError('JOB_INFRASTRUCTURE_FAILURE', { receiptId: rcptId }, chalk));
67
+ return { status: 'failed', exitCode: null, runtimeMs: 0, reason: 'infrastructure' };
68
+ }
69
+
70
+ console.log(chalk.dim('\n ── Running (Ctrl+C to stop) ─────────────────────────────────────\n'));
71
+
72
+ const seenLines = new Set();
73
+ const attachStart = Date.now();
74
+ let tearing = false;
75
+ let shuttingDown = false;
76
+
77
+ function handleSIGINT() { shuttingDown = true; }
78
+ process.once('SIGINT', handleSIGINT);
79
+
80
+ async function teardown(reason) {
81
+ if (tearing) return;
82
+ tearing = true;
83
+ const msgs = {
84
+ 'max-runtime': chalk.yellow('\n ⏱ Max runtime reached — stopping...'),
85
+ 'max-cost': chalk.yellow('\n 💰 Spend cap reached — stopping...'),
86
+ 'interrupted': chalk.yellow('\n Stopping...'),
87
+ };
88
+ console.log(msgs[reason] ?? chalk.yellow('\n Stopping...'));
89
+ try { await terminateDeployment(config, depId); } catch {}
90
+ const runtimeMs = Date.now() - attachStart;
91
+ const finalCost = ratePerHour * (runtimeMs / 3_600_000);
92
+ updateReceipt(rcptId, {
93
+ status: reason,
94
+ runtimeSeconds: Math.round(runtimeMs / 1000),
95
+ finalCost,
96
+ });
97
+ console.log(chalk.dim(` Runtime: ${fmtRuntime(runtimeMs)} • Est. cost: $${finalCost.toFixed(4)}`));
98
+ console.log(chalk.dim(' Stopped. Billing ended.\n'));
99
+ }
100
+
101
+ while (true) {
102
+ if (shuttingDown || tearing) break;
103
+ await new Promise(r => setTimeout(r, POLL_MS));
104
+ if (shuttingDown || tearing) break;
105
+
106
+ const elapsed = Date.now() - attachStart;
107
+ const spent = ratePerHour * (elapsed / 3_600_000);
108
+
109
+ if (maxCost !== null && spent >= maxCost) { await teardown('max-cost'); break; }
110
+ if (elapsed >= maxRuntimeMs) { await teardown('max-runtime'); break; }
111
+
112
+ let dep;
113
+ try {
114
+ dep = await callApi(`/deployments/${depId}`, {
115
+ apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 10_000,
116
+ });
117
+ } catch { continue; }
118
+
119
+ try {
120
+ const logData = await callApi(`/deployments/${depId}/logs`, {
121
+ apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 10_000,
122
+ });
123
+ for (const line of (logData?.logs ?? [])) {
124
+ if (seenLines.has(line)) continue;
125
+ seenLines.add(line);
126
+ if (LOG_META_RE.test(line)) continue;
127
+ if (/\b(gpu_util|cpu_util|provider_status|uptime)=/.test(line)) continue;
128
+ const isErr = /^error\b/i.test(line) || /Error response from daemon/i.test(line);
129
+ console.log(` ${isErr ? chalk.red(line) : chalk.dim(line)}`);
130
+ }
131
+ } catch {}
132
+
133
+ if (TERMINAL.has(dep.status)) {
134
+ process.removeListener('SIGINT', handleSIGINT);
135
+ const exitCode = dep.exit_code ?? null;
136
+ const runtimeMs = Date.now() - attachStart;
137
+ const finalCost = ratePerHour * (runtimeMs / 3_600_000);
138
+ try { await terminateDeployment(config, depId); } catch {}
139
+ updateReceipt(rcptId, {
140
+ status: dep.status,
141
+ exitCode,
142
+ runtimeSeconds: Math.round(runtimeMs / 1000),
143
+ finalCost,
144
+ });
145
+ return { status: dep.status, exitCode, runtimeMs, reason: 'complete' };
146
+ }
147
+ }
148
+
149
+ process.removeListener('SIGINT', handleSIGINT);
150
+
151
+ if (shuttingDown) {
152
+ await teardown('interrupted');
153
+ return { status: 'interrupted', exitCode: null, runtimeMs: Date.now() - attachStart, reason: 'interrupted' };
154
+ }
155
+
156
+ return { status: 'capped', exitCode: null, runtimeMs: Date.now() - attachStart, reason: 'capped' };
157
+ }