badgr-cli 1.0.31 → 1.0.34

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/HOW_IT_WORKS.md CHANGED
@@ -11,7 +11,7 @@ npm install -g badgr-cli
11
11
  badgr login
12
12
  ```
13
13
 
14
- `badgr login` prompts for your API key and base URL, then writes them to `~/.gpu/config.json`. Every subsequent command reads that file — no env vars required.
14
+ `badgr login` prompts for your API key and base URL, then writes them to `~/.badgr/config.json`. Every subsequent command reads that file — no env vars required.
15
15
 
16
16
  ---
17
17
 
@@ -142,7 +142,7 @@ badgr receipts [n] # default: last 10
142
142
 
143
143
  Shows two sets of receipts:
144
144
 
145
- 1. **CLI action receipts** — every `badgr serve` / `badgr down` recorded locally in `~/.gpu/deployments.json`, with provider, retries, latency, and cost.
145
+ 1. **CLI action receipts** — every `badgr serve` / `badgr down` recorded locally in `~/.badgr/deployments.json`, with provider, retries, latency, and cost.
146
146
  2. **Inference receipts** — per-request records fetched from `GET /v1/receipts` on the API (requires `badgr login`).
147
147
 
148
148
  Every action — including failures — generates a receipt. Receipt IDs are printed on every command output so you can look them up later.
@@ -202,10 +202,10 @@ GPU type aliases are normalized automatically: `rtx-4090`, `rtx4090`, `4090`, `R
202
202
 
203
203
  ## Local State
204
204
 
205
- All CLI state lives in `~/.gpu/`:
205
+ All CLI state lives in `~/.badgr/`:
206
206
 
207
207
  ```
208
- ~/.gpu/
208
+ ~/.badgr/
209
209
  config.json API key + base URL
210
210
  deployments.json Active deployments + receipt log (last 200)
211
211
  ```
package/README.md CHANGED
@@ -14,16 +14,19 @@ npm install -g badgr-cli
14
14
  # 1. Authenticate once
15
15
  badgr login
16
16
 
17
- # 2. Serve an OpenAI-compatible inference endpoint
18
- badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
17
+ # 2. Verify the stack end-to-end
18
+ badgr test
19
19
 
20
- # 3. Use the endpoint with any OpenAI SDK client
21
- # client = OpenAI(api_key="sk-...", base_url="https://dep-a1b2c3.api.badgr.ai/v1")
20
+ # 3. Serve an OpenAI-compatible inference endpoint
21
+ badgr serve meta-llama/Llama-3.1-8B-Instruct
22
22
 
23
- # 4. View cost, route, and retry receipts
23
+ # 4. Use the endpoint with any OpenAI SDK client
24
+ # client = OpenAI(api_key="sk-...", base_url="https://dep-a1b2c3.aibadgr.com/v1")
25
+
26
+ # 5. View cost, route, and retry receipts
24
27
  badgr receipts
25
28
 
26
- # 5. Stop billing
29
+ # 6. Stop billing
27
30
  badgr down <deployment-id>
