badgr-cli 1.0.31 → 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/HOW_IT_WORKS.md +4 -4
- package/README.md +37 -18
- package/package.json +12 -4
- package/src/api.js +138 -20
- package/src/badgr.js +81 -37
- package/src/commands/billing.js +93 -0
- package/src/commands/capacity.js +111 -0
- package/src/commands/down.js +26 -23
- package/src/commands/login.js +23 -7
- package/src/commands/logs.js +57 -5
- package/src/commands/models.js +25 -6
- package/src/commands/receipts.js +19 -4
- package/src/commands/run.js +548 -90
- package/src/commands/serve.js +284 -66
- package/src/commands/status.js +35 -48
- package/src/commands/test-run.js +240 -0
- package/src/commands/up.js +32 -26
- package/src/config.js +49 -4
- package/src/fallback.js +179 -0
- package/src/router.js +16 -73
- package/src/store.js +10 -1
- package/tests/commands.test.js +234 -2
- package/tests/config.test.js +24 -1
- package/tests/router.test.js +9 -68
- package/tests/run-lifecycle.test.js +498 -0
- package/tests/serve-lifecycle.test.js +499 -0
- package/tests/store.test.js +41 -1
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 `~/.
|
|
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 `~/.
|
|
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 `~/.
|
|
205
|
+
All CLI state lives in `~/.badgr/`:
|
|
206
206
|
|
|
207
207
|
```
|
|
208
|
-
~/.
|
|
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.
|
|
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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "badgr-cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Badgr
|
|
3
|
+
"version": "1.0.32",
|
|
4
|
+
"description": "Badgr, run or serve GPU workloads from one command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"badgr": "
|
|
7
|
+
"badgr": "src/badgr.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/badgr.js",
|
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=18.0.0"
|
|
23
23
|
},
|
|
24
|
-
"keywords": [
|
|
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
|
-
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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: 30_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: 30_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`, {
|
|
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}`, {
|
|
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('
|
|
18
|
-
${chalk.cyan('badgr login')}
|
|
19
|
-
${chalk.cyan('badgr run <
|
|
20
|
-
${chalk.cyan('badgr serve <model>
|
|
21
|
-
${chalk.cyan('badgr status')}
|
|
22
|
-
${chalk.cyan('badgr logs <id>')}
|
|
23
|
-
${chalk.cyan('badgr down <id>')}
|
|
24
|
-
${chalk.cyan('badgr receipts
|
|
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('
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
35
|
-
|
|
36
|
-
|
|
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.
|
|
42
|
-
badgr
|
|
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
|
-
|
|
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
|
|
49
|
-
badgr down
|
|
50
|
-
badgr receipts
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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': {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { requireApiKey } from '../config.js';
|
|
3
|
+
import { callApi } from '../api.js';
|
|
4
|
+
|
|
5
|
+
const BILLING_HELP = `
|
|
6
|
+
badgr billing — manage your AI Badgr balance
|
|
7
|
+
|
|
8
|
+
COMMANDS
|
|
9
|
+
badgr billing status Show current balance
|
|
10
|
+
badgr billing add <amount> Open checkout to add balance (minimum $10)
|
|
11
|
+
|
|
12
|
+
EXAMPLES
|
|
13
|
+
badgr billing status
|
|
14
|
+
badgr billing add 10
|
|
15
|
+
badgr billing add 20
|
|
16
|
+
badgr billing add 50
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
function openBrowser(url) {
|
|
20
|
+
const platform = process.platform;
|
|
21
|
+
try {
|
|
22
|
+
if (platform === 'darwin') execSync(`open "${url}"`);
|
|
23
|
+
else if (platform === 'win32') execSync(`start "" "${url}"`);
|
|
24
|
+
else execSync(`xdg-open "${url}"`);
|
|
25
|
+
} catch {
|
|
26
|
+
// Silently ignore — we print the URL anyway
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function billingStatus(config, chalk) {
|
|
31
|
+
requireApiKey(config);
|
|
32
|
+
try {
|
|
33
|
+
const apiUrl = config.baseUrl.replace('/v1', '').replace('/api/v1', '');
|
|
34
|
+
const data = await callApi('/api/me', {
|
|
35
|
+
apiKey: config.apiKey,
|
|
36
|
+
baseUrl: apiUrl,
|
|
37
|
+
});
|
|
38
|
+
const credits = data.credits ?? 0;
|
|
39
|
+
const balanceUsd = (credits / 10000).toFixed(2);
|
|
40
|
+
console.log();
|
|
41
|
+
console.log(chalk.bold(' Balance'));
|
|
42
|
+
console.log(` ${chalk.bold(chalk.blue(`$${balanceUsd}`))}`);
|
|
43
|
+
console.log();
|
|
44
|
+
if (credits === 0) {
|
|
45
|
+
console.log(chalk.yellow(' Balance is $0.00. Add balance before making API calls or running GPU jobs.'));
|
|
46
|
+
console.log(chalk.dim(' Add balance: badgr billing add 10'));
|
|
47
|
+
console.log(chalk.dim(' Or visit: https://aibadgr.com/billing/top-up'));
|
|
48
|
+
}
|
|
49
|
+
console.log();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (err.isPaymentRequired) {
|
|
52
|
+
console.error(err.message);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
console.error(chalk.red(' Could not fetch balance: ' + err.message));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function billingAdd(config, amount, chalk) {
|
|
61
|
+
requireApiKey(config);
|
|
62
|
+
const amountInt = parseInt(amount, 10);
|
|
63
|
+
if (!amountInt || amountInt < 10) {
|
|
64
|
+
console.error(chalk.red(' Minimum top-up is $10. Example: badgr billing add 10'));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const url = `https://aibadgr.com/dashboard#billing`;
|
|
69
|
+
console.log();
|
|
70
|
+
console.log(chalk.bold(` Opening dashboard billing to add $${amountInt}...`));
|
|
71
|
+
console.log();
|
|
72
|
+
console.log(` ${chalk.dim(url)}`);
|
|
73
|
+
console.log();
|
|
74
|
+
openBrowser(url);
|
|
75
|
+
console.log(chalk.dim(' Complete payment in your browser, then rerun your command.'));
|
|
76
|
+
console.log();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function billingCommand(config, args, chalk) {
|
|
80
|
+
const [sub, ...rest] = args;
|
|
81
|
+
|
|
82
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
83
|
+
console.log(BILLING_HELP);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (sub === 'status') return billingStatus(config, chalk);
|
|
88
|
+
if (sub === 'add') return billingAdd(config, rest[0], chalk);
|
|
89
|
+
|
|
90
|
+
console.error(chalk.red(` Unknown billing command: ${sub}`));
|
|
91
|
+
console.log(chalk.dim(' Run: badgr billing --help'));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|