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.
- package/HOW_IT_WORKS.md +4 -4
- package/package.json +4 -12
- package/src/api.js +10 -87
- package/src/badgr.js +37 -67
- package/src/commands/down.js +23 -26
- package/src/commands/login.js +7 -23
- package/src/commands/logs.js +5 -57
- package/src/commands/models.js +6 -25
- package/src/commands/receipts.js +4 -19
- package/src/commands/run.js +84 -489
- package/src/commands/serve.js +51 -146
- package/src/commands/status.js +48 -35
- package/src/commands/up.js +26 -32
- package/src/config.js +4 -49
- package/src/router.js +73 -16
- package/src/store.js +1 -10
- package/tests/commands.test.js +2 -183
- package/tests/config.test.js +1 -24
- package/tests/router.test.js +68 -9
- package/tests/store.test.js +1 -41
- package/src/commands/billing.js +0 -93
- package/src/commands/capacity.js +0 -111
- package/src/commands/test-run.js +0 -240
- package/src/fallback.js +0 -95
package/src/commands/serve.js
CHANGED
|
@@ -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,
|
|
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
|
-
*
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
43
|
-
const
|
|
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(
|
|
48
|
-
if (res.ok)
|
|
39
|
+
const res = await fetch(modelsUrl, { signal: AbortSignal.timeout(8000) });
|
|
40
|
+
if (res.ok) return true;
|
|
49
41
|
} catch {
|
|
50
|
-
//
|
|
42
|
+
// network not up yet — keep polling
|
|
51
43
|
}
|
|
52
|
-
const elapsed = Math.round((Date.now() -
|
|
44
|
+
const elapsed = Math.round((Date.now() - (deadline - timeoutMs)) / 1000);
|
|
53
45
|
process.stdout.write(
|
|
54
|
-
`\r ${chalk.dim(
|
|
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
|
-
|
|
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
|
-
|
|
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:')} ${
|
|
85
|
-
if (effectiveTier === '2') console.log(` ${chalk.dim('(tier 2 — marketplace routing)')}`);
|
|
69
|
+
console.log(` ${chalk.bold('GPU:')} ${gpu}`);
|
|
86
70
|
console.log();
|
|
87
|
-
|
|
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:
|
|
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
|
-
|
|
113
|
-
|
|
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
|
|
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
|
-
// ──
|
|
133
|
+
// ── Print result ──────────────────────────────────────────────────────────
|
|
228
134
|
if (endpointReady) {
|
|
229
|
-
console.log(chalk.green('
|
|
135
|
+
console.log(chalk.green('✓ Endpoint ready\n'));
|
|
230
136
|
} else {
|
|
231
|
-
console.log(chalk.yellow('
|
|
232
|
-
console.log(
|
|
233
|
-
console.log(
|
|
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('
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
console.log(
|
|
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
|
}
|
package/src/commands/up.js
CHANGED
|
@@ -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
|
|
5
|
+
import { createDeployment } from '../api.js';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
const { matches = [], alternatives = [] } = suggestions;
|
|
7
|
+
const OVERHEAD_PCT = 0.25;
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
-
|
|
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://
|
|
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
|
-
|
|
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
|
|
18
|
+
return { ...DEFAULTS };
|
|
64
19
|
}
|
|
65
20
|
}
|
|
66
21
|
|