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/HOW_IT_WORKS.md
CHANGED
|
@@ -11,7 +11,7 @@ npm install -g badgr-cli
|
|
|
11
11
|
badgr login
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
`badgr login` prompts for your API key and base URL, then writes them to `~/.
|
|
14
|
+
`badgr login` prompts for your API key and base URL, then writes them to `~/.gpu/config.json`. Every subsequent command reads that file — no env vars required.
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -142,7 +142,7 @@ badgr receipts [n] # default: last 10
|
|
|
142
142
|
|
|
143
143
|
Shows two sets of receipts:
|
|
144
144
|
|
|
145
|
-
1. **CLI action receipts** — every `badgr serve` / `badgr down` recorded locally in `~/.
|
|
145
|
+
1. **CLI action receipts** — every `badgr serve` / `badgr down` recorded locally in `~/.gpu/deployments.json`, with provider, retries, latency, and cost.
|
|
146
146
|
2. **Inference receipts** — per-request records fetched from `GET /v1/receipts` on the API (requires `badgr login`).
|
|
147
147
|
|
|
148
148
|
Every action — including failures — generates a receipt. Receipt IDs are printed on every command output so you can look them up later.
|
|
@@ -202,10 +202,10 @@ GPU type aliases are normalized automatically: `rtx-4090`, `rtx4090`, `4090`, `R
|
|
|
202
202
|
|
|
203
203
|
## Local State
|
|
204
204
|
|
|
205
|
-
All CLI state lives in `~/.
|
|
205
|
+
All CLI state lives in `~/.gpu/`:
|
|
206
206
|
|
|
207
207
|
```
|
|
208
|
-
~/.
|
|
208
|
+
~/.gpu/
|
|
209
209
|
config.json API key + base URL
|
|
210
210
|
deployments.json Active deployments + receipt log (last 200)
|
|
211
211
|
```
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "badgr-cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Badgr
|
|
3
|
+
"version": "1.0.31",
|
|
4
|
+
"description": "Badgr — run or serve GPU workloads from one command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"badgr": "src/badgr.js"
|
|
7
|
+
"badgr": "./src/badgr.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/badgr.js",
|
|
@@ -21,14 +21,6 @@
|
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=18.0.0"
|
|
23
23
|
},
|
|
24
|
-
"keywords": [
|
|
25
|
-
"gpu",
|
|
26
|
-
"cli",
|
|
27
|
-
"ai",
|
|
28
|
-
"compute",
|
|
29
|
-
"modal",
|
|
30
|
-
"gateway",
|
|
31
|
-
"openai"
|
|
32
|
-
],
|
|
24
|
+
"keywords": ["gpu", "cli", "ai", "compute", "modal", "gateway", "openai"],
|
|
33
25
|
"license": "MIT"
|
|
34
26
|
}
|
package/src/api.js
CHANGED
|
@@ -1,93 +1,16 @@
|
|
|
1
|
-
const DEBUG = process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true';
|
|
2
|
-
|
|
3
|
-
function dbg(...args) {
|
|
4
|
-
if (DEBUG) console.error('[badgr:debug]', ...args);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
1
|
export async function callApi(path, { method = 'GET', apiKey, baseUrl, body } = {}) {
|
|
8
2
|
const url = `${baseUrl}${path}`;
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
res = await fetch(url, {
|
|
19
|
-
method,
|
|
20
|
-
headers: {
|
|
21
|
-
'Content-Type': 'application/json',
|
|
22
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
23
|
-
},
|
|
24
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
25
|
-
});
|
|
26
|
-
} catch (cause) {
|
|
27
|
-
const elapsed = Date.now() - startMs;
|
|
28
|
-
dbg(`Fetch threw after ${elapsed}ms:`, cause);
|
|
29
|
-
// Network-level failure: DNS, connection refused, timeout, etc.
|
|
30
|
-
const msg = cause?.message ?? String(cause);
|
|
31
|
-
const code = cause?.cause?.code ?? cause?.code ?? '';
|
|
32
|
-
const hint =
|
|
33
|
-
(code === 'ECONNREFUSED' || msg.includes('ECONNREFUSED'))
|
|
34
|
-
? `\n Hint: Connection refused — is the server running at ${baseUrl}?` :
|
|
35
|
-
(code === 'ENOTFOUND' || msg.includes('ENOTFOUND'))
|
|
36
|
-
? `\n Hint: DNS lookup failed for ${baseUrl}\n Check your internet or set BADGR_API_URL to the correct host` :
|
|
37
|
-
(code === 'ETIMEDOUT' || msg.includes('ETIMEDOUT'))
|
|
38
|
-
? `\n Hint: Request timed out — server may be overloaded` :
|
|
39
|
-
(msg.includes('fetch failed') || msg === 'fetch failed')
|
|
40
|
-
? `\n Hint: Network error reaching ${url}\n • Check internet connection\n • Run: badgr config (verify baseUrl)\n • Try: BADGR_DEBUG=1 badgr run … for full details\n • Test: curl -v ${baseUrl}/models` :
|
|
41
|
-
`\n Hint: Check your network and that BADGR_API_URL is correct (${baseUrl})`;
|
|
42
|
-
throw new Error(`Cannot reach ${url} (${msg})${hint}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const elapsed = Date.now() - startMs;
|
|
46
|
-
dbg(`Response: HTTP ${res.status} in ${elapsed}ms`);
|
|
47
|
-
|
|
3
|
+
const res = await fetch(url, {
|
|
4
|
+
method,
|
|
5
|
+
headers: {
|
|
6
|
+
'Content-Type': 'application/json',
|
|
7
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
8
|
+
},
|
|
9
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
10
|
+
});
|
|
48
11
|
if (!res.ok) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
let errorData = null;
|
|
52
|
-
try {
|
|
53
|
-
rawBody = await res.text();
|
|
54
|
-
dbg('Error response body:', rawBody);
|
|
55
|
-
const json = JSON.parse(rawBody);
|
|
56
|
-
errorData = json;
|
|
57
|
-
detail = json?.detail ?? json?.message ?? json?.error ?? rawBody;
|
|
58
|
-
} catch {
|
|
59
|
-
detail = rawBody || res.statusText || '';
|
|
60
|
-
}
|
|
61
|
-
const isCapacityError = errorData?.code === 'NO_CAPACITY_MATCH';
|
|
62
|
-
if (res.status === 402) {
|
|
63
|
-
// Payment required — format a clear, actionable error
|
|
64
|
-
const d = errorData?.detail ?? errorData ?? {};
|
|
65
|
-
const detailObj = typeof d === 'object' ? d : {};
|
|
66
|
-
const balanceUsd = typeof detailObj.balance_usd === 'number' ? detailObj.balance_usd : null;
|
|
67
|
-
const requiredUsd = typeof detailObj.required_usd === 'number' ? detailObj.required_usd : null;
|
|
68
|
-
const topupUrl = detailObj.topup_url || 'https://aibadgr.com/dashboard#billing';
|
|
69
|
-
|
|
70
|
-
let msg = '\nPayment required.\n';
|
|
71
|
-
if (balanceUsd !== null) msg += `Your balance is $${balanceUsd.toFixed(2)}.`;
|
|
72
|
-
if (requiredUsd !== null) msg += ` This job needs a $${requiredUsd.toFixed(2)} reserve.`;
|
|
73
|
-
msg += '\n\nAdd balance:\n ' + topupUrl + '\n';
|
|
74
|
-
const err = new Error(msg);
|
|
75
|
-
err.errorData = errorData;
|
|
76
|
-
err.httpStatus = 402;
|
|
77
|
-
err.isPaymentRequired = true;
|
|
78
|
-
throw err;
|
|
79
|
-
}
|
|
80
|
-
const hint =
|
|
81
|
-
res.status === 401 ? '\n Hint: Invalid or missing API key — run: badgr login' :
|
|
82
|
-
res.status === 403 ? '\n Hint: Access denied — check your API key permissions' :
|
|
83
|
-
res.status === 404 ? `\n Hint: Endpoint not found — check BADGR_API_URL (currently: ${baseUrl})` :
|
|
84
|
-
(res.status === 502 || res.status === 503) && !isCapacityError
|
|
85
|
-
? '\n Hint: Server error — no GPU capacity available or backend is down' :
|
|
86
|
-
'';
|
|
87
|
-
const err = new Error(`${method} ${path} → HTTP ${res.status}: ${detail}${hint}`);
|
|
88
|
-
err.errorData = errorData;
|
|
89
|
-
err.httpStatus = res.status;
|
|
90
|
-
throw err;
|
|
12
|
+
const text = await res.text().catch(() => '');
|
|
13
|
+
throw new Error(`${method} ${path} → ${res.status}: ${text}`);
|
|
91
14
|
}
|
|
92
15
|
return res.json();
|
|
93
16
|
}
|
package/src/badgr.js
CHANGED
|
@@ -10,80 +10,53 @@ import { receiptsCommand } from './commands/receipts.js';
|
|
|
10
10
|
import { runCommand } from './commands/run.js';
|
|
11
11
|
import { serveCommand } from './commands/serve.js';
|
|
12
12
|
import { modelsCommand } from './commands/models.js';
|
|
13
|
-
import { capacityCommand } from './commands/capacity.js';
|
|
14
|
-
import { testCommand } from './commands/test-run.js';
|
|
15
|
-
import { billingCommand } from './commands/billing.js';
|
|
16
13
|
|
|
17
14
|
const HELP = `
|
|
18
15
|
${chalk.bold('badgr')} — run or serve GPU workloads from one command
|
|
19
16
|
|
|
20
|
-
${chalk.bold('COMMANDS')}
|
|
21
|
-
${chalk.cyan('badgr login')}
|
|
22
|
-
${chalk.cyan('badgr run <
|
|
23
|
-
${chalk.cyan('badgr serve <model>')}
|
|
24
|
-
${chalk.cyan('badgr status')}
|
|
25
|
-
${chalk.cyan('badgr logs <id>')}
|
|
26
|
-
${chalk.cyan('badgr down <id>')}
|
|
27
|
-
${chalk.cyan('badgr receipts')}
|
|
28
|
-
${chalk.cyan('badgr test')} Run an end-to-end test (provision → run → teardown)
|
|
29
|
-
${chalk.cyan('badgr capacity')} Check what GPU capacity is available right now
|
|
30
|
-
${chalk.cyan('badgr billing')} Show balance and add funds
|
|
17
|
+
${chalk.bold('CORE COMMANDS')}
|
|
18
|
+
${chalk.cyan('badgr login')} Authenticate (save API key to ~/.badgr/config.json)
|
|
19
|
+
${chalk.cyan('badgr run <cmd...> --gpu <type>')} Run a one-off GPU job
|
|
20
|
+
${chalk.cyan('badgr serve <model> --gpu <type>')} Serve a model (OpenAI-compatible endpoint)
|
|
21
|
+
${chalk.cyan('badgr status')} Show active deployments + endpoint URLs
|
|
22
|
+
${chalk.cyan('badgr logs <id>')} Stream logs for a deployment
|
|
23
|
+
${chalk.cyan('badgr down <id>')} Terminate a deployment (stop billing)
|
|
24
|
+
${chalk.cyan('badgr receipts [<id>|<n>]')} Show receipts — pass ID for single, number for list
|
|
31
25
|
|
|
32
|
-
${chalk.bold('
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
26
|
+
${chalk.bold('badgr run OPTIONS')}
|
|
27
|
+
--gpu <type> GPU: RTX_4090, A100, L40S, H100 (default: RTX_4090)
|
|
28
|
+
--image <image> Docker image (default: python:3.11-slim)
|
|
29
|
+
--count <n> GPU count (default: 1)
|
|
30
|
+
--region US|EU|AU Region preference (default: US)
|
|
31
|
+
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
32
|
+
--name <name> Job name (auto-generated if omitted)
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
${chalk.bold('badgr serve OPTIONS')}
|
|
35
|
+
--gpu <type> GPU: L40S, A100, H100, RTX_4090 (default: L40S)
|
|
36
|
+
--count <n> GPU count (default: 1)
|
|
37
|
+
--region US|EU|AU Region preference (default: US)
|
|
38
|
+
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
39
|
+
--name <name> Deployment name (auto-generated if omitted)
|
|
43
40
|
|
|
44
|
-
|
|
41
|
+
${chalk.bold('EXAMPLES')}
|
|
42
|
+
badgr login
|
|
45
43
|
badgr run python train.py --gpu A100
|
|
44
|
+
badgr run --image my/image:latest --gpu L40S
|
|
46
45
|
badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
|
|
47
|
-
|
|
48
|
-
${chalk.dim('# Add safety caps:')}
|
|
49
|
-
badgr run python train.py --max-runtime 60 --max-cost 5
|
|
50
|
-
|
|
51
|
-
${chalk.dim('# Manage a running deployment:')}
|
|
46
|
+
badgr serve mistralai/Mistral-7B-v0.1 --gpu RTX_4090
|
|
52
47
|
badgr status
|
|
53
|
-
badgr logs
|
|
54
|
-
badgr down
|
|
55
|
-
badgr receipts
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
--max-runtime <min> Auto-stop after N minutes (recommended)
|
|
66
|
-
--max-cost <$> Auto-stop when spend reaches this amount
|
|
67
|
-
--detach Return immediately, don't stream logs
|
|
68
|
-
|
|
69
|
-
${chalk.bold('badgr serve OPTIONS')}
|
|
70
|
-
--gpu <type> GPU type (default: auto — inferred from model size)
|
|
71
|
-
--tier 1 Managed provider routing (default)
|
|
72
|
-
--tier 2 Marketplace provider routing, lower-cost options
|
|
73
|
-
--count <n> Number of GPUs (default: 1)
|
|
74
|
-
--region US|EU|AU Region preference
|
|
75
|
-
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
76
|
-
--no-wait Skip endpoint health check
|
|
77
|
-
|
|
78
|
-
${chalk.bold('AFTER SERVING')}
|
|
79
|
-
${chalk.dim('Point any OpenAI client at the returned URL:')}
|
|
80
|
-
${chalk.dim(' from openai import OpenAI')}
|
|
81
|
-
${chalk.dim(' client = OpenAI(base_url="<endpoint url>", api_key="<your key>")')}
|
|
82
|
-
${chalk.dim(' client.chat.completions.create(model="<model>", messages=[...])')}
|
|
83
|
-
|
|
84
|
-
${chalk.bold('DEBUG')}
|
|
85
|
-
${chalk.dim('BADGR_DEBUG=1 badgr run python train.py # full request/response trace')}
|
|
86
|
-
${chalk.dim('badgr config # show current API config')}
|
|
48
|
+
badgr logs dep_abc123
|
|
49
|
+
badgr down dep_abc123
|
|
50
|
+
badgr receipts
|
|
51
|
+
badgr receipts dep_abc123
|
|
52
|
+
|
|
53
|
+
${chalk.bold('OPENAI-COMPATIBLE SERVING')}
|
|
54
|
+
${chalk.dim('After `badgr serve`, point any OpenAI client at the returned URL:')}
|
|
55
|
+
${chalk.dim(' client = OpenAI(api_key="sk-...", base_url="https://api.badgr.ai/v1")')}
|
|
56
|
+
${chalk.dim(' client.chat.completions.create(model="dep_xxx", messages=[...])')}
|
|
57
|
+
|
|
58
|
+
${chalk.bold('ROUTING')}
|
|
59
|
+
${chalk.dim('Badgr automatically selects best available GPU capacity for your request.')}
|
|
87
60
|
`;
|
|
88
61
|
|
|
89
62
|
async function main() {
|
|
@@ -104,9 +77,6 @@ async function main() {
|
|
|
104
77
|
case 'down': return downCommand(config, rest, chalk);
|
|
105
78
|
case 'receipts': return receiptsCommand(config, rest, chalk);
|
|
106
79
|
case 'models': return modelsCommand(config, chalk);
|
|
107
|
-
case 'capacity': return capacityCommand(config, rest, chalk);
|
|
108
|
-
case 'test': return testCommand(config, rest, chalk);
|
|
109
|
-
case 'billing': return billingCommand(config, rest, chalk);
|
|
110
80
|
// legacy aliases kept for compatibility
|
|
111
81
|
case 'up': return upCommand(config, rest, chalk);
|
|
112
82
|
case 'config': {
|
package/src/commands/down.js
CHANGED
|
@@ -12,46 +12,43 @@ export async function downCommand(config, args, chalk) {
|
|
|
12
12
|
|
|
13
13
|
requireApiKey(config);
|
|
14
14
|
|
|
15
|
-
const localDep
|
|
15
|
+
const localDep = findDeployment(idOrName);
|
|
16
|
+
|
|
17
|
+
// Resolve the deployment ID to send to the backend
|
|
16
18
|
const deploymentId = localDep?.id ?? idOrName;
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
console.log(chalk.dim(` Terminating ${deploymentId}...`));
|
|
19
21
|
|
|
20
22
|
let dep;
|
|
21
23
|
try {
|
|
22
24
|
dep = await terminateDeployment(config, deploymentId);
|
|
23
25
|
} catch (err) {
|
|
24
|
-
|
|
25
|
-
console.error(chalk.red(`\n ✗ Could not stop deployment: ${err.message}\n`));
|
|
26
|
+
console.error(chalk.red(`\n ✗ Terminate failed: ${err.message}\n`));
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
process.stdout.write('\n');
|
|
30
|
-
|
|
31
|
-
// Compute final cost from the deployment timestamps
|
|
32
|
-
const stoppedAt = dep.stopped_at ?? (Date.now() / 1000);
|
|
33
|
-
const startedAt = dep.started_at ?? stoppedAt;
|
|
34
|
-
const runtimeMin = Math.round((stoppedAt - startedAt) / 60);
|
|
35
|
-
const runtimeHr = (stoppedAt - startedAt) / 3600;
|
|
36
|
-
const finalCost = (dep.cost_per_hour || 0) * runtimeHr;
|
|
37
|
-
|
|
38
30
|
const rcptId = generateReceiptId();
|
|
39
31
|
addReceipt({
|
|
40
|
-
receiptId:
|
|
41
|
-
action:
|
|
42
|
-
deploymentId:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
receiptId: rcptId,
|
|
33
|
+
action: 'badgr down',
|
|
34
|
+
deploymentId: dep.deployment_id,
|
|
35
|
+
name: dep.name,
|
|
36
|
+
provider: dep.provider,
|
|
37
|
+
gpu: dep.gpu_type,
|
|
38
|
+
latencyMs: 0,
|
|
39
|
+
status: 'terminated',
|
|
40
|
+
createdAt: new Date().toISOString(),
|
|
48
41
|
});
|
|
49
42
|
|
|
43
|
+
// Remove from local store
|
|
50
44
|
removeDeployment(idOrName);
|
|
51
45
|
|
|
52
|
-
console.log(chalk.green(
|
|
53
|
-
console.log(chalk.
|
|
54
|
-
console.log(` ${chalk.bold('
|
|
55
|
-
if (
|
|
56
|
-
|
|
46
|
+
console.log(chalk.green(`\n✓ Deployment stopped: ${dep.name}\n`));
|
|
47
|
+
console.log(` ${chalk.bold('ID:')} ${dep.deployment_id}`);
|
|
48
|
+
console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
|
|
49
|
+
if (dep.started_at) {
|
|
50
|
+
const uptime = Math.round((Date.now() / 1000 - dep.started_at) / 60);
|
|
51
|
+
console.log(` ${chalk.bold('Uptime:')} ${uptime} min`);
|
|
52
|
+
}
|
|
53
|
+
console.log(`\n ${chalk.bold('Receipt ID:')} ${chalk.dim(rcptId)}\n`);
|
|
57
54
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -1,36 +1,20 @@
|
|
|
1
1
|
import { input } from '@inquirer/prompts';
|
|
2
|
-
import { DEFAULTS } from '../config.js';
|
|
3
|
-
import { callApi } from '../api.js';
|
|
4
2
|
|
|
5
3
|
export async function loginCommand(chalk, saveConfigFn) {
|
|
6
|
-
console.log(chalk.bold('\
|
|
4
|
+
console.log(chalk.bold('\n🔑 Badgr Login\n'));
|
|
7
5
|
|
|
8
6
|
const apiKey = await input({
|
|
9
7
|
message: 'Enter your Badgr API key:',
|
|
10
8
|
validate: v => v.trim() ? true : 'API key is required',
|
|
11
9
|
});
|
|
12
10
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
const baseUrl = await input({
|
|
12
|
+
message: 'API base URL:',
|
|
13
|
+
default: 'https://api.badgr.ai/v1',
|
|
16
14
|
});
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
console.log(chalk.
|
|
20
|
-
|
|
21
|
-
// Verify the key works against the live API
|
|
22
|
-
try {
|
|
23
|
-
await callApi('/models', { apiKey: config.apiKey, baseUrl: config.baseUrl });
|
|
24
|
-
console.log(chalk.green('✓ API reachable'));
|
|
25
|
-
console.log(chalk.green('✓ API key valid\n'));
|
|
26
|
-
} catch (err) {
|
|
27
|
-
if (err.httpStatus === 401 || err.httpStatus === 403) {
|
|
28
|
-
console.log(chalk.yellow('⚠ API key may be invalid — double-check and run badgr login again\n'));
|
|
29
|
-
} else {
|
|
30
|
-
console.log(chalk.yellow('⚠ Could not reach the API right now — check your internet connection\n'));
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
console.log(chalk.dim(` Run ${chalk.cyan('badgr run python train.py')} to launch your first job.\n`));
|
|
16
|
+
const config = saveConfigFn({ apiKey: apiKey.trim(), baseUrl: baseUrl.trim() });
|
|
17
|
+
console.log(chalk.green('\n✓ Logged in — config saved to ~/.badgr/config.json\n'));
|
|
18
|
+
console.log(chalk.dim(` Base URL: ${config.baseUrl}\n`));
|
|
35
19
|
return config;
|
|
36
20
|
}
|
package/src/commands/logs.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { findDeployment, listDeployments } from '../store.js';
|
|
2
2
|
import { requireApiKey } from '../config.js';
|
|
3
|
-
import { getDeploymentLogs
|
|
4
|
-
|
|
5
|
-
const TERMINAL_STATUSES = new Set(['succeeded', 'completed', 'failed', 'stopped', 'terminated']);
|
|
6
|
-
const FOLLOW_POLL_MS = 3000;
|
|
7
|
-
|
|
8
|
-
// Lines that carry structured metadata shown elsewhere (status bar in `badgr run`).
|
|
9
|
-
const LOG_META_RE = /^\[dep-[^\]]+\] (status|gpu|region|endpoint|cost|receipt|provider_status|uptime)=/;
|
|
3
|
+
import { getDeploymentLogs } from '../api.js';
|
|
10
4
|
|
|
11
5
|
export async function logsCommand(config, args, chalk) {
|
|
12
6
|
const idOrName = args.find(a => !a.startsWith('--'));
|
|
@@ -32,18 +26,13 @@ export async function logsCommand(config, args, chalk) {
|
|
|
32
26
|
|
|
33
27
|
console.log(chalk.bold(`\n📋 Logs: ${localDep?.name ?? deploymentId}\n`));
|
|
34
28
|
|
|
35
|
-
// Fetch and print initial batch of logs.
|
|
36
|
-
const seen = new Set();
|
|
37
29
|
try {
|
|
38
30
|
const data = await getDeploymentLogs(config, deploymentId);
|
|
39
31
|
const lines = data?.logs ?? [];
|
|
40
|
-
if (lines.length === 0
|
|
32
|
+
if (lines.length === 0) {
|
|
41
33
|
console.log(chalk.dim(' No log lines available yet.\n'));
|
|
42
34
|
} else {
|
|
43
|
-
|
|
44
|
-
seen.add(line);
|
|
45
|
-
if (!LOG_META_RE.test(line)) console.log(` ${chalk.dim(line)}`);
|
|
46
|
-
}
|
|
35
|
+
lines.forEach(l => console.log(` ${chalk.dim(l)}`));
|
|
47
36
|
}
|
|
48
37
|
} catch (err) {
|
|
49
38
|
console.log(chalk.yellow(` Could not fetch logs: ${err.message}`));
|
|
@@ -51,51 +40,10 @@ export async function logsCommand(config, args, chalk) {
|
|
|
51
40
|
console.log(chalk.dim(`\n GPU: ${localDep.gpu} Type: ${localDep.type}`));
|
|
52
41
|
console.log(chalk.dim(` Endpoint: ${localDep.endpointUrl}`));
|
|
53
42
|
}
|
|
54
|
-
if (!follow) { console.log(); return; }
|
|
55
43
|
}
|
|
56
44
|
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
// --follow: poll until the deployment reaches a terminal state.
|
|
60
|
-
console.log(chalk.dim(' (following — Ctrl+C to stop)\n'));
|
|
61
|
-
|
|
62
|
-
let stopping = false;
|
|
63
|
-
process.once('SIGINT', () => { stopping = true; });
|
|
64
|
-
|
|
65
|
-
while (!stopping) {
|
|
66
|
-
await new Promise(r => setTimeout(r, FOLLOW_POLL_MS));
|
|
67
|
-
if (stopping) break;
|
|
68
|
-
|
|
69
|
-
let status = null;
|
|
70
|
-
try {
|
|
71
|
-
const dep = await callApi(`/deployments/${deploymentId}`, {
|
|
72
|
-
apiKey: config.apiKey,
|
|
73
|
-
baseUrl: config.baseUrl,
|
|
74
|
-
});
|
|
75
|
-
status = dep?.status ?? null;
|
|
76
|
-
} catch {
|
|
77
|
-
// network blip — keep following
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
const data = await getDeploymentLogs(config, deploymentId);
|
|
82
|
-
for (const line of (data?.logs ?? [])) {
|
|
83
|
-
if (seen.has(line)) continue;
|
|
84
|
-
seen.add(line);
|
|
85
|
-
if (!LOG_META_RE.test(line)) {
|
|
86
|
-
const isErr = /^error\b/i.test(line) || /Error response from daemon/i.test(line);
|
|
87
|
-
console.log(` ${isErr ? chalk.red(line) : chalk.dim(line)}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
// logs endpoint temporarily unavailable
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (status && TERMINAL_STATUSES.has(status)) {
|
|
95
|
-
console.log(chalk.dim(`\n Job ${status}. No more logs.\n`));
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
45
|
+
if (follow) {
|
|
46
|
+
console.log(chalk.yellow('\n --follow: live log streaming not yet supported. Poll with `badgr logs`.\n'));
|
|
98
47
|
}
|
|
99
|
-
|
|
100
48
|
console.log();
|
|
101
49
|
}
|
package/src/commands/models.js
CHANGED
|
@@ -1,38 +1,19 @@
|
|
|
1
|
-
import { listModels
|
|
1
|
+
import { listModels } from '../api.js';
|
|
2
2
|
import { listAll } from '../router.js';
|
|
3
3
|
|
|
4
4
|
export async function modelsCommand(config, chalk) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
let gpus = null;
|
|
8
|
-
if (config.apiKey) {
|
|
9
|
-
try {
|
|
10
|
-
const data = await callApi('/gpus', { apiKey: config.apiKey, baseUrl: config.baseUrl });
|
|
11
|
-
if (data?.gpus?.length) {
|
|
12
|
-
gpus = data.gpus.map(g => ({
|
|
13
|
-
id: g.id,
|
|
14
|
-
name: g.name,
|
|
15
|
-
vramGb: g.vram_gb,
|
|
16
|
-
ratePerHour: g.rate_per_hour,
|
|
17
|
-
})).sort((a, b) => a.ratePerHour - b.ratePerHour);
|
|
18
|
-
}
|
|
19
|
-
} catch {
|
|
20
|
-
// fallback to local catalog on error
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (!gpus) gpus = listAll();
|
|
5
|
+
const gpus = listAll();
|
|
25
6
|
|
|
7
|
+
console.log(chalk.bold('\n📦 GPU Options (cheapest first)\n'));
|
|
26
8
|
console.log(
|
|
27
|
-
` ${'ID'.padEnd(14)} ${'Name'.padEnd(24)} ${'VRAM'.padEnd(8)} Rate/hr
|
|
9
|
+
` ${'ID'.padEnd(14)} ${'Name'.padEnd(24)} ${'VRAM'.padEnd(8)} Rate/hr`
|
|
28
10
|
);
|
|
29
|
-
console.log(` ${'─'.repeat(
|
|
11
|
+
console.log(` ${'─'.repeat(58)}`);
|
|
30
12
|
gpus.forEach(g => {
|
|
31
13
|
console.log(
|
|
32
|
-
` ${chalk.cyan(g.id.padEnd(14))} ${g.name.padEnd(24)} ${`${g.vramGb}GB`.padEnd(8)}
|
|
14
|
+
` ${chalk.cyan(g.id.padEnd(14))} ${g.name.padEnd(24)} ${`${g.vramGb}GB`.padEnd(8)} $${g.ratePerHour.toFixed(2)}`
|
|
33
15
|
);
|
|
34
16
|
});
|
|
35
|
-
console.log(chalk.dim(' Actual billing is set at job start — use `badgr run` to see live rates.'));
|
|
36
17
|
|
|
37
18
|
console.log(chalk.bold('\n🤖 LLM Models\n'));
|
|
38
19
|
if (!config.apiKey) {
|
package/src/commands/receipts.js
CHANGED
|
@@ -5,34 +5,19 @@ function fmtMs(ms) {
|
|
|
5
5
|
return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
function fmtRuntime(s) {
|
|
9
|
-
if (s < 60) return `${s}s`;
|
|
10
|
-
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
8
|
function printReceipt(r, chalk) {
|
|
14
9
|
const id = r.receiptId ?? r.request_id ?? r.id ?? '—';
|
|
15
10
|
const ts = r.createdAt ?? r.created_at ?? '—';
|
|
16
|
-
const
|
|
11
|
+
const cost = r.route?.ratePerHour ?? r.cost_usd ?? 0;
|
|
12
|
+
const prov = r.route?.provider ?? r.provider ?? r.model_provider ?? '—';
|
|
17
13
|
const lat = r.latencyMs ?? r.latency_ms;
|
|
14
|
+
const action = r.action ?? r.endpoint ?? '—';
|
|
18
15
|
|
|
19
16
|
console.log(` ${chalk.cyan(id)}`);
|
|
20
17
|
console.log(` ${chalk.bold('action:')} ${action}`);
|
|
21
|
-
|
|
22
|
-
// GPU job fields (from badgr run / badgr down)
|
|
23
|
-
if (r.gpu) console.log(` ${chalk.bold('gpu:')} ${r.gpu}`);
|
|
24
|
-
if (r.runtimeSeconds) console.log(` ${chalk.bold('runtime:')} ${fmtRuntime(r.runtimeSeconds)}`);
|
|
25
|
-
if (r.finalCost) console.log(` ${chalk.bold('cost:')} $${r.finalCost.toFixed(4)}`);
|
|
26
|
-
else if (r.cost_usd) console.log(` ${chalk.bold('cost:')} $${r.cost_usd.toFixed(4)}`);
|
|
27
|
-
if (r.exitCode !== undefined && r.exitCode !== null) {
|
|
28
|
-
console.log(` ${chalk.bold('exit:')} ${r.exitCode !== 0 ? chalk.red(r.exitCode) : chalk.green(r.exitCode)}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// API inference receipt fields
|
|
32
18
|
if (lat !== undefined) console.log(` ${chalk.bold('latency:')} ${fmtMs(lat)}`);
|
|
19
|
+
if (cost) console.log(` ${chalk.bold('cost:')} $${typeof cost === 'number' ? cost.toFixed(4) : cost}/hr`);
|
|
33
20
|
if (r.retries !== undefined) console.log(` ${chalk.bold('retries:')} ${r.retries}`);
|
|
34
|
-
if (r.route?.ratePerHour) console.log(` ${chalk.bold('rate:')} $${r.route.ratePerHour.toFixed(4)}/hr`);
|
|
35
|
-
|
|
36
21
|
if (r.status) console.log(` ${chalk.bold('status:')} ${r.status}`);
|
|
37
22
|
console.log(` ${chalk.dim(ts)}`);
|
|
38
23
|
console.log();
|