28
31
  ```
29
32
 
@@ -36,9 +39,13 @@ badgr down <deployment-id>
36
39
  | `badgr login` | Save API key to `~/.badgr/config.json` |
37
40
  | `badgr serve <model>` | Start a persistent OpenAI-compatible endpoint |
38
41
  | `badgr run <command>` | Run a one-off GPU job (any container command) |
42
+ | `badgr status` | Show what's running and what's billing |
39
43
  | `badgr down <id>` | Terminate a deployment — stops billing immediately |
40
44
  | `badgr logs <id>` | Fetch log output from a deployment |
41
45
  | `badgr receipts [n]` | Cost, route, and retry receipts (default 10) |
46
+ | `badgr capacity` | Check available GPU capacity right now |
47
+ | `badgr billing` | Show balance and add funds |
48
+ | `badgr test` | Run an end-to-end test (provision → run → teardown) |
42
49
 
43
50
  `badgr serve` — for anything that needs a persistent endpoint: LLM serving, embeddings, image generation APIs, transcription APIs.
44
51
 
@@ -54,41 +61,56 @@ badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S --region EU
54
61
 
55
62
  | Flag | Default | Description |
56
63
  |------|---------|-------------|
57
- | `--gpu <type>` | RTX_4090 | GPU: RTX_4090, L40S, A6000, A100, H100 |
58
- | `--region <region>` | — | Optional region preference. If omitted, Badgr chooses best available capacity. |
64
+ | `--gpu <type>` | auto | GPU type override Badgr Auto selects based on model size if omitted |
65
+ | `--image <img>` | — | Serve a custom container instead of a HuggingFace model |
66
+ | `--task <task>` | — | vLLM task override, e.g. `embed` for embedding models |
67
+ | `--env KEY=VALUE` | — | Environment variable (repeatable) |
68
+ | `--tier 1\|2` | 1 | `1` = reliable execution (default); `2` = lower-cost burst capacity |
59
69
  | `--count <n>` | 1 | Number of GPUs (1–8) |
70
+ | `--region US\|EU\|AU` | — | Optional region preference. If omitted, Badgr chooses best available capacity. |
60
71
  | `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
61
- | `--dry-run` | — | Preview routing without provisioning |
72
+ | `--max-cost <$>` | — | Auto-stop when total spend reaches this amount |
73
+ | `--health-path <path>` | auto | Readiness path to poll (auto-detected for ComfyUI → `/system_stats`) |
74
+ | `--no-wait` | — | Skip endpoint health check and return immediately |
62
75
 
63
76
  ---
64
77
 
65
78
  ## `badgr run` options
66
79
 
67
80
  ```bash
68
- badgr run python train.py --gpu A100 --env HF_TOKEN=$HF_TOKEN
81
+ badgr run python train.py --gpu A100 --env HF_TOKEN=$HF_TOKEN --max-runtime 60
69
82
  ```
70
83
 
71
84
  | Flag | Default | Description |
72
85
  |------|---------|-------------|
73
- | `--gpu <type>` | RTX_4090 | GPU type |
86
+ | `--gpu <type>` | auto | GPU type override — Badgr Auto selects if omitted |
87
+ | `--min-vram <GB>` | — | Minimum VRAM in GB — optional constraint for Auto routing |
74
88
  | `--image <img>` | python:3.11-slim | Docker image |
75
89
  | `--env KEY=VALUE` | — | Environment variable (repeatable) |
76
- | `--region <region>` | | Optional region preference. If omitted, Badgr chooses best available capacity. |
90
+ | `--tier 1\|2` | 1 | `1` = reliable execution (default); `2` = lower-cost burst capacity |
91
+ | `--count <n>` | 1 | Number of GPUs |
92
+ | `--region US\|EU\|AU` | — | Optional region preference. If omitted, Badgr chooses best available capacity. |
77
93
  | `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
78
- | `--detach` | — | Launch and return immediately |
94
+ | `--max-runtime <min>` | — | Auto-stop after N minutes (recommended) |
95
+ | `--max-cost <$>` | — | Auto-stop when total spend reaches this amount |
96
+ | `--detach` | — | Launch and return immediately, don't stream logs |
79
97
 
80
98
  ---
81
99
 
82
100
  ## Routing
83
101
 
84
- Badgr automatically searches available GPU capacity across its verified compute network.
102
+ 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.
103
+
104
+ Most users should use the default Badgr Auto route. Tiers are an optional advanced control.
105
+
106
+ **Tier 1** (default) — Reliable execution. Best for production workloads, model serving, and jobs where startup reliability matters most. Uses managed routing, readiness checks, fallback, and teardown controls.
85
107
 
86
- Badgr automatically searches verified GPU capacity and chooses the best eligible route for your GPU type, workload, price cap, and optional region preference.
108
+ **Tier 2** Lower-cost burst execution. An optional advanced control for cost-sensitive workloads. Availability may vary.
87
109
 
88
110
  Preview before provisioning:
89
111
 
90
112
  ```bash
