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 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,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>` | 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 — 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
- | `--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 — 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
- | `--region <region>` | | Optional region preference. If omitted, Badgr chooses best available capacity. |
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
- | `--detach` | — | Launch and return immediately |
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.api.badgr.ai/v1", # from badgr serve output
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.api.badgr.ai/v1",
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 [badgr.ai](https://badgr.ai)
172
+ - A Badgr account — sign up at [aibadgr.com](https://aibadgr.com)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badgr-cli",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "Badgr, run or serve GPU workloads from one command",
5
5
  "type": "module",
6
6
  "bin": {
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
- export async function callApi(path, { method = 'GET', apiKey, baseUrl, body } = {}) {
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, 8)}…` : '(not set)';
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`, { 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
+ });
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}`, { apiKey: config.apiKey, baseUrl: config.baseUrl });
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')}