badgr-cli 1.0.30 → 1.0.32
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 +37 -18
- package/package.json +1 -1
- package/src/api.js +56 -15
- package/src/badgr.js +14 -0
- package/src/commands/run.js +151 -61
- package/src/commands/serve.js +189 -43
- package/src/fallback.js +41 -32
- package/tests/commands.test.js +52 -1
- package/tests/run-lifecycle.test.js +498 -0
- package/tests/serve-lifecycle.test.js +499 -0
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.
|
|
18
|
-
badgr
|
|
17
|
+
# 2. Verify the stack end-to-end
|
|
18
|
+
badgr test
|
|
19
19
|
|
|
20
|
-
# 3.
|
|
21
|
-
|
|
20
|
+
# 3. Serve an OpenAI-compatible inference endpoint
|
|
21
|
+
badgr serve meta-llama/Llama-3.1-8B-Instruct
|
|
22
22
|
|
|
23
|
-
# 4.
|
|
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
|
-
#
|
|
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,37 +61,49 @@ 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>` |
|
|
58
|
-
| `--
|
|
64
|
+
| `--gpu <type>` | auto | GPU type — Badgr infers from model size if omitted. Options: RTX_4090, L40S, A6000, A100, H100 |
|
|
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` = managed provider routing (default); `2` = marketplace routing, lower cost |
|
|
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
|
-
| `--
|
|
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>` |
|
|
86
|
+
| `--gpu <type>` | auto | GPU type — Badgr picks best available if omitted |
|
|
74
87
|
| `--image <img>` | python:3.11-slim | Docker image |
|
|
75
88
|
| `--env KEY=VALUE` | — | Environment variable (repeatable) |
|
|
76
|
-
| `--
|
|
89
|
+
| `--tier 1\|2` | 1 | `1` = managed provider routing (default); `2` = marketplace routing, lower cost |
|
|
90
|
+
| `--count <n>` | 1 | Number of GPUs |
|
|
91
|
+
| `--region US\|EU\|AU` | — | Optional region preference. If omitted, Badgr chooses best available capacity. |
|
|
77
92
|
| `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
|
|
78
|
-
| `--
|
|
93
|
+
| `--max-runtime <min>` | — | Auto-stop after N minutes (recommended) |
|
|
94
|
+
| `--max-cost <$>` | — | Auto-stop when total spend reaches this amount |
|
|
95
|
+
| `--detach` | — | Launch and return immediately, don't stream logs |
|
|
79
96
|
|
|
80
97
|
---
|
|
81
98
|
|
|
82
99
|
## Routing
|
|
83
100
|
|
|
84
|
-
Badgr automatically searches available GPU capacity across its verified compute network.
|
|
85
|
-
|
|
86
101
|
Badgr automatically searches verified GPU capacity and chooses the best eligible route for your GPU type, workload, price cap, and optional region preference.
|
|
87
102
|
|
|
103
|
+
**Tier 1** (default) — managed provider routing with guaranteed SLAs.
|
|
104
|
+
|
|
105
|
+
**Tier 2** — marketplace routing for lower-cost options when Tier 1 capacity is constrained.
|
|
106
|
+
|
|
88
107
|
Preview before provisioning:
|
|
89
108
|
|
|
90
109
|
```bash
|
|
@@ -115,7 +134,7 @@ from openai import OpenAI
|
|
|
115
134
|
|
|
116
135
|
client = OpenAI(
|
|
117
136
|
api_key="your-badgr-api-key",
|
|
118
|
-
base_url="https://dep-a1b2c3.
|
|
137
|
+
base_url="https://dep-a1b2c3.aibadgr.com/v1", # from badgr serve output
|
|
119
138
|
)
|
|
120
139
|
resp = client.chat.completions.create(
|
|
121
140
|
model="meta-llama/Llama-3.1-8B-Instruct",
|
|
@@ -127,7 +146,7 @@ resp = client.chat.completions.create(
|
|
|
127
146
|
import OpenAI from "openai";
|
|
128
147
|
const client = new OpenAI({
|
|
129
148
|
apiKey: process.env.BADGR_API_KEY,
|
|
130
|
-
baseURL: "https://dep-a1b2c3.
|
|
149
|
+
baseURL: "https://dep-a1b2c3.aibadgr.com/v1",
|
|
131
150
|
});
|
|
132
151
|
```
|
|
133
152
|
|
|
@@ -150,4 +169,4 @@ Prices shown are estimated Badgr rates. Final $/GPU-hour is confirmed before pro
|
|
|
150
169
|
## Requirements
|
|
151
170
|
|
|
152
171
|
- Node.js 18+
|
|
153
|
-
- A Badgr account — sign up at [
|
|
172
|
+
- A Badgr account — sign up at [aibadgr.com](https://aibadgr.com)
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -4,13 +4,19 @@ function dbg(...args) {
|
|
|
4
4
|
if (DEBUG) console.error('[badgr:debug]', ...args);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
|
|
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 } = {}) {
|
|
8
14
|
const url = `${baseUrl}${path}`;
|
|
9
|
-
const keyPreview = apiKey ? `${apiKey.slice(0,
|
|
15
|
+
const keyPreview = apiKey ? `${apiKey.slice(0, 4)}…` : '(not set)';
|
|
10
16
|
|
|
11
17
|
dbg(`${method} ${url}`);
|
|
12
18
|
dbg(`API key: ${keyPreview}`);
|
|
13
|
-
if (body !== undefined) dbg('Request body:', JSON.stringify(body));
|
|
19
|
+
if (body !== undefined) dbg('Request body:', JSON.stringify(redactBody(body)));
|
|
14
20
|
|
|
15
21
|
let res;
|
|
16
22
|
const startMs = Date.now();
|
|
@@ -22,11 +28,11 @@ export async function callApi(path, { method = 'GET', apiKey, baseUrl, body } =
|
|
|
22
28
|
'Authorization': `Bearer ${apiKey}`,
|
|
23
29
|
},
|
|
24
30
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
31
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
25
32
|
});
|
|
26
33
|
} catch (cause) {
|
|
27
34
|
const elapsed = Date.now() - startMs;
|
|
28
35
|
dbg(`Fetch threw after ${elapsed}ms:`, cause);
|
|
29
|
-
// Network-level failure: DNS, connection refused, timeout, etc.
|
|
30
36
|
const msg = cause?.message ?? String(cause);
|
|
31
37
|
const code = cause?.cause?.code ?? cause?.code ?? '';
|
|
32
38
|
const hint =
|
|
@@ -34,7 +40,7 @@ export async function callApi(path, { method = 'GET', apiKey, baseUrl, body } =
|
|
|
34
40
|
? `\n Hint: Connection refused — is the server running at ${baseUrl}?` :
|
|
35
41
|
(code === 'ENOTFOUND' || msg.includes('ENOTFOUND'))
|
|
36
42
|
? `\n Hint: DNS lookup failed for ${baseUrl}\n Check your internet or set BADGR_API_URL to the correct host` :
|
|
37
|
-
(code === 'ETIMEDOUT' || msg.includes('ETIMEDOUT'))
|
|
43
|
+
(code === 'ETIMEDOUT' || msg.includes('ETIMEDOUT') || cause?.name === 'TimeoutError')
|
|
38
44
|
? `\n Hint: Request timed out — server may be overloaded` :
|
|
39
45
|
(msg.includes('fetch failed') || msg === 'fetch failed')
|
|
40
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` :
|
|
@@ -60,7 +66,6 @@ export async function callApi(path, { method = 'GET', apiKey, baseUrl, body } =
|
|
|
60
66
|
}
|
|
61
67
|
const isCapacityError = errorData?.code === 'NO_CAPACITY_MATCH';
|
|
62
68
|
if (res.status === 402) {
|
|
63
|
-
// Payment required — format a clear, actionable error
|
|
64
69
|
const d = errorData?.detail ?? errorData ?? {};
|
|
65
70
|
const detailObj = typeof d === 'object' ? d : {};
|
|
66
71
|
const balanceUsd = typeof detailObj.balance_usd === 'number' ? detailObj.balance_usd : null;
|
|
@@ -100,6 +105,7 @@ export function runJob(config, body) {
|
|
|
100
105
|
apiKey: config.apiKey,
|
|
101
106
|
baseUrl: config.baseUrl,
|
|
102
107
|
body,
|
|
108
|
+
timeoutMs: 30_000,
|
|
103
109
|
});
|
|
104
110
|
}
|
|
105
111
|
|
|
@@ -109,6 +115,7 @@ export function serveModel(config, body) {
|
|
|
109
115
|
apiKey: config.apiKey,
|
|
110
116
|
baseUrl: config.baseUrl,
|
|
111
117
|
body,
|
|
118
|
+
timeoutMs: 30_000,
|
|
112
119
|
});
|
|
113
120
|
}
|
|
114
121
|
|
|
@@ -120,27 +127,53 @@ export function createDeployment(config, spec) {
|
|
|
120
127
|
apiKey: config.apiKey,
|
|
121
128
|
baseUrl: config.baseUrl,
|
|
122
129
|
body: spec,
|
|
130
|
+
timeoutMs: 30_000,
|
|
123
131
|
});
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
export function listDeployments(config) {
|
|
127
|
-
return callApi('/deployments', { apiKey: config.apiKey, baseUrl: config.baseUrl });
|
|
135
|
+
return callApi('/deployments', { apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 10_000 });
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
export function getDeployment(config, deploymentId) {
|
|
131
|
-
return callApi(`/deployments/${deploymentId}`, { apiKey: config.apiKey, baseUrl: config.baseUrl });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function terminateDeployment(config, deploymentId) {
|
|
135
139
|
return callApi(`/deployments/${deploymentId}`, {
|
|
136
|
-
method: 'DELETE',
|
|
137
140
|
apiKey: config.apiKey,
|
|
138
141
|
baseUrl: config.baseUrl,
|
|
142
|
+
timeoutMs: 10_000,
|
|
139
143
|
});
|
|
140
144
|
}
|
|
141
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
|
+
|
|
142
171
|
export function getDeploymentLogs(config, deploymentId) {
|
|
143
|
-
return callApi(`/deployments/${deploymentId}/logs`, {
|
|
172
|
+
return callApi(`/deployments/${deploymentId}/logs`, {
|
|
173
|
+
apiKey: config.apiKey,
|
|
174
|
+
baseUrl: config.baseUrl,
|
|
175
|
+
timeoutMs: 10_000,
|
|
176
|
+
});
|
|
144
177
|
}
|
|
145
178
|
|
|
146
179
|
// ---- Receipts ---------------------------------------------------------------
|
|
@@ -155,6 +188,7 @@ export function listReceipts(config, { limit = 20, status, fromTs, toTs } = {})
|
|
|
155
188
|
return callApi(`/receipts${qs ? `?${qs}` : ''}`, {
|
|
156
189
|
apiKey: config.apiKey,
|
|
157
190
|
baseUrl: config.baseUrl,
|
|
191
|
+
timeoutMs: 10_000,
|
|
158
192
|
});
|
|
159
193
|
}
|
|
160
194
|
|
|
@@ -162,13 +196,14 @@ export function getReceipt(config, receiptId) {
|
|
|
162
196
|
return callApi(`/receipts/${receiptId}`, {
|
|
163
197
|
apiKey: config.apiKey,
|
|
164
198
|
baseUrl: config.baseUrl,
|
|
199
|
+
timeoutMs: 10_000,
|
|
165
200
|
});
|
|
166
201
|
}
|
|
167
202
|
|
|
168
203
|
// ---- Inference --------------------------------------------------------------
|
|
169
204
|
|
|
170
205
|
export function listModels(config) {
|
|
171
|
-
return callApi('/models', { apiKey: config.apiKey, baseUrl: config.baseUrl });
|
|
206
|
+
return callApi('/models', { apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 10_000 });
|
|
172
207
|
}
|
|
173
208
|
|
|
174
209
|
export function chatCompletion(config, messages, options = {}) {
|
|
@@ -178,6 +213,7 @@ export function chatCompletion(config, messages, options = {}) {
|
|
|
178
213
|
apiKey: config.apiKey,
|
|
179
214
|
baseUrl: config.baseUrl,
|
|
180
215
|
body: { model: model ?? config.defaultModel, messages, stream },
|
|
216
|
+
timeoutMs: 30_000,
|
|
181
217
|
});
|
|
182
218
|
}
|
|
183
219
|
|
|
@@ -189,9 +225,14 @@ export function submitJob(config, job) {
|
|
|
189
225
|
apiKey: config.apiKey,
|
|
190
226
|
baseUrl: config.baseUrl,
|
|
191
227
|
body: job,
|
|
228
|
+
timeoutMs: 30_000,
|
|
192
229
|
});
|
|
193
230
|
}
|
|
194
231
|
|
|
195
232
|
export function getJobStatus(config, jobId) {
|
|
196
|
-
return callApi(`/jobs/${jobId}`, {
|
|
233
|
+
return callApi(`/jobs/${jobId}`, {
|
|
234
|
+
apiKey: config.apiKey,
|
|
235
|
+
baseUrl: config.baseUrl,
|
|
236
|
+
timeoutMs: 10_000,
|
|
237
|
+
});
|
|
197
238
|
}
|
package/src/badgr.js
CHANGED
|
@@ -45,6 +45,15 @@ ${chalk.bold('EXAMPLES')}
|
|
|
45
45
|
badgr run python train.py --gpu A100
|
|
46
46
|
badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
|
|
47
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
|
+
|
|
48
57
|
${chalk.dim('# Add safety caps:')}
|
|
49
58
|
badgr run python train.py --max-runtime 60 --max-cost 5
|
|
50
59
|
|
|
@@ -59,6 +68,7 @@ ${chalk.bold('badgr run OPTIONS')}
|
|
|
59
68
|
--tier 1 Managed provider routing (default)
|
|
60
69
|
--tier 2 Marketplace provider routing, lower-cost options
|
|
61
70
|
--image <image> Docker image (default: python:3.11-slim)
|
|
71
|
+
--env KEY=VALUE Set an environment variable (repeatable)
|
|
62
72
|
--count <n> Number of GPUs (default: 1)
|
|
63
73
|
--region US|EU|AU Region preference
|
|
64
74
|
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
@@ -68,11 +78,15 @@ ${chalk.bold('badgr run OPTIONS')}
|
|
|
68
78
|
|
|
69
79
|
${chalk.bold('badgr serve OPTIONS')}
|
|
70
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)
|
|
71
84
|
--tier 1 Managed provider routing (default)
|
|
72
85
|
--tier 2 Marketplace provider routing, lower-cost options
|
|
73
86
|
--count <n> Number of GPUs (default: 1)
|
|
74
87
|
--region US|EU|AU Region preference
|
|
75
88
|
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
89
|
+
--health-path <path> Readiness path to poll (auto-detected for comfyui → /system_stats)
|
|
76
90
|
--no-wait Skip endpoint health check
|
|
77
91
|
|
|
78
92
|
${chalk.bold('AFTER SERVING')}
|