91
- badgr serve mistral-7b --gpu RTX_4090 --dry-run
113
+ badgr serve meta-llama/Llama-3.1-8B-Instruct --dry-run
92
114
  ```
93
115
 
94
116
  ---
@@ -115,7 +137,7 @@ from openai import OpenAI
115
137
 
116
138
  client = OpenAI(
117
139
  api_key="your-badgr-api-key",
118
- base_url="https://dep-a1b2c3.api.badgr.ai/v1", # from badgr serve output
140
+ base_url="https://dep-a1b2c3.aibadgr.com/v1", # from badgr serve output
119
141
  )
120
142
  resp = client.chat.completions.create(
121
143
  model="meta-llama/Llama-3.1-8B-Instruct",
@@ -127,7 +149,7 @@ resp = client.chat.completions.create(
127
149
  import OpenAI from "openai";
128
150
  const client = new OpenAI({
129
151
  apiKey: process.env.BADGR_API_KEY,
130
- baseURL: "https://dep-a1b2c3.api.badgr.ai/v1",
152
+ baseURL: "https://dep-a1b2c3.aibadgr.com/v1",
131
153
  });
132
154
  ```
133
155
 
@@ -135,19 +157,27 @@ const client = new OpenAI({
135
157
 
136
158
  ## GPU options
137
159
 
138
- | Flag value | GPU | VRAM | Estimated Badgr price/hr |
139
- |-----------|-----|------|-------------|
140
- | RTX_4090 | NVIDIA RTX 4090 | 24 GB | $0.65–0.89 |
141
- | L40S | NVIDIA L40S | 48 GB | $1.10–1.40 |
142
- | A6000 | NVIDIA RTX A6000 | 48 GB | $1.05–1.35 |
143
- | A100 | NVIDIA A100 | 80 GB | $1.20–1.50 |
144
- | H100 | NVIDIA H100 | 80 GB | $2.80–3.10 |
160
+ Badgr Auto selects the best eligible GPU for your workload. Add `--gpu <type>` or `--min-vram <GB>` only when you need more control.
161
+
162
+ Available GPU types may vary by region and current capacity. Run `badgr capacity` or use `--dry-run` to confirm availability before provisioning.
163
+
164
+ | Flag value | GPU | VRAM | Best for |
165
+ |-----------|-----|------|---------|
166
+ | RTX_3090 | NVIDIA RTX 3090 | 24 GB | Dev, inference |
167
+ | RTX_4090 | NVIDIA RTX 4090 | 24 GB | Inference, training, dev |
168
+ | L40S | NVIDIA L40S | 48 GB | Inference, vLLM, embeddings |
169
+ | A100 | NVIDIA A100 | 40–80 GB | Training, inference |
170
+ | H100 | NVIDIA H100 | 80 GB | Large model training |
171
+
172
+ Additional GPU types may be routable depending on current capacity — check with `badgr capacity`.
173
+
174
+ Pricing is confirmed before provisioning. Use `--dry-run` to see pricing before committing.
145
175
 
146
- Prices shown are estimated Badgr rates. Final $/GPU-hour is confirmed before provisioning and may vary by GPU type, availability, region, workload, and runtime.
176
+ Full GPU support details: see [GPU_SUPPORT.md](../../GPU_SUPPORT.md) in the repo root.
147
177
 
148
178
  ---
149
179
 
150
180
  ## Requirements
151
181
 
152
182
  - Node.js 18+
153
- - A Badgr account — sign up at [badgr.ai](https://badgr.ai)
183
+ - A Badgr account — sign up at [aibadgr.com](https://aibadgr.com)
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "badgr-cli",
3
- "version": "1.0.31",
4
- "description": "Badgr run or serve GPU workloads from one command",
3
+ "version": "1.0.34",
4
+ "description": "Badgr, run or serve GPU workloads from one command",
5
5
  "type": "module",
6
6
  "bin": {
7
- "badgr": "./src/badgr.js"
7
+ "badgr": "src/badgr.js"
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node src/badgr.js",
@@ -12,15 +12,23 @@
12
12
  "test:watch": "vitest"
13
13
  },
14
14
  "dependencies": {
15
- "@inquirer/prompts": "^5.1.0",
15
+ "@inquirer/prompts": "^8.5.2",
16
16
  "chalk": "^5.3.0"
17
17
  },
18
18
  "devDependencies": {
19
- "vitest": "^1.6.0"
19
+ "vitest": "^4.1.8"
20
20
  },
21
21
  "engines": {
22
22
  "node": ">=18.0.0"
23
23
  },
24
- "keywords": ["gpu", "cli", "ai", "compute", "modal", "gateway", "openai"],
24
+ "keywords": [
25
+ "gpu",
26
+ "cli",
27
+ "ai",
28
+ "compute",
29
+ "modal",
30
+ "gateway",
31
+ "openai"
32
+ ],
25
33
  "license": "MIT"
26
34
  }
package/src/api.js CHANGED
@@ -1,16 +1,98 @@
1
- export async function callApi(path, { method = 'GET', apiKey, baseUrl, body } = {}) {
1
+ const DEBUG = process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true';
2
+
3
+ function dbg(...args) {
4
+ if (DEBUG) console.error('[badgr:debug]', ...args);
5
+ }
6
+
7
+ // Redact env values so secrets never appear in debug output.
8
+ function redactBody(body) {
9
+ if (!body || !body.env || typeof body.env !== 'object') return body;
10
+ return { ...body, env: Object.fromEntries(Object.keys(body.env).map(k => [k, '***'])) };
11
+ }
12
+
13
+ export async function callApi(path, { method = 'GET', apiKey, baseUrl, body, timeoutMs = 15_000 } = {}) {
2
14
  const url = `${baseUrl}${path}`;
3
- const res = await fetch(url, {
4
- method,
5
- headers: {
6
- 'Content-Type': 'application/json',
7
- 'Authorization': `Bearer ${apiKey}`,
8
- },
9
- body: body !== undefined ? JSON.stringify(body) : undefined,
10
- });
15
+ const keyPreview = apiKey ? `${apiKey.slice(0, 4)}…` : '(not set)';
16
+
17
+ dbg(`${method} ${url}`);
18
+ dbg(`API key: ${keyPreview}`);
19
+ if (body !== undefined) dbg('Request body:', JSON.stringify(redactBody(body)));
20
+
21
+ let res;
22
+ const startMs = Date.now();
23
+ try {
24
+ res = await fetch(url, {
25
+ method,
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ 'Authorization': `Bearer ${apiKey}`,
29
+ },
30
+ body: body !== undefined ? JSON.stringify(body) : undefined,
31
+ signal: AbortSignal.timeout(timeoutMs),
32
+ });
33
+ } catch (cause) {
34
+ const elapsed = Date.now() - startMs;
35
+ dbg(`Fetch threw after ${elapsed}ms:`, cause);
36
+ const msg = cause?.message ?? String(cause);
37
+ const code = cause?.cause?.code ?? cause?.code ?? '';
38
+ const hint =
39
+ (code === 'ECONNREFUSED' || msg.includes('ECONNREFUSED'))
40
+ ? `\n Hint: Connection refused — is the server running at ${baseUrl}?` :
41
+ (code === 'ENOTFOUND' || msg.includes('ENOTFOUND'))
42
+ ? `\n Hint: DNS lookup failed for ${baseUrl}\n Check your internet or set BADGR_API_URL to the correct host` :
43
+ (code === 'ETIMEDOUT' || msg.includes('ETIMEDOUT') || cause?.name === 'TimeoutError')
44
+ ? `\n Hint: Request timed out — server may be overloaded` :
45
+ (msg.includes('fetch failed') || msg === 'fetch failed')
46
+ ? `\n Hint: Network error reaching ${url}\n • Check internet connection\n • Run: badgr config (verify baseUrl)\n • Try: BADGR_DEBUG=1 badgr run … for full details\n • Test: curl -v ${baseUrl}/models` :
47
+ `\n Hint: Check your network and that BADGR_API_URL is correct (${baseUrl})`;
48
+ throw new Error(`Cannot reach ${url} (${msg})${hint}`);
49
+ }
50
+
51
+ const elapsed = Date.now() - startMs;
52
+ dbg(`Response: HTTP ${res.status} in ${elapsed}ms`);
53
+
11
54
  if (!res.ok) {
12
- const text = await res.text().catch(() => '');
13
- throw new Error(`${method} ${path} → ${res.status}: ${text}`);
55
+ let detail = '';
56
+ let rawBody = '';
57
+ let errorData = null;
58
+ try {
59
+ rawBody = await res.text();
60
+ dbg('Error response body:', rawBody);
61
+ const json = JSON.parse(rawBody);
62
+ errorData = json;
63
+ detail = json?.detail ?? json?.message ?? json?.error ?? rawBody;
64
+ } catch {
65
+ detail = rawBody || res.statusText || '';
66
+ }
67
+ const isCapacityError = errorData?.code === 'NO_CAPACITY_MATCH';
68
+ if (res.status === 402) {
69
+ const d = errorData?.detail ?? errorData ?? {};
70
+ const detailObj = typeof d === 'object' ? d : {};
71
+ const balanceUsd = typeof detailObj.balance_usd === 'number' ? detailObj.balance_usd : null;
72
+ const requiredUsd = typeof detailObj.required_usd === 'number' ? detailObj.required_usd : null;
73
+ const topupUrl = detailObj.topup_url || 'https://aibadgr.com/dashboard#billing';
74
+
75
+ let msg = '\nPayment required.\n';
76
+ if (balanceUsd !== null) msg += `Your balance is $${balanceUsd.toFixed(2)}.`;
77
+ if (requiredUsd !== null) msg += ` This job needs a $${requiredUsd.toFixed(2)} reserve.`;
78
+ msg += '\n\nAdd balance:\n ' + topupUrl + '\n';
79
+ const err = new Error(msg);
80
+ err.errorData = errorData;
81
+ err.httpStatus = 402;
82
+ err.isPaymentRequired = true;
83
+ throw err;
84
+ }
85
+ const hint =
86
+ res.status === 401 ? '\n Hint: Invalid or missing API key — run: badgr login' :
87
+ res.status === 403 ? '\n Hint: Access denied — check your API key permissions' :
88
+ res.status === 404 ? `\n Hint: Endpoint not found — check BADGR_API_URL (currently: ${baseUrl})` :
89
+ (res.status === 502 || res.status === 503) && !isCapacityError
90
+ ? '\n Hint: Server error — no GPU capacity available or backend is down' :
91
+ '';
92
+ const err = new Error(`${method} ${path} → HTTP ${res.status}: ${detail}${hint}`);
93
+ err.errorData = errorData;
94
+ err.httpStatus = res.status;
95
+ throw err;
14
96
  }
15
97
  return res.json();
16
98
  }
@@ -23,6 +105,7 @@ export function runJob(config, body) {
23
105
  apiKey: config.apiKey,
24
106
  baseUrl: config.baseUrl,
25
107
  body,
108
+ timeoutMs: 75_000,
26
109
  });
27
110
  }
28
111
 
@@ -32,6 +115,7 @@ export function serveModel(config, body) {
32
115
  apiKey: config.apiKey,
33
116
  baseUrl: config.baseUrl,
34
117
  body,
118
+ timeoutMs: 75_000,
35
119
  });
