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,12 +1,13 @@
1
1
  import { requireApiKey } from '../config.js';
2
- import { addDeployment, addReceipt, updateReceipt, generateReceiptId } from '../store.js';
3
- import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD } from '../fallback.js';
2
+ import { callApi } from '../api.js';
3
+ import { addDeployment, addReceipt, generateReceiptId } from '../store.js';
4
4
 
5
5
  /**
6
- * badgr serve meta-llama/Llama-3.1-8B-Instruct
7
6
  * badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
8
7
  *
9
- * 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>`.
10
11
  */
11
12
  export function parseServeArgs(args) {
12
13
  const flags = {};
@@ -16,56 +17,33 @@ export function parseServeArgs(args) {
16
17
  if (args[i] === '--gpu') { flags.gpu = args[++i]; i++; continue; }
17
18
  if (args[i] === '--count') { flags.count = parseInt(args[++i], 10); i++; continue; }
18
19
  if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
19
- if (args[i] === '--tier') { flags.tier = args[++i]; i++; continue; }
20
20
  if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
21
21
  if (args[i] === '--name') { flags.name = args[++i]; i++; continue; }
22
- if (args[i] === '--no-wait') { flags.noWait = true; i++; continue; }
23
- if (args[i] === '--max-cost') { flags.maxCost = parseFloat(args[++i]); i++; continue; }
24
- if (args[i] === '--strict-capacity') { flags.noMarketplaceFallback = true; i++; continue; }
25
- if (args[i] === '--no-expanded-search') { flags.noMarketplaceFallback = true; i++; continue; }
22
+ if (args[i] === '--no-wait') { flags.noWait = true; i++; continue; }
26
23
  positional.push(args[i++]);
27
24
  }
28
25
  const model = positional[0] || null;
29
26
  return { model, flags };
30
27
  }
31
28
 
