badgr-cli 1.0.31 → 1.0.32
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/README.md +37 -18
- package/package.json +12 -4
- package/src/api.js +138 -20
- package/src/badgr.js +81 -37
- package/src/commands/billing.js +93 -0
- package/src/commands/capacity.js +111 -0
- package/src/commands/down.js +26 -23
- package/src/commands/login.js +23 -7
- package/src/commands/logs.js +57 -5
- package/src/commands/models.js +25 -6
- package/src/commands/receipts.js +19 -4
- package/src/commands/run.js +548 -90
- package/src/commands/serve.js +284 -66
- package/src/commands/status.js +35 -48
- package/src/commands/test-run.js +240 -0
- package/src/commands/up.js +32 -26
- package/src/config.js +49 -4
- package/src/fallback.js +179 -0
- package/src/router.js +16 -73
- package/src/store.js +10 -1
- package/tests/commands.test.js +234 -2
- package/tests/config.test.js +24 -1
- package/tests/router.test.js +9 -68
- package/tests/run-lifecycle.test.js +498 -0
- package/tests/serve-lifecycle.test.js +499 -0
- package/tests/store.test.js +41 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { requireApiKey } from '../config.js';
|
|
2
|
+
import { callApi } from '../api.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* badgr capacity # show what's available right now (auto)
|
|
6
|
+
* badgr capacity --gpu A100 # check a specific GPU type
|
|
7
|
+
*/
|
|
8
|
+
function parseCapacityArgs(args) {
|
|
9
|
+
const flags = {};
|
|
10
|
+
let i = 0;
|
|
11
|
+
while (i < args.length) {
|
|
12
|
+
if (args[i] === '--gpu') { flags.gpu = args[++i]; i++; continue; }
|
|
13
|
+
if (args[i] === '--region') { flags.region = args[++i]; i++; continue; }
|
|
14
|
+
if (args[i] === '--max-price') { flags.maxPrice = parseFloat(args[++i]); i++; continue; }
|
|
15
|
+
i++;
|
|
16
|
+
}
|
|
17
|
+
return flags;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function capacityCommand(config, args, chalk) {
|
|
21
|
+
const flags = parseCapacityArgs(args);
|
|
22
|
+
requireApiKey(config);
|
|
23
|
+
|
|
24
|
+
const maxPrice = flags.maxPrice ?? 10;
|
|
25
|
+
|
|
26
|
+
// No --gpu: show cheapest runnable GPU across all types
|
|
27
|
+
if (!flags.gpu) {
|
|
28
|
+
console.log(chalk.bold('\nAvailable GPU capacity\n'));
|
|
29
|
+
process.stdout.write(chalk.dim(' Checking availability...\n'));
|
|
30
|
+
|
|
31
|
+
let data;
|
|
32
|
+
try {
|
|
33
|
+
const params = new URLSearchParams({ max_price: String(maxPrice) });
|
|
34
|
+
if (flags.region) params.set('region', flags.region.toUpperCase());
|
|
35
|
+
data = await callApi(`/capacity/auto?${params}`, {
|
|
36
|
+
apiKey: config.apiKey,
|
|
37
|
+
baseUrl: config.baseUrl,
|
|
38
|
+
});
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err.errorData?.code === 'NO_CAPACITY_MATCH') {
|
|
41
|
+
console.log(chalk.dim('\n No GPU capacity available right now.\n'));
|
|
42
|
+
console.log(chalk.dim(' Try again in a few minutes, or check a specific GPU with --gpu <type>.\n'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.error(chalk.red(`\n ✗ Capacity check failed: ${err.message}\n`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log();
|
|
50
|
+
console.log(` ${chalk.bold('Cheapest available:')} ${chalk.cyan(data.gpu)} in ${data.region} ${chalk.green('$' + data.price.toFixed(2) + '/hr')}`);
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(` ${chalk.bold('Run now:')}`);
|
|
53
|
+
console.log(chalk.cyan(` badgr run python train.py`));
|
|
54
|
+
console.log(chalk.cyan(` badgr serve meta-llama/Llama-3.1-8B-Instruct`));
|
|
55
|
+
console.log();
|
|
56
|
+
console.log(chalk.dim(` Use badgr capacity --gpu A100 to check a specific GPU type.`));
|
|
57
|
+
console.log();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --gpu specified: show availability for that type
|
|
62
|
+
const gpu = flags.gpu.toUpperCase().replace('-', '_');
|
|
63
|
+
const params = new URLSearchParams({ gpu, max_price: String(maxPrice) });
|
|
64
|
+
if (flags.region) params.set('region', flags.region.toUpperCase());
|
|
65
|
+
|
|
66
|
+
console.log(chalk.bold(`\nCapacity: ${gpu}\n`));
|
|
67
|
+
process.stdout.write(chalk.dim(' Checking...\n'));
|
|
68
|
+
|
|
69
|
+
let data;
|
|
70
|
+
try {
|
|
71
|
+
data = await callApi(`/capacity/suggestions?${params}`, {
|
|
72
|
+
apiKey: config.apiKey,
|
|
73
|
+
baseUrl: config.baseUrl,
|
|
74
|
+
});
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(chalk.red(`\n ✗ Capacity check failed: ${err.message}\n`));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const matches = data.matches ?? [];
|
|
81
|
+
if (matches.length > 0) {
|
|
82
|
+
console.log(chalk.bold('\n Available now:\n'));
|
|
83
|
+
for (const m of matches) {
|
|
84
|
+
console.log(` ${chalk.green('●')} ${m.gpu} in ${m.region} ${chalk.green('$' + m.price.toFixed(2) + '/hr')}`);
|
|
85
|
+
}
|
|
86
|
+
console.log();
|
|
87
|
+
const regionFlag = flags.region ? ` --region ${flags.region.toUpperCase()}` : '';
|
|
88
|
+
console.log(` ${chalk.bold('Run:')}`);
|
|
89
|
+
console.log(chalk.cyan(` badgr run python train.py --gpu ${gpu}${regionFlag}`));
|
|
90
|
+
console.log();
|
|
91
|
+
} else {
|
|
92
|
+
const regionLabel = flags.region ? ` in ${flags.region.toUpperCase()}` : '';
|
|
93
|
+
console.log(chalk.dim(`\n No ${gpu} available right now under $${maxPrice.toFixed(2)}/hr${regionLabel}.\n`));
|
|
94
|
+
|
|
95
|
+
const alternatives = data.alternatives ?? [];
|
|
96
|
+
if (alternatives.length > 0) {
|
|
97
|
+
console.log(chalk.bold(' Available alternatives:\n'));
|
|
98
|
+
for (const a of alternatives) {
|
|
99
|
+
console.log(` ${chalk.dim('●')} ${a.gpu} in ${a.region} $${a.price.toFixed(2)}/hr`);
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
console.log(` ${chalk.bold('Try:')}`);
|
|
103
|
+
for (const a of alternatives.slice(0, 3)) {
|
|
104
|
+
console.log(chalk.cyan(` badgr run python train.py --gpu ${a.gpu}`));
|
|
105
|
+
}
|
|
106
|
+
console.log();
|
|
107
|
+
} else {
|
|
108
|
+
console.log(chalk.dim(' No alternatives found. Try `badgr capacity` to see global availability.\n'));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
package/src/commands/down.js
CHANGED
|
@@ -12,43 +12,46 @@ export async function downCommand(config, args, chalk) {
|
|
|
12
12
|
|
|
13
13
|
requireApiKey(config);
|
|
14
14
|
|
|
15
|
-
const localDep
|
|
16
|
-
|
|
17
|
-
// Resolve the deployment ID to send to the backend
|
|
15
|
+
const localDep = findDeployment(idOrName);
|
|
18
16
|
const deploymentId = localDep?.id ?? idOrName;
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
process.stdout.write(chalk.dim(` Stopping ${deploymentId}...`));
|
|
21
19
|
|
|
22
20
|
let dep;
|
|
23
21
|
try {
|
|
24
22
|
dep = await terminateDeployment(config, deploymentId);
|
|
25
23
|
} catch (err) {
|
|
26
|
-
|
|
24
|
+
process.stdout.write('\n');
|
|
25
|
+
console.error(chalk.red(`\n ✗ Could not stop deployment: ${err.message}\n`));
|
|
27
26
|
return;
|
|
28
27
|
}
|
|
29
28
|
|
|
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
|
+
|
|
30
38
|
const rcptId = generateReceiptId();
|
|
31
39
|
addReceipt({
|
|
32
|
-
receiptId:
|
|
33
|
-
action:
|
|
34
|
-
deploymentId:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
createdAt: new Date().toISOString(),
|
|
40
|
+
receiptId: rcptId,
|
|
41
|
+
action: 'badgr down',
|
|
42
|
+
deploymentId: dep.deployment_id,
|
|
43
|
+
gpu: dep.gpu_type,
|
|
44
|
+
runtimeSeconds: Math.round(stoppedAt - startedAt),
|
|
45
|
+
finalCost,
|
|
46
|
+
status: 'terminated',
|
|
47
|
+
createdAt: new Date().toISOString(),
|
|
41
48
|
});
|
|
42
49
|
|
|
43
|
-
// Remove from local store
|
|
44
50
|
removeDeployment(idOrName);
|
|
45
51
|
|
|
46
|
-
console.log(chalk.green(
|
|
47
|
-
console.log(
|
|
48
|
-
console.log(` ${chalk.bold('
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
console.log(` ${chalk.bold('Uptime:')} ${uptime} min`);
|
|
52
|
-
}
|
|
53
|
-
console.log(`\n ${chalk.bold('Receipt ID:')} ${chalk.dim(rcptId)}\n`);
|
|
52
|
+
console.log(chalk.green('\n✓ Stopped'));
|
|
53
|
+
console.log(chalk.green(' Billing ended\n'));
|
|
54
|
+
console.log(` ${chalk.bold('Runtime:')} ${runtimeMin}m`);
|
|
55
|
+
if (finalCost > 0) console.log(` ${chalk.bold('Final cost:')} $${finalCost.toFixed(4)}`);
|
|
56
|
+
console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}\n`);
|
|
54
57
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
import { input } from '@inquirer/prompts';
|
|
2
|
+
import { DEFAULTS } from '../config.js';
|
|
3
|
+
import { callApi } from '../api.js';
|
|
2
4
|
|
|
3
5
|
export async function loginCommand(chalk, saveConfigFn) {
|
|
4
|
-
console.log(chalk.bold('\
|
|
6
|
+
console.log(chalk.bold('\nBadgr Login\n'));
|
|
5
7
|
|
|
6
8
|
const apiKey = await input({
|
|
7
9
|
message: 'Enter your Badgr API key:',
|
|
8
10
|
validate: v => v.trim() ? true : 'API key is required',
|
|
9
11
|
});
|
|
10
12
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const config = saveConfigFn({
|
|
14
|
+
apiKey: apiKey.trim(),
|
|
15
|
+
baseUrl: DEFAULTS.baseUrl,
|
|
14
16
|
});
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
console.log(chalk.
|
|
18
|
-
|
|
18
|
+
console.log(chalk.green('\n✓ Logged in'));
|
|
19
|
+
console.log(chalk.dim(` Config saved to ~/.badgr/config.json`));
|
|
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`));
|
|
19
35
|
return config;
|
|
20
36
|
}
|
package/src/commands/logs.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { findDeployment, listDeployments } from '../store.js';
|
|
2
2
|
import { requireApiKey } from '../config.js';
|
|
3
|
-
import { getDeploymentLogs } from '../api.js';
|
|
3
|
+
import { getDeploymentLogs, callApi } from '../api.js';
|
|
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)=/;
|
|
4
10
|
|
|
5
11
|
export async function logsCommand(config, args, chalk) {
|
|
6
12
|
const idOrName = args.find(a => !a.startsWith('--'));
|
|
@@ -26,13 +32,18 @@ export async function logsCommand(config, args, chalk) {
|
|
|
26
32
|
|
|
27
33
|
console.log(chalk.bold(`\n📋 Logs: ${localDep?.name ?? deploymentId}\n`));
|
|
28
34
|
|
|
35
|
+
// Fetch and print initial batch of logs.
|
|
36
|
+
const seen = new Set();
|
|
29
37
|
try {
|
|
30
38
|
const data = await getDeploymentLogs(config, deploymentId);
|
|
31
39
|
const lines = data?.logs ?? [];
|
|
32
|
-
if (lines.length === 0) {
|
|
40
|
+
if (lines.length === 0 && !follow) {
|
|
33
41
|
console.log(chalk.dim(' No log lines available yet.\n'));
|
|
34
42
|
} else {
|
|
35
|
-
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
seen.add(line);
|
|
45
|
+
if (!LOG_META_RE.test(line)) console.log(` ${chalk.dim(line)}`);
|
|
46
|
+
}
|
|
36
47
|
}
|
|
37
48
|
} catch (err) {
|
|
38
49
|
console.log(chalk.yellow(` Could not fetch logs: ${err.message}`));
|
|
@@ -40,10 +51,51 @@ export async function logsCommand(config, args, chalk) {
|
|
|
40
51
|
console.log(chalk.dim(`\n GPU: ${localDep.gpu} Type: ${localDep.type}`));
|
|
41
52
|
console.log(chalk.dim(` Endpoint: ${localDep.endpointUrl}`));
|
|
42
53
|
}
|
|
54
|
+
if (!follow) { console.log(); return; }
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
if (follow) {
|
|
46
|
-
|
|
57
|
+
if (!follow) { console.log(); return; }
|
|
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
|
+
}
|
|
47
98
|
}
|
|
99
|
+
|
|
48
100
|
console.log();
|
|
49
101
|
}
|
package/src/commands/models.js
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
|
-
import { listModels } from '../api.js';
|
|
1
|
+
import { listModels, callApi } from '../api.js';
|
|
2
2
|
import { listAll } from '../router.js';
|
|
3
3
|
|
|
4
4
|
export async function modelsCommand(config, chalk) {
|
|
5
|
-
const gpus = listAll();
|
|
6
|
-
|
|
7
5
|
console.log(chalk.bold('\n📦 GPU Options (cheapest first)\n'));
|
|
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();
|
|
25
|
+
|
|
8
26
|
console.log(
|
|
9
|
-
` ${'ID'.padEnd(14)} ${'Name'.padEnd(24)} ${'VRAM'.padEnd(8)} Rate/hr`
|
|
27
|
+
` ${'ID'.padEnd(14)} ${'Name'.padEnd(24)} ${'VRAM'.padEnd(8)} Rate/hr (indicative)`
|
|
10
28
|
);
|
|
11
|
-
console.log(` ${'─'.repeat(
|
|
29
|
+
console.log(` ${'─'.repeat(68)}`);
|
|
12
30
|
gpus.forEach(g => {
|
|
13
31
|
console.log(
|
|
14
|
-
` ${chalk.cyan(g.id.padEnd(14))} ${g.name.padEnd(24)} ${`${g.vramGb}GB`.padEnd(8)}
|
|
32
|
+
` ${chalk.cyan(g.id.padEnd(14))} ${g.name.padEnd(24)} ${`${g.vramGb}GB`.padEnd(8)} ~$${g.ratePerHour.toFixed(2)}`
|
|
15
33
|
);
|
|
16
34
|
});
|
|
35
|
+
console.log(chalk.dim(' Actual billing is set at job start — use `badgr run` to see live rates.'));
|
|
17
36
|
|
|
18
37
|
console.log(chalk.bold('\n🤖 LLM Models\n'));
|
|
19
38
|
if (!config.apiKey) {
|
package/src/commands/receipts.js
CHANGED
|
@@ -5,19 +5,34 @@ 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
|
+
|
|
8
13
|
function printReceipt(r, chalk) {
|
|
9
14
|
const id = r.receiptId ?? r.request_id ?? r.id ?? '—';
|
|
10
15
|
const ts = r.createdAt ?? r.created_at ?? '—';
|
|
11
|
-
const cost = r.route?.ratePerHour ?? r.cost_usd ?? 0;
|
|
12
|
-
const prov = r.route?.provider ?? r.provider ?? r.model_provider ?? '—';
|
|
13
|
-
const lat = r.latencyMs ?? r.latency_ms;
|
|
14
16
|
const action = r.action ?? r.endpoint ?? '—';
|
|
17
|
+
const lat = r.latencyMs ?? r.latency_ms;
|
|
15
18
|
|
|
16
19
|
console.log(` ${chalk.cyan(id)}`);
|
|
17
20
|
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
|
|
18
32
|
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`);
|
|
20
33
|
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
|
+
|
|
21
36
|
if (r.status) console.log(` ${chalk.bold('status:')} ${r.status}`);
|
|
22
37
|
console.log(` ${chalk.dim(ts)}`);
|
|
23
38
|
console.log();
|