36
120
  }
37
121
 
@@ -43,27 +127,53 @@ export function createDeployment(config, spec) {
43
127
  apiKey: config.apiKey,
44
128
  baseUrl: config.baseUrl,
45
129
  body: spec,
130
+ timeoutMs: 30_000,
46
131
  });
47
132
  }
48
133
 
49
134
  export function listDeployments(config) {
50
- return callApi('/deployments', { apiKey: config.apiKey, baseUrl: config.baseUrl });
135
+ return callApi('/deployments', { apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 10_000 });
51
136
  }
52
137
 
53
138
  export function getDeployment(config, deploymentId) {
54
- return callApi(`/deployments/${deploymentId}`, { apiKey: config.apiKey, baseUrl: config.baseUrl });
55
- }
56
-
57
- export function terminateDeployment(config, deploymentId) {
58
139
  return callApi(`/deployments/${deploymentId}`, {
59
- method: 'DELETE',
60
140
  apiKey: config.apiKey,
61
141
  baseUrl: config.baseUrl,
142
+ timeoutMs: 10_000,
62
143
  });
63
144
  }
64
145
 
146
+ /**
147
+ * Terminate a deployment with up to 3 retries on transient failures.
148
+ * Throws on final failure so callers can decide whether to continue.
149
+ */
150
+ export async function terminateDeployment(config, deploymentId) {
151
+ const MAX_ATTEMPTS = 3;
152
+ let lastErr;
153
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
154
+ try {
155
+ return await callApi(`/deployments/${deploymentId}`, {
156
+ method: 'DELETE',
157
+ apiKey: config.apiKey,
158
+ baseUrl: config.baseUrl,
159
+ timeoutMs: 20_000,
160
+ });
161
+ } catch (err) {
162
+ lastErr = err;
163
+ if (attempt < MAX_ATTEMPTS) {
164
+ await new Promise(r => setTimeout(r, attempt * 1000));
165
+ }
166
+ }
167
+ }
168
+ throw lastErr;
169
+ }
170
+
65
171
  export function getDeploymentLogs(config, deploymentId) {
66
- return callApi(`/deployments/${deploymentId}/logs`, { apiKey: config.apiKey, baseUrl: config.baseUrl });
172
+ return callApi(`/deployments/${deploymentId}/logs`, {
173
+ apiKey: config.apiKey,
174
+ baseUrl: config.baseUrl,
175
+ timeoutMs: 10_000,
176
+ });
67
177
  }
