badgr-cli 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -14
- package/package.json +2 -2
- package/src/api.js +28 -0
- package/src/badgr.js +1 -1
- package/src/commands/artifacts.js +5 -3
- package/src/commands/batch.js +6 -5
- package/src/commands/billing.js +3 -3
- package/src/commands/capacity.js +2 -2
- package/src/commands/deploy.js +2 -2
- package/src/commands/diagnose.js +276 -87
- package/src/commands/down.js +69 -14
- package/src/commands/embed.js +2 -2
- package/src/commands/heartbeat.js +3 -3
- package/src/commands/job.js +14 -3
- package/src/commands/login.js +4 -1
- package/src/commands/logs.js +35 -18
- package/src/commands/pull.js +4 -3
- package/src/commands/rerun.js +5 -3
- package/src/commands/restart.js +5 -3
- package/src/commands/run.js +5 -23
- package/src/commands/sbatch.js +2 -1
- package/src/commands/serve.js +145 -24
- package/src/commands/shell.js +2 -2
- package/src/commands/status.js +57 -13
- package/src/commands/test-run.js +2 -2
- package/src/commands/train.js +3 -3
- package/src/commands/transcribe.js +2 -2
- package/src/commands/up.js +2 -1
- package/src/config.js +19 -0
- package/src/deploymentLog.js +45 -0
- package/src/errors.js +7 -3
- package/src/fallback.js +51 -31
- package/src/onboarding.js +63 -4
- package/src/progress.js +5 -2
package/src/fallback.js
CHANGED
|
@@ -31,7 +31,7 @@ export class CapacityError extends Error {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* Submit one deployment. Provider and tier fallback are backend-owned.
|
|
35
35
|
* Returns the deployment object on success.
|
|
36
36
|
* Throws CapacityError (pre-formatted for display) on unrecoverable failure.
|
|
37
37
|
* Re-throws payment errors (err.isPaymentRequired) for callers to handle.
|
|
@@ -42,18 +42,11 @@ export class CapacityError extends Error {
|
|
|
42
42
|
* @param {string} effectiveTier
|
|
43
43
|
* @param {object} chalk
|
|
44
44
|
* @param {object} labels - { thing: 'job'|'endpoint', cmd: 'badgr run'|'badgr serve' }
|
|
45
|
-
* @param {object} [opts]
|
|
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)
|
|
50
45
|
*/
|
|
51
|
-
export async function callWithFallback(endpoint, callOpts, buildBody, effectiveTier, chalk, labels
|
|
46
|
+
export async function callWithFallback(endpoint, callOpts, buildBody, effectiveTier, chalk, labels) {
|
|
52
47
|
const { callApi } = await import('./api.js');
|
|
53
48
|
const thing = labels?.thing ?? 'job';
|
|
54
49
|
const cmd = labels?.cmd ?? 'badgr run';
|
|
55
|
-
const singleAttempt = opts.singleAttempt === true;
|
|
56
|
-
const allowTier2Fallback = !singleAttempt && opts.allowTier2Fallback !== false; // default true
|
|
57
50
|
|
|
58
51
|
// 220s: comfortably above backend's BADGR_PROVISION_TIMEOUT_SECONDS (default
|
|
59
52
|
// 200s, itself set above deployment_service.py's 180s routing-search
|
|
@@ -122,31 +115,58 @@ export async function callWithFallback(endpoint, callOpts, buildBody, effectiveT
|
|
|
122
115
|
firstErr = err;
|
|
123
116
|
}
|
|
124
117
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
118
|
+
// A stale/expired/revoked API key is not capacity trouble — the request
|
|
119
|
+
// never got past auth, so retrying against another provider or expanding
|
|
120
|
+
// to tier-2 can't help and just prints misleading "expanding search"
|
|
121
|
+
// language over a problem those retries cannot fix. Short-circuit before
|
|
122
|
+
// any fallback attempt — but in an interactive terminal, "short-circuit"
|
|
123
|
+
// means offering the same browser login-and-resume flow every other
|
|
124
|
+
// auth failure gets, not just printing a hint and exiting: the person is
|
|
125
|
+
// sitting right there. Retry exactly once with the fresh key; a repeat
|
|
126
|
+
// 401/403 (or non-interactive/CI) falls through to the plain AUTH_FAILED
|
|
127
|
+
// message, same as before this existed.
|
|
128
|
+
if (firstErr.httpStatus === 401 || firstErr.httpStatus === 403) {
|
|
129
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
130
|
+
try {
|
|
131
|
+
const { ensureLoggedIn } = await import('./onboarding.js');
|
|
132
|
+
console.log(chalk.yellow('\n Your saved API key was rejected. Signing in again...'));
|
|
133
|
+
const fresh = await ensureLoggedIn({ apiKey: callOpts.apiKey, baseUrl: callOpts.baseUrl }, chalk);
|
|
134
|
+
callOpts.apiKey = fresh.apiKey;
|
|
135
|
+
return await attempt(buildBody());
|
|
136
|
+
} catch (retryErr) {
|
|
137
|
+
if (retryErr.isPaymentRequired) throw retryErr;
|
|
138
|
+
// Falls through to the same AUTH_FAILED message below, whether the
|
|
139
|
+
// re-login itself failed or the retried request 401/403'd again.
|
|
140
|
+
}
|
|
138
141
|
}
|
|
142
|
+
throw new CapacityError(formatCliError('AUTH_FAILED', {}, chalk));
|
|
139
143
|
}
|
|
140
144
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
const d = firstErr.errorData;
|
|
146
|
+
|
|
147
|
+
// No client-side "provider retry" here on purpose. The backend
|
|
148
|
+
// (reliability_engine.py / deployment_service.py's legacy provisioning
|
|
149
|
+
// loop) already tries every viable provider/offer for a request within
|
|
150
|
+
// the ONE deployment it creates before ever returning a failure -- a
|
|
151
|
+
// PROVISIONING_FAILED/PROVIDER_ADAPTER_ERROR response means that whole
|
|
152
|
+
// internal search already ran and a real, billable resource was very
|
|
153
|
+
// likely created and destroyed along the way (see deployment_service.
|
|
154
|
+
// _reliability_result_failure_reason and the legacy loop's own
|
|
155
|
+
// smoke-check-failure path, both of which only report these codes once
|
|
156
|
+
// a resource actually existed). A second /run or /serve call here would
|
|
157
|
+
// be an entirely new deployment repeating that same internal search from
|
|
158
|
+
// scratch -- duplicate billable exposure for a workload that was never a
|
|
159
|
+
// capacity problem in the first place. This CLI previously did retry
|
|
160
|
+
// once here with a `prefer_different_provider` flag the backend never
|
|
161
|
+
// actually reads (dead parameter, verified against the API source) --
|
|
162
|
+
// that retry never changed routing behavior, it just doubled exposure.
|
|
163
|
+
// A truthful WORKLOAD_START_FAILED/COMMAND_NOT_FOUND response (see
|
|
164
|
+
// serverToKey below) isn't even reachable here -- CapacityError is only
|
|
165
|
+
// for the capacity-flavored codes this function's own contract covers.
|
|
166
|
+
|
|
167
|
+
// The backend owns all provider and tier expansion for the deployment.
|
|
168
|
+
// Even NO_CAPACITY is terminal for this one submission: a second POST
|
|
169
|
+
// would create a second deployment id and split cleanup/audit ownership.
|
|
150
170
|
|
|
151
171
|
throw buildCapacityError(firstErr, false);
|
|
152
172
|
}
|
package/src/onboarding.js
CHANGED
|
@@ -115,17 +115,76 @@ async function ensureFunded(config, chalk) {
|
|
|
115
115
|
console.log(chalk.green('\n ✓ Payment confirmed\n'));
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Just-in-time login only, no funding gate — for commands that manage or
|
|
120
|
+
* inspect an *existing* resource (stop it, read its logs, download its
|
|
121
|
+
* artifacts, reset its idle timer) rather than provisioning new spend.
|
|
122
|
+
* Critically, `badgr down` must never be blocked behind "add funds first" —
|
|
123
|
+
* a $0-balance user with a still-running deployment needs to be able to
|
|
124
|
+
* stop it without a funding detour. Same browser-login-and-resume flow as
|
|
125
|
+
* ensureBadgrReady, just without the ensureFunded step.
|
|
126
|
+
*/
|
|
127
|
+
export async function ensureLoggedInReady(config, chalk) {
|
|
119
128
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
120
129
|
// No human to click a browser link — fail fast with the existing message.
|
|
121
130
|
requireApiKey(config);
|
|
122
131
|
return config;
|
|
123
132
|
}
|
|
133
|
+
if (!config.apiKey) {
|
|
134
|
+
return ensureLoggedIn(config, chalk);
|
|
135
|
+
}
|
|
136
|
+
return config;
|
|
137
|
+
}
|
|
124
138
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Login + funding gate — for commands that provision new spend (run,
|
|
141
|
+
* serve, comfyui, launch/job, batch run, train, transcribe, embed, deploy,
|
|
142
|
+
* shell, sbatch, up). Reuses ensureLoggedInReady for the login half so
|
|
143
|
+
* there is exactly one login flow, not two.
|
|
144
|
+
*/
|
|
145
|
+
export async function ensureBadgrReady(config, chalk) {
|
|
146
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
147
|
+
// No human to click a browser link — fail fast with the existing
|
|
148
|
+
// message, same as ensureLoggedInReady, and skip ensureFunded too:
|
|
149
|
+
// there's no one to click the billing checkout link either.
|
|
150
|
+
requireApiKey(config);
|
|
151
|
+
return config;
|
|
128
152
|
}
|
|
153
|
+
const cfg = await ensureLoggedInReady(config, chalk);
|
|
129
154
|
await ensureFunded(cfg, chalk);
|
|
130
155
|
return cfg;
|
|
131
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Both ensureLoggedInReady and ensureBadgrReady only ever check whether a
|
|
160
|
+
* key is *configured* -- neither can tell a stored key is stale/expired/
|
|
161
|
+
* revoked without actually calling the API, so a command with a bad saved
|
|
162
|
+
* key sailed straight past both checks and only found out from the real
|
|
163
|
+
* request's own 401, which every command before this helper just printed
|
|
164
|
+
* ("Invalid API key ... run: badgr login") and exited. That's a dead end
|
|
165
|
+
* for the exact case ensureLoggedInReady/ensureBadgrReady exist to avoid:
|
|
166
|
+
* the person is sitting right there at an interactive terminal.
|
|
167
|
+
*
|
|
168
|
+
* withReauthRetry(config, chalk, fn) calls fn(config) once; on a 401/403
|
|
169
|
+
* in an interactive TTY, it opens the same browser login link
|
|
170
|
+
* ensureLoggedIn always has, waits for it, then retries fn exactly once
|
|
171
|
+
* with the fresh key. Any other error (including a repeat 401/403 after a
|
|
172
|
+
* successful re-login, or non-interactive/CI) is re-thrown unchanged for
|
|
173
|
+
* the caller's own existing error handling.
|
|
174
|
+
*
|
|
175
|
+
* Returns { config, result } -- callers should keep using the returned
|
|
176
|
+
* config (not their original variable) for anything they do afterward, so
|
|
177
|
+
* a refreshed key from a mid-command reauth is actually used for the rest
|
|
178
|
+
* of that same run instead of only being picked up by the next command.
|
|
179
|
+
*/
|
|
180
|
+
export async function withReauthRetry(config, chalk, fn) {
|
|
181
|
+
try {
|
|
182
|
+
return { config, result: await fn(config) };
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (err.httpStatus !== 401 && err.httpStatus !== 403) throw err;
|
|
185
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw err;
|
|
186
|
+
console.log(chalk.yellow('\n Your saved API key was rejected. Signing in again...'));
|
|
187
|
+
const freshConfig = await ensureLoggedIn(config, chalk);
|
|
188
|
+
return { config: freshConfig, result: await fn(freshConfig) };
|
|
189
|
+
}
|
|
190
|
+
}
|
package/src/progress.js
CHANGED
|
@@ -33,11 +33,14 @@ export function clearBlock(lineCount) {
|
|
|
33
33
|
// `badgr serve` (deployment) and `badgr run` (job): elapsed time, a status
|
|
34
34
|
// word, an *estimated* spend (never presented as confirmed while billing is
|
|
35
35
|
// still live), and the exact commands to inspect logs or stop billing.
|
|
36
|
-
export function renderLiveBlock(chalk, { stageLine, elapsedSec, statusWord, spend, id }) {
|
|
36
|
+
export function renderLiveBlock(chalk, { stageLine, elapsedSec, statusWord, spend, id, maxCost }) {
|
|
37
|
+
const spendLine = maxCost
|
|
38
|
+
? ` Estimated spend: ~$${spend.toFixed(2)} / $${maxCost.toFixed(2)}`
|
|
39
|
+
: ` Estimated spend: ~$${spend.toFixed(2)}`;
|
|
37
40
|
return [
|
|
38
41
|
chalk.dim(`${stageLine} ${elapsedSec}s elapsed`),
|
|
39
42
|
chalk.dim(` Status: ${statusWord}`),
|
|
40
|
-
chalk.dim(
|
|
43
|
+
chalk.dim(spendLine),
|
|
41
44
|
chalk.dim(` Logs: badgr logs ${id}`),
|
|
42
45
|
chalk.dim(` Stop billing: badgr down ${id}`),
|
|
43
46
|
];
|