badgr-cli 1.0.28 → 1.0.30
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/package.json +1 -1
- package/src/commands/logs.js +57 -5
- package/src/commands/run.js +73 -106
- package/src/commands/serve.js +83 -106
- package/src/fallback.js +75 -0
package/package.json
CHANGED
package/src/commands/logs.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { findDeployment, listDeployments } from '../store.js';
|
|
2
2
|
import { requireApiKey } from '../config.js';
|
|
3
|
-
import { getDeploymentLogs } from '../api.js';
|
|
3
|
+
import { getDeploymentLogs, callApi } from '../api.js';
|
|
4
|
+
|
|
5
|
+
const TERMINAL_STATUSES = new Set(['succeeded', 'completed', 'failed', 'stopped', 'terminated']);
|
|
6
|
+
const FOLLOW_POLL_MS = 3000;
|
|
7
|
+
|
|
8
|
+
// Lines that carry structured metadata shown elsewhere (status bar in `badgr run`).
|
|
9
|
+
const LOG_META_RE = /^\[dep-[^\]]+\] (status|gpu|region|endpoint|cost|receipt|provider_status|uptime)=/;
|
|
4
10
|
|
|
5
11
|
export async function logsCommand(config, args, chalk) {
|
|
6
12
|
const idOrName = args.find(a => !a.startsWith('--'));
|
|
@@ -26,13 +32,18 @@ export async function logsCommand(config, args, chalk) {
|
|
|
26
32
|
|
|
27
33
|
console.log(chalk.bold(`\n📋 Logs: ${localDep?.name ?? deploymentId}\n`));
|
|
28
34
|
|
|
35
|
+
// Fetch and print initial batch of logs.
|
|
36
|
+
const seen = new Set();
|
|
29
37
|
try {
|
|
30
38
|
const data = await getDeploymentLogs(config, deploymentId);
|
|
31
39
|
const lines = data?.logs ?? [];
|
|
32
|
-
if (lines.length === 0) {
|
|
40
|
+
if (lines.length === 0 && !follow) {
|
|
33
41
|
console.log(chalk.dim(' No log lines available yet.\n'));
|
|
34
42
|
} else {
|
|
35
|
-
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
seen.add(line);
|
|
45
|
+
if (!LOG_META_RE.test(line)) console.log(` ${chalk.dim(line)}`);
|
|
46
|
+
}
|
|
36
47
|
}
|
|
37
48
|
} catch (err) {
|
|
38
49
|
console.log(chalk.yellow(` Could not fetch logs: ${err.message}`));
|
|
@@ -40,10 +51,51 @@ export async function logsCommand(config, args, chalk) {
|
|
|
40
51
|
console.log(chalk.dim(`\n GPU: ${localDep.gpu} Type: ${localDep.type}`));
|
|
41
52
|
console.log(chalk.dim(` Endpoint: ${localDep.endpointUrl}`));
|
|
42
53
|
}
|
|
54
|
+
if (!follow) { console.log(); return; }
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
if (follow) {
|
|
46
|
-
|
|
57
|
+
if (!follow) { console.log(); return; }
|
|
58
|
+
|
|
59
|
+
// --follow: poll until the deployment reaches a terminal state.
|
|
60
|
+
console.log(chalk.dim(' (following — Ctrl+C to stop)\n'));
|
|
61
|
+
|
|
62
|
+
let stopping = false;
|
|
63
|
+
process.once('SIGINT', () => { stopping = true; });
|
|
64
|
+
|
|
65
|
+
while (!stopping) {
|
|
66
|
+
await new Promise(r => setTimeout(r, FOLLOW_POLL_MS));
|
|
67
|
+
if (stopping) break;
|
|
68
|
+
|
|
69
|
+
let status = null;
|
|
70
|
+
try {
|
|
71
|
+
const dep = await callApi(`/deployments/${deploymentId}`, {
|
|
72
|
+
apiKey: config.apiKey,
|
|
73
|
+
baseUrl: config.baseUrl,
|
|
74
|
+
});
|
|
75
|
+
status = dep?.status ?? null;
|
|
76
|
+
} catch {
|
|
77
|
+
// network blip — keep following
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const data = await getDeploymentLogs(config, deploymentId);
|
|
82
|
+
for (const line of (data?.logs ?? [])) {
|
|
83
|
+
if (seen.has(line)) continue;
|
|
84
|
+
seen.add(line);
|
|
85
|
+
if (!LOG_META_RE.test(line)) {
|
|
86
|
+
const isErr = /^error\b/i.test(line) || /Error response from daemon/i.test(line);
|
|
87
|
+
console.log(` ${isErr ? chalk.red(line) : chalk.dim(line)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// logs endpoint temporarily unavailable
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (status && TERMINAL_STATUSES.has(status)) {
|
|
95
|
+
console.log(chalk.dim(`\n Job ${status}. No more logs.\n`));
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
47
98
|
}
|
|
99
|
+
|
|
48
100
|
console.log();
|
|
49
101
|
}
|
package/src/commands/run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import readline from 'readline';
|
|
2
1
|
import { requireApiKey } from '../config.js';
|
|
3
2
|
import { callApi, terminateDeployment } from '../api.js';
|
|
4
3
|
import { addReceipt, updateReceipt, generateReceiptId } from '../store.js';
|
|
4
|
+
import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD } from '../fallback.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* badgr run python train.py # gpu=auto, attached
|
|
@@ -21,8 +21,10 @@ export function parseRunArgs(args) {
|
|
|
21
21
|
if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
|
|
22
22
|
if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
|
|
23
23
|
if (args[i] === '--detach') { flags.detach = true; i++; continue; }
|
|
24
|
-
if (args[i] === '--fallback')
|
|
25
|
-
if (args[i] === '--no-fallback')
|
|
24
|
+
if (args[i] === '--fallback') { flags.fallback = args[++i]; i++; continue; }
|
|
25
|
+
if (args[i] === '--no-fallback') { flags.noFallback = true; i++; continue; }
|
|
26
|
+
if (args[i] === '--strict-capacity') { flags.noFallback = true; i++; continue; }
|
|
27
|
+
if (args[i] === '--no-expanded-search') { flags.noFallback = true; i++; continue; }
|
|
26
28
|
if (args[i] === '--max-runtime') { flags.maxRuntime = parseFloat(args[++i]); i++; continue; }
|
|
27
29
|
if (args[i] === '--max-cost') { flags.maxCost = parseFloat(args[++i]); i++; continue; }
|
|
28
30
|
positional.push(args[i++]);
|
|
@@ -30,6 +32,25 @@ export function parseRunArgs(args) {
|
|
|
30
32
|
return { flags, positional };
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
// Mirror of backend workload_profile.py — kept in sync for pre-flight display.
|
|
36
|
+
const _PROFILES = {
|
|
37
|
+
smoke_test: { label: 'smoke test', vram: '4 GB', gpus: ['RTX 3080', 'RTX 3090', 'RTX 4090'] },
|
|
38
|
+
lora_finetune: { label: 'fine-tuning (LoRA)', vram: '40+ GB', gpus: ['A6000', 'L40S', 'A100'] },
|
|
39
|
+
image_gen: { label: 'image generation', vram: '16+ GB', gpus: ['RTX 4090', 'A6000', 'L40S'] },
|
|
40
|
+
inference_small: { label: 'inference (7B–8B model)', vram: '24+ GB', gpus: ['RTX 4090', 'A6000', 'L40S'] },
|
|
41
|
+
general: { label: 'GPU job', vram: '16+ GB', gpus: ['RTX 4090', 'RTX 3090', 'A6000', 'L40S'] },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function inferProfileFromCommand(cmdStr) {
|
|
45
|
+
const s = cmdStr.toLowerCase();
|
|
46
|
+
if (/print\s*\(|['"]hello/.test(s) && s.length < 100) return 'smoke_test';
|
|
47
|
+
if (/lora|qlora|finetune|fine[_-]tun|peft/.test(s)) return 'lora_finetune';
|
|
48
|
+
if (/diffusion|stable.?diff|sdxl|sd.?xl|comfyui|a1111|invoke|kohya/.test(s)) return 'image_gen';
|
|
49
|
+
if (/vllm|tgi|text.generation.inference/.test(s)) return 'inference_small';
|
|
50
|
+
if (/\btrain\.py\b/.test(s)) return 'lora_finetune';
|
|
51
|
+
return 'general';
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
function fmtRuntime(ms) {
|
|
34
55
|
const s = Math.round(ms / 1000);
|
|
35
56
|
if (s < 60) return `${s}s`;
|
|
@@ -82,7 +103,7 @@ function renderStatusBar(chalk, { elapsedMs, ratePerHour, gpuUtil, cpuUtil, maxR
|
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
// Wait for status to leave 'starting'/'queued'/'provisioning'.
|
|
85
|
-
//
|
|
106
|
+
// The runtime limit does NOT start until this returns.
|
|
86
107
|
// Returns the final dep object with status 'running' or 'failed'.
|
|
87
108
|
async function waitForRunning(config, depId, chalk) {
|
|
88
109
|
const POLL_MS = 3000;
|
|
@@ -285,11 +306,6 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
|
|
|
285
306
|
}
|
|
286
307
|
}
|
|
287
308
|
|
|
288
|
-
function askConfirm(prompt) {
|
|
289
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
290
|
-
return new Promise(resolve => rl.question(prompt, ans => { rl.close(); resolve(ans.trim()); }));
|
|
291
|
-
}
|
|
292
|
-
|
|
293
309
|
export async function runCommand(config, args, chalk) {
|
|
294
310
|
const { flags, positional } = parseRunArgs(args);
|
|
295
311
|
|
|
@@ -312,10 +328,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
312
328
|
const maxRuntimeMs = flags.maxRuntime ? flags.maxRuntime * 60 * 1000 : null;
|
|
313
329
|
const maxCost = flags.maxCost ?? null;
|
|
314
330
|
|
|
315
|
-
|
|
316
|
-
const effectiveTier = (flags.tier === '2' || flags.tier === 'tier2' || flags.tier === 'tier-2')
|
|
317
|
-
? '2'
|
|
318
|
-
: (flags.tier || '1');
|
|
331
|
+
const effectiveTier = normalizeTier(flags.tier);
|
|
319
332
|
|
|
320
333
|
// ── Auto GPU selection (no --gpu specified) ────────────────────────────────
|
|
321
334
|
let gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : 'auto';
|
|
@@ -324,7 +337,16 @@ export async function runCommand(config, args, chalk) {
|
|
|
324
337
|
console.log(chalk.bold('\nâš¡ Running GPU job\n'));
|
|
325
338
|
if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
|
|
326
339
|
if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
|
|
327
|
-
|
|
340
|
+
// Show workload estimate so the user knows what Badgr inferred.
|
|
341
|
+
if (command) {
|
|
342
|
+
const profKey = inferProfileFromCommand(cmdStr);
|
|
343
|
+
const prof = _PROFILES[profKey];
|
|
344
|
+
console.log();
|
|
345
|
+
console.log(` ${chalk.bold('Estimated workload:')} ${prof.label}`);
|
|
346
|
+
console.log(` ${chalk.bold('Estimated minimum VRAM:')} ${prof.vram}`);
|
|
347
|
+
if (maxCost) console.log(` ${chalk.bold('Max cost:')} $${maxCost.toFixed(2)}`);
|
|
348
|
+
if (flags.maxRuntime) console.log(` ${chalk.bold('Max runtime:')} ${flags.maxRuntime}min`);
|
|
349
|
+
}
|
|
328
350
|
console.log();
|
|
329
351
|
} else {
|
|
330
352
|
// Specific GPU requested — show header
|
|
@@ -343,12 +365,12 @@ export async function runCommand(config, args, chalk) {
|
|
|
343
365
|
}
|
|
344
366
|
}
|
|
345
367
|
|
|
346
|
-
console.log(chalk.dim(' Finding
|
|
368
|
+
console.log(chalk.dim(' Finding suitable capacity...'));
|
|
347
369
|
if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
|
|
348
370
|
console.log(chalk.dim(` API: ${config.baseUrl}`));
|
|
349
371
|
}
|
|
350
372
|
|
|
351
|
-
function buildBody(gpuOverride) {
|
|
373
|
+
function buildBody(gpuOverride, tierOverride) {
|
|
352
374
|
const effectiveRegion = flags.region ? flags.region.toUpperCase() : undefined;
|
|
353
375
|
return {
|
|
354
376
|
command,
|
|
@@ -358,114 +380,58 @@ export async function runCommand(config, args, chalk) {
|
|
|
358
380
|
...(effectiveRegion ? { region: effectiveRegion } : {}),
|
|
359
381
|
max_price_per_hour: flags.maxPrice,
|
|
360
382
|
name: flags.name,
|
|
361
|
-
tier: effectiveTier,
|
|
383
|
+
tier: tierOverride || effectiveTier,
|
|
362
384
|
};
|
|
363
385
|
}
|
|
364
386
|
|
|
365
387
|
let dep;
|
|
366
388
|
try {
|
|
367
|
-
dep = await
|
|
368
|
-
|
|
369
|
-
apiKey: config.apiKey,
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
389
|
+
dep = await callWithFallback(
|
|
390
|
+
'/run',
|
|
391
|
+
{ apiKey: config.apiKey, baseUrl: config.baseUrl },
|
|
392
|
+
(tierOverride) => buildBody(undefined, tierOverride),
|
|
393
|
+
effectiveTier,
|
|
394
|
+
chalk,
|
|
395
|
+
{ thing: 'job', cmd: 'badgr run' },
|
|
396
|
+
);
|
|
373
397
|
} catch (err) {
|
|
374
|
-
|
|
375
|
-
if (d?.code === 'NO_CAPACITY_MATCH') {
|
|
376
|
-
if (effectiveTier === '2') {
|
|
377
|
-
console.error(chalk.red('\n ✗ No GPU capacity available right now on any provider.\n'));
|
|
378
|
-
console.error(chalk.dim(' Try `badgr capacity` to see what\'s available, or try again shortly.'));
|
|
379
|
-
process.exit(1);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Tier 1 out of capacity — offer tier 2 marketplace routing.
|
|
383
|
-
if (process.stdin.isTTY) {
|
|
384
|
-
const answer = await askConfirm(
|
|
385
|
-
`\n No tier 1 capacity available. Try tier 2 marketplace routing? [${chalk.bold('Enter')}/${chalk.bold('q')}]: `
|
|
386
|
-
);
|
|
387
|
-
if (answer.toLowerCase() === 'q') {
|
|
388
|
-
console.log(chalk.dim('\n Cancelled.\n'));
|
|
389
|
-
process.exit(0);
|
|
390
|
-
}
|
|
391
|
-
} else {
|
|
392
|
-
console.log(chalk.dim('\n No tier 1 capacity — trying tier 2 marketplace routing...\n'));
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
console.log(chalk.dim(' Searching tier 2 capacity...'));
|
|
396
|
-
try {
|
|
397
|
-
dep = await callApi('/run', {
|
|
398
|
-
method: 'POST',
|
|
399
|
-
apiKey: config.apiKey,
|
|
400
|
-
baseUrl: config.baseUrl,
|
|
401
|
-
body: { ...buildBody(), tier: '2' },
|
|
402
|
-
});
|
|
403
|
-
} catch (err2) {
|
|
404
|
-
const d2 = err2.errorData;
|
|
405
|
-
if (d2?.code === 'NO_CAPACITY_MATCH') {
|
|
406
|
-
console.error(chalk.red('\n ✗ No GPU capacity available right now on any provider.\n'));
|
|
407
|
-
console.error(chalk.dim(' Try `badgr capacity` to see what\'s available, or try again shortly.'));
|
|
408
|
-
} else if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
409
|
-
console.error(chalk.red(`\n ✗ Budget provider found capacity but could not start the machine. Please try again.\n`));
|
|
410
|
-
// (error detail kept below)
|
|
411
|
-
if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
|
|
412
|
-
if (d2?.debug_error) console.error(chalk.dim(` Provider detail: ${d2.debug_error}`));
|
|
413
|
-
} else {
|
|
414
|
-
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.\n`));
|
|
415
|
-
}
|
|
416
|
-
} else {
|
|
417
|
-
console.error(chalk.red(`\n ✗ Could not start job on tier 2: ${err2.message}\n`));
|
|
418
|
-
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.`));
|
|
419
|
-
}
|
|
420
|
-
process.exit(1);
|
|
421
|
-
}
|
|
422
|
-
} else if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
423
|
-
if (d?.low_cost_provider_failed) {
|
|
424
|
-
console.error(chalk.red(`\n ✗ Tier 2 unavailable. Tier 1 also unavailable. Try again shortly.\n`));
|
|
425
|
-
} else {
|
|
426
|
-
console.error(chalk.red(`\n ✗ Badgr found ${gpu} capacity but could not start the machine. Please try again.\n`));
|
|
427
|
-
}
|
|
428
|
-
if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
|
|
429
|
-
if (d?.debug_error) console.error(chalk.dim(` Provider detail: ${d.debug_error}`));
|
|
430
|
-
} else {
|
|
431
|
-
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.\n`));
|
|
432
|
-
}
|
|
433
|
-
process.exit(1);
|
|
434
|
-
} else if (err.isPaymentRequired) {
|
|
398
|
+
if (err.isPaymentRequired) {
|
|
435
399
|
console.error(chalk.yellow(err.message));
|
|
436
400
|
const rerun = ['badgr run', ...args].join(' ');
|
|
437
401
|
console.error(chalk.dim(`After payment, rerun:\n ${rerun}\n`));
|
|
438
402
|
process.exit(1);
|
|
439
|
-
} else {
|
|
440
|
-
console.error(chalk.red(`\n ✗ Could not start job: ${err.message}\n`));
|
|
441
|
-
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr run … for a full trace.`));
|
|
442
|
-
console.error(chalk.dim(` Check config: badgr config\n`));
|
|
443
|
-
process.exit(1);
|
|
444
403
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (dep.provider_fallback_note === 'tier2_failed_using_tier1') {
|
|
448
|
-
console.log(chalk.yellow(' ℹ Tier 2 unavailable. Running on Tier 1 instead.\n'));
|
|
404
|
+
throw err;
|
|
449
405
|
}
|
|
450
406
|
|
|
451
407
|
const rcptId = dep.receipt_id || generateReceiptId();
|
|
452
408
|
addReceipt({
|
|
453
|
-
receiptId:
|
|
454
|
-
action:
|
|
455
|
-
deploymentId:
|
|
456
|
-
gpu:
|
|
457
|
-
|
|
458
|
-
|
|
409
|
+
receiptId: rcptId,
|
|
410
|
+
action: 'badgr run',
|
|
411
|
+
deploymentId: dep.deployment_id,
|
|
412
|
+
gpu: dep.gpu_type,
|
|
413
|
+
providerRoute: dep.provider ?? null,
|
|
414
|
+
tier: dep.tier ?? null,
|
|
415
|
+
maxCost: maxCost ?? null,
|
|
416
|
+
maxRuntime: flags.maxRuntime ?? null,
|
|
417
|
+
status: dep.status,
|
|
418
|
+
createdAt: new Date().toISOString(),
|
|
459
419
|
});
|
|
460
420
|
|
|
461
|
-
|
|
421
|
+
const rate = dep.cost_per_hour || 0;
|
|
422
|
+
|
|
423
|
+
console.log(chalk.dim(' Capacity found.\n'));
|
|
462
424
|
console.log(chalk.bold(` Job ID: ${chalk.cyan(dep.deployment_id)}`));
|
|
463
425
|
console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
|
|
464
|
-
if (
|
|
465
|
-
if (
|
|
466
|
-
if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
|
|
426
|
+
if (rate > 0) console.log(` ${chalk.bold('Rate:')} $${rate.toFixed(2)}/hr`);
|
|
427
|
+
if (maxCost) console.log(` ${chalk.bold('Max cost:')} $${maxCost.toFixed(2)}`);
|
|
467
428
|
console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
|
|
468
429
|
|
|
430
|
+
if (rate > HIGH_RATE_THRESHOLD && !maxCost) {
|
|
431
|
+
console.log(chalk.yellow(`\n Selected capacity rate: $${rate.toFixed(2)}/hr`));
|
|
432
|
+
console.log(chalk.dim(' Tip: use --max-cost to enforce a hard ceiling.'));
|
|
433
|
+
}
|
|
434
|
+
|
|
469
435
|
if (detach) {
|
|
470
436
|
console.log(`\n ${chalk.bold('Logs:')} ${dep.logs_url || `badgr logs ${dep.deployment_id}`}`);
|
|
471
437
|
console.log(chalk.dim(`\n Detached. Track progress: badgr logs ${dep.deployment_id}\n`));
|
|
@@ -473,7 +439,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
473
439
|
}
|
|
474
440
|
|
|
475
441
|
// Wait through startup phases (queued → provisioning → starting → running).
|
|
476
|
-
// The max-runtime clock does NOT start until this returns
|
|
442
|
+
// The max-runtime clock does NOT start until this returns.
|
|
477
443
|
const STARTUP_STATES = new Set(['queued', 'provisioning', 'starting']);
|
|
478
444
|
if (STARTUP_STATES.has(dep.status)) {
|
|
479
445
|
console.log();
|
|
@@ -506,7 +472,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
506
472
|
}
|
|
507
473
|
const runtimeMs = Date.now() - attachStart;
|
|
508
474
|
const finalCost = ratePerHour * (runtimeMs / 3_600_000);
|
|
509
|
-
updateReceipt(rcptId, { status: reason, runtimeSeconds: Math.round(runtimeMs / 1000), finalCost });
|
|
475
|
+
updateReceipt(rcptId, { status: reason, teardownStatus: 'terminated', runtimeSeconds: Math.round(runtimeMs / 1000), finalCost });
|
|
510
476
|
console.log(chalk.dim(` Runtime: ${fmtRuntime(runtimeMs)} • Est. cost: $${finalCost.toFixed(4)}`));
|
|
511
477
|
console.log(chalk.dim(' Job stopped. Billing ended.\n'));
|
|
512
478
|
process.exit(reason === 'interrupted' ? 0 : 1);
|
|
@@ -525,11 +491,12 @@ export async function runCommand(config, args, chalk) {
|
|
|
525
491
|
|
|
526
492
|
const finalCost = ratePerHour * (runtimeMs / 3_600_000);
|
|
527
493
|
updateReceipt(rcptId, {
|
|
528
|
-
status:
|
|
494
|
+
status: finalStatus,
|
|
529
495
|
exitCode,
|
|
530
496
|
runtimeSeconds: Math.round(runtimeMs / 1000),
|
|
531
497
|
finalCost,
|
|
532
498
|
failureType,
|
|
499
|
+
teardownStatus: (finalStatus === 'completed' || finalStatus === 'succeeded') ? 'terminated' : 'failed',
|
|
533
500
|
});
|
|
534
501
|
|
|
535
502
|
console.log(` ${chalk.bold('Runtime:')} ${fmtRuntime(runtimeMs)}`);
|
package/src/commands/serve.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import readline from 'readline';
|
|
2
1
|
import { requireApiKey } from '../config.js';
|
|
3
|
-
import { callApi } from '../api.js';
|
|
4
2
|
import { addDeployment, addReceipt, updateReceipt, generateReceiptId } from '../store.js';
|
|
5
|
-
|
|
6
|
-
function askConfirm(prompt) {
|
|
7
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
-
return new Promise(resolve => rl.question(prompt, ans => { rl.close(); resolve(ans.trim()); }));
|
|
9
|
-
}
|
|
3
|
+
import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD } from '../fallback.js';
|
|
10
4
|
|
|
11
5
|
/**
|
|
12
6
|
* badgr serve meta-llama/Llama-3.1-8B-Instruct
|
|
@@ -25,13 +19,33 @@ export function parseServeArgs(args) {
|
|
|
25
19
|
if (args[i] === '--tier') { flags.tier = args[++i]; i++; continue; }
|
|
26
20
|
if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
|
|
27
21
|
if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
|
|
28
|
-
if (args[i] === '--no-wait')
|
|
22
|
+
if (args[i] === '--no-wait') { flags.noWait = true; i++; continue; }
|
|
23
|
+
if (args[i] === '--max-cost') { flags.maxCost = parseFloat(args[++i]); i++; continue; }
|
|
24
|
+
if (args[i] === '--strict-capacity') { flags.noMarketplaceFallback = true; i++; continue; }
|
|
25
|
+
if (args[i] === '--no-expanded-search') { flags.noMarketplaceFallback = true; i++; continue; }
|
|
29
26
|
positional.push(args[i++]);
|
|
30
27
|
}
|
|
31
28
|
const model = positional[0] || null;
|
|
32
29
|
return { model, flags };
|
|
33
30
|
}
|
|
34
31
|
|
|
32
|
+
// Mirror of backend workload_profile.py infer_profile_from_model — for pre-flight display.
|
|
33
|
+
function _inferServeProfile(modelName) {
|
|
34
|
+
const s = modelName.toLowerCase();
|
|
35
|
+
const moe = s.match(/(\d+)x(\d+)b/);
|
|
36
|
+
let paramsB;
|
|
37
|
+
if (moe) {
|
|
38
|
+
paramsB = parseInt(moe[1]) * parseInt(moe[2]);
|
|
39
|
+
} else {
|
|
40
|
+
const m = s.match(/(\d+)b/);
|
|
41
|
+
paramsB = m ? parseInt(m[1]) : null;
|
|
42
|
+
}
|
|
43
|
+
if (paramsB === null) return { label: 'inference (7B–8B model)', vram: '24+ GB', gpus: ['RTX 4090', 'A6000', 'L40S'] };
|
|
44
|
+
if (paramsB <= 9) return { label: 'inference (7B–8B model)', vram: '24+ GB', gpus: ['RTX 4090', 'A6000', 'L40S'] };
|
|
45
|
+
if (paramsB <= 35) return { label: 'inference (30B–34B model)', vram: '40+ GB', gpus: ['A6000', 'L40S', 'A100'] };
|
|
46
|
+
return { label: 'inference (70B+ model)', vram: '80+ GB', gpus: ['H100', 'A100'] };
|
|
47
|
+
}
|
|
48
|
+
|
|
35
49
|
function _serveStageLabel(elapsedSec) {
|
|
36
50
|
if (elapsedSec < 45) return 'Starting vLLM…';
|
|
37
51
|
if (elapsedSec < 150) return 'Downloading model…';
|
|
@@ -74,118 +88,68 @@ export async function serveCommand(config, args, chalk) {
|
|
|
74
88
|
const gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : 'AUTO';
|
|
75
89
|
const gpuLabel = gpu === 'AUTO' ? 'auto' : gpu;
|
|
76
90
|
|
|
77
|
-
|
|
78
|
-
const effectiveTier = (flags.tier === '2' || flags.tier === 'tier2' || flags.tier === 'tier-2')
|
|
79
|
-
? '2'
|
|
80
|
-
: (flags.tier || '1');
|
|
91
|
+
const effectiveTier = normalizeTier(flags.tier);
|
|
81
92
|
|
|
82
|
-
console.log(chalk.bold('\
|
|
93
|
+
console.log(chalk.bold('\nâš¡ Serving model\n'));
|
|
83
94
|
console.log(` ${chalk.bold('Model:')} ${model}`);
|
|
84
95
|
console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
|
|
85
|
-
|
|
96
|
+
|
|
97
|
+
// Show inferred workload so the user knows what Badgr detected.
|
|
98
|
+
if (gpu === 'AUTO') {
|
|
99
|
+
const prof = _inferServeProfile(model);
|
|
100
|
+
console.log();
|
|
101
|
+
console.log(` ${chalk.bold('Estimated workload:')} ${prof.label}`);
|
|
102
|
+
console.log(` ${chalk.bold('Estimated minimum VRAM:')} ${prof.vram}`);
|
|
103
|
+
if (flags.maxCost) console.log(` ${chalk.bold('Max cost:')} $${flags.maxCost.toFixed(2)}`);
|
|
104
|
+
}
|
|
86
105
|
console.log();
|
|
87
|
-
process.stdout.write(chalk.dim(' Finding
|
|
106
|
+
process.stdout.write(chalk.dim(' Finding suitable capacity...\n'));
|
|
88
107
|
|
|
89
108
|
const effectiveRegion = flags.region ? flags.region.toUpperCase() : undefined;
|
|
90
109
|
|
|
91
|
-
function buildBody(gpuOverride,
|
|
110
|
+
function buildBody(gpuOverride, tierOverride) {
|
|
92
111
|
return {
|
|
93
112
|
model,
|
|
94
113
|
gpu: gpuOverride || gpu,
|
|
95
114
|
gpu_count: flags.count || 1,
|
|
96
|
-
...(
|
|
115
|
+
...(effectiveRegion ? { region: effectiveRegion } : {}),
|
|
97
116
|
max_price_per_hour: flags.maxPrice,
|
|
98
117
|
name: flags.name,
|
|
99
|
-
tier: effectiveTier,
|
|
118
|
+
tier: tierOverride || effectiveTier,
|
|
100
119
|
};
|
|
101
120
|
}
|
|
102
121
|
|
|
103
122
|
let dep;
|
|
104
123
|
try {
|
|
105
|
-
dep = await
|
|
106
|
-
|
|
107
|
-
apiKey: config.apiKey,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
124
|
+
dep = await callWithFallback(
|
|
125
|
+
'/serve',
|
|
126
|
+
{ apiKey: config.apiKey, baseUrl: config.baseUrl },
|
|
127
|
+
(tierOverride) => buildBody(undefined, tierOverride),
|
|
128
|
+
effectiveTier,
|
|
129
|
+
chalk,
|
|
130
|
+
{ thing: 'endpoint', cmd: 'badgr serve' },
|
|
131
|
+
);
|
|
111
132
|
} catch (err) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
console.log(chalk.dim('\n Cancelled.\n'));
|
|
127
|
-
process.exit(0);
|
|
128
|
-
}
|
|
129
|
-
} else {
|
|
130
|
-
console.log(chalk.dim('\n No tier 1 capacity — trying tier 2 marketplace routing...\n'));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
console.log(chalk.dim(' Searching tier 2 capacity...'));
|
|
134
|
-
try {
|
|
135
|
-
dep = await callApi('/serve', {
|
|
136
|
-
method: 'POST',
|
|
137
|
-
apiKey: config.apiKey,
|
|
138
|
-
baseUrl: config.baseUrl,
|
|
139
|
-
body: { ...buildBody(), tier: '2' },
|
|
140
|
-
});
|
|
141
|
-
} catch (err2) {
|
|
142
|
-
const d2 = err2.errorData;
|
|
143
|
-
if (d2?.code === 'NO_CAPACITY_MATCH') {
|
|
144
|
-
console.error(chalk.red('\n ✗ No GPU capacity available right now on any provider.\n'));
|
|
145
|
-
console.error(chalk.dim(' Run `badgr capacity` to see alternatives, or try again shortly.'));
|
|
146
|
-
} else if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
147
|
-
console.error(chalk.red(`\n ✗ Budget provider found capacity but could not start the endpoint. Please try again.\n`));
|
|
148
|
-
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
|
|
149
|
-
} else {
|
|
150
|
-
console.error(chalk.red(`\n ✗ Could not start endpoint on tier 2: ${err2.message}\n`));
|
|
151
|
-
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
|
|
152
|
-
}
|
|
153
|
-
process.exit(1);
|
|
154
|
-
}
|
|
155
|
-
} else if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
156
|
-
if (d?.low_cost_provider_failed) {
|
|
157
|
-
console.error(chalk.red(`\n ✗ Tier 2 unavailable. Tier 1 also unavailable. Try again shortly.\n`));
|
|
158
|
-
} else {
|
|
159
|
-
console.error(chalk.red(`\n ✗ Badgr found capacity but could not start the endpoint. Please try again.\n`));
|
|
160
|
-
}
|
|
161
|
-
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
|
|
162
|
-
process.exit(1);
|
|
163
|
-
} else {
|
|
164
|
-
const failRcptId = generateReceiptId();
|
|
165
|
-
addReceipt({
|
|
166
|
-
receiptId: failRcptId,
|
|
167
|
-
action: 'badgr serve',
|
|
168
|
-
model,
|
|
169
|
-
gpu: gpuLabel,
|
|
170
|
-
status: 'failed',
|
|
171
|
-
failureType: 'infrastructure',
|
|
172
|
-
createdAt: new Date().toISOString(),
|
|
173
|
-
});
|
|
174
|
-
if (err.isPaymentRequired) {
|
|
175
|
-
console.error(chalk.yellow(err.message));
|
|
176
|
-
const rerun = `badgr serve ${args.join(' ')}`;
|
|
177
|
-
console.error(chalk.dim(`After payment, rerun:\n ${rerun}\n`));
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
console.error(chalk.red(`\n ✗ Could not start endpoint: ${err.message}`));
|
|
181
|
-
console.error(chalk.dim(`\n Receipt: ${failRcptId}`));
|
|
182
|
-
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
|
|
133
|
+
const failRcptId = generateReceiptId();
|
|
134
|
+
addReceipt({
|
|
135
|
+
receiptId: failRcptId,
|
|
136
|
+
action: 'badgr serve',
|
|
137
|
+
model,
|
|
138
|
+
gpu: gpuLabel,
|
|
139
|
+
status: 'failed',
|
|
140
|
+
failureType: 'infrastructure',
|
|
141
|
+
createdAt: new Date().toISOString(),
|
|
142
|
+
});
|
|
143
|
+
if (err.isPaymentRequired) {
|
|
144
|
+
console.error(chalk.yellow(err.message));
|
|
145
|
+
const rerun = `badgr serve ${args.join(' ')}`;
|
|
146
|
+
console.error(chalk.dim(`After payment, rerun:\n ${rerun}\n`));
|
|
183
147
|
return;
|
|
184
148
|
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
149
|
+
console.error(chalk.red(`\n ✗ Could not start endpoint: ${err.message}`));
|
|
150
|
+
console.error(chalk.dim(`\n Receipt: ${failRcptId}`));
|
|
151
|
+
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
|
|
152
|
+
return;
|
|
189
153
|
}
|
|
190
154
|
|
|
191
155
|
addDeployment({
|
|
@@ -200,6 +164,8 @@ export async function serveCommand(config, args, chalk) {
|
|
|
200
164
|
receiptId: dep.receipt_id,
|
|
201
165
|
createdAt: new Date().toISOString(),
|
|
202
166
|
costPerHour: dep.cost_per_hour || 0,
|
|
167
|
+
providerRoute: dep.provider ?? null,
|
|
168
|
+
tier: dep.tier ?? null,
|
|
203
169
|
});
|
|
204
170
|
|
|
205
171
|
const rcptId = dep.receipt_id || generateReceiptId();
|
|
@@ -208,6 +174,8 @@ export async function serveCommand(config, args, chalk) {
|
|
|
208
174
|
action: 'badgr serve',
|
|
209
175
|
deploymentId: dep.deployment_id,
|
|
210
176
|
gpu: dep.gpu_type,
|
|
177
|
+
providerRoute: dep.provider ?? null,
|
|
178
|
+
tier: dep.tier ?? null,
|
|
211
179
|
status: dep.status,
|
|
212
180
|
createdAt: new Date().toISOString(),
|
|
213
181
|
});
|
|
@@ -234,15 +202,24 @@ export async function serveCommand(config, args, chalk) {
|
|
|
234
202
|
console.log();
|
|
235
203
|
}
|
|
236
204
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
console.log(` ${chalk.bold('
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
console.log(` ${chalk.bold('
|
|
243
|
-
console.log(` ${chalk.bold('
|
|
205
|
+
const serveRate = dep.cost_per_hour || 0;
|
|
206
|
+
|
|
207
|
+
console.log(` ${chalk.bold('Base URL:')} ${chalk.cyan(endpointUrl)}`);
|
|
208
|
+
console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
|
|
209
|
+
console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
|
|
210
|
+
if (serveRate > 0) console.log(` ${chalk.bold('Rate:')} $${serveRate.toFixed(2)}/hr`);
|
|
211
|
+
if (flags.maxCost) console.log(` ${chalk.bold('Max cost:')} $${flags.maxCost.toFixed(2)}`);
|
|
212
|
+
console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
|
|
213
|
+
console.log(` ${chalk.bold('Logs:')} ${chalk.dim(`badgr logs ${dep.deployment_id}`)}`);
|
|
214
|
+
console.log(` ${chalk.bold('Stop billing:')} ${chalk.cyan(`badgr down ${dep.deployment_id}`)}`);
|
|
244
215
|
console.log();
|
|
245
216
|
|
|
217
|
+
if (serveRate > HIGH_RATE_THRESHOLD && !flags.maxCost) {
|
|
218
|
+
console.log(chalk.yellow(` Selected capacity rate: $${serveRate.toFixed(2)}/hr`));
|
|
219
|
+
console.log(chalk.dim(' Tip: use --max-cost to enforce a hard ceiling.\n'));
|
|
220
|
+
}
|
|
221
|
+
console.log(chalk.dim(' Billing continues until you run: ') + chalk.cyan(`badgr down ${dep.deployment_id}`));
|
|
222
|
+
|
|
246
223
|
if (endpointReady) {
|
|
247
224
|
const keySnip = config.apiKey?.slice(0, 8) || 'sk-...';
|
|
248
225
|
console.log(` ${chalk.bold('Use with OpenAI SDK:')}`);
|
package/src/fallback.js
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
import readline from 'readline';
|
|
2
2
|
|
|
3
|
+
// ── Shared routing helpers ────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/** Rates above this threshold trigger a visible warning when no --max-cost is set. */
|
|
6
|
+
export const HIGH_RATE_THRESHOLD = 3.00;
|
|
7
|
+
|
|
8
|
+
/** Normalise --tier flag variants to '1' or '2'. */
|
|
9
|
+
export function normalizeTier(tier) {
|
|
10
|
+
return (tier === '2' || tier === 'tier2' || tier === 'tier-2') ? '2' : (tier || '1');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Call an API endpoint with automatic tier-2 expansion on NO_CAPACITY_MATCH.
|
|
15
|
+
* Returns the deployment object on success; throws or calls process.exit on failure.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} endpoint - '/run' or '/serve'
|
|
18
|
+
* @param {object} callOpts - { apiKey, baseUrl }
|
|
19
|
+
* @param {function} buildBody - (tierOverride?) => body object
|
|
20
|
+
* @param {string} effectiveTier
|
|
21
|
+
* @param {object} chalk
|
|
22
|
+
* @param {object} labels - { thing: 'job'|'endpoint', cmd: 'badgr run'|'badgr serve' }
|
|
23
|
+
*/
|
|
24
|
+
export async function callWithFallback(endpoint, callOpts, buildBody, effectiveTier, chalk, labels) {
|
|
25
|
+
const { callApi } = await import('./api.js');
|
|
26
|
+
const thing = labels?.thing ?? 'job';
|
|
27
|
+
const cmd = labels?.cmd ?? 'badgr run';
|
|
28
|
+
|
|
29
|
+
async function attempt(body) {
|
|
30
|
+
return callApi(endpoint, { method: 'POST', ...callOpts, body });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function handleErr(err, isFallback) {
|
|
34
|
+
const d = err.errorData;
|
|
35
|
+
if (d?.code === 'NO_CAPACITY_MATCH') {
|
|
36
|
+
console.error(chalk.red('\n ✗ No suitable GPU capacity available right now.\n'));
|
|
37
|
+
console.error(chalk.dim(' Try `badgr capacity` to see what\'s available, or try again shortly.'));
|
|
38
|
+
} else if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
39
|
+
if (d?.low_cost_provider_failed) {
|
|
40
|
+
console.error(chalk.red(`\n ✗ No suitable capacity available right now. Try again shortly.\n`));
|
|
41
|
+
} else {
|
|
42
|
+
console.error(chalk.red(`\n ✗ Capacity found but ${thing} failed to start. Please try again.\n`));
|
|
43
|
+
if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
|
|
44
|
+
if (d?.debug_error) console.error(chalk.dim(` Detail: ${d.debug_error}`));
|
|
45
|
+
} else {
|
|
46
|
+
console.error(chalk.dim(` Run BADGR_DEBUG=1 ${cmd} … for a full trace.\n`));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
console.error(chalk.red(`\n ✗ Could not start ${thing}: ${err.message}\n`));
|
|
51
|
+
console.error(chalk.dim(` Run BADGR_DEBUG=1 ${cmd} … for a full trace.`));
|
|
52
|
+
if (!isFallback) console.error(chalk.dim(` Check config: badgr config\n`));
|
|
53
|
+
}
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return await attempt(buildBody());
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const d = err.errorData;
|
|
61
|
+
|
|
62
|
+
if (d?.code === 'NO_CAPACITY_MATCH' && effectiveTier !== '2') {
|
|
63
|
+
console.log(chalk.dim('\n Primary capacity unavailable — expanding search...\n'));
|
|
64
|
+
try {
|
|
65
|
+
return await attempt(buildBody('2'));
|
|
66
|
+
} catch (err2) {
|
|
67
|
+
handleErr(err2, true);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (err.isPaymentRequired) throw err; // let caller handle payment errors
|
|
72
|
+
handleErr(err, false);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── GPU fallback prompt ───────────────────────────────────────────────────────
|
|
77
|
+
|
|
3
78
|
// GPU descriptions for display only — no scoring logic lives here.
|
|
4
79
|
// Ranking is computed server-side and returned as `rank` on each alternative.
|
|
5
80
|
const GPU_DISPLAY = {
|