68
178
 
69
179
  // ---- Receipts ---------------------------------------------------------------
@@ -78,6 +188,7 @@ export function listReceipts(config, { limit = 20, status, fromTs, toTs } = {})
78
188
  return callApi(`/receipts${qs ? `?${qs}` : ''}`, {
79
189
  apiKey: config.apiKey,
80
190
  baseUrl: config.baseUrl,
191
+ timeoutMs: 10_000,
81
192
  });
82
193
  }
83
194
 
@@ -85,13 +196,14 @@ export function getReceipt(config, receiptId) {
85
196
  return callApi(`/receipts/${receiptId}`, {
86
197
  apiKey: config.apiKey,
87
198
  baseUrl: config.baseUrl,
199
+ timeoutMs: 10_000,
88
200
  });
89
201
  }
90
202
 
91
203
  // ---- Inference --------------------------------------------------------------
92
204
 
93
205
  export function listModels(config) {
94
- return callApi('/models', { apiKey: config.apiKey, baseUrl: config.baseUrl });
206
+ return callApi('/models', { apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 10_000 });
95
207
  }
96
208
 
97
209
  export function chatCompletion(config, messages, options = {}) {
@@ -101,6 +213,7 @@ export function chatCompletion(config, messages, options = {}) {
101
213
  apiKey: config.apiKey,
102
214
  baseUrl: config.baseUrl,
103
215
  body: { model: model ?? config.defaultModel, messages, stream },
216
+ timeoutMs: 30_000,
104
217
  });