32
- // Mirror of backend workload_profile.py infer_profile_from_model for pre-flight display.
33
- function _inferServeProfile(modelName) {
34
- const s = modelName.toLowerCase();
35
- const moe = s.match(/(\d+)x(\d+)b/);
36
- let paramsB;
37
- if (moe) {
38
- paramsB = parseInt(moe[1]) * parseInt(moe[2]);
39
- } else {
40
- const m = s.match(/(\d+)b/);
41
- paramsB = m ? parseInt(m[1]) : null;
42
- }
43
- if (paramsB === null) return { label: 'inference (7B–8B model)', vram: '24+ GB', gpus: ['RTX 4090', 'A6000', 'L40S'] };
44
- if (paramsB <= 9) return { label: 'inference (7B–8B model)', vram: '24+ GB', gpus: ['RTX 4090', 'A6000', 'L40S'] };
45
- if (paramsB <= 35) return { label: 'inference (30B–34B model)', vram: '40+ GB', gpus: ['A6000', 'L40S', 'A100'] };
46
- return { label: 'inference (70B+ model)', vram: '80+ GB', gpus: ['H100', 'A100'] };
47
- }
48
-
49
- function _serveStageLabel(elapsedSec) {
50
- if (elapsedSec < 45) return 'Starting vLLM…';
51
- if (elapsedSec < 150) return 'Downloading model…';
52
- return 'Waiting for /v1/models…';
53
- }
54
-
29
+ // Poll GET <endpointUrl>/models until it returns 200 or timeout expires.
30
+ // Returns true if healthy, false if timed out.
55
31
  async function waitForEndpoint(endpointUrl, timeoutMs = 5 * 60 * 1000, chalk) {
56
- const startMs = Date.now();
57
- const deadline = startMs + timeoutMs;
32
+ const deadline = Date.now() + timeoutMs;
33
+ const modelsUrl = `${endpointUrl}/models`;
34
+ let attempt = 0;
58
35
 
59
36
  while (Date.now() < deadline) {
37
+ attempt++;
60
38
  try {
61
- const res = await fetch(`${endpointUrl}/models`, { signal: AbortSignal.timeout(8000) });
62
- 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;
63
41
  } catch {
64
- // still starting
42
+ // network not up yet — keep polling
65
43
  }
66
- const elapsed = Math.round((Date.now() - startMs) / 1000);
44
+ const elapsed = Math.round((Date.now() - (deadline - timeoutMs)) / 1000);
67
45
  process.stdout.write(
68
- `\r ${chalk.dim(_serveStageLabel(elapsed) + ` (${elapsed}s)`)} `
46
+ `\r ${chalk.dim(`Waiting for endpoint… ${elapsed}s (attempt ${attempt})`)} `
69
47
  );
70
48
  await new Promise(r => setTimeout(r, 8000));
71
49
  }
@@ -77,81 +55,42 @@ export async function serveCommand(config, args, chalk) {
77
55
  const { model, flags } = parseServeArgs(args);
78
56
 
79
57
  if (!model) {
80
- console.error(chalk.red('Usage: badgr serve <model>'));
81
- 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'));
82
60
  return;
83
61
  }
84
62
 
85
63
  requireApiKey(config);
86
64
 
87
- // gpu=AUTO tells the backend to infer the right GPU from model size
88
- const gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : 'AUTO';
89
- const gpuLabel = gpu === 'AUTO' ? 'auto' : gpu;
65
+ const gpu = flags.gpu || 'L40S';
90
66
 
91
- const effectiveTier = normalizeTier(flags.tier);
92
-
93
- console.log(chalk.bold('\n⚡ Serving model\n'));
67
+ console.log(chalk.bold('\n🚀 Serving model\n'));
94
68
  console.log(` ${chalk.bold('Model:')} ${model}`);
95
- console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
96
-
97
- // Show inferred workload so the user knows what Badgr detected.
98
- if (gpu === 'AUTO') {
99
- const prof = _inferServeProfile(model);
100
- console.log();
101
- console.log(` ${chalk.bold('Estimated workload:')} ${prof.label}`);
102
- console.log(` ${chalk.bold('Estimated minimum VRAM:')} ${prof.vram}`);
103
- if (flags.maxCost) console.log(` ${chalk.bold('Max cost:')} $${flags.maxCost.toFixed(2)}`);
104
- }
69
+ console.log(` ${chalk.bold('GPU:')} ${gpu}`);
105
70
  console.log();
106
- process.stdout.write(chalk.dim(' Finding suitable capacity...\n'));
107
-
108
- const effectiveRegion = flags.region ? flags.region.toUpperCase() : undefined;
109
-
110
- function buildBody(gpuOverride, tierOverride) {
111
- return {
112
- model,
113
- gpu: gpuOverride || gpu,
114
- gpu_count: flags.count || 1,
115
- ...(effectiveRegion ? { region: effectiveRegion } : {}),
116
- max_price_per_hour: flags.maxPrice,
117
- name: flags.name,
118
- tier: tierOverride || effectiveTier,
119
- };
120
- }
71
+ console.log(chalk.dim(' Finding best available GPU capacity...'));
121
72
 
122
73
  let dep;
123
74
  try {
124
- dep = await callWithFallback(
125
- '/serve',
126
- { apiKey: config.apiKey, baseUrl: config.baseUrl },
127
- (tierOverride) => buildBody(undefined, tierOverride),
128
- effectiveTier,
129
- chalk,
130
- { thing: 'endpoint', cmd: 'badgr serve' },
131
- );
132
- } catch (err) {
133
- const failRcptId = generateReceiptId();
134
- addReceipt({
135
- receiptId: failRcptId,
136
- action: 'badgr serve',
137
- model,
138
- gpu: gpuLabel,
139
- status: 'failed',
140
- failureType: 'infrastructure',
141
- createdAt: new Date().toISOString(),
75
+ dep = await callApi('/serve', {
76
+ method: 'POST',
77
+ apiKey: config.apiKey,
78
+ baseUrl: config.baseUrl,
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
+ },
142
87
  });
143
- if (err.isPaymentRequired) {
144
- console.error(chalk.yellow(err.message));
145
- const rerun = `badgr serve ${args.join(' ')}`;
146
- console.error(chalk.dim(`After payment, rerun:\n ${rerun}\n`));
147
- return;
148
- }
149
- console.error(chalk.red(`\n ✗ Could not start endpoint: ${err.message}`));
150
- console.error(chalk.dim(`\n Receipt: ${failRcptId}`));
151
- console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
88
+ } catch (err) {
89
+ console.error(chalk.red(`\n ✗ Serve failed: ${err.message}\n`));
152
90
  return;
153
91
  }
154
92
 
93
+ // Mirror to local store so badgr down/status/receipts work offline
155
94
  addDeployment({
156
95
  id: dep.deployment_id,
157
96
  name: dep.name,
@@ -159,13 +98,12 @@ export async function serveCommand(config, args, chalk) {
159
98
  model: dep.model || model,
160
99
  gpu: dep.gpu_type,
161
100
  count: dep.gpu_count,
101
+ provider: dep.provider,
162
102
  status: dep.status,
163
103
  endpointUrl: dep.endpoint_url || dep.openai_base_url,
164
104
  receiptId: dep.receipt_id,
165
105
  createdAt: new Date().toISOString(),
166
106
  costPerHour: dep.cost_per_hour || 0,
167
- providerRoute: dep.provider ?? null,
168
- tier: dep.tier ?? null,
169
107
  });
170
108
 
171
109
  const rcptId = dep.receipt_id || generateReceiptId();
@@ -173,59 +111,49 @@ export async function serveCommand(config, args, chalk) {
173
111
  receiptId: rcptId,
174
112
  action: 'badgr serve',
175
113
  deploymentId: dep.deployment_id,
114
+ provider: dep.provider,
176
115
  gpu: dep.gpu_type,
177
- providerRoute: dep.provider ?? null,
178
- tier: dep.tier ?? null,
179
116
  status: dep.status,
180
117
  createdAt: new Date().toISOString(),
181
118
  });
182
119
 
183
120
  const endpointUrl = dep.endpoint_url || dep.openai_base_url || config.baseUrl;
184
121
 
185
- // ── Health check ──────────────────────────────────────────────────────────
122
+ // ── Health check before declaring "ready" ─────────────────────────────────
186
123
  let endpointReady = false;
124
+
187
125
  if (flags.noWait) {
188
- 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'));
189
127
  } else {
128
+ console.log(chalk.dim('\n Health-checking endpoint (up to 5 min)...'));
190
129
  endpointReady = await waitForEndpoint(endpointUrl, 5 * 60 * 1000, chalk);
191
130
  process.stdout.write('\n');
192
- if (!endpointReady) updateReceipt(rcptId, { status: 'health_check_timeout' });
193
131
  }
194
132
 
195
- // ── Result ────────────────────────────────────────────────────────────────
133
+ // ── Print result ──────────────────────────────────────────────────────────
196
134
  if (endpointReady) {
197
- console.log(chalk.green('\n ✓ Endpoint ready\n'));
135
+ console.log(chalk.green('✓ Endpoint ready\n'));
198
136
  } else {
199
- console.log(chalk.yellow('\n ⏳ Endpoint still starting — model download may still be in progress.\n'));
200
- console.log(` ${chalk.bold('Stop billing now:')} ${chalk.dim(`badgr down ${dep.deployment_id}`)}`);
201
- 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`));
202
141
  console.log();
203
142
  }
204
143
 
205
- const serveRate = dep.cost_per_hour || 0;
206
-
207
- console.log(` ${chalk.bold('Base URL:')} ${chalk.cyan(endpointUrl)}`);
208
- console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
209
- console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
210
- if (serveRate > 0) console.log(` ${chalk.bold('Rate:')} $${serveRate.toFixed(2)}/hr`);
211
- if (flags.maxCost) console.log(` ${chalk.bold('Max cost:')} $${flags.maxCost.toFixed(2)}`);
212
- console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
213
- console.log(` ${chalk.bold('Logs:')} ${chalk.dim(`badgr logs ${dep.deployment_id}`)}`);
214
- console.log(` ${chalk.bold('Stop billing:')} ${chalk.cyan(`badgr down ${dep.deployment_id}`)}`);
215
- console.log();
216
-
217
- if (serveRate > HIGH_RATE_THRESHOLD && !flags.maxCost) {
218
- console.log(chalk.yellow(` Selected capacity rate: $${serveRate.toFixed(2)}/hr`));
219
- console.log(chalk.dim(' Tip: use --max-cost to enforce a hard ceiling.\n'));
220
- }
221
- console.log(chalk.dim(' Billing continues until you run: ') + chalk.cyan(`badgr down ${dep.deployment_id}`));
144
+ console.log(` ${chalk.bold('Deployment:')} ${chalk.cyan(dep.deployment_id)}`);
145
+ console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
146
+ console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
147
+ console.log(` ${chalk.bold('Endpoint:')} ${chalk.cyan(endpointUrl)}`);
148
+ if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
149
+ console.log(`\n ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
222
150
 
223
151
  if (endpointReady) {
224
- const keySnip = config.apiKey?.slice(0, 8) || 'sk-...';
225
- console.log(` ${chalk.bold('Use with OpenAI SDK:')}`);
152
+ console.log(`\n ${chalk.bold('Use with OpenAI SDK:')}`);
226
153
  console.log(chalk.dim(` from openai import OpenAI`));
227
- 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}")`));
228
155
  console.log(chalk.dim(` resp = client.chat.completions.create(model="${dep.model || model}", messages=[...])`));
229
- console.log();
230
156
  }
157
+
158
+ console.log(`\n ${chalk.dim(`Stop billing: badgr down ${dep.deployment_id}`)}\n`);
231
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