badgr-cli 1.0.29 → 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,18 +1,13 @@
1
- import readline from 'readline';
2
1
  import { requireApiKey } from '../config.js';
3
2
  import { callApi } from '../api.js';
4
- 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 { addDeployment, addReceipt, generateReceiptId } from '../store.js';
10
4
 
11
5
  /**
12
- * badgr serve meta-llama/Llama-3.1-8B-Instruct
13
6
  * badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
14
7
  *
15
- * GPU defaults to "AUTO" backend infers from model size.
8
+ * Provisions a persistent vLLM endpoint, health-checks it before printing
9
+ * "Endpoint ready", and returns an OpenAI-compatible base URL.
10
+ * Stop billing with `badgr down <id>`.
16
11
  */
17
12
  export function parseServeArgs(args) {
18
13
  const flags = {};
@@ -22,7 +17,6 @@ export function parseServeArgs(args) {
22
17
  if (args[i] === '--gpu') { flags.gpu = args[++i]; i++; continue; }
23
18
  if (args[i] === '--count') { flags.count = parseInt(args[++i], 10); i++; continue; }
24
19
  if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
25
- 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
22
  if (args[i] === '--no-wait') { flags.noWait = true; i++; continue; }
@@ -32,26 +26,24 @@ export function parseServeArgs(args) {
32
26
  return { model, flags };
33
27
  }
34
28
 
35
- function _serveStageLabel(elapsedSec) {
36
- if (elapsedSec < 45) return 'Starting vLLM…';
37
- if (elapsedSec < 150) return 'Downloading model…';
38
- return 'Waiting for /v1/models…';
39
- }
40
-
29
+ // Poll GET <endpointUrl>/models until it returns 200 or timeout expires.
30
+ // Returns true if healthy, false if timed out.
41
31
  async function waitForEndpoint(endpointUrl, timeoutMs = 5 * 60 * 1000, chalk) {
42
- const startMs = Date.now();
43
- const deadline = startMs + timeoutMs;
32
+ const deadline = Date.now() + timeoutMs;
33
+ const modelsUrl = `${endpointUrl}/models`;
34
+ let attempt = 0;
44
35
 
45
36
  while (Date.now() < deadline) {
37
+ attempt++;
46
38
  try {
47
- const res = await fetch(`${endpointUrl}/models`, { signal: AbortSignal.timeout(8000) });
48
- if (res.ok) { process.stdout.write('\n'); return true; }
39
+ const res = await fetch(modelsUrl, { signal: AbortSignal.timeout(8000) });
40
+ if (res.ok) return true;
49
41
  } catch {
50
- // still starting
42
+ // network not up yet — keep polling
51
43
  }
52
- const elapsed = Math.round((Date.now() - startMs) / 1000);
44
+ const elapsed = Math.round((Date.now() - (deadline - timeoutMs)) / 1000);
53
45
  process.stdout.write(
54
- `\r ${chalk.dim(_serveStageLabel(elapsed) + ` (${elapsed}s)`)} `
46
+ `\r ${chalk.dim(`Waiting for endpoint… ${elapsed}s (attempt ${attempt})`)} `
55
47
  );
56
48
  await new Promise(r => setTimeout(r, 8000));
57
49
  }
@@ -63,42 +55,20 @@ export async function serveCommand(config, args, chalk) {
63
55
  const { model, flags } = parseServeArgs(args);
64
56
 
65
57
  if (!model) {
66
- console.error(chalk.red('Usage: badgr serve <model>'));
67
- console.error(chalk.red(' badgr serve meta-llama/Llama-3.1-8B-Instruct'));
58
+ console.error(chalk.red('Usage: badgr serve <model> --gpu <type>'));
59
+ console.error(chalk.red(' badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S'));
68
60
  return;
69
61
  }
70
62
 
71
63
  requireApiKey(config);
72
64
 
73
- // gpu=AUTO tells the backend to infer the right GPU from model size
74
- const gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : 'AUTO';
75
- const gpuLabel = gpu === 'AUTO' ? 'auto' : gpu;
65
+ const gpu = flags.gpu || 'L40S';
76
66
 
77
- // Tier 1 = managed routing (default). Tier 2 = marketplace routing, opt-in via --tier 2.
78
- const effectiveTier = (flags.tier === '2' || flags.tier === 'tier2' || flags.tier === 'tier-2')
79
- ? '2'
80
- : (flags.tier || '1');
81
-
82
- console.log(chalk.bold('\nServing model\n'));
67
+ console.log(chalk.bold('\n🚀 Serving model\n'));
83
68
  console.log(` ${chalk.bold('Model:')} ${model}`);
84
- console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
85
- if (effectiveTier === '2') console.log(` ${chalk.dim('(tier 2 — marketplace routing)')}`);
69
+ console.log(` ${chalk.bold('GPU:')} ${gpu}`);
86
70
  console.log();
87
- process.stdout.write(chalk.dim(' Finding reliable capacity...\n'));
88
-
89
- const effectiveRegion = flags.region ? flags.region.toUpperCase() : undefined;
90
-
91
- function buildBody(gpuOverride, regionOverride) {
92
- return {
93
- model,
94
- gpu: gpuOverride || gpu,
95
- gpu_count: flags.count || 1,
96
- ...(regionOverride || effectiveRegion ? { region: regionOverride || effectiveRegion } : {}),
97
- max_price_per_hour: flags.maxPrice,
98
- name: flags.name,
99
- tier: effectiveTier,
100
- };
101
- }
71
+ console.log(chalk.dim(' Finding best available GPU capacity...'));
102
72
 
103
73
  let dep;
104
74
  try {
@@ -106,88 +76,21 @@ export async function serveCommand(config, args, chalk) {
106
76
  method: 'POST',
107
77
  apiKey: config.apiKey,
108
78
  baseUrl: config.baseUrl,
109
- body: buildBody(),
79
+ body: {
80
+ model,
81
+ gpu: gpu.toUpperCase().replace('-', '_'),
82
+ gpu_count: flags.count || 1,
83
+ region: flags.region || 'US',
84
+ max_price_per_hour: flags.maxPrice,
85
+ name: flags.name,
86
+ },
110
87
  });
111
88
  } catch (err) {
112
- const d = err.errorData;
113
- if (d?.code === 'NO_CAPACITY_MATCH') {
114
- if (effectiveTier === '2') {
115
- console.error(chalk.red('\n ✗ No GPU capacity available right now on any provider.\n'));
116
- console.error(chalk.dim(' Run `badgr capacity` to see alternatives, or try again shortly.'));
117
- process.exit(1);
118
- }
119
-
120
- // Tier 1 out of capacity — offer tier 2 marketplace routing.
121
- if (process.stdin.isTTY) {
122
- const answer = await askConfirm(
123
- `\n No tier 1 capacity available. Try tier 2 marketplace routing? [${chalk.bold('Enter')}/${chalk.bold('q')}]: `
124
- );
125
- if (answer.toLowerCase() === 'q') {
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`));
183
- return;
184
- }
185
- }
186
-
187
- if (dep.provider_fallback_note === 'tier2_failed_using_tier1') {
188
- console.log(chalk.yellow(' ℹ Tier 2 unavailable. Running on Tier 1 instead.\n'));
89
+ console.error(chalk.red(`\n ✗ Serve failed: ${err.message}\n`));
90
+ return;
189
91
  }
190
92
 
93
+ // Mirror to local store so badgr down/status/receipts work offline
191
94
  addDeployment({
192
95
  id: dep.deployment_id,
193
96
  name: dep.name,
@@ -195,6 +98,7 @@ export async function serveCommand(config, args, chalk) {
195
98
  model: dep.model || model,
196
99
  gpu: dep.gpu_type,
197
100
  count: dep.gpu_count,
101
+ provider: dep.provider,
198
102
  status: dep.status,
199
103
  endpointUrl: dep.endpoint_url || dep.openai_base_url,
200
104
  receiptId: dep.receipt_id,
@@ -207,6 +111,7 @@ export async function serveCommand(config, args, chalk) {
207
111
  receiptId: rcptId,
208
112
  action: 'badgr serve',
209
113
  deploymentId: dep.deployment_id,
114
+ provider: dep.provider,
210
115
  gpu: dep.gpu_type,
211
116
  status: dep.status,
212
117
  createdAt: new Date().toISOString(),
@@ -214,41 +119,41 @@ export async function serveCommand(config, args, chalk) {
214
119
 
215
120
  const endpointUrl = dep.endpoint_url || dep.openai_base_url || config.baseUrl;
216
121
 
217
- // ── Health check ──────────────────────────────────────────────────────────
122
+ // ── Health check before declaring "ready" ─────────────────────────────────
218
123
  let endpointReady = false;
124
+
219
125
  if (flags.noWait) {
220
- console.log(chalk.yellow('\n Skipped health check (--no-wait)\n'));
126
+ console.log(chalk.yellow('\n Endpoint provisioning (skipped health check — use --no-wait)\n'));
221
127
  } else {
128
+ console.log(chalk.dim('\n Health-checking endpoint (up to 5 min)...'));
222
129
  endpointReady = await waitForEndpoint(endpointUrl, 5 * 60 * 1000, chalk);
223
130
  process.stdout.write('\n');
224
- if (!endpointReady) updateReceipt(rcptId, { status: 'health_check_timeout' });
225
131
  }
226
132
 
227
- // ── Result ────────────────────────────────────────────────────────────────
133
+ // ── Print result ──────────────────────────────────────────────────────────
228
134
  if (endpointReady) {
229
- console.log(chalk.green('\n ✓ Endpoint ready\n'));
135
+ console.log(chalk.green('✓ Endpoint ready\n'));
230
136
  } else {
231
- console.log(chalk.yellow('\n ⏳ Endpoint still starting — model download may still be in progress.\n'));
232
- console.log(` ${chalk.bold('Stop billing now:')} ${chalk.dim(`badgr down ${dep.deployment_id}`)}`);
233
- console.log(` ${chalk.bold('Continue watching:')} ${chalk.dim(`badgr logs ${dep.deployment_id}`)}`);
137
+ console.log(chalk.yellow('⏳ Endpoint starting\n'));
138
+ console.log(chalk.dim(` Not yet responding. Check status:`));
139
+ console.log(chalk.dim(` badgr status`));
140
+ console.log(chalk.dim(` curl ${endpointUrl}/models`));
234
141
  console.log();
235
142
  }
236
143
 
237
- console.log(` ${chalk.bold('Base URL:')} ${chalk.cyan(endpointUrl)}`);
144
+ console.log(` ${chalk.bold('Deployment:')} ${chalk.cyan(dep.deployment_id)}`);
238
145
  console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
239
146
  console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
240
- if (dep.tier) console.log(` ${chalk.bold('Tier:')} ${dep.tier}`);
147
+ console.log(` ${chalk.bold('Endpoint:')} ${chalk.cyan(endpointUrl)}`);
241
148
  if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
242
- console.log(` ${chalk.bold('Logs:')} ${chalk.dim(`badgr logs ${dep.deployment_id}`)}`);
243
- console.log(` ${chalk.bold('Stop billing:')} ${chalk.dim(`badgr down ${dep.deployment_id}`)}`);
244
- console.log();
149
+ console.log(`\n ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
245
150
 
246
151
  if (endpointReady) {
247
- const keySnip = config.apiKey?.slice(0, 8) || 'sk-...';
248
- console.log(` ${chalk.bold('Use with OpenAI SDK:')}`);
152
+ console.log(`\n ${chalk.bold('Use with OpenAI SDK:')}`);
249
153
  console.log(chalk.dim(` from openai import OpenAI`));
250
- console.log(chalk.dim(` client = OpenAI(base_url="${endpointUrl}", api_key="${keySnip}...")`));
154
+ console.log(chalk.dim(` client = OpenAI(api_key="${config.apiKey?.slice(0, 8) || 'sk-...'}...", base_url="${endpointUrl}")`));
251
155
  console.log(chalk.dim(` resp = client.chat.completions.create(model="${dep.model || model}", messages=[...])`));
252
- console.log();
253
156
  }
157
+
158
+ console.log(`\n ${chalk.dim(`Stop billing: badgr down ${dep.deployment_id}`)}\n`);
254
159
  }
@@ -2,20 +2,24 @@ import { listDeployments as localDeployments } from '../store.js';
2
2
  import { listDeployments as apiDeployments } from '../api.js';
3
3
 
4
4
  export async function statusCommand(config, args, chalk) {
5
+ console.log(chalk.bold('\n📊 GPU Status\n'));
6
+
5
7
  let deployments = [];
6
8
 
9
+ // ── Live data from the backend ────────────────────────────────────────────
7
10
  if (config.apiKey) {
8
11
  try {
9
12
  const data = await apiDeployments(config);
10
13
  deployments = data?.deployments ?? [];
11
14
  } catch (err) {
12
- console.log(chalk.yellow(`\n Could not reach API: ${err.message}. Showing local state.\n`));
15
+ console.log(chalk.yellow(` Could not reach API: ${err.message}. Showing local state.\n`));
13
16
  deployments = localDeployments().map(d => ({
14
17
  deployment_id: d.id,
15
18
  name: d.name,
16
19
  workload_type: d.type,
17
20
  gpu_type: d.gpu,
18
21
  gpu_count: d.count,
22
+ provider: d.provider,
19
23
  status: d.status,
20
24
  cost_per_hour: d.costPerHour,
21
25
  endpoint_url: d.endpointUrl,
@@ -29,6 +33,7 @@ export async function statusCommand(config, args, chalk) {
29
33
  workload_type: d.type,
30
34
  gpu_type: d.gpu,
31
35
  gpu_count: d.count,
36
+ provider: d.provider,
32
37
  status: d.status,
33
38
  cost_per_hour: d.costPerHour,
34
39
  endpoint_url: d.endpointUrl,
@@ -36,46 +41,54 @@ export async function statusCommand(config, args, chalk) {
36
41
  }));
37
42
  }
38
43
 
39
- const running = deployments.filter(d => d.status === 'running' || d.status === 'provisioning');
40
-
41
- if (running.length === 0) {
42
- console.log(chalk.dim('\n Nothing running.\n'));
43
- console.log(chalk.dim(' badgr run python train.py'));
44
- console.log(chalk.dim(' badgr serve meta-llama/Llama-3.1-8B-Instruct\n'));
44
+ if (deployments.length === 0) {
45
+ console.log(chalk.dim(' No active deployments.'));
46
+ console.log(chalk.dim(' Run `badgr serve <model> --gpu L40S` to provision one.\n'));
45
47
  return;
46
48
  }
47
49
 
48
- console.log(chalk.bold('\nRunning now:\n'));
49
- for (const d of running) {
50
- const id = d.deployment_id || d.name;
51
- const type = d.workload_type === 'endpoint' ? 'endpoint' : 'job';
52
- const gpu = d.gpu_type || '—';
53
- const rate = d.cost_per_hour > 0 ? chalk.yellow(`$${d.cost_per_hour.toFixed(2)}/hr`) : '';
54
- const badge = d.status === 'provisioning'
55
- ? chalk.yellow('● starting')
56
- : chalk.green('● running');
50
+ const cols = { name: 16, type: 9, gpu: 11, status: 14, price: 9 };
57
51
 
58
- console.log(` ${badge} ${chalk.bold(id)} ${type} ${gpu} ${rate}`);
52
+ const header =
53
+ 'Name'.padEnd(cols.name) +
54
+ 'Type'.padEnd(cols.type) +
55
+ 'GPU'.padEnd(cols.gpu) +
56
+ 'Status'.padEnd(cols.status) +
57
+ 'Price/hr';
58
+ console.log(' ' + chalk.bold(header));
59
+ console.log(' ' + '─'.repeat(Object.values(cols).reduce((a, b) => a + b, 0) + 8));
59
60
 
60
- if (d.workload_type === 'endpoint' && d.endpoint_url) {
61
- console.log(` ${chalk.dim('URL:')} ${d.endpoint_url}`);
62
- }
63
- if (d.model) {
64
- console.log(` ${chalk.dim('Model:')} ${d.model}`);
65
- }
66
- console.log();
67
- }
61
+ deployments.forEach(d => {
62
+ const statusColor = d.status === 'running'
63
+ ? chalk.green(d.status.padEnd(cols.status))
64
+ : d.status === 'provisioning'
65
+ ? chalk.yellow(d.status.padEnd(cols.status))
66
+ : chalk.dim(d.status.padEnd(cols.status));
67
+ const price = d.cost_per_hour > 0 ? `$${d.cost_per_hour.toFixed(2)}` : '—';
68
+ const name = (d.name || d.deployment_id || '').slice(0, cols.name - 1);
69
+ const gpu = (d.gpu_type || '—').slice(0, cols.gpu - 1).padEnd(cols.gpu);
70
+ console.log(
71
+ ' ' +
72
+ name.padEnd(cols.name) +
73
+ (d.workload_type || '—').padEnd(cols.type) +
74
+ gpu +
75
+ statusColor +
76
+ price
77
+ );
78
+ });
68
79
 
69
- const billable = running.filter(d => (d.cost_per_hour || 0) > 0);
70
- if (billable.length > 0) {
71
- const totalPerHr = billable.reduce((s, d) => s + (d.cost_per_hour || 0), 0);
72
- console.log(` ${chalk.bold('Total billing:')} ${chalk.yellow(`$${totalPerHr.toFixed(2)}/hr`)}\n`);
73
- }
80
+ console.log();
74
81
 
75
- console.log(chalk.bold('Stop billing:\n'));
76
- for (const d of running) {
77
- const id = d.deployment_id || d.name;
78
- console.log(` ${chalk.cyan(`badgr down ${id}`)}`);
82
+ // Show endpoint URLs for running endpoint deployments
83
+ const endpoints = deployments.filter(d => d.workload_type === 'endpoint' && d.status === 'running');
84
+ if (endpoints.length > 0) {
85
+ console.log(chalk.bold(' Endpoints\n'));
86
+ endpoints.forEach(d => {
87
+ const url = d.endpoint_url || config.baseUrl;
88
+ console.log(` ${chalk.cyan(d.name || d.deployment_id)}`);
89
+ console.log(` ${chalk.dim('URL:')} ${url}`);
90
+ if (d.model) console.log(` ${chalk.dim('model:')} ${d.model}`);
91
+ });
92
+ console.log();
79
93
  }
80
- console.log();
81
94
  }
@@ -1,31 +1,37 @@
1
1
  import { parseSpec, validateSpec, specLines } from '../spec.js';
2
+ import { getRoutePlan } from '../router.js';
2
3
  import { requireApiKey } from '../config.js';
3
4
  import { generateDeploymentId, generateReceiptId, addDeployment, addReceipt } from '../store.js';
4
- import { createDeployment, callApi } from '../api.js';
5
+ import { createDeployment } from '../api.js';
5
6
 
6
- function printLivePlan(suggestions, gpu, chalk) {
7
- const { matches = [], alternatives = [] } = suggestions;
7
+ const OVERHEAD_PCT = 0.25;
8
8
 
9
- console.log(chalk.bold(' Live availability'));
9
+ function printRoutePlan(plan, chalk) {
10
+ const { lane1, lane2, cheapestRate, costWithOverhead, canonical } = plan;
11
+
12
+ console.log(chalk.bold(' Lane 1 — Own GPU Hosts'));
10
13
  console.log(` ${'─'.repeat(40)}`);
11
- if (matches.length === 0) {
12
- console.log(chalk.yellow(` No ${gpu} capacity found right now`));
13
- } else {
14
- const cheapest = matches[0];
15
- console.log(` ${chalk.cyan(gpu)} available — from ${chalk.green('$' + cheapest.price.toFixed(2) + '/hr')}`);
16
- console.log(chalk.dim(` ${matches.length} offer(s) found`));
17
- }
14
+ console.log(chalk.dim(` ${lane1.description}`));
18
15
  console.log();
19
16
 
20
- if (alternatives.length > 0) {
21
- console.log(chalk.bold(' Alternatives if unavailable'));
22
- console.log(` ${'─'.repeat(40)}`);
23
- alternatives.slice(0, 3).forEach((a, i) => {
24
- const diff = a.diff_desc ? chalk.dim(` — ${a.diff_desc}`) : '';
25
- console.log(` ${i + 1}. ${chalk.cyan(a.gpu)} in ${a.region} $${a.price.toFixed(2)}/hr${diff}`);
17
+ console.log(chalk.bold(' Lane 2 — Overflow Providers (cheapest-first)'));
18
+ console.log(` ${'─'.repeat(40)}`);
19
+ if (lane2.length === 0) {
20
+ console.log(chalk.yellow(` No pricing data for ${canonical}`));
21
+ } else {
22
+ lane2.forEach((p, i) => {
23
+ const arrow = i === 0 ? chalk.green(' ← primary') : '';
24
+ console.log(
25
+ ` ${String(i + 1)}. ${p.provider.padEnd(12)} ${canonical.padEnd(10)}` +
26
+ ` $${p.ratePerHour.toFixed(2)}/hr` +
27
+ ` reliability: ${Math.round(p.reliability * 100)}%${arrow}`
28
+ );
26
29
  });
27
30
  console.log();
31
+ console.log(` Estimated range: $${cheapestRate.toFixed(2)}–$${lane2[lane2.length - 1].ratePerHour.toFixed(2)}/hr`);
32
+ console.log(chalk.dim(` Estimated Badgr price: ~$${costWithOverhead.toFixed(2)}/hr`));
28
33
  }
34
+ console.log();
29
35
  console.log(chalk.dim(' Remove --dry-run to provision.'));
30
36
  console.log();
31
37
  }
@@ -39,27 +45,15 @@ export async function upCommand(config, args, chalk) {
39
45
  return;
40
46
  }
41
47
 
42
- // ── Dry-run: show live route plan from backend ───────────────────────────
48
+ // ── Dry-run: show route plan and exit ─────────────────────────────────────
43
49
  if (spec.dryRun) {
44
50
  console.log(chalk.bold('\n🔍 Dry Run — Route Plan\n'));
45
51
  console.log(chalk.bold(' Spec'));
46
52
  console.log(` ${'─'.repeat(40)}`);
47
53
  specLines(spec).forEach(l => console.log(` ${l}`));
48
54
  console.log();
49
-
50
- requireApiKey(config);
51
- try {
52
- const params = new URLSearchParams({ gpu: spec.gpu, max_price: String(spec.maxPrice ?? 10) });
53
- if (spec.region) params.set('region', spec.region);
54
- const suggestions = await callApi(`/capacity/suggestions?${params}`, {
55
- apiKey: config.apiKey,
56
- baseUrl: config.baseUrl,
57
- });
58
- printLivePlan(suggestions, spec.gpu, chalk);
59
- } catch (err) {
60
- console.log(chalk.yellow(` Could not fetch live availability: ${err.message}`));
61
- console.log(chalk.dim(' Tip: check `badgr capacity` for live data.\n'));
62
- }
55
+ const plan = getRoutePlan(spec.gpu, spec.count);
56
+ printRoutePlan(plan, chalk);
63
57
  return;
64
58
  }
65
59
 
package/src/config.js CHANGED
@@ -6,61 +6,16 @@ export const CONFIG_DIR = join(homedir(), '.badgr');
6
6
  export const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
7
 
8
8
  export const DEFAULTS = {
9
- baseUrl: 'https://aibadgr.com/v1',
9
+ baseUrl: 'https://api.badgr.ai/v1',
10
10
  defaultModel: 'meta-llama/Llama-3.1-8B-Instruct',
11
11
  };
12
12
 
13
- /** URLs that do not resolve or are superseded by aibadgr.com/v1 (nginx) */
14
- const LEGACY_BASE_URLS = new Set([
15
- 'https://api.badgr.ai/v1',
16
- 'https://api.badgr.ai',
17
- 'http://api.badgr.ai/v1',
18
- 'http://api.badgr.ai',
19
- 'https://api.aibadgr.com/v1',
20
- 'https://api.aibadgr.com',
21
- 'http://api.aibadgr.com/v1',
22
- 'http://api.aibadgr.com',
23
- ]);
24
-
25
- export function normalizeBaseUrl(url) {
26
- if (!url?.trim()) return DEFAULTS.baseUrl;
27
- const trimmed = url.trim().replace(/\/+$/, '');
28
- if (LEGACY_BASE_URLS.has(trimmed)) return DEFAULTS.baseUrl;
29
- return trimmed.endsWith('/v1') ? trimmed : `${trimmed}/v1`;
30
- }
31
-
32
- function applyEnvOverrides(config) {
33
- const envBase = process.env.BADGR_API_URL?.trim();
34
- if (envBase) config.baseUrl = normalizeBaseUrl(envBase);
35
- const envKey = process.env.BADGR_API_KEY?.trim();
36
- if (envKey) config.apiKey = envKey;
37
- return config;
38
- }
39
-
40
13
  export function loadConfig(configFile = CONFIG_FILE) {
41
- if (!existsSync(configFile)) {
42
- return applyEnvOverrides({ ...DEFAULTS });
43
- }
14
+ if (!existsSync(configFile)) return { ...DEFAULTS };
44
15
  try {
45
- const parsed = JSON.parse(readFileSync(configFile, 'utf8'));
46
- const previousBase = parsed.baseUrl;
47
- const config = applyEnvOverrides({
48
- ...DEFAULTS,
49
- ...parsed,
50
- baseUrl: normalizeBaseUrl(parsed.baseUrl ?? DEFAULTS.baseUrl),
51
- });
52
- if (
53
- configFile === CONFIG_FILE &&
54
- previousBase &&
55
- normalizeBaseUrl(previousBase) !== previousBase
56
- ) {
57
- const merged = { ...parsed, baseUrl: config.baseUrl };
58
- mkdirSync(dirname(configFile), { recursive: true });
59
- writeFileSync(configFile, JSON.stringify(merged, null, 2));
60
- }
61
- return config;
16
+ return { ...DEFAULTS, ...JSON.parse(readFileSync(configFile, 'utf8')) };
62
17
  } catch {
63
- return applyEnvOverrides({ ...DEFAULTS });
18
+ return { ...DEFAULTS };
64
19
  }
65
20
  }
66
21