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/src/commands/serve.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { requireApiKey } from '../config.js';
|
|
2
2
|
import { callApi } from '../api.js';
|
|
3
|
-
import { addDeployment, addReceipt, generateReceiptId } from '../store.js';
|
|
3
|
+
import { addDeployment, addReceipt, updateReceipt, generateReceiptId } from '../store.js';
|
|
4
|
+
import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD } from '../fallback.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
7
|
+
* badgr serve meta-llama/Llama-3.1-8B-Instruct
|
|
6
8
|
* badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
|
|
9
|
+
* badgr serve BAAI/bge-large-en-v1.5 --task embed
|
|
10
|
+
* badgr serve --image ghcr.io/my-org/diffusers-api:latest --gpu L40S --env MODEL_ID=flux
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
* "Endpoint ready", and returns an OpenAI-compatible base URL.
|
|
10
|
-
* Stop billing with `badgr down <id>`.
|
|
12
|
+
* GPU defaults to "AUTO" — backend infers from model size.
|
|
11
13
|
*/
|
|
12
14
|
export function parseServeArgs(args) {
|
|
13
15
|
const flags = {};
|
|
@@ -15,82 +17,220 @@ export function parseServeArgs(args) {
|
|
|
15
17
|
let i = 0;
|
|
16
18
|
while (i < args.length) {
|
|
17
19
|
if (args[i] === '--gpu') { flags.gpu = args[++i]; i++; continue; }
|
|
20
|
+
if (args[i] === '--image') { flags.image = args[++i]; i++; continue; }
|
|
21
|
+
if (args[i] === '--task') { flags.task = args[++i]; i++; continue; }
|
|
18
22
|
if (args[i] === '--count') { flags.count = parseInt(args[++i], 10); i++; continue; }
|
|
19
23
|
if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
|
|
24
|
+
if (args[i] === '--tier') { flags.tier = args[++i]; i++; continue; }
|
|
20
25
|
if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
|
|
21
26
|
if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
|
|
22
|
-
if (args[i] === '--no-wait')
|
|
27
|
+
if (args[i] === '--no-wait') { flags.noWait = true; i++; continue; }
|
|
28
|
+
if (args[i] === '--max-cost') { flags.maxCost = parseFloat(args[++i]); i++; continue; }
|
|
29
|
+
if (args[i] === '--health-path') { flags.healthPath = args[++i]; i++; continue; }
|
|
30
|
+
// All three aliases map to noMarketplaceFallback
|
|
31
|
+
if (args[i] === '--no-fallback' ||
|
|
32
|
+
args[i] === '--strict-capacity' ||
|
|
33
|
+
args[i] === '--no-expanded-search') { flags.noMarketplaceFallback = true; i++; continue; }
|
|
34
|
+
if (args[i] === '--env') {
|
|
35
|
+
const kv = args[++i]; i++;
|
|
36
|
+
if (!flags.env) flags.env = [];
|
|
37
|
+
flags.env.push(kv);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
23
40
|
positional.push(args[i++]);
|
|
24
41
|
}
|
|
25
42
|
const model = positional[0] || null;
|
|
26
43
|
return { model, flags };
|
|
27
44
|
}
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
function parseEnvFlag(envList) {
|
|
47
|
+
const obj = {};
|
|
48
|
+
for (const kv of (envList || [])) {
|
|
49
|
+
const idx = kv.indexOf('=');
|
|
50
|
+
if (idx > 0) obj[kv.slice(0, idx)] = kv.slice(idx + 1);
|
|
51
|
+
}
|
|
52
|
+
return obj;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Mirror of backend workload_profile.py infer_profile_from_model — for pre-flight display.
|
|
56
|
+
function _inferServeProfile(modelName) {
|
|
57
|
+
const s = modelName.toLowerCase();
|
|
58
|
+
const moe = s.match(/(\d+)x(\d+)b/);
|
|
59
|
+
let paramsB;
|
|
60
|
+
if (moe) {
|
|
61
|
+
paramsB = parseInt(moe[1]) * parseInt(moe[2]);
|
|
62
|
+
} else {
|
|
63
|
+
const m = s.match(/(\d+)b/);
|
|
64
|
+
paramsB = m ? parseInt(m[1]) : null;
|
|
65
|
+
}
|
|
66
|
+
if (paramsB === null) return { label: 'inference (7B–8B model)', vram: '24+ GB', gpus: ['RTX 4090', 'A6000', 'L40S'] };
|
|
67
|
+
if (paramsB <= 9) return { label: 'inference (7B–8B model)', vram: '24+ GB', gpus: ['RTX 4090', 'A6000', 'L40S'] };
|
|
68
|
+
if (paramsB <= 35) return { label: 'inference (30B–34B model)', vram: '40+ GB', gpus: ['A6000', 'L40S', 'A100'] };
|
|
69
|
+
return { label: 'inference (70B+ model)', vram: '80+ GB', gpus: ['H100', 'A100'] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _serveStageLabel(elapsedSec, healthPath = '/models') {
|
|
73
|
+
if (healthPath === '/models') {
|
|
74
|
+
if (elapsedSec < 45) return 'Starting vLLM…';
|
|
75
|
+
if (elapsedSec < 150) return 'Downloading model…';
|
|
76
|
+
return 'Waiting for /v1/models…';
|
|
77
|
+
}
|
|
78
|
+
if (elapsedSec < 30) return 'Starting container…';
|
|
79
|
+
if (elapsedSec < 120) return 'Container starting…';
|
|
80
|
+
return `Waiting for ${healthPath}…`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _detectHealthPath(image) {
|
|
84
|
+
if (!image) return null;
|
|
85
|
+
return image.toLowerCase().includes('comfyui') ? '/system_stats' : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Poll the model endpoint until /models returns 200, or timeout.
|
|
90
|
+
* Also checks deployment status on each iteration — fails fast if the dep is dead.
|
|
91
|
+
* Returns { ready: boolean, timedOut: boolean, depFailed: boolean, failReason?: string }
|
|
92
|
+
*/
|
|
93
|
+
async function waitForEndpoint(endpointUrl, deploymentId, config, timeoutMs = 5 * 60 * 1000, chalk, healthPath = '/models') {
|
|
94
|
+
const startMs = Date.now();
|
|
95
|
+
const deadline = startMs + timeoutMs;
|
|
35
96
|
|
|
36
97
|
while (Date.now() < deadline) {
|
|
37
|
-
|
|
98
|
+
// Check deployment status first — fail fast on OOM/crash before waiting more
|
|
38
99
|
try {
|
|
39
|
-
const
|
|
40
|
-
|
|
100
|
+
const dep = await callApi(`/deployments/${deploymentId}`, {
|
|
101
|
+
apiKey: config.apiKey,
|
|
102
|
+
baseUrl: config.baseUrl,
|
|
103
|
+
timeoutMs: 10_000,
|
|
104
|
+
});
|
|
105
|
+
if (['failed', 'terminated', 'error', 'stopped'].includes(dep.status)) {
|
|
106
|
+
process.stdout.write('\n');
|
|
107
|
+
return { ready: false, timedOut: false, depFailed: true, failReason: dep.error || dep.status };
|
|
108
|
+
}
|
|
41
109
|
} catch {
|
|
42
|
-
//
|
|
110
|
+
// status check failed — continue to endpoint check
|
|
43
111
|
}
|
|
44
|
-
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const res = await fetch(`${endpointUrl}${healthPath}`, { signal: AbortSignal.timeout(8000) });
|
|
115
|
+
if (res.ok) { process.stdout.write('\n'); return { ready: true, timedOut: false, depFailed: false }; }
|
|
116
|
+
} catch {
|
|
117
|
+
// still starting
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const elapsed = Math.round((Date.now() - startMs) / 1000);
|
|
45
121
|
process.stdout.write(
|
|
46
|
-
`\r ${chalk.dim(
|
|
122
|
+
`\r ${chalk.dim(_serveStageLabel(elapsed, healthPath) + ` (${elapsed}s)`)} `
|
|
47
123
|
);
|
|
48
124
|
await new Promise(r => setTimeout(r, 8000));
|
|
49
125
|
}
|
|
126
|
+
|
|
50
127
|
process.stdout.write('\n');
|
|
51
|
-
return false;
|
|
128
|
+
return { ready: false, timedOut: true, depFailed: false };
|
|
52
129
|
}
|
|
53
130
|
|
|
54
131
|
export async function serveCommand(config, args, chalk) {
|
|
55
132
|
const { model, flags } = parseServeArgs(args);
|
|
133
|
+
const customImage = flags.image || null;
|
|
56
134
|
|
|
57
|
-
if (!model) {
|
|
58
|
-
console.error(chalk.red('Usage: badgr serve <model>
|
|
59
|
-
console.error(chalk.red(' badgr serve meta-llama/Llama-3.1-8B-Instruct
|
|
135
|
+
if (!model && !customImage) {
|
|
136
|
+
console.error(chalk.red('Usage: badgr serve <model>'));
|
|
137
|
+
console.error(chalk.red(' badgr serve meta-llama/Llama-3.1-8B-Instruct'));
|
|
138
|
+
console.error(chalk.red(' badgr serve --image ghcr.io/my-org/api:latest --gpu L40S'));
|
|
60
139
|
return;
|
|
61
140
|
}
|
|
62
141
|
|
|
63
142
|
requireApiKey(config);
|
|
64
143
|
|
|
65
|
-
|
|
144
|
+
// ── Validate flags early ───────────────────────────────────────────────────
|
|
145
|
+
if (flags.count !== undefined && (!Number.isFinite(flags.count) || flags.count < 1)) {
|
|
146
|
+
console.error(chalk.red(' ✗ --count must be an integer greater than 0'));
|
|
147
|
+
process.exitCode = 1;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (flags.maxCost !== undefined && (!Number.isFinite(flags.maxCost) || flags.maxCost <= 0)) {
|
|
151
|
+
console.error(chalk.red(' ✗ --max-cost must be a number greater than 0'));
|
|
152
|
+
process.exitCode = 1;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (flags.region !== undefined && !['US', 'EU', 'AU'].includes(flags.region.toUpperCase())) {
|
|
156
|
+
console.error(chalk.red(' ✗ --region must be US, EU, or AU'));
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const envObj = parseEnvFlag(flags.env);
|
|
162
|
+
|
|
163
|
+
const gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : (customImage ? 'L40S' : 'AUTO');
|
|
164
|
+
const gpuLabel = gpu === 'AUTO' ? 'auto' : gpu;
|
|
165
|
+
|
|
166
|
+
const effectiveTier = normalizeTier(flags.tier);
|
|
167
|
+
|
|
168
|
+
if (customImage) {
|
|
169
|
+
console.log(chalk.bold('\n⚡ Serving custom container\n'));
|
|
170
|
+
console.log(` ${chalk.bold('Image:')} ${customImage}`);
|
|
171
|
+
console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
|
|
172
|
+
if (flags.task) console.log(` ${chalk.bold('Task:')} ${flags.task}`);
|
|
173
|
+
if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
|
|
174
|
+
} else {
|
|
175
|
+
console.log(chalk.bold('\n⚡ Serving model\n'));
|
|
176
|
+
console.log(` ${chalk.bold('Model:')} ${model}`);
|
|
177
|
+
console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
|
|
178
|
+
if (flags.task) console.log(` ${chalk.bold('Task:')} ${flags.task}`);
|
|
179
|
+
if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
|
|
66
180
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
181
|
+
if (gpu === 'AUTO') {
|
|
182
|
+
const prof = _inferServeProfile(model);
|
|
183
|
+
console.log();
|
|
184
|
+
console.log(` ${chalk.bold('Estimated workload:')} ${prof.label}`);
|
|
185
|
+
console.log(` ${chalk.bold('Estimated minimum VRAM:')} ${prof.vram}`);
|
|
186
|
+
if (flags.maxCost) console.log(` ${chalk.bold('Max cost:')} $${flags.maxCost.toFixed(2)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
70
189
|
console.log();
|
|
71
|
-
|
|
190
|
+
process.stdout.write(chalk.dim(' Finding suitable capacity...\n'));
|
|
191
|
+
|
|
192
|
+
const effectiveRegion = flags.region ? flags.region.toUpperCase() : undefined;
|
|
193
|
+
|
|
194
|
+
function buildBody(gpuOverride, tierOverride) {
|
|
195
|
+
return {
|
|
196
|
+
...(model ? { model } : {}),
|
|
197
|
+
...(customImage ? { image: customImage } : {}),
|
|
198
|
+
...(flags.task ? { task: flags.task } : {}),
|
|
199
|
+
gpu: gpuOverride || gpu,
|
|
200
|
+
gpu_count: flags.count || 1,
|
|
201
|
+
...(effectiveRegion ? { region: effectiveRegion } : {}),
|
|
202
|
+
max_price_per_hour: flags.maxPrice,
|
|
203
|
+
name: flags.name,
|
|
204
|
+
tier: tierOverride || effectiveTier,
|
|
205
|
+
...(Object.keys(envObj).length > 0 ? { env: envObj } : {}),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
72
208
|
|
|
73
209
|
let dep;
|
|
74
210
|
try {
|
|
75
|
-
dep = await
|
|
76
|
-
|
|
77
|
-
apiKey: config.apiKey,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
max_price_per_hour: flags.maxPrice,
|
|
85
|
-
name: flags.name,
|
|
86
|
-
},
|
|
87
|
-
});
|
|
211
|
+
dep = await callWithFallback(
|
|
212
|
+
'/serve',
|
|
213
|
+
{ apiKey: config.apiKey, baseUrl: config.baseUrl },
|
|
214
|
+
(tierOverride) => buildBody(undefined, tierOverride),
|
|
215
|
+
effectiveTier,
|
|
216
|
+
chalk,
|
|
217
|
+
{ thing: 'endpoint', cmd: 'badgr serve' },
|
|
218
|
+
{ allowTier2Fallback: !flags.noMarketplaceFallback },
|
|
219
|
+
);
|
|
88
220
|
} catch (err) {
|
|
89
|
-
|
|
221
|
+
if (err.isPaymentRequired) {
|
|
222
|
+
console.error(chalk.yellow(err.message));
|
|
223
|
+
const rerun = `badgr serve ${args.join(' ')}`;
|
|
224
|
+
console.error(chalk.dim(`After payment, rerun:\n ${rerun}\n`));
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// CapacityError message is pre-formatted with chalk
|
|
229
|
+
console.error(err.message);
|
|
230
|
+
process.exitCode = 1;
|
|
90
231
|
return;
|
|
91
232
|
}
|
|
92
233
|
|
|
93
|
-
// Mirror to local store so badgr down/status/receipts work offline
|
|
94
234
|
addDeployment({
|
|
95
235
|
id: dep.deployment_id,
|
|
96
236
|
name: dep.name,
|
|
@@ -98,12 +238,13 @@ export async function serveCommand(config, args, chalk) {
|
|
|
98
238
|
model: dep.model || model,
|
|
99
239
|
gpu: dep.gpu_type,
|
|
100
240
|
count: dep.gpu_count,
|
|
101
|
-
provider: dep.provider,
|
|
102
241
|
status: dep.status,
|
|
103
242
|
endpointUrl: dep.endpoint_url || dep.openai_base_url,
|
|
104
243
|
receiptId: dep.receipt_id,
|
|
105
244
|
createdAt: new Date().toISOString(),
|
|
106
245
|
costPerHour: dep.cost_per_hour || 0,
|
|
246
|
+
providerRoute: dep.provider ?? null,
|
|
247
|
+
tier: dep.tier ?? null,
|
|
107
248
|
});
|
|
108
249
|
|
|
109
250
|
const rcptId = dep.receipt_id || generateReceiptId();
|
|
@@ -111,49 +252,126 @@ export async function serveCommand(config, args, chalk) {
|
|
|
111
252
|
receiptId: rcptId,
|
|
112
253
|
action: 'badgr serve',
|
|
113
254
|
deploymentId: dep.deployment_id,
|
|
114
|
-
provider: dep.provider,
|
|
115
255
|
gpu: dep.gpu_type,
|
|
256
|
+
providerRoute: dep.provider ?? null,
|
|
257
|
+
tier: dep.tier ?? null,
|
|
116
258
|
status: dep.status,
|
|
117
259
|
createdAt: new Date().toISOString(),
|
|
118
260
|
});
|
|
119
261
|
|
|
120
|
-
|
|
262
|
+
// ── Fix 7: never fall back to config.baseUrl for endpoint health check ─────
|
|
263
|
+
const endpointUrl = dep.endpoint_url || dep.openai_base_url;
|
|
264
|
+
if (!endpointUrl) {
|
|
265
|
+
console.error(chalk.red(
|
|
266
|
+
`\n ✗ Deployment ${dep.deployment_id} has no endpoint URL yet.\n` +
|
|
267
|
+
` Run: badgr status ${dep.deployment_id}\n`
|
|
268
|
+
));
|
|
269
|
+
updateReceipt(rcptId, { status: 'no_endpoint_url' });
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
121
273
|
|
|
122
|
-
// ──
|
|
123
|
-
|
|
274
|
+
// ── Determine health check path ───────────────────────────────────────────
|
|
275
|
+
// Priority: explicit --health-path > auto-detect comfyui > /models for vLLM > null (skip)
|
|
276
|
+
let resolvedHealthPath;
|
|
277
|
+
if (flags.healthPath) {
|
|
278
|
+
resolvedHealthPath = flags.healthPath;
|
|
279
|
+
} else if (!customImage) {
|
|
280
|
+
resolvedHealthPath = '/models';
|
|
281
|
+
} else {
|
|
282
|
+
resolvedHealthPath = _detectHealthPath(customImage); // '/system_stats' for comfyui, null otherwise
|
|
283
|
+
}
|
|
124
284
|
|
|
285
|
+
// ── Health check ──────────────────────────────────────────────────────────
|
|
286
|
+
let endpointReady = false;
|
|
125
287
|
if (flags.noWait) {
|
|
126
|
-
console.log(chalk.yellow('\n
|
|
288
|
+
console.log(chalk.yellow('\n Skipped health check (--no-wait)\n'));
|
|
289
|
+
} else if (resolvedHealthPath === null) {
|
|
290
|
+
console.log(chalk.yellow(
|
|
291
|
+
'\n Skipping health check (custom image — add --health-path /your-readiness-path to enable)\n'
|
|
292
|
+
));
|
|
127
293
|
} else {
|
|
128
|
-
|
|
129
|
-
|
|
294
|
+
if (resolvedHealthPath !== '/models') {
|
|
295
|
+
process.stdout.write(chalk.dim(` Checking ${resolvedHealthPath} for readiness…\n`));
|
|
296
|
+
}
|
|
297
|
+
// Check deployment status once before starting the 5-min wait
|
|
298
|
+
try {
|
|
299
|
+
const latest = await callApi(`/deployments/${dep.deployment_id}`, {
|
|
300
|
+
apiKey: config.apiKey,
|
|
301
|
+
baseUrl: config.baseUrl,
|
|
302
|
+
timeoutMs: 10_000,
|
|
303
|
+
});
|
|
304
|
+
if (['failed', 'terminated', 'error'].includes(latest.status)) {
|
|
305
|
+
console.error(chalk.red(
|
|
306
|
+
`\n ✗ Deployment failed before endpoint became ready: ${latest.error || latest.status}\n`
|
|
307
|
+
));
|
|
308
|
+
updateReceipt(rcptId, { status: 'failed', failReason: latest.error || latest.status });
|
|
309
|
+
process.exitCode = 1;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
// status check failed — proceed with endpoint poll anyway
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const healthResult = await waitForEndpoint(endpointUrl, dep.deployment_id, config, 5 * 60 * 1000, chalk, resolvedHealthPath);
|
|
130
317
|
process.stdout.write('\n');
|
|
318
|
+
|
|
319
|
+
if (healthResult.depFailed) {
|
|
320
|
+
console.error(chalk.red(
|
|
321
|
+
`\n ✗ Deployment failed during startup: ${healthResult.failReason || 'unknown error'}\n` +
|
|
322
|
+
` Check logs: badgr logs ${dep.deployment_id}\n`
|
|
323
|
+
));
|
|
324
|
+
updateReceipt(rcptId, { status: 'failed', failReason: healthResult.failReason });
|
|
325
|
+
process.exitCode = 1;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
endpointReady = healthResult.ready;
|
|
330
|
+
if (!endpointReady) updateReceipt(rcptId, { status: 'health_check_timeout' });
|
|
131
331
|
}
|
|
132
332
|
|
|
133
|
-
// ──
|
|
333
|
+
// ── Update receipt with final state ───────────────────────────────────────
|
|
334
|
+
updateReceipt(rcptId, {
|
|
335
|
+
status: endpointReady ? 'ready' : 'starting',
|
|
336
|
+
endpointUrl,
|
|
337
|
+
updatedAt: new Date().toISOString(),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ── Result ────────────────────────────────────────────────────────────────
|
|
134
341
|
if (endpointReady) {
|
|
135
|
-
console.log(chalk.green('✓ Endpoint ready\n'));
|
|
342
|
+
console.log(chalk.green('\n ✓ Endpoint ready\n'));
|
|
136
343
|
} else {
|
|
137
|
-
console.log(chalk.yellow('⏳ Endpoint starting
|
|
138
|
-
console.log(chalk.dim(`
|
|
139
|
-
console.log(chalk.dim(`
|
|
140
|
-
console.log(chalk.dim(` curl ${endpointUrl}/models`));
|
|
344
|
+
console.log(chalk.yellow('\n ⏳ Endpoint still starting — model download may still be in progress.\n'));
|
|
345
|
+
console.log(` ${chalk.bold('Stop billing now:')} ${chalk.dim(`badgr down ${dep.deployment_id}`)}`);
|
|
346
|
+
console.log(` ${chalk.bold('Continue watching:')} ${chalk.dim(`badgr logs ${dep.deployment_id}`)}`);
|
|
141
347
|
console.log();
|
|
142
348
|
}
|
|
143
349
|
|
|
144
|
-
|
|
145
|
-
console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
|
|
146
|
-
console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
|
|
147
|
-
console.log(` ${chalk.bold('Endpoint:')} ${chalk.cyan(endpointUrl)}`);
|
|
148
|
-
if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
|
|
149
|
-
console.log(`\n ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
|
|
350
|
+
const serveRate = dep.cost_per_hour || 0;
|
|
150
351
|
|
|
151
|
-
|
|
152
|
-
|
|
352
|
+
console.log(` ${chalk.bold('Base URL:')} ${chalk.cyan(endpointUrl)}`);
|
|
353
|
+
if (dep.model || model) console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
|
|
354
|
+
if (customImage) console.log(` ${chalk.bold('Image:')} ${customImage}`);
|
|
355
|
+
console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
|
|
356
|
+
if (serveRate > 0) console.log(` ${chalk.bold('Rate:')} $${serveRate.toFixed(2)}/hr`);
|
|
357
|
+
if (flags.maxCost) console.log(` ${chalk.bold('Max cost:')} $${flags.maxCost.toFixed(2)}`);
|
|
358
|
+
console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
|
|
359
|
+
console.log(` ${chalk.bold('Logs:')} ${chalk.dim(`badgr logs ${dep.deployment_id}`)}`);
|
|
360
|
+
console.log(` ${chalk.bold('Stop billing:')} ${chalk.cyan(`badgr down ${dep.deployment_id}`)}`);
|
|
361
|
+
console.log();
|
|
362
|
+
|
|
363
|
+
if (serveRate > HIGH_RATE_THRESHOLD && !flags.maxCost) {
|
|
364
|
+
console.log(chalk.yellow(` Selected capacity rate: $${serveRate.toFixed(2)}/hr`));
|
|
365
|
+
console.log(chalk.dim(' Tip: use --max-cost to enforce a hard ceiling.\n'));
|
|
366
|
+
}
|
|
367
|
+
console.log(chalk.dim(' Billing continues until you run: ') + chalk.cyan(`badgr down ${dep.deployment_id}`));
|
|
368
|
+
|
|
369
|
+
if (endpointReady && !customImage) {
|
|
370
|
+
const keySnip = config.apiKey?.slice(0, 4) || 'sk-...';
|
|
371
|
+
console.log(` ${chalk.bold('Use with OpenAI SDK:')}`);
|
|
153
372
|
console.log(chalk.dim(` from openai import OpenAI`));
|
|
154
|
-
console.log(chalk.dim(` client = OpenAI(
|
|
373
|
+
console.log(chalk.dim(` client = OpenAI(base_url="${endpointUrl}", api_key="${keySnip}...")`));
|
|
155
374
|
console.log(chalk.dim(` resp = client.chat.completions.create(model="${dep.model || model}", messages=[...])`));
|
|
375
|
+
console.log();
|
|
156
376
|
}
|
|
157
|
-
|
|
158
|
-
console.log(`\n ${chalk.dim(`Stop billing: badgr down ${dep.deployment_id}`)}\n`);
|
|
159
377
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -2,24 +2,20 @@ import { listDeployments as localDeployments } from '../store.js';
|
|
|
2
2
|
import { listDeployments as apiDeployments } from '../api.js';
|
|
3
3
|
|
|
4
4
|
export async function statusCommand(config, args, chalk) {
|
|
5
|
-
console.log(chalk.bold('\n📊 GPU Status\n'));
|
|
6
|
-
|
|
7
5
|
let deployments = [];
|
|
8
6
|
|
|
9
|
-
// ── Live data from the backend ────────────────────────────────────────────
|
|
10
7
|
if (config.apiKey) {
|
|
11
8
|
try {
|
|
12
9
|
const data = await apiDeployments(config);
|
|
13
10
|
deployments = data?.deployments ?? [];
|
|
14
11
|
} catch (err) {
|
|
15
|
-
console.log(chalk.yellow(
|
|
12
|
+
console.log(chalk.yellow(`\n Could not reach API: ${err.message}. Showing local state.\n`));
|
|
16
13
|
deployments = localDeployments().map(d => ({
|
|
17
14
|
deployment_id: d.id,
|
|
18
15
|
name: d.name,
|
|
19
16
|
workload_type: d.type,
|
|
20
17
|
gpu_type: d.gpu,
|
|
21
18
|
gpu_count: d.count,
|
|
22
|
-
provider: d.provider,
|
|
23
19
|
status: d.status,
|
|
24
20
|
cost_per_hour: d.costPerHour,
|
|
25
21
|
endpoint_url: d.endpointUrl,
|
|
@@ -33,7 +29,6 @@ export async function statusCommand(config, args, chalk) {
|
|
|
33
29
|
workload_type: d.type,
|
|
34
30
|
gpu_type: d.gpu,
|
|
35
31
|
gpu_count: d.count,
|
|
36
|
-
provider: d.provider,
|
|
37
32
|
status: d.status,
|
|
38
33
|
cost_per_hour: d.costPerHour,
|
|
39
34
|
endpoint_url: d.endpointUrl,
|
|
@@ -41,54 +36,46 @@ export async function statusCommand(config, args, chalk) {
|
|
|
41
36
|
}));
|
|
42
37
|
}
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
const running = deployments.filter(d => d.status === 'running' || d.status === 'provisioning');
|
|
40
|
+
|
|
41
|
+
if (running.length === 0) {
|
|
42
|
+
console.log(chalk.dim('\n Nothing running.\n'));
|
|
43
|
+
console.log(chalk.dim(' badgr run python train.py'));
|
|
44
|
+
console.log(chalk.dim(' badgr serve meta-llama/Llama-3.1-8B-Instruct\n'));
|
|
47
45
|
return;
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
console.log(chalk.bold('\nRunning now:\n'));
|
|
49
|
+
for (const d of running) {
|
|
50
|
+
const id = d.deployment_id || d.name;
|
|
51
|
+
const type = d.workload_type === 'endpoint' ? 'endpoint' : 'job';
|
|
52
|
+
const gpu = d.gpu_type || '—';
|
|
53
|
+
const rate = d.cost_per_hour > 0 ? chalk.yellow(`$${d.cost_per_hour.toFixed(2)}/hr`) : '';
|
|
54
|
+
const badge = d.status === 'provisioning'
|
|
55
|
+
? chalk.yellow('● starting')
|
|
56
|
+
: chalk.green('● running');
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
'Name'.padEnd(cols.name) +
|
|
54
|
-
'Type'.padEnd(cols.type) +
|
|
55
|
-
'GPU'.padEnd(cols.gpu) +
|
|
56
|
-
'Status'.padEnd(cols.status) +
|
|
57
|
-
'Price/hr';
|
|
58
|
-
console.log(' ' + chalk.bold(header));
|
|
59
|
-
console.log(' ' + '─'.repeat(Object.values(cols).reduce((a, b) => a + b, 0) + 8));
|
|
58
|
+
console.log(` ${badge} ${chalk.bold(id)} ${type} ${gpu} ${rate}`);
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const gpu = (d.gpu_type || '—').slice(0, cols.gpu - 1).padEnd(cols.gpu);
|
|
70
|
-
console.log(
|
|
71
|
-
' ' +
|
|
72
|
-
name.padEnd(cols.name) +
|
|
73
|
-
(d.workload_type || '—').padEnd(cols.type) +
|
|
74
|
-
gpu +
|
|
75
|
-
statusColor +
|
|
76
|
-
price
|
|
77
|
-
);
|
|
78
|
-
});
|
|
60
|
+
if (d.workload_type === 'endpoint' && d.endpoint_url) {
|
|
61
|
+
console.log(` ${chalk.dim('URL:')} ${d.endpoint_url}`);
|
|
62
|
+
}
|
|
63
|
+
if (d.model) {
|
|
64
|
+
console.log(` ${chalk.dim('Model:')} ${d.model}`);
|
|
65
|
+
}
|
|
66
|
+
console.log();
|
|
67
|
+
}
|
|
79
68
|
|
|
80
|
-
|
|
69
|
+
const billable = running.filter(d => (d.cost_per_hour || 0) > 0);
|
|
70
|
+
if (billable.length > 0) {
|
|
71
|
+
const totalPerHr = billable.reduce((s, d) => s + (d.cost_per_hour || 0), 0);
|
|
72
|
+
console.log(` ${chalk.bold('Total billing:')} ${chalk.yellow(`$${totalPerHr.toFixed(2)}/hr`)}\n`);
|
|
73
|
+
}
|
|
81
74
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
console.log(chalk.
|
|
86
|
-
endpoints.forEach(d => {
|
|
87
|
-
const url = d.endpoint_url || config.baseUrl;
|
|
88
|
-
console.log(` ${chalk.cyan(d.name || d.deployment_id)}`);
|
|
89
|
-
console.log(` ${chalk.dim('URL:')} ${url}`);
|
|
90
|
-
if (d.model) console.log(` ${chalk.dim('model:')} ${d.model}`);
|
|
91
|
-
});
|
|
92
|
-
console.log();
|
|
75
|
+
console.log(chalk.bold('Stop billing:\n'));
|
|
76
|
+
for (const d of running) {
|
|
77
|
+
const id = d.deployment_id || d.name;
|
|
78
|
+
console.log(` ${chalk.cyan(`badgr down ${id}`)}`);
|
|
93
79
|
}
|
|
80
|
+
console.log();
|
|
94
81
|
}
|