105
218
  }
106
219
 
@@ -112,9 +225,14 @@ export function submitJob(config, job) {
112
225
  apiKey: config.apiKey,
113
226
  baseUrl: config.baseUrl,
114
227
  body: job,
228
+ timeoutMs: 30_000,
115
229
  });
116
230
  }
117
231
 
118
232
  export function getJobStatus(config, jobId) {
119
- return callApi(`/jobs/${jobId}`, { apiKey: config.apiKey, baseUrl: config.baseUrl });
233
+ return callApi(`/jobs/${jobId}`, {
234
+ apiKey: config.apiKey,
235
+ baseUrl: config.baseUrl,
236
+ timeoutMs: 10_000,
237
+ });
120
238
  }
package/src/badgr.js CHANGED
@@ -10,53 +10,94 @@ import { receiptsCommand } from './commands/receipts.js';
10
10
  import { runCommand } from './commands/run.js';
11
11
  import { serveCommand } from './commands/serve.js';
12
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';
13
16
 
14
17
  const HELP = `
15
18
  ${chalk.bold('badgr')} — run or serve GPU workloads from one command
16
19
 
17
- ${chalk.bold('CORE COMMANDS')}
18
- ${chalk.cyan('badgr login')} Authenticate (save API key to ~/.badgr/config.json)
19
- ${chalk.cyan('badgr run <cmd...> --gpu <type>')} Run a one-off GPU job
20
- ${chalk.cyan('badgr serve <model> --gpu <type>')} Serve a model (OpenAI-compatible endpoint)
21
- ${chalk.cyan('badgr status')} Show active deployments + endpoint URLs
22
- ${chalk.cyan('badgr logs <id>')} Stream logs for a deployment
23
- ${chalk.cyan('badgr down <id>')} Terminate a deployment (stop billing)
24
- ${chalk.cyan('badgr receipts [<id>|<n>]')} Show receipts — pass ID for single, number for list
20
+ ${chalk.bold('COMMANDS')}
21
+ ${chalk.cyan('badgr login')} Authenticate with your API key
22
+ ${chalk.cyan('badgr run <command>')} Run a one-off GPU job
23
+ ${chalk.cyan('badgr serve <model>')} Serve a model with an OpenAI-compatible endpoint
24
+ ${chalk.cyan('badgr status')} Show what's running and what's billing
25
+ ${chalk.cyan('badgr logs <id>')} Stream logs for a running job or endpoint
26
+ ${chalk.cyan('badgr down <id>')} Stop a deployment and end billing
27
+ ${chalk.cyan('badgr receipts')} Show cost history
28
+ ${chalk.cyan('badgr test')} Run an end-to-end test (provision → run → teardown)
29
+ ${chalk.cyan('badgr capacity')} Check what GPU capacity is available right now
30
+ ${chalk.cyan('badgr billing')} Show balance and add funds
25
31
 
26
- ${chalk.bold('badgr run OPTIONS')}
27
- --gpu <type> GPU: RTX_4090, A100, L40S, H100 (default: RTX_4090)
28
- --image <image> Docker image (default: python:3.11-slim)
29
- --count <n> GPU count (default: 1)
30
- --region US|EU|AU Region preference (default: US)
31
- --max-price <$/hr> Hard spend cap per GPU-hour
32
- --name <name> Job name (auto-generated if omitted)
32
+ ${chalk.bold('EXAMPLES')}
33
+ ${chalk.dim('# Verify the stack works end-to-end:')}
34
+ badgr test
33
35
 
34
- ${chalk.bold('badgr serve OPTIONS')}
35
- --gpu <type> GPU: L40S, A100, H100, RTX_4090 (default: L40S)
36
- --count <n> GPU count (default: 1)
37
- --region US|EU|AU Region preference (default: US)
38
- --max-price <$/hr> Hard spend cap per GPU-hour
39
- --name <name> Deployment name (auto-generated if omitted)
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
40
39
 
41
- ${chalk.bold('EXAMPLES')}
42
- badgr login
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
43
+
44
+ ${chalk.dim('# Pin a specific GPU:')}
43
45
  badgr run python train.py --gpu A100
44
- badgr run --image my/image:latest --gpu L40S
45
46
  badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
46
- badgr serve mistralai/Mistral-7B-v0.1 --gpu RTX_4090
47
+
48
+ ${chalk.dim('# Pass environment variables:')}
49
+ badgr run python train.py --env HF_TOKEN=$HF_TOKEN --env DATASET=my/data
50
+
51
+ ${chalk.dim('# Serve an embedding model:')}
52
+ badgr serve BAAI/bge-large-en-v1.5 --task embed
53
+
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
56
+
57
+ ${chalk.dim('# Add safety caps:')}
58
+ badgr run python train.py --max-runtime 60 --max-cost 5
59
+
60
+ ${chalk.dim('# Manage a running deployment:')}
47
61
  badgr status
48
- badgr logs dep_abc123
49
- badgr down dep_abc123
50
- badgr receipts
51
- badgr receipts dep_abc123
52
-
53
- ${chalk.bold('OPENAI-COMPATIBLE SERVING')}
54
- ${chalk.dim('After `badgr serve`, point any OpenAI client at the returned URL:')}
55
- ${chalk.dim(' client = OpenAI(api_key="sk-...", base_url="https://api.badgr.ai/v1")')}
56
- ${chalk.dim(' client.chat.completions.create(model="dep_xxx", messages=[...])')}
57
-
58
- ${chalk.bold('ROUTING')}
59
- ${chalk.dim('Badgr automatically selects best available GPU capacity for your request.')}
62
+ badgr logs dep-abc123
63
+ badgr down dep-abc123
64
+ badgr receipts dep-abc123
65
+
66
+ ${chalk.bold('badgr run OPTIONS')}
67
+ --gpu <type> GPU type (default: auto — Badgr picks best available)
68
+ --tier 1 Managed provider routing (default)
69
+ --tier 2 Marketplace provider routing, lower-cost options
70
+ --image <image> Docker image (default: python:3.11-slim)
71
+ --env KEY=VALUE Set an environment variable (repeatable)
72
+ --count <n> Number of GPUs (default: 1)
73
+ --region US|EU|AU Region preference
74
+ --max-price <$/hr> Hard spend cap per GPU-hour
75
+ --max-runtime <min> Auto-stop after N minutes (recommended)
76
+ --max-cost <$> Auto-stop when spend reaches this amount
77
+ --detach Return immediately, don't stream logs
78
+
79
+ ${chalk.bold('badgr serve OPTIONS')}
80
+ --gpu <type> GPU type (default: auto — inferred from model size)
81
+ --image <image> Serve a custom container instead of a HuggingFace model
82
+ --task <task> vLLM task override, e.g. embed for embedding models
83
+ --env KEY=VALUE Set an environment variable (repeatable)
84
+ --tier 1 Managed provider routing (default)
85
+ --tier 2 Marketplace provider routing, lower-cost options
86
+ --count <n> Number of GPUs (default: 1)
87
+ --region US|EU|AU Region preference
88
+ --max-price <$/hr> Hard spend cap per GPU-hour
89
+ --health-path <path> Readiness path to poll (auto-detected for comfyui → /system_stats)
90
+ --no-wait Skip endpoint health check
91
+
92
+ ${chalk.bold('AFTER SERVING')}
93
+ ${chalk.dim('Point any OpenAI client at the returned URL:')}
94
+ ${chalk.dim(' from openai import OpenAI')}
95
+ ${chalk.dim(' client = OpenAI(base_url="<endpoint url>", api_key="<your key>")')}
96
+ ${chalk.dim(' client.chat.completions.create(model="<model>", messages=[...])')}
97
+
98
+ ${chalk.bold('DEBUG')}
99
+ ${chalk.dim('BADGR_DEBUG=1 badgr run python train.py # full request/response trace')}
100
+ ${chalk.dim('badgr config # show current API config')}
60
101
  `;
61
102
 
62
103
  async function main() {
@@ -77,6 +118,9 @@ async function main() {
77
118
  case 'down': return downCommand(config, rest, chalk);
78
119
  case 'receipts': return receiptsCommand(config, rest, chalk);
79
120
  case 'models': return modelsCommand(config, chalk);
121
+ case 'capacity': return capacityCommand(config, rest, chalk);
122
+ case 'test': return testCommand(config, rest, chalk);
123
+ case 'billing': return billingCommand(config, rest, chalk);
80
124
  // legacy aliases kept for compatibility
81
125
  case 'up': return upCommand(config, rest, chalk);
82
126
  case 'config': {