badgr-cli 1.0.30 → 1.0.31

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.
@@ -1,111 +0,0 @@
1
- import { requireApiKey } from '../config.js';
2
- import { callApi } from '../api.js';
3
-
4
- /**
5
- * badgr capacity # show what's available right now (auto)
6
- * badgr capacity --gpu A100 # check a specific GPU type
7
- */
8
- function parseCapacityArgs(args) {
9
- const flags = {};
10
- let i = 0;
11
- while (i < args.length) {
12
- if (args[i] === '--gpu') { flags.gpu = args[++i]; i++; continue; }
13
- if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
14
- if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
15
- i++;
16
- }
17
- return flags;
18
- }
19
-
20
- export async function capacityCommand(config, args, chalk) {
21
- const flags = parseCapacityArgs(args);
22
- requireApiKey(config);
23
-
24
- const maxPrice = flags.maxPrice ?? 10;
25
-
26
- // No --gpu: show cheapest runnable GPU across all types
27
- if (!flags.gpu) {
28
- console.log(chalk.bold('\nAvailable GPU capacity\n'));
29
- process.stdout.write(chalk.dim(' Checking availability...\n'));
30
-
31
- let data;
32
- try {
33
- const params = new URLSearchParams({ max_price: String(maxPrice) });
34
- if (flags.region) params.set('region', flags.region.toUpperCase());
35
- data = await callApi(`/capacity/auto?${params}`, {
36
- apiKey: config.apiKey,
37
- baseUrl: config.baseUrl,
38
- });
39
- } catch (err) {
40
- if (err.errorData?.code === 'NO_CAPACITY_MATCH') {
41
- console.log(chalk.dim('\n No GPU capacity available right now.\n'));
42
- console.log(chalk.dim(' Try again in a few minutes, or check a specific GPU with --gpu <type>.\n'));
43
- return;
44
- }
45
- console.error(chalk.red(`\n ✗ Capacity check failed: ${err.message}\n`));
46
- process.exit(1);
47
- }
48
-
49
- console.log();
50
- console.log(` ${chalk.bold('Cheapest available:')} ${chalk.cyan(data.gpu)} in ${data.region} ${chalk.green('$' + data.price.toFixed(2) + '/hr')}`);
51
- console.log();
52
- console.log(` ${chalk.bold('Run now:')}`);
53
- console.log(chalk.cyan(` badgr run python train.py`));
54
- console.log(chalk.cyan(` badgr serve meta-llama/Llama-3.1-8B-Instruct`));
55
- console.log();
56
- console.log(chalk.dim(` Use badgr capacity --gpu A100 to check a specific GPU type.`));
57
- console.log();
58
- return;
59
- }
60
-
61
- // --gpu specified: show availability for that type
62
- const gpu = flags.gpu.toUpperCase().replace('-', '_');
63
- const params = new URLSearchParams({ gpu, max_price: String(maxPrice) });
64
- if (flags.region) params.set('region', flags.region.toUpperCase());
65
-
66
- console.log(chalk.bold(`\nCapacity: ${gpu}\n`));
67
- process.stdout.write(chalk.dim(' Checking...\n'));
68
-
69
- let data;
70
- try {
71
- data = await callApi(`/capacity/suggestions?${params}`, {
72
- apiKey: config.apiKey,
73
- baseUrl: config.baseUrl,
74
- });
75
- } catch (err) {
76
- console.error(chalk.red(`\n ✗ Capacity check failed: ${err.message}\n`));
77
- process.exit(1);
78
- }
79
-
80
- const matches = data.matches ?? [];
81
- if (matches.length > 0) {
82
- console.log(chalk.bold('\n Available now:\n'));
83
- for (const m of matches) {
84
- console.log(` ${chalk.green('●')} ${m.gpu} in ${m.region} ${chalk.green('$' + m.price.toFixed(2) + '/hr')}`);
85
- }
86
- console.log();
87
- const regionFlag = flags.region ? ` --region ${flags.region.toUpperCase()}` : '';
88
- console.log(` ${chalk.bold('Run:')}`);
89
- console.log(chalk.cyan(` badgr run python train.py --gpu ${gpu}${regionFlag}`));
90
- console.log();
91
- } else {
92
- const regionLabel = flags.region ? ` in ${flags.region.toUpperCase()}` : '';
93
- console.log(chalk.dim(`\n No ${gpu} available right now under $${maxPrice.toFixed(2)}/hr${regionLabel}.\n`));
94
-
95
- const alternatives = data.alternatives ?? [];
96
- if (alternatives.length > 0) {
97
- console.log(chalk.bold(' Available alternatives:\n'));
98
- for (const a of alternatives) {
99
- console.log(` ${chalk.dim('●')} ${a.gpu} in ${a.region} $${a.price.toFixed(2)}/hr`);
100
- }
101
- console.log();
102
- console.log(` ${chalk.bold('Try:')}`);
103
- for (const a of alternatives.slice(0, 3)) {
104
- console.log(chalk.cyan(` badgr run python train.py --gpu ${a.gpu}`));
105
- }
106
- console.log();
107
- } else {
108
- console.log(chalk.dim(' No alternatives found. Try `badgr capacity` to see global availability.\n'));
109
- }
110
- }
111
- }
@@ -1,240 +0,0 @@
1
- import { requireApiKey } from '../config.js';
2
- import { callApi, terminateDeployment } from '../api.js';
3
- import { addReceipt, generateReceiptId } from '../store.js';
4
-
5
- // max $0.80/hr × 2 min ≈ $0.027 total spend cap (smoke / Modal T4 tier)
6
- const TEST_MAX_PRICE = 0.80;
7
- const TEST_MAX_RUNTIME_MS = 2 * 60 * 1000;
8
- const TEST_COMMAND = ['python', '-c', "print('hello from badgr')"];
9
- // Use alpine (7MB) instead of slim (50MB) — dramatically faster image pull for smoke tests.
10
- // Falls back gracefully: alpine has python3 and supports the test command identically.
11
- const TEST_IMAGE = 'python:3.11-alpine';
12
- const EXPECTED_OUTPUT = 'hello from badgr';
13
-
14
- // --provider flag resolves to a backend tier value.
15
- // 'tier1' → managed routing (default), 'tier2' → marketplace routing, 'secondary' → secondary dispatch.
16
- const PROVIDER_TO_TIER = { tier1: '1', tier2: '2', secondary: 'modal' };
17
-
18
- export function parseTestArgs(args) {
19
- const flags = {};
20
- let i = 0;
21
- while (i < args.length) {
22
- if (args[i] === '--provider' && args[i + 1]) { flags.provider = args[++i]; i++; continue; }
23
- if (args[i] === '--no-tier-fallback') { flags.noTierFallback = true; i++; continue; }
24
- i++;
25
- }
26
- return flags;
27
- }
28
-
29
- function step(chalk, ok, msg, detail = '') {
30
- const icon = ok ? chalk.green('✓') : chalk.red('✗');
31
- const suffix = detail ? chalk.dim(` — ${detail}`) : '';
32
- console.log(` ${icon} ${msg}${suffix}`);
33
- }
34
-
35
- async function pollStatus(config, depId, targetStatuses, timeoutMs) {
36
- const deadline = Date.now() + timeoutMs;
37
- while (Date.now() < deadline) {
38
- await new Promise(r => setTimeout(r, 3000));
39
- try {
40
- const dep = await callApi(`/deployments/${depId}`, {
41
- apiKey: config.apiKey,
42
- baseUrl: config.baseUrl,
43
- });
44
- if (targetStatuses.has(dep.status)) return dep;
45
- } catch { /* retry */ }
46
- }
47
- return null;
48
- }
49
-
50
- async function pollOutputOrDone(config, depId, expected, timeoutMs) {
51
- const deadline = Date.now() + timeoutMs;
52
- while (Date.now() < deadline) {
53
- await new Promise(r => setTimeout(r, 3000));
54
- try {
55
- const dep = await callApi(`/deployments/${depId}`, {
56
- apiKey: config.apiKey,
57
- baseUrl: config.baseUrl,
58
- });
59
- if (dep.status === 'succeeded' || dep.status === 'failed') {
60
- const data = await callApi(`/deployments/${depId}/logs`, {
61
- apiKey: config.apiKey,
62
- baseUrl: config.baseUrl,
63
- });
64
- const lines = data?.logs ?? [];
65
- return {
66
- done: true,
67
- ok: dep.status === 'succeeded' && lines.some(l => l.includes(expected)),
68
- exitCode: dep.exit_code,
69
- };
70
- }
71
- const data = await callApi(`/deployments/${depId}/logs`, {
72
- apiKey: config.apiKey,
73
- baseUrl: config.baseUrl,
74
- });
75
- const lines = data?.logs ?? [];
76
- if (lines.some(l => l.includes(expected))) return { done: true, ok: true, exitCode: 0 };
77
- } catch { /* retry */ }
78
- }
79
- return { done: false, ok: false, exitCode: null };
80
- }
81
-
82
- export async function testCommand(config, args, chalk) {
83
- requireApiKey(config);
84
-
85
- const flags = parseTestArgs(Array.isArray(args) ? args : []);
86
- const providerKey = flags.provider ? flags.provider.toLowerCase() : 'tier1';
87
-
88
- if (providerKey === 'secondary') {
89
- // Secondary dispatch provider uses a webhook model, not direct GPU rental.
90
- // Verify the backend reports it as configured.
91
- console.log(chalk.bold('\n⚡ Testing secondary dispatch provider\n'));
92
- let routes;
93
- try {
94
- routes = await callApi('/compute/routes', { apiKey: config.apiKey, baseUrl: config.baseUrl });
95
- } catch {
96
- routes = null;
97
- }
98
- const secondaryRoute = Array.isArray(routes) ? routes.find(r => r.name === 'modal') : null;
99
- if (secondaryRoute?.available) {
100
- step(chalk, true, 'Secondary provider configured');
101
- console.log(chalk.green('\n ✓ Secondary dispatch provider is ready\n'));
102
- } else {
103
- step(chalk, false, 'Secondary provider configured', 'contact support to enable secondary dispatch');
104
- console.log(chalk.red('\n ✗ Secondary dispatch provider is not configured\n'));
105
- process.exit(1);
106
- }
107
- return;
108
- }
109
-
110
- const tier = PROVIDER_TO_TIER[providerKey] ?? '1';
111
- const tierLabel = tier === '1' ? 'tier 1 (managed routing)' : 'tier 2 (marketplace routing)';
112
-
113
- console.log(chalk.bold('\n⚡ Running end-to-end test\n'));
114
- console.log(chalk.dim(` Command: ${TEST_COMMAND.join(' ')}`));
115
- console.log(chalk.dim(` Routing: ${tier === '1' ? 'tier 1 — managed provider routing' : 'tier 2 — marketplace routing'}`));
116
- console.log(chalk.dim(` Budget: max $${TEST_MAX_PRICE.toFixed(2)}/hr · 2 minute cap (~$0.05 max)`));
117
- console.log();
118
-
119
- const rcptId = generateReceiptId();
120
- let depId;
121
-
122
- // ── 1. Provision ─────────────────────────────────────────────────────────
123
- process.stdout.write(chalk.dim(` Provisioning GPU (${tierLabel})...\n`));
124
- let dep;
125
- const baseBody = {
126
- command: TEST_COMMAND,
127
- image: TEST_IMAGE,
128
- gpu: 'auto',
129
- max_price_per_hour: TEST_MAX_PRICE,
130
- };
131
- try {
132
- dep = await callApi('/run', {
133
- method: 'POST',
134
- apiKey: config.apiKey,
135
- baseUrl: config.baseUrl,
136
- body: { ...baseBody, tier },
137
- });
138
- } catch (err) {
139
- if (err.errorData?.code === 'NO_CAPACITY_MATCH' && tier === '1' && !flags.noTierFallback) {
140
- process.stdout.write('\n');
141
- process.stdout.write(chalk.dim(' No tier 1 capacity — trying tier 2 marketplace routing...'));
142
- try {
143
- dep = await callApi('/run', {
144
- method: 'POST',
145
- apiKey: config.apiKey,
146
- baseUrl: config.baseUrl,
147
- body: { ...baseBody, tier: '2' },
148
- });
149
- } catch (err2) {
150
- process.stdout.write('\n');
151
- step(chalk, false, 'Provisioned', err2.message);
152
- console.log();
153
- console.error(chalk.red(' Test failed — no GPU capacity available on any provider.\n'));
154
- process.exit(1);
155
- }
156
- } else if (err.errorData?.code === 'NO_CAPACITY_MATCH' && flags.noTierFallback) {
157
- process.stdout.write('\n');
158
- step(chalk, false, 'Provisioned', 'no tier 1 capacity');
159
- console.log();
160
- console.error(chalk.red(' Test failed — no tier 1 capacity (strict mode, no tier 2 fallback).\n'));
161
- process.exit(1);
162
- } else {
163
- process.stdout.write('\n');
164
- step(chalk, false, 'Provisioned', err.message);
165
- console.log();
166
- console.error(chalk.red(' Test failed — could not provision GPU.\n'));
167
- process.exit(1);
168
- }
169
- }
170
- depId = dep.deployment_id;
171
- process.stdout.write('\n');
172
- step(chalk, true, 'Provisioned', `${dep.deployment_id} on ${dep.gpu_type}`);
173
-
174
- // ── 2. Container started ─────────────────────────────────────────────────
175
- process.stdout.write(chalk.dim(' Waiting for container to start...'));
176
- const started = await pollStatus(
177
- config, depId,
178
- // Modal serverless jobs may skip straight to succeeded when the callback fires.
179
- new Set(['running', 'starting', 'succeeded', 'failed', 'stopped', 'completed']),
180
- TEST_MAX_RUNTIME_MS,
181
- );
182
- process.stdout.write('\n');
183
-
184
- if (!started || started.status === 'failed') {
185
- step(chalk, false, 'Container started', started?.status ?? 'timeout');
186
- console.log();
187
- console.error(chalk.red(' Test failed — container did not start.\n'));
188
- try { await terminateDeployment(config, depId); } catch { /* best-effort */ }
189
- process.exit(1);
190
- }
191
- step(chalk, true, 'Container started');
192
-
193
- // ── 3. Command output ────────────────────────────────────────────────────
194
- process.stdout.write(chalk.dim(' Checking command output...'));
195
- const outputResult = await pollOutputOrDone(config, depId, EXPECTED_OUTPUT, 90_000);
196
- process.stdout.write('\n');
197
- const gotOutput = outputResult.ok;
198
- if (gotOutput) {
199
- step(chalk, true, 'Command printed output');
200
- } else if (outputResult.done && outputResult.exitCode !== 0) {
201
- step(chalk, false, 'Command printed output', `exit ${outputResult.exitCode}`);
202
- } else {
203
- step(chalk, false, 'Command printed output', 'not found in logs (logs may be buffered)');
204
- }
205
-
206
- // ── 4. Stop billing ──────────────────────────────────────────────────────
207
- process.stdout.write(chalk.dim(' Stopping deployment...'));
208
- let stopped = false;
209
- try {
210
- await terminateDeployment(config, depId);
211
- stopped = true;
212
- } catch { /* best-effort */ }
213
- process.stdout.write('\n');
214
- step(chalk, stopped, 'Billing stopped');
215
-
216
- // ── 5. Receipt ───────────────────────────────────────────────────────────
217
- addReceipt({
218
- receiptId: rcptId,
219
- action: 'badgr test',
220
- deploymentId: depId,
221
- gpu: dep.gpu_type,
222
- status: gotOutput ? 'test_passed' : 'test_failed',
223
- createdAt: new Date().toISOString(),
224
- });
225
- step(chalk, true, 'Receipt created', rcptId);
226
-
227
- // ── Summary ──────────────────────────────────────────────────────────────
228
- console.log();
229
- const passed = stopped && gotOutput;
230
- if (passed) {
231
- console.log(chalk.green(chalk.bold(' ✓ Test passed\n')));
232
- } else {
233
- if (!gotOutput) {
234
- console.error(chalk.red(' Test failed — expected output not found in logs\n'));
235
- } else {
236
- console.error(chalk.red(' Test failed — could not stop billing\n'));
237
- }
238
- process.exit(1);
239
- }
240
- }
package/src/fallback.js DELETED
@@ -1,170 +0,0 @@
1
- import readline from 'readline';
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
-
78
- // GPU descriptions for display only — no scoring logic lives here.
79
- // Ranking is computed server-side and returned as `rank` on each alternative.
80
- const GPU_DISPLAY = {
81
- RTX_3080: { desc: 'Dev, light inference' },
82
- RTX_4080: { desc: 'Inference and dev workloads' },
83
- RTX_4090: { desc: 'Inference, training, dev' },
84
- L40S: { desc: 'Inference, vLLM, batch jobs' },
85
- A6000: { desc: 'Training, large models, inference' },
86
- A100: { desc: 'Large-scale training and inference' },
87
- H100: { desc: 'Large model training, best throughput' },
88
- };
89
-
90
- /**
91
- * Sort alternatives for display.
92
- *
93
- * - mode 'closest' → use backend `rank` field (1 = best match); fall back to price.
94
- * - mode 'cheapest' → sort by price ascending only.
95
- *
96
- * The scoring algorithm has been removed from the CLI; the backend now
97
- * computes and attaches `rank` and `diff_desc` to every alternative.
98
- */
99
- export function rankAlternatives(requestedGpu, alternatives, mode = 'closest') {
100
- if (!alternatives || alternatives.length === 0) return [];
101
- if (mode === 'cheapest') return [...alternatives].sort((a, b) => a.price - b.price);
102
- // 'closest': honour backend rank when present
103
- if (alternatives.some(a => a.rank != null)) {
104
- return [...alternatives].sort((a, b) => (a.rank ?? 999) - (b.rank ?? 999));
105
- }
106
- // Older backend without rank — fall back to price
107
- return [...alternatives].sort((a, b) => a.price - b.price);
108
- }
109
-
110
- /**
111
- * One-line diff for display. Uses `diff_desc` from backend when present,
112
- * otherwise omits the diff rather than duplicating the scoring logic.
113
- */
114
- export function diffDescription(requestedGpu, altGpu, alt = {}) {
115
- return alt.diff_desc || '';
116
- }
117
-
118
- function ask(prompt) {
119
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
120
- return new Promise(resolve => rl.question(prompt, ans => { rl.close(); resolve(ans.trim()); }));
121
- }
122
-
123
- /**
124
- * Show the interactive fallback prompt and return the chosen alternative or null.
125
- * Non-TTY environments auto-select the top-ranked option.
126
- */
127
- export async function promptFallback(requestedGpu, ranked, chalk) {
128
- if (ranked.length === 0) return null;
129
-
130
- const top = ranked[0];
131
- const others = ranked.slice(1, 4);
132
- const topDisp = GPU_DISPLAY[top.gpu];
133
-
134
- console.log(chalk.yellow(`\n ${requestedGpu} isn't available right now.\n`));
135
- console.log(chalk.bold(' Closest match:'));
136
- console.log(` ${chalk.cyan(top.gpu)} in ${top.region}`);
137
- console.log(` ${chalk.green('$' + top.price.toFixed(2) + '/hr')} estimated price`);
138
- if (top.diff_desc) console.log(` ${chalk.dim(top.diff_desc)}`);
139
- else if (topDisp?.desc) console.log(` ${topDisp.desc}`);
140
- console.log(chalk.dim(' (availability estimated from market data — not pre-verified)'));
141
- console.log();
142
-
143
- if (!process.stdin.isTTY) {
144
- console.log(chalk.dim(` Auto-selecting ${top.gpu} (non-interactive).`));
145
- return top;
146
- }
147
-
148
- console.log(` Press ${chalk.bold('Enter')} to run on ${chalk.cyan(top.gpu)}`);
149
- if (others.length > 0) {
150
- console.log(' or type:');
151
- for (const [i, alt] of others.entries()) {
152
- const disp = GPU_DISPLAY[alt.gpu];
153
- const hint = disp ? disp.desc.split(',')[0].toLowerCase() : '';
154
- console.log(` ${chalk.bold(String(i + 1))} = ${alt.gpu}${hint ? ', ' + hint : ''}`);
155
- }
156
- }
157
- console.log(` ${chalk.bold('q')} = cancel`);
158
- console.log();
159
-
160
- const answer = await ask(' > ');
161
-
162
- if (answer === '') return top;
163
- if (answer.toLowerCase() === 'q') return null;
164
-
165
- const idx = parseInt(answer, 10);
166
- if (!isNaN(idx) && idx >= 1 && idx <= others.length) return others[idx - 1];
167
-
168
- console.log(chalk.dim(` Unrecognised input — using ${top.gpu}.`));
169
- return top;
170
- }