badgr-cli 1.1.1 → 1.1.3
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/LICENSE +207 -0
- package/README.md +13 -6
- package/package.json +44 -2
- package/src/api.js +16 -0
- package/src/badgr.js +26 -10
- package/src/commands/batch.js +11 -0
- package/src/commands/billing.js +3 -3
- package/src/commands/comfyui.js +31 -15
- package/src/commands/connect.js +4 -1
- package/src/commands/diagnose.js +493 -0
- package/src/commands/embed.js +13 -10
- package/src/commands/job.js +246 -0
- package/src/commands/launch.js +152 -16
- package/src/commands/login.js +75 -20
- package/src/commands/run.js +74 -16
- package/src/commands/sbatch.js +6 -1
- package/src/commands/serve.js +44 -30
- package/src/commands/train.js +8 -12
- package/src/commands/transcribe.js +13 -10
- package/src/credentials.js +33 -0
- package/src/envFlag.js +10 -0
- package/src/fallback.js +13 -2
- package/src/onboarding.js +8 -1
- package/src/progress.js +48 -0
- package/src/commands/task.js +0 -25
- package/tests/agent-images.test.js +0 -17
- package/tests/api.test.js +0 -168
- package/tests/artifactDownload.test.js +0 -113
- package/tests/artifacts.test.js +0 -168
- package/tests/batch.test.js +0 -641
- package/tests/browser.test.js +0 -51
- package/tests/capacity.test.js +0 -68
- package/tests/commands.test.js +0 -417
- package/tests/config.test.js +0 -96
- package/tests/connect.test.js +0 -83
- package/tests/detect.test.js +0 -191
- package/tests/down.test.js +0 -150
- package/tests/errors.test.js +0 -130
- package/tests/fallback-timeout.test.js +0 -41
- package/tests/fanout.test.js +0 -124
- package/tests/gpu-doctor-classifiers.test.js +0 -402
- package/tests/gpu-doctor-doctor.test.js +0 -304
- package/tests/gpu-doctor-probe-cache.test.js +0 -110
- package/tests/gpu-doctor-probes.test.js +0 -257
- package/tests/heartbeat.test.js +0 -70
- package/tests/job-progress-poll.test.js +0 -136
- package/tests/launch-command-argv.test.js +0 -93
- package/tests/launch-readiness.test.js +0 -403
- package/tests/launch.test.js +0 -440
- package/tests/onboarding.test.js +0 -134
- package/tests/productized-dry-run.test.js +0 -141
- package/tests/productized-runners.test.js +0 -237
- package/tests/pull.test.js +0 -266
- package/tests/rerun.test.js +0 -94
- package/tests/restart.test.js +0 -88
- package/tests/router.test.js +0 -98
- package/tests/run-lifecycle.test.js +0 -1054
- package/tests/sbatch.test.js +0 -190
- package/tests/secrets.test.js +0 -16
- package/tests/serve-apps.test.js +0 -189
- package/tests/serve-lifecycle.test.js +0 -931
- package/tests/slurm.test.js +0 -77
- package/tests/spec.test.js +0 -201
- package/tests/status.test.js +0 -73
- package/tests/store.test.js +0 -187
- package/tests/task.test.js +0 -109
- package/tests/template.test.js +0 -556
- package/tests/train-lora-dataset.test.js +0 -176
- package/tests/upload.test.js +0 -79
- package/tests/workload-rerun.test.js +0 -56
- package/tests/workload-spec.test.js +0 -180
- package/tests/workload-templates.test.js +0 -865
- package/tests/workload-workspace-paths.test.js +0 -46
package/src/commands/run.js
CHANGED
|
@@ -5,13 +5,14 @@ import { createWriteStream } from 'fs';
|
|
|
5
5
|
import { requireApiKey } from '../config.js';
|
|
6
6
|
import { callApi, terminateDeployment, uploadBlob, quoteRun } from '../api.js';
|
|
7
7
|
import { addReceipt, updateReceipt, generateReceiptId, selectedComputeFromDeployment } from '../store.js';
|
|
8
|
-
import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD } from '../fallback.js';
|
|
8
|
+
import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD, SMOKE_MAX_COST_USD, SMOKE_MAX_RUNTIME_MINUTES } from '../fallback.js';
|
|
9
9
|
import { formatCliError } from '../errors.js';
|
|
10
10
|
import { TEMPLATE_MAP, buildTemplateFlags, parseTemplateOverrides } from '../catalog.js';
|
|
11
|
-
import { stage as _stage, stageDone as _stageDone, writeBlock as _writeBlock, clearBlock as _clearBlock, renderLiveBlock as _renderLiveBlock, printFailureClass as _printFailureClass } from '../progress.js';
|
|
11
|
+
import { stage as _stage, stageDone as _stageDone, writeBlock as _writeBlock, clearBlock as _clearBlock, renderLiveBlock as _renderLiveBlock, printFailureClass as _printFailureClass, printCapacityPreview, formatTierLabel } from '../progress.js';
|
|
12
12
|
import { detectWorkload, workloadTypeLabel } from '../detect.js';
|
|
13
13
|
import { ensureBadgrReady } from '../onboarding.js';
|
|
14
14
|
import { VM_CLASSES, parseGbSize } from '../spec.js';
|
|
15
|
+
import { parseEnvFlag } from '../envFlag.js';
|
|
15
16
|
|
|
16
17
|
function vmClassLine(sizeKey) {
|
|
17
18
|
const vmClass = VM_CLASSES[sizeKey];
|
|
@@ -56,6 +57,7 @@ export function parseRunArgs(args) {
|
|
|
56
57
|
if (flagArgs[i] === '--count') { flags.count = parseInt(flagArgs[++i], 10); i++; continue; }
|
|
57
58
|
if (flagArgs[i] === '--region') { flags.region = flagArgs[++i]; i++; continue; }
|
|
58
59
|
if (flagArgs[i] === '--tier') { flags.tier = flagArgs[++i]; i++; continue; }
|
|
60
|
+
if (flagArgs[i] === '--smoke') { flags.smoke = true; i++; continue; }
|
|
59
61
|
if (flagArgs[i] === '--max-price') { flags.maxPrice = parseFloat(flagArgs[++i]); i++; continue; }
|
|
60
62
|
if (flagArgs[i] === '--name') { flags.name = flagArgs[++i]; i++; continue; }
|
|
61
63
|
if (flagArgs[i] === '--detach') { flags.detach = true; i++; continue; }
|
|
@@ -98,15 +100,6 @@ export function parseRunArgs(args) {
|
|
|
98
100
|
return { flags, positional, commandArgv };
|
|
99
101
|
}
|
|
100
102
|
|
|
101
|
-
function parseEnvFlag(envList) {
|
|
102
|
-
const obj = {};
|
|
103
|
-
for (const kv of (envList || [])) {
|
|
104
|
-
const idx = kv.indexOf('=');
|
|
105
|
-
if (idx > 0) obj[kv.slice(0, idx)] = kv.slice(idx + 1);
|
|
106
|
-
}
|
|
107
|
-
return obj;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
103
|
// Heuristic for --env keys that look like secrets — used to warn (not
|
|
111
104
|
// block) since there is no dashboard --profile injection path yet and
|
|
112
105
|
// --env is currently the only way to get a provider key into a launch VM.
|
|
@@ -417,7 +410,7 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
|
|
|
417
410
|
|
|
418
411
|
// Known badgr run flags — used to detect broken shell line continuation.
|
|
419
412
|
const _KNOWN_RUN_FLAGS = new Set([
|
|
420
|
-
'--gpu', '--image', '--count', '--region', '--tier', '--max-price', '--name',
|
|
413
|
+
'--gpu', '--image', '--count', '--region', '--tier', '--smoke', '--max-price', '--name',
|
|
421
414
|
'--detach', '--no-detach', '--fallback', '--no-fallback', '--strict-capacity',
|
|
422
415
|
'--no-expanded-search', '--max-runtime', '--max-cost', '--min-vram', '--gpu-memory',
|
|
423
416
|
'--cpu', '--memory', '--no-gpu', '--env',
|
|
@@ -458,7 +451,10 @@ async function _zipDirectory(dirPath, chalk) {
|
|
|
458
451
|
throw new Error(`Directory not found: ${absDir}`);
|
|
459
452
|
}
|
|
460
453
|
|
|
461
|
-
|
|
454
|
+
// Random suffix (not just Date.now()) avoids two concurrent zips
|
|
455
|
+
// (e.g. a real upload racing a --dry-run size estimate) colliding on the
|
|
456
|
+
// same filename within the same millisecond.
|
|
457
|
+
const tmpFile = path.join(os.tmpdir(), `badgr-upload-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.zip`);
|
|
462
458
|
|
|
463
459
|
const { default: archiver } = await import('archiver');
|
|
464
460
|
await new Promise((resolve, reject) => {
|
|
@@ -478,6 +474,18 @@ async function _zipDirectory(dirPath, chalk) {
|
|
|
478
474
|
return tmpFile;
|
|
479
475
|
}
|
|
480
476
|
|
|
477
|
+
// Local-only size estimate for a dry run: zips the project exactly like a
|
|
478
|
+
// real upload would, measures it, then deletes the temp file — no network
|
|
479
|
+
// call, no uploadBlob, so `--dry-run` never actually uploads the project.
|
|
480
|
+
async function _estimateUploadSizeMb(dirPath) {
|
|
481
|
+
const tmpFile = await _zipDirectory(dirPath);
|
|
482
|
+
try {
|
|
483
|
+
return (fs.statSync(tmpFile).size / 1024 / 1024).toFixed(1);
|
|
484
|
+
} finally {
|
|
485
|
+
fs.unlinkSync(tmpFile);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
481
489
|
export async function _uploadCodeZip(config, dirPath, chalk) {
|
|
482
490
|
process.stdout.write(chalk.dim(' Packing project...'));
|
|
483
491
|
const tmpFile = await _zipDirectory(dirPath, chalk);
|
|
@@ -685,6 +693,28 @@ export async function runCommand(config, args, chalk, opts = {}) {
|
|
|
685
693
|
return;
|
|
686
694
|
}
|
|
687
695
|
|
|
696
|
+
// ── Smoke mode: cheapest compatible provider for local/dev test runs ──────
|
|
697
|
+
// --smoke, or BADGR_DEV_CHEAPEST=1 in the environment for a local default so
|
|
698
|
+
// it doesn't have to be typed every time. An explicit --tier always wins —
|
|
699
|
+
// the user asked for a specific tier, so smoke's routing/caps don't apply.
|
|
700
|
+
const devCheapestEnv = process.env.BADGR_DEV_CHEAPEST === '1' || process.env.BADGR_DEV_CHEAPEST === 'true';
|
|
701
|
+
const smokeMode = !flags.tier && (flags.smoke || devCheapestEnv);
|
|
702
|
+
|
|
703
|
+
if (smokeMode) {
|
|
704
|
+
if (flags.detach) {
|
|
705
|
+
console.error(chalk.red(' ✗ --smoke requires teardown to run in the foreground — remove --detach.'));
|
|
706
|
+
process.exitCode = 1;
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (flags.workspace) {
|
|
710
|
+
console.error(chalk.red(' ✗ --smoke does not support --workspace (no persistent storage for smoke runs).'));
|
|
711
|
+
process.exitCode = 1;
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (flags.maxCost === undefined) flags.maxCost = SMOKE_MAX_COST_USD;
|
|
715
|
+
if (flags.maxRuntime === undefined) flags.maxRuntime = SMOKE_MAX_RUNTIME_MINUTES;
|
|
716
|
+
}
|
|
717
|
+
|
|
688
718
|
if (!flags.maxCost && !flags.dryRun && isLocalPath && process.stdin.isTTY && process.stdout.isTTY) {
|
|
689
719
|
try {
|
|
690
720
|
const { input } = await import('@inquirer/prompts');
|
|
@@ -775,6 +805,12 @@ export async function runCommand(config, args, chalk, opts = {}) {
|
|
|
775
805
|
console.log(` ${chalk.bold(isLaunch || flags.noGpu ? 'Compute:' : 'GPU:')} ${isLaunch || flags.noGpu ? 'CPU VM (no GPU)' : (gpu || chalk.dim('auto'))}`);
|
|
776
806
|
if (isLaunch && flags.size) console.log(` ${chalk.bold('VM class:')} ${vmClassLine(flags.size)}`);
|
|
777
807
|
if (isLaunch && quotedRate != null) console.log(` ${chalk.bold('Badgr rate:')} $${quotedRate.toFixed(2)}/hour`);
|
|
808
|
+
if (isLaunch && flags.authRequired?.provider) {
|
|
809
|
+
const authLine = flags.authRequired.status === 'connected'
|
|
810
|
+
? chalk.green(`${flags.authRequired.provider} connected`)
|
|
811
|
+
: chalk.yellow(`${flags.authRequired.provider} not connected — will prompt (or run: badgr connect ${flags.authRequired.provider})`);
|
|
812
|
+
console.log(` ${chalk.bold('Auth:')} ${authLine}`);
|
|
813
|
+
}
|
|
778
814
|
if (flags.minVram) console.log(` ${chalk.bold('Min VRAM:')} ${flags.minVram} GB`);
|
|
779
815
|
if (flags.cpu) console.log(` ${chalk.bold('CPU:')} ${flags.cpu} cores`);
|
|
780
816
|
if (flags.memory) console.log(` ${chalk.bold('Memory:')} ${flags.memory} GB`);
|
|
@@ -787,7 +823,28 @@ export async function runCommand(config, args, chalk, opts = {}) {
|
|
|
787
823
|
if (flags.retrySafe) console.log(` ${chalk.bold('Retry-safe:')} enabled`);
|
|
788
824
|
if (flags.resumeCmd) console.log(` ${chalk.bold('Resume cmd:')} ${flags.resumeCmd}`);
|
|
789
825
|
if (flags.artifacts?.length) console.log(` ${chalk.bold('Artifacts:')} ${flags.artifacts.join(', ')}`);
|
|
790
|
-
console.log(chalk.
|
|
826
|
+
if (!isLaunch) console.log(` ${chalk.bold('Tier:')} ${formatTierLabel(effectiveTier)}`);
|
|
827
|
+
if (smokeMode) console.log(` ${chalk.bold('Routing:')} cheapest compatible (smoke mode)`);
|
|
828
|
+
|
|
829
|
+
// Upload-size estimate is GPU-job-specific (spec: "badgr run ... upload
|
|
830
|
+
// size") and does a real local zip pass — skip it for CPU launches
|
|
831
|
+
// (badgr launch always sources from '.'), where it would add a real,
|
|
832
|
+
// possibly-slow filesystem operation to every dry-run preview for no
|
|
833
|
+
// requested benefit.
|
|
834
|
+
if (isLocalPath && !isLaunch) {
|
|
835
|
+
try {
|
|
836
|
+
const sizeMb = await _estimateUploadSizeMb(path.resolve(firstArg));
|
|
837
|
+
console.log(` ${chalk.bold('Upload size:')} ~${sizeMb} MB (not uploaded)`);
|
|
838
|
+
} catch (err) {
|
|
839
|
+
console.log(chalk.dim(` Upload size: could not estimate (${err.message})`));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
console.log();
|
|
844
|
+
if (!isLaunch && !flags.noGpu) {
|
|
845
|
+
await printCapacityPreview(chalk, config, { gpu, region: flags.region?.toUpperCase(), maxPrice: flags.maxPrice });
|
|
846
|
+
}
|
|
847
|
+
console.log(chalk.dim(' Remove --dry-run to provision.\n'));
|
|
791
848
|
return;
|
|
792
849
|
}
|
|
793
850
|
|
|
@@ -815,6 +872,7 @@ export async function runCommand(config, args, chalk, opts = {}) {
|
|
|
815
872
|
console.log(` ${chalk.bold('Max cost:')} ${maxCostLabel}`);
|
|
816
873
|
console.log(` ${chalk.bold('Max runtime:')} ${runtimeLabel}`);
|
|
817
874
|
console.log(` ${chalk.bold('Auto-stop:')} ${maxCost ? 'enabled' : chalk.yellow('disabled — stop manually with badgr down')}`);
|
|
875
|
+
if (smokeMode) console.log(` ${chalk.bold('Routing:')} cheapest compatible (smoke mode)`);
|
|
818
876
|
if (flags.maxPrice) console.log(` ${chalk.bold('Max price:')} $${flags.maxPrice.toFixed(2)}/hr`);
|
|
819
877
|
if (detach) console.log(` ${chalk.dim('(detached — returns immediately)')}`);
|
|
820
878
|
if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${redactEnvForDisplay(flags.env)}`);
|
|
@@ -824,7 +882,6 @@ export async function runCommand(config, args, chalk, opts = {}) {
|
|
|
824
882
|
if (flags.artifacts?.length) console.log(` ${chalk.bold('Artifacts:')} ${flags.artifacts.join(', ')}`);
|
|
825
883
|
console.log();
|
|
826
884
|
|
|
827
|
-
|
|
828
885
|
// Resolve --workspace name → ws_… ID before submitting
|
|
829
886
|
let resolvedWorkspaceId = flags.workspace ?? null;
|
|
830
887
|
if (resolvedWorkspaceId && !resolvedWorkspaceId.startsWith('ws_')) {
|
|
@@ -880,6 +937,7 @@ export async function runCommand(config, args, chalk, opts = {}) {
|
|
|
880
937
|
max_price_per_hour: flags.maxPrice,
|
|
881
938
|
name: flags.name,
|
|
882
939
|
tier: tierOverride || effectiveTier,
|
|
940
|
+
...(smokeMode ? { routing: 'cheapest' } : {}),
|
|
883
941
|
...(Object.keys(envObj).length > 0 ? { env: envObj } : {}),
|
|
884
942
|
max_runtime_seconds: effectiveMaxRuntime * 60,
|
|
885
943
|
...(maxCost ? { max_cost_usd: maxCost } : {}),
|
|
@@ -904,7 +962,7 @@ export async function runCommand(config, args, chalk, opts = {}) {
|
|
|
904
962
|
effectiveTier,
|
|
905
963
|
chalk,
|
|
906
964
|
{ thing: 'job', cmd: cmdName },
|
|
907
|
-
{ allowTier2Fallback: !flags.noFallback },
|
|
965
|
+
{ allowTier2Fallback: !flags.noFallback, singleAttempt: smokeMode },
|
|
908
966
|
);
|
|
909
967
|
} catch (err) {
|
|
910
968
|
if (err.isPaymentRequired) {
|
package/src/commands/sbatch.js
CHANGED
|
@@ -22,6 +22,7 @@ import { normalizeGpuType } from '../spec.js';
|
|
|
22
22
|
import { monitorBatchJob, fmtRuntime } from '../batch.js';
|
|
23
23
|
import { runFanOut, DEFAULT_CONCURRENCY } from '../fanout.js';
|
|
24
24
|
import { callApi } from '../api.js';
|
|
25
|
+
import { printCapacityPreview } from '../progress.js';
|
|
25
26
|
|
|
26
27
|
const DEFAULT_IMAGE = 'python:3.11-slim';
|
|
27
28
|
const DEFAULT_MAX_RUNTIME_MIN = 60;
|
|
@@ -153,9 +154,13 @@ export async function sbatchCommand(config, args, chalk) {
|
|
|
153
154
|
const taskIds = isArray ? job.arrayIndices : [null];
|
|
154
155
|
const concurrency = flags.maxConcurrency ?? DEFAULT_CONCURRENCY;
|
|
155
156
|
|
|
157
|
+
const resolvedGpu = job.gpuCount > 0 ? (job.gpuType ? normalizeGpuType(job.gpuType) : 'AUTO') : 'NONE';
|
|
158
|
+
|
|
156
159
|
if (flags.dryRun) {
|
|
160
|
+
requireApiKey(config);
|
|
157
161
|
console.log(chalk.dim(` Dry run — no GPU provisioned. Would submit ${taskIds.length} job(s) (max ${Math.min(concurrency, taskIds.length)} concurrent) with:`));
|
|
158
|
-
console.log(chalk.dim(` image=${opts.image} tier=${opts.tier} max_cost=$${opts.maxCostUsd} max_runtime=${opts.maxRuntimeMinutes}min\n`));
|
|
162
|
+
console.log(chalk.dim(` image=${opts.image} gpu=${resolvedGpu} tier=${opts.tier} max_cost=$${opts.maxCostUsd} max_runtime=${opts.maxRuntimeMinutes}min\n`));
|
|
163
|
+
if (resolvedGpu !== 'NONE') await printCapacityPreview(chalk, config, { gpu: resolvedGpu, region: opts.region?.toUpperCase(), maxPrice: undefined });
|
|
159
164
|
return;
|
|
160
165
|
}
|
|
161
166
|
|
package/src/commands/serve.js
CHANGED
|
@@ -3,8 +3,10 @@ import { callApi, listDeployments } from '../api.js';
|
|
|
3
3
|
import { addDeployment, addReceipt, updateReceipt, generateReceiptId } from '../store.js';
|
|
4
4
|
import { normalizeTier, callWithFallback } from '../fallback.js';
|
|
5
5
|
import { formatCliError } from '../errors.js';
|
|
6
|
+
import { requireApiKey } from '../config.js';
|
|
7
|
+
import { parseEnvFlag } from '../envFlag.js';
|
|
6
8
|
import { TEMPLATE_MAP, buildTemplateFlags, parseTemplateOverrides, BLESSED_VLLM_MODELS, isLikelyGatedModel } from '../catalog.js';
|
|
7
|
-
import { stage as _stage, stageDone as _stageDone, writeBlock as _writeBlock, clearBlock as _clearBlock, renderLiveBlock as _renderLiveBlock, printFailureClass as _printFailureClass } from '../progress.js';
|
|
9
|
+
import { stage as _stage, stageDone as _stageDone, writeBlock as _writeBlock, clearBlock as _clearBlock, renderLiveBlock as _renderLiveBlock, printFailureClass as _printFailureClass, printCapacityPreview, formatTierLabel } from '../progress.js';
|
|
8
10
|
|
|
9
11
|
const LLAMA_CPP_IMAGE = 'michaelmanleyx/llama-cpp:server-cuda';
|
|
10
12
|
|
|
@@ -45,6 +47,7 @@ export function parseServeArgs(args) {
|
|
|
45
47
|
if (args[i] === '--hf-repo') { flags.hfRepo = args[++i]; i++; continue; }
|
|
46
48
|
if (args[i] === '--hf-file') { flags.hfFile = args[++i]; i++; continue; }
|
|
47
49
|
if (args[i] === '--list-aliases') { flags.listAliases = true; i++; continue; }
|
|
50
|
+
if (args[i] === '--dry-run') { flags.dryRun = true; i++; continue; }
|
|
48
51
|
if (args[i] === '--env') {
|
|
49
52
|
const kv = args[++i]; i++;
|
|
50
53
|
if (!flags.env) flags.env = [];
|
|
@@ -57,15 +60,6 @@ export function parseServeArgs(args) {
|
|
|
57
60
|
return { model, flags };
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
function parseEnvFlag(envList) {
|
|
61
|
-
const obj = {};
|
|
62
|
-
for (const kv of (envList || [])) {
|
|
63
|
-
const idx = kv.indexOf('=');
|
|
64
|
-
if (idx > 0) obj[kv.slice(0, idx)] = kv.slice(idx + 1);
|
|
65
|
-
}
|
|
66
|
-
return obj;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
63
|
function envObjHasHfToken(envList) {
|
|
70
64
|
return (envList || []).some(kv => kv.startsWith('HF_TOKEN='));
|
|
71
65
|
}
|
|
@@ -122,6 +116,17 @@ function _detectHealthPath(image) {
|
|
|
122
116
|
return null;
|
|
123
117
|
}
|
|
124
118
|
|
|
119
|
+
// Priority: explicit --health-path > llama.cpp → /health > task-specific >
|
|
120
|
+
// vLLM → /models > auto-detect custom image > null. Pure (no `dep` needed),
|
|
121
|
+
// so both the dry-run preview and the real post-launch health check use the
|
|
122
|
+
// exact same resolution and can never show a different path than they poll.
|
|
123
|
+
function _resolveHealthPath({ healthPath, isLlamaCpp, customImage, task }) {
|
|
124
|
+
if (healthPath) return healthPath;
|
|
125
|
+
if (isLlamaCpp) return '/health';
|
|
126
|
+
if (!customImage) return (task === 'transcribe' || task === 'image') ? '/health' : '/models';
|
|
127
|
+
return _detectHealthPath(customImage); // '/system_stats' for comfyui, null otherwise
|
|
128
|
+
}
|
|
129
|
+
|
|
125
130
|
/**
|
|
126
131
|
* Wait for the deployment's app-level endpoint to become ready by polling Badgr's
|
|
127
132
|
* own deployment status (GET /deployments/{id}) — never the RunPod proxy/pod
|
|
@@ -211,7 +216,7 @@ const _KNOWN_SERVE_FLAGS = new Set([
|
|
|
211
216
|
'--gpu', '--image', '--task', '--count', '--region', '--tier', '--max-price',
|
|
212
217
|
'--name', '--no-wait', '--max-cost', '--idle-timeout', '--health-path', '--check-nodes',
|
|
213
218
|
'--no-fallback', '--strict-capacity', '--no-expanded-search', '--env',
|
|
214
|
-
'--persistent', '--yes', '-y', '--runtime', '--hf-repo', '--hf-file',
|
|
219
|
+
'--persistent', '--yes', '-y', '--runtime', '--hf-repo', '--hf-file', '--dry-run',
|
|
215
220
|
]);
|
|
216
221
|
|
|
217
222
|
function extractFlag(args, flagName) {
|
|
@@ -396,7 +401,14 @@ export async function serveCommand(config, args, chalk) {
|
|
|
396
401
|
return;
|
|
397
402
|
}
|
|
398
403
|
|
|
399
|
-
|
|
404
|
+
// A dry run previews the plan only and never provisions or spends anything,
|
|
405
|
+
// so it doesn't need the interactive browser login — just a stored key, to
|
|
406
|
+
// keep its existing fail-fast behavior in non-interactive contexts (tests/CI).
|
|
407
|
+
if (flags.dryRun) {
|
|
408
|
+
requireApiKey(config);
|
|
409
|
+
} else {
|
|
410
|
+
config = await ensureBadgrReady(config, chalk);
|
|
411
|
+
}
|
|
400
412
|
|
|
401
413
|
// ── Validate flags early ───────────────────────────────────────────────────
|
|
402
414
|
if (flags.count !== undefined && (!Number.isFinite(flags.count) || flags.count < 1)) {
|
|
@@ -416,7 +428,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
416
428
|
}
|
|
417
429
|
|
|
418
430
|
// Endpoints bill continuously — require explicit cost control.
|
|
419
|
-
if (!flags.maxCost && !flags.persistent) {
|
|
431
|
+
if (!flags.maxCost && !flags.persistent && !flags.dryRun) {
|
|
420
432
|
const example = model || (customImage ? '--image ...' : '<model>');
|
|
421
433
|
console.error(chalk.red('\n ✗ Endpoints bill continuously until stopped. Specify a spending limit:\n'));
|
|
422
434
|
console.error(chalk.dim(` --max-cost 5 auto-stop when $5 is reached`));
|
|
@@ -469,8 +481,15 @@ export async function serveCommand(config, args, chalk) {
|
|
|
469
481
|
if (flags.gpu) headerLines.push(['GPU', gpuLabel]);
|
|
470
482
|
if (flags.task) headerLines.push(['Task', flags.task]);
|
|
471
483
|
if (flags.env?.length) headerLines.push(['Env', flags.env.join(', ')]);
|
|
484
|
+
if (flags.dryRun) {
|
|
485
|
+
headerLines.push(['Tier', formatTierLabel(effectiveTier)]);
|
|
486
|
+
const previewHealthPath = _resolveHealthPath({ healthPath: flags.healthPath, isLlamaCpp, customImage, task: flags.task });
|
|
487
|
+
headerLines.push(['Health path', previewHealthPath || chalk.dim('none (custom image, unset)')]);
|
|
488
|
+
}
|
|
472
489
|
|
|
473
|
-
console.log(chalk.bold(
|
|
490
|
+
console.log(chalk.bold(flags.dryRun
|
|
491
|
+
? `\n⚡ Dry run — no GPU will be provisioned — ${title}\n`
|
|
492
|
+
: `\n⚡ Serving ${title}\n`));
|
|
474
493
|
const labelWidth = Math.max(...headerLines.map(([label]) => label.length)) + 1;
|
|
475
494
|
for (const [label, value] of headerLines) {
|
|
476
495
|
console.log(` ${chalk.bold(`${label}:`.padEnd(labelWidth + 1))}${value}`);
|
|
@@ -488,6 +507,16 @@ export async function serveCommand(config, args, chalk) {
|
|
|
488
507
|
}
|
|
489
508
|
console.log();
|
|
490
509
|
|
|
510
|
+
if (flags.dryRun) {
|
|
511
|
+
await printCapacityPreview(chalk, config, {
|
|
512
|
+
gpu,
|
|
513
|
+
region: flags.region ? flags.region.toUpperCase() : undefined,
|
|
514
|
+
maxPrice: flags.maxPrice,
|
|
515
|
+
});
|
|
516
|
+
console.log(chalk.dim(' Remove --dry-run to provision.\n'));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
491
520
|
const STAGE_TOTAL = 5;
|
|
492
521
|
let stageN = 1;
|
|
493
522
|
|
|
@@ -620,22 +649,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
620
649
|
}
|
|
621
650
|
|
|
622
651
|
// ── Determine health check path ───────────────────────────────────────────
|
|
623
|
-
|
|
624
|
-
let resolvedHealthPath;
|
|
625
|
-
if (flags.healthPath) {
|
|
626
|
-
resolvedHealthPath = flags.healthPath;
|
|
627
|
-
} else if (isLlamaCpp) {
|
|
628
|
-
resolvedHealthPath = '/health';
|
|
629
|
-
} else if (!customImage) {
|
|
630
|
-
// Managed runtimes for transcribe/image expose /health; vLLM (chat, embed) uses /models
|
|
631
|
-
if (flags.task === 'transcribe' || flags.task === 'image') {
|
|
632
|
-
resolvedHealthPath = '/health';
|
|
633
|
-
} else {
|
|
634
|
-
resolvedHealthPath = '/models';
|
|
635
|
-
}
|
|
636
|
-
} else {
|
|
637
|
-
resolvedHealthPath = _detectHealthPath(customImage); // '/system_stats' for comfyui, null otherwise
|
|
638
|
-
}
|
|
652
|
+
const resolvedHealthPath = _resolveHealthPath({ healthPath: flags.healthPath, isLlamaCpp, customImage, task: flags.task });
|
|
639
653
|
|
|
640
654
|
// Gated-model guidance is only shown when it's actually needed — on failure —
|
|
641
655
|
// not up front, so common launches stay short and uncluttered.
|
package/src/commands/train.js
CHANGED
|
@@ -10,8 +10,9 @@ import { requireApiKey } from '../config.js';
|
|
|
10
10
|
import { addReceipt, updateReceipt, generateReceiptId } from '../store.js';
|
|
11
11
|
import { normalizeTier, callWithFallback } from '../fallback.js';
|
|
12
12
|
import { monitorBatchJob, fmtRuntime } from '../batch.js';
|
|
13
|
-
import { pollJobUntilTerminal, renderJobClosingBlock } from '../progress.js';
|
|
13
|
+
import { pollJobUntilTerminal, renderJobClosingBlock, printCapacityPreview, formatTierLabel } from '../progress.js';
|
|
14
14
|
import { uploadBlob } from '../api.js';
|
|
15
|
+
import { parseEnvFlag } from '../envFlag.js';
|
|
15
16
|
|
|
16
17
|
const MAX_CONFIG_B = 512 * 1024; // 512 KB config limit
|
|
17
18
|
|
|
@@ -64,15 +65,6 @@ export function parseTrainArgs(args) {
|
|
|
64
65
|
return { configFile: positional[0] || null, flags };
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
function parseEnvFlag(envList) {
|
|
68
|
-
const obj = {};
|
|
69
|
-
for (const kv of (envList || [])) {
|
|
70
|
-
const idx = kv.indexOf('=');
|
|
71
|
-
if (idx > 0) obj[kv.slice(0, idx)] = kv.slice(idx + 1);
|
|
72
|
-
}
|
|
73
|
-
return obj;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
68
|
/**
|
|
77
69
|
* Detect training framework from config content.
|
|
78
70
|
* Returns 'axolotl' | 'unsloth' | 'trl' | 'generic'.
|
|
@@ -155,20 +147,24 @@ export async function trainLoraCommand(config, args, chalk) {
|
|
|
155
147
|
if (flags.dryRun) {
|
|
156
148
|
const preset = flags.preset || 'small';
|
|
157
149
|
const presetInfo = LORA_PRESET_INFO[preset];
|
|
150
|
+
const resolvedGpu = flags.gpuType || presetInfo?.gpu_type;
|
|
158
151
|
console.log(chalk.bold('\n⚡ Dry run — no GPU will be provisioned\n'));
|
|
159
152
|
console.log(` ${chalk.bold('Base model:')} ${flags.baseModel}`);
|
|
160
153
|
console.log(` ${chalk.bold('Dataset:')} ${flags.fileId || flags.dataset || chalk.dim('(none given)')}`);
|
|
161
154
|
console.log(` ${chalk.bold('Preset:')} ${preset}${presetInfo ? '' : chalk.yellow(' (unknown — server will reject this)')}`);
|
|
162
155
|
if (presetInfo) {
|
|
163
|
-
console.log(` ${chalk.bold('GPU:')} ${
|
|
156
|
+
console.log(` ${chalk.bold('GPU:')} ${resolvedGpu}`);
|
|
164
157
|
console.log(` ${chalk.bold('LoRA rank:')} ${presetInfo.rank}`);
|
|
165
158
|
console.log(` ${chalk.bold('Epochs:')} ${presetInfo.epochs}`);
|
|
166
159
|
console.log(` ${chalk.dim(presetInfo.description)}`);
|
|
167
160
|
}
|
|
161
|
+
console.log(` ${chalk.bold('Tier:')} ${formatTierLabel(normalizeTier(flags.tier))}`);
|
|
168
162
|
if (flags.maxCost) console.log(` ${chalk.bold('Max cost:')} $${flags.maxCost}`);
|
|
169
163
|
console.log(` ${chalk.bold('Max runtime:')} ${flags.maxRuntime ?? 240}min`);
|
|
170
164
|
if (flags.resume) console.log(` ${chalk.bold('Resume from:')} ${flags.resume}`);
|
|
171
|
-
console.log(
|
|
165
|
+
console.log();
|
|
166
|
+
if (resolvedGpu) await printCapacityPreview(chalk, config, { gpu: resolvedGpu });
|
|
167
|
+
console.log(chalk.dim(' Remove --dry-run to submit (local file datasets are uploaded first).\n'));
|
|
172
168
|
return;
|
|
173
169
|
}
|
|
174
170
|
|
|
@@ -10,6 +10,8 @@ import { requireApiKey } from '../config.js';
|
|
|
10
10
|
import { addReceipt, generateReceiptId } from '../store.js';
|
|
11
11
|
import { normalizeTier, callWithFallback } from '../fallback.js';
|
|
12
12
|
import { monitorBatchJob, fmtRuntime } from '../batch.js';
|
|
13
|
+
import { parseEnvFlag } from '../envFlag.js';
|
|
14
|
+
import { printCapacityPreview, formatTierLabel } from '../progress.js';
|
|
13
15
|
|
|
14
16
|
const WHISPER_IMAGE = 'fedirz/faster-whisper-server:latest-cuda';
|
|
15
17
|
const DEFAULT_WHISPER_MODEL = 'large-v3';
|
|
@@ -32,6 +34,7 @@ export function parseTranscribeArgs(args) {
|
|
|
32
34
|
if (a === '--language') { flags.language = args[++i]; i++; continue; }
|
|
33
35
|
if (a === '--output') { flags.output = args[++i]; i++; continue; }
|
|
34
36
|
if (a === '--detach') { flags.detach = true; i++; continue; }
|
|
37
|
+
if (a === '--dry-run') { flags.dryRun = true; i++; continue; }
|
|
35
38
|
if (a === '--env') {
|
|
36
39
|
const kv = args[++i]; i++;
|
|
37
40
|
if (!flags.env) flags.env = [];
|
|
@@ -43,15 +46,6 @@ export function parseTranscribeArgs(args) {
|
|
|
43
46
|
return { input: positional[0] || null, flags };
|
|
44
47
|
}
|
|
45
48
|
|
|
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
49
|
function isUrl(s) {
|
|
56
50
|
return /^(https?|s3|gs|hf):\/\//i.test(s);
|
|
57
51
|
}
|
|
@@ -116,15 +110,24 @@ export async function transcribeCommand(config, args, chalk) {
|
|
|
116
110
|
...(flags.output ? { WHISPER_OUTPUT_FORMAT: flags.output } : {}),
|
|
117
111
|
};
|
|
118
112
|
|
|
119
|
-
console.log(chalk.bold('\n🎙 Transcription\n'));
|
|
113
|
+
console.log(chalk.bold(flags.dryRun ? '\n🎙 Dry run — no GPU will be provisioned — Transcription\n' : '\n🎙 Transcription\n'));
|
|
120
114
|
console.log(` ${chalk.bold('Input:')} ${resolved.inputLabel}`);
|
|
121
115
|
console.log(` ${chalk.bold('Model:')} ${whisperModel}`);
|
|
122
116
|
console.log(` ${chalk.bold('Image:')} ${WHISPER_IMAGE}`);
|
|
123
117
|
console.log(` ${chalk.bold('GPU:')} ${gpu === 'AUTO' ? chalk.dim('auto (8+ GB VRAM)') : gpu}`);
|
|
118
|
+
if (flags.dryRun) console.log(` ${chalk.bold('Tier:')} ${formatTierLabel(effectiveTier)}`);
|
|
124
119
|
console.log(` ${chalk.bold('Max runtime:')} ${maxRuntimeMin}min`);
|
|
125
120
|
if (maxCost) console.log(` ${chalk.bold('Max cost:')} $${maxCost.toFixed(2)}`);
|
|
126
121
|
if (flags.language) console.log(` ${chalk.bold('Language:')} ${flags.language}`);
|
|
122
|
+
if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
|
|
127
123
|
console.log();
|
|
124
|
+
|
|
125
|
+
if (flags.dryRun) {
|
|
126
|
+
await printCapacityPreview(chalk, config, { gpu, region: flags.region?.toUpperCase(), maxPrice: flags.maxPrice });
|
|
127
|
+
console.log(chalk.dim(' Remove --dry-run to submit.\n'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
128
131
|
process.stdout.write(chalk.dim(' Finding suitable capacity...\n'));
|
|
129
132
|
|
|
130
133
|
function buildBody(tierOverride) {
|
package/src/credentials.js
CHANGED
|
@@ -8,10 +8,43 @@ export const CREDENTIALS_FILE = join(CONFIG_DIR, 'credentials.json');
|
|
|
8
8
|
export const PROVIDER_ENV_KEYS = {
|
|
9
9
|
anthropic: 'ANTHROPIC_API_KEY',
|
|
10
10
|
openai: 'OPENAI_API_KEY',
|
|
11
|
+
// OpenAI-compatible BYOK lanes for `badgr launch cline --provider <name>`
|
|
12
|
+
// (see MODEL_PROVIDERS below) — the cline agent image only ever reads
|
|
13
|
+
// OPENAI_API_KEY/OPENAI_BASE_URL/MODEL (images/badgr-agent-cline/
|
|
14
|
+
// badgr-cline-run), regardless of which upstream provider the key
|
|
15
|
+
// actually belongs to.
|
|
16
|
+
openrouter: 'OPENAI_API_KEY',
|
|
17
|
+
deepseek: 'OPENAI_API_KEY',
|
|
18
|
+
glm: 'OPENAI_API_KEY',
|
|
19
|
+
custom: 'OPENAI_API_KEY',
|
|
11
20
|
};
|
|
12
21
|
|
|
13
22
|
export const KNOWN_PROVIDERS = Object.keys(PROVIDER_ENV_KEYS);
|
|
14
23
|
|
|
24
|
+
// `badgr launch cline --provider <name> --model <id> [--base-url <url>]` —
|
|
25
|
+
// BYOK / OpenAI-compatible model selection. `custom` has no default base
|
|
26
|
+
// URL: it must always be supplied explicitly, since there is nothing sane
|
|
27
|
+
// to default it to. Every other entry's defaultBaseUrl is overridable with
|
|
28
|
+
// an explicit --base-url too (e.g. a company-hosted DeepSeek-compatible
|
|
29
|
+
// gateway).
|
|
30
|
+
export const MODEL_PROVIDERS = {
|
|
31
|
+
openrouter: { label: 'OpenRouter', defaultBaseUrl: 'https://openrouter.ai/api/v1' },
|
|
32
|
+
deepseek: { label: 'DeepSeek', defaultBaseUrl: 'https://api.deepseek.com/v1' },
|
|
33
|
+
glm: { label: 'GLM (Zhipu)', defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4' },
|
|
34
|
+
custom: { label: 'Custom (OpenAI-compatible)', defaultBaseUrl: null },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const KNOWN_MODEL_PROVIDERS = Object.keys(MODEL_PROVIDERS);
|
|
38
|
+
|
|
39
|
+
// Which env vars each API kind expects inside the agent container.
|
|
40
|
+
// `claude` uses the Anthropic SDK (ANTHROPIC_*); `cline` and `codex` use the
|
|
41
|
+
// OpenAI SDK (OPENAI_*). `model` is always 'MODEL' — every agent wrapper
|
|
42
|
+
// reads that env var and maps it to its own --model flag.
|
|
43
|
+
export const ENV_KEYS_FOR_API_KIND = {
|
|
44
|
+
openai: { apiKey: 'OPENAI_API_KEY', baseUrl: 'OPENAI_BASE_URL', model: 'MODEL' },
|
|
45
|
+
anthropic: { apiKey: 'ANTHROPIC_API_KEY', baseUrl: 'ANTHROPIC_BASE_URL', model: 'MODEL' },
|
|
46
|
+
};
|
|
47
|
+
|
|
15
48
|
/**
|
|
16
49
|
* Credential storage for `badgr connect <provider>`. This is a local file
|
|
17
50
|
* under ~/.badgr with owner-only permissions (chmod 600) — not the
|
package/src/envFlag.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Shared by every command that accepts repeated `--env KEY=VALUE` flags
|
|
2
|
+
// (run, serve, comfyui, train, transcribe, embed).
|
|
3
|
+
export function parseEnvFlag(envList) {
|
|
4
|
+
const obj = {};
|
|
5
|
+
for (const kv of (envList || [])) {
|
|
6
|
+
const idx = kv.indexOf('=');
|
|
7
|
+
if (idx > 0) obj[kv.slice(0, idx)] = kv.slice(idx + 1);
|
|
8
|
+
}
|
|
9
|
+
return obj;
|
|
10
|
+
}
|
package/src/fallback.js
CHANGED
|
@@ -6,6 +6,13 @@ import { CATALOG, formatCliError } from './errors.js';
|
|
|
6
6
|
/** Rates above this threshold trigger a visible warning when no --max-cost is set. */
|
|
7
7
|
export const HIGH_RATE_THRESHOLD = 3.00;
|
|
8
8
|
|
|
9
|
+
// ── badgr run --smoke defaults ────────────────────────────────────────────────
|
|
10
|
+
// Hardcoded default smoke caps, not a config system — --max-cost/--max-runtime
|
|
11
|
+
// still override them when passed explicitly (see run.js). The goal is to stop
|
|
12
|
+
// expensive local smoke tests by default, not build another configuration system.
|
|
13
|
+
export const SMOKE_MAX_COST_USD = 0.25;
|
|
14
|
+
export const SMOKE_MAX_RUNTIME_MINUTES = 10;
|
|
15
|
+
|
|
9
16
|
/** Normalise --tier flag variants to '1' or '2'. */
|
|
10
17
|
export function normalizeTier(tier) {
|
|
11
18
|
return (tier === '2' || tier === 'tier2' || tier === 'tier-2') ? '2' : (tier || '1');
|
|
@@ -37,12 +44,16 @@ export class CapacityError extends Error {
|
|
|
37
44
|
* @param {object} labels - { thing: 'job'|'endpoint', cmd: 'badgr run'|'badgr serve' }
|
|
38
45
|
* @param {object} [opts]
|
|
39
46
|
* @param {boolean} [opts.allowTier2Fallback=true] - set false to disable tier-2 expansion
|
|
47
|
+
* @param {boolean} [opts.singleAttempt=false] - set true to skip both the same-tier
|
|
48
|
+
* provider retry and tier-2 expansion, failing immediately on the first error
|
|
49
|
+
* (badgr run --smoke's "one attempt" guarantee)
|
|
40
50
|
*/
|
|
41
51
|
export async function callWithFallback(endpoint, callOpts, buildBody, effectiveTier, chalk, labels, opts = {}) {
|
|
42
52
|
const { callApi } = await import('./api.js');
|
|
43
53
|
const thing = labels?.thing ?? 'job';
|
|
44
54
|
const cmd = labels?.cmd ?? 'badgr run';
|
|
45
|
-
const
|
|
55
|
+
const singleAttempt = opts.singleAttempt === true;
|
|
56
|
+
const allowTier2Fallback = !singleAttempt && opts.allowTier2Fallback !== false; // default true
|
|
46
57
|
|
|
47
58
|
// 220s: comfortably above backend's BADGR_PROVISION_TIMEOUT_SECONDS (default
|
|
48
59
|
// 200s, itself set above deployment_service.py's 180s routing-search
|
|
@@ -113,7 +124,7 @@ export async function callWithFallback(endpoint, callOpts, buildBody, effectiveT
|
|
|
113
124
|
// Provider retry: PROVISIONING_FAILED means the selected provider couldn't launch the slot.
|
|
114
125
|
// Retry once with prefer_different_provider so the backend routes to a different provider
|
|
115
126
|
// (e.g. RunPod failed → try Vast.ai or Hyperstack) within the same max_cost budget.
|
|
116
|
-
if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
127
|
+
if (!singleAttempt && (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR')) {
|
|
117
128
|
console.log(chalk.dim('\n Provider unavailable — trying alternative provider...\n'));
|
|
118
129
|
try {
|
|
119
130
|
const retryBody = { ...buildBody(), prefer_different_provider: true };
|
package/src/onboarding.js
CHANGED
|
@@ -50,7 +50,14 @@ async function pollCliSession(config, sessionId) {
|
|
|
50
50
|
throw new Error('Timed out waiting for login in the browser. Run `badgr login` directly.');
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Opens the browser-based login link and polls until it completes, saving
|
|
55
|
+
* the returned API key. Exported so `badgr login` (an explicit, standalone
|
|
56
|
+
* invocation) can reuse the exact same flow `ensureBadgrReady` triggers
|
|
57
|
+
* just-in-time from `run`/`launch`/`serve`/`comfyui`, rather than having a
|
|
58
|
+
* second, divergent login implementation.
|
|
59
|
+
*/
|
|
60
|
+
export async function ensureLoggedIn(config, chalk) {
|
|
54
61
|
console.log(chalk.dim("\n You're not logged in to Badgr."));
|
|
55
62
|
console.log(chalk.dim(' Badgr uses prepaid credits to pay for the VM and model usage.\n'));
|
|
56
63
|
|
package/src/progress.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// numbered stage lines plus an in-place-redrawing status block, so neither
|
|
3
3
|
// command spams repeated identical loading lines.
|
|
4
4
|
|
|
5
|
+
import { getCapacityPreview } from './api.js';
|
|
6
|
+
|
|
5
7
|
export function stage(n, total, label) {
|
|
6
8
|
return ` [${n}/${total}] ${label}`;
|
|
7
9
|
}
|
|
@@ -200,3 +202,49 @@ export function renderJobClosingBlock(chalk, detail, rcptId) {
|
|
|
200
202
|
|
|
201
203
|
return lines.join('\n') + '\n';
|
|
202
204
|
}
|
|
205
|
+
|
|
206
|
+
// Shared across every dry-run preview that shows routing tier — normalizeTier()
|
|
207
|
+
// already collapses anything else to 1, so this only ever distinguishes 1 vs 2.
|
|
208
|
+
export function formatTierLabel(tier) {
|
|
209
|
+
return tier === 2 ? '2 (marketplace)' : '1 (managed)';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Shared "live capacity preview" block for GPU-provisioning dry runs
|
|
214
|
+
* (badgr serve/run/comfyui/train/batch/sbatch --dry-run). Calls the
|
|
215
|
+
* no-provisioning GET /v1/capacity/suggestions route (api.js's
|
|
216
|
+
* getCapacityPreview) and prints an observed rate/alternatives — same shape
|
|
217
|
+
* `badgr up --dry-run` already showed, now reused everywhere else instead of
|
|
218
|
+
* every dry-run only echoing back the flags the user typed. Best-effort:
|
|
219
|
+
* a failed or empty lookup prints a one-line fallback, never blocks or
|
|
220
|
+
* throws, since a dry run must always finish even if live pricing can't be
|
|
221
|
+
* reached (offline, GPU not currently listed, transient API error, etc).
|
|
222
|
+
*/
|
|
223
|
+
export async function printCapacityPreview(chalk, config, { gpu, region, maxPrice }) {
|
|
224
|
+
if (!gpu || gpu === 'AUTO' || gpu === 'NONE') {
|
|
225
|
+
console.log(chalk.dim(` GPU is auto-selected — resolved to a specific type and priced at provision time.`));
|
|
226
|
+
console.log();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const suggestions = await getCapacityPreview(config, { gpu, region, maxPrice });
|
|
231
|
+
const matches = suggestions?.matches ?? suggestions?.routes ?? [];
|
|
232
|
+
const alternatives = suggestions?.alternatives ?? [];
|
|
233
|
+
console.log(chalk.bold(' Live capacity'));
|
|
234
|
+
if (matches.length === 0) {
|
|
235
|
+
console.log(chalk.yellow(` No ${gpu} capacity found right now`));
|
|
236
|
+
} else {
|
|
237
|
+
const cheapest = matches[0];
|
|
238
|
+
console.log(` ${chalk.cyan(gpu)} available — from ${chalk.green('$' + cheapest.price.toFixed(2) + '/hr')} (${matches.length} offer(s), tier ${cheapest.tier})`);
|
|
239
|
+
}
|
|
240
|
+
if (alternatives.length > 0) {
|
|
241
|
+
alternatives.slice(0, 2).forEach(a => {
|
|
242
|
+
console.log(chalk.dim(` alt: ${a.gpu} in ${a.region} $${a.price.toFixed(2)}/hr${a.diff_desc ? ` — ${a.diff_desc}` : ''}`));
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
console.log();
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.log(chalk.dim(` Live capacity: could not fetch (${err.message})`));
|
|
248
|
+
console.log();
|
|
249
|
+
}
|
|
250
|
+
}
|