sublera-cli 0.0.0-stage → 1.3.0
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/CHANGELOG.md +172 -0
- package/LICENSE +21 -0
- package/README.md +585 -2
- package/SECURITY.md +78 -0
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agent.d.ts +270 -0
- package/dist/agent.js +1867 -0
- package/dist/commands.d.ts +76 -0
- package/dist/commands.js +841 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +95 -0
- package/dist/engine/binaryManager.d.ts +67 -0
- package/dist/engine/binaryManager.js +590 -0
- package/dist/engine/gguf.d.ts +88 -0
- package/dist/engine/gguf.js +458 -0
- package/dist/engine/llamaRunner.d.ts +78 -0
- package/dist/engine/llamaRunner.js +282 -0
- package/dist/engine/modelManager.d.ts +63 -0
- package/dist/engine/modelManager.js +510 -0
- package/dist/engine/selfCheck.d.ts +34 -0
- package/dist/engine/selfCheck.js +108 -0
- package/dist/exitCodes.d.ts +20 -0
- package/dist/exitCodes.js +20 -0
- package/dist/hardware/gpuMonitor.d.ts +32 -0
- package/dist/hardware/gpuMonitor.js +378 -0
- package/dist/hardware/sleepGuard.d.ts +42 -0
- package/dist/hardware/sleepGuard.js +149 -0
- package/dist/hardware/types.d.ts +40 -0
- package/dist/hardware/types.js +2 -0
- package/dist/health.d.ts +17 -0
- package/dist/health.js +38 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +268 -0
- package/dist/logger.d.ts +53 -0
- package/dist/logger.js +201 -0
- package/dist/network/proxy.d.ts +58 -0
- package/dist/network/proxy.js +328 -0
- package/dist/network/streamProxy.d.ts +51 -0
- package/dist/network/streamProxy.js +286 -0
- package/dist/network/tunnelClient.d.ts +54 -0
- package/dist/network/tunnelClient.js +257 -0
- package/dist/settings.d.ts +53 -0
- package/dist/settings.js +171 -0
- package/dist/state.d.ts +52 -0
- package/dist/state.js +71 -0
- package/dist/storageCommand.d.ts +22 -0
- package/dist/storageCommand.js +81 -0
- package/dist/types.d.ts +222 -0
- package/dist/types.js +43 -0
- package/dist/ui/progress.d.ts +29 -0
- package/dist/ui/progress.js +82 -0
- package/dist/ui/terminalDashboard.d.ts +88 -0
- package/dist/ui/terminalDashboard.js +295 -0
- package/dist/util/env.d.ts +3 -0
- package/dist/util/env.js +6 -0
- package/dist/util/exec.d.ts +23 -0
- package/dist/util/exec.js +116 -0
- package/dist/util/format.d.ts +47 -0
- package/dist/util/format.js +125 -0
- package/dist/util/fs.d.ts +18 -0
- package/dist/util/fs.js +140 -0
- package/dist/util/http.d.ts +28 -0
- package/dist/util/http.js +128 -0
- package/dist/util/instanceLock.d.ts +3 -0
- package/dist/util/instanceLock.js +108 -0
- package/dist/util/open.d.ts +16 -0
- package/dist/util/open.js +65 -0
- package/dist/util/storage.d.ts +10 -0
- package/dist/util/storage.js +69 -0
- package/dist/version.d.ts +15 -0
- package/dist/version.js +29 -0
- package/package.json +68 -4
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { lstat, readdir, unlink } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { binDir, cacheDir, logsDir, modelsDir, subletHome } from './config.js';
|
|
4
|
+
import { directoryBytes, storagePolicy } from './util/storage.js';
|
|
5
|
+
import { freeDiskBytes } from './util/fs.js';
|
|
6
|
+
import { acquireInstanceLock } from './util/instanceLock.js';
|
|
7
|
+
import { UsageError } from './exitCodes.js';
|
|
8
|
+
/** Maintenance is deliberately serialized against serving. Only regular stale
|
|
9
|
+
* partial files in the managed model root are eligible, never complete models. */
|
|
10
|
+
export async function storageReport(prune, apply, env = process.env) {
|
|
11
|
+
const release = await acquireInstanceLock(env);
|
|
12
|
+
try {
|
|
13
|
+
const root = modelsDir(env);
|
|
14
|
+
const candidates = [];
|
|
15
|
+
if (prune) {
|
|
16
|
+
// Validate the root before even enumerating candidates: readdir follows
|
|
17
|
+
// directory symlinks (including Windows junctions), unlike lstat.
|
|
18
|
+
const checkRoot = async () => {
|
|
19
|
+
try {
|
|
20
|
+
const info = await lstat(root);
|
|
21
|
+
if (info.isSymbolicLink() || !info.isDirectory())
|
|
22
|
+
throw new Error(`Prune refuses a linked or non-directory model root: ${root}`);
|
|
23
|
+
return info;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code === 'ENOENT')
|
|
27
|
+
return null;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const rootInfo = await checkRoot();
|
|
32
|
+
let names = [];
|
|
33
|
+
try {
|
|
34
|
+
if (rootInfo)
|
|
35
|
+
names = await readdir(root);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code !== 'ENOENT')
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
for (const name of names) {
|
|
42
|
+
if (!/^[^/\\]+\.gguf\.part(?:\.json)?$/i.test(name))
|
|
43
|
+
continue;
|
|
44
|
+
const path = join(root, name);
|
|
45
|
+
const info = await lstat(path);
|
|
46
|
+
if (!info.isFile() || info.isSymbolicLink() || Date.now() - info.mtimeMs < 7 * 24 * 3600_000)
|
|
47
|
+
continue;
|
|
48
|
+
candidates.push({ path, bytes: info.size });
|
|
49
|
+
}
|
|
50
|
+
if (apply) {
|
|
51
|
+
for (const candidate of candidates) {
|
|
52
|
+
const currentRoot = await checkRoot();
|
|
53
|
+
if (!currentRoot || !rootInfo || currentRoot.dev !== rootInfo.dev || currentRoot.ino !== rootInfo.ino) {
|
|
54
|
+
throw new Error('Prune model root changed; retry with all agents stopped');
|
|
55
|
+
}
|
|
56
|
+
const info = await lstat(candidate.path);
|
|
57
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size !== candidate.bytes
|
|
58
|
+
|| Date.now() - info.mtimeMs < 7 * 24 * 3600_000)
|
|
59
|
+
throw new Error('Prune candidate changed; retry with all agents stopped');
|
|
60
|
+
await unlink(candidate.path);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const usage = {};
|
|
65
|
+
for (const [name, dir] of Object.entries({ models: root, engines: binDir(env), cache: cacheDir(env), logs: logsDir(env) }))
|
|
66
|
+
usage[name] = await directoryBytes(dir);
|
|
67
|
+
return { schemaVersion: 1, home: subletHome(env), policy: storagePolicy(env), freeBytes: await freeDiskBytes(subletHome(env)), usage, prune, dryRun: !apply, candidates };
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
await release();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function storageAction(positionals, force) {
|
|
74
|
+
const action = positionals[0] || 'status';
|
|
75
|
+
if (positionals.length > 1 || !['status', 'prune'].includes(action))
|
|
76
|
+
throw new UsageError('Usage: sublera storage [status|prune] [--force] [--json]. Prune is dry-run unless --force is supplied.');
|
|
77
|
+
if (force && action !== 'prune')
|
|
78
|
+
throw new UsageError('--force is only supported with storage prune');
|
|
79
|
+
return { prune: action === 'prune', apply: force };
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=storageCommand.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
export type ChatRole = 'system' | 'user' | 'assistant' | 'tool';
|
|
2
|
+
export type ChatMessage = {
|
|
3
|
+
role: ChatRole;
|
|
4
|
+
content: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
};
|
|
7
|
+
export type WorkerJob = {
|
|
8
|
+
type: 'infer';
|
|
9
|
+
jobId: string;
|
|
10
|
+
messages: ChatMessage[];
|
|
11
|
+
maxTokens: number;
|
|
12
|
+
temperature: number;
|
|
13
|
+
model?: string;
|
|
14
|
+
};
|
|
15
|
+
/** One generated token with its log probability and the top alternatives, as llama-server reports them. */
|
|
16
|
+
export type TokenPosition = {
|
|
17
|
+
t: string;
|
|
18
|
+
l: number;
|
|
19
|
+
top: Array<[string, number]>;
|
|
20
|
+
};
|
|
21
|
+
export type LoadJob = {
|
|
22
|
+
type: 'load';
|
|
23
|
+
requestId: string;
|
|
24
|
+
url: string;
|
|
25
|
+
file: string | null;
|
|
26
|
+
sha256: string | null;
|
|
27
|
+
layers: number | null;
|
|
28
|
+
context: number;
|
|
29
|
+
/** Shared bookings: one llama-server slot for each buyer the host's benchmark allows. */
|
|
30
|
+
slots?: number;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The server starts a booking's clock. Several bookings can be active at once on the
|
|
34
|
+
* loaded model: speed seats (`tokS` set) side by side, or one full-GPU rental.
|
|
35
|
+
*/
|
|
36
|
+
export type BookingFrame = {
|
|
37
|
+
type: 'booking';
|
|
38
|
+
requestId: string;
|
|
39
|
+
timerStartedAt?: number;
|
|
40
|
+
timerEndsAt?: number;
|
|
41
|
+
hours?: number;
|
|
42
|
+
pricePerHourNanos?: number;
|
|
43
|
+
minTokS?: number;
|
|
44
|
+
/** True for a speed seat that shares the GPU with other renters and per-token buyers. */
|
|
45
|
+
shared?: boolean;
|
|
46
|
+
/** The reserved speed of a seat booking; null or absent means the full GPU. */
|
|
47
|
+
tokS?: number | null;
|
|
48
|
+
};
|
|
49
|
+
export type ServerMessage = {
|
|
50
|
+
type: 'welcome';
|
|
51
|
+
username?: string;
|
|
52
|
+
} | WorkerJob | LoadJob | {
|
|
53
|
+
type: 'cancel';
|
|
54
|
+
jobId: string;
|
|
55
|
+
} | {
|
|
56
|
+
type: 'verified';
|
|
57
|
+
requestId: string;
|
|
58
|
+
fingerprint?: string;
|
|
59
|
+
} | BookingFrame | {
|
|
60
|
+
type: 'reject';
|
|
61
|
+
requestId: string;
|
|
62
|
+
reason?: string;
|
|
63
|
+
} | {
|
|
64
|
+
type: 'error';
|
|
65
|
+
message?: string;
|
|
66
|
+
} | {
|
|
67
|
+
type: string;
|
|
68
|
+
[key: string]: unknown;
|
|
69
|
+
};
|
|
70
|
+
export type CachedModelInfo = {
|
|
71
|
+
sha256: string;
|
|
72
|
+
file: string;
|
|
73
|
+
sizeBytes: number;
|
|
74
|
+
};
|
|
75
|
+
export type HelloPayload = {
|
|
76
|
+
type: 'hello';
|
|
77
|
+
agentVersion: string;
|
|
78
|
+
platform: string;
|
|
79
|
+
arch: string;
|
|
80
|
+
gpuName: string;
|
|
81
|
+
gpuVendor: string;
|
|
82
|
+
vramMb: number;
|
|
83
|
+
freeVramMb: number;
|
|
84
|
+
modelId: string;
|
|
85
|
+
modelLabel: string;
|
|
86
|
+
repo: string;
|
|
87
|
+
file: string;
|
|
88
|
+
quant: string;
|
|
89
|
+
paramB: number | null;
|
|
90
|
+
layers: number | null;
|
|
91
|
+
context: number;
|
|
92
|
+
slots: number;
|
|
93
|
+
sha256: string | null;
|
|
94
|
+
hashVerified: boolean;
|
|
95
|
+
tokPerSec: number;
|
|
96
|
+
ttftMs: number;
|
|
97
|
+
priceInUsd: number;
|
|
98
|
+
priceOutUsd: number;
|
|
99
|
+
/** The host's price per hour for the full GPU; 0 means no hourly rentals. The server derives seat prices from it. */
|
|
100
|
+
priceHourUsd: number;
|
|
101
|
+
simulate: boolean;
|
|
102
|
+
publicMode: boolean;
|
|
103
|
+
/** Set by `sublera start <code>`: the request this agent wants to serve. */
|
|
104
|
+
requestId?: string | null;
|
|
105
|
+
cachedModels?: CachedModelInfo[];
|
|
106
|
+
};
|
|
107
|
+
export type HeartbeatPayload = {
|
|
108
|
+
type: 'heartbeat';
|
|
109
|
+
tokPerSec: number;
|
|
110
|
+
ttftMs: number;
|
|
111
|
+
llamaUp: boolean;
|
|
112
|
+
sha256: string | null;
|
|
113
|
+
file: string;
|
|
114
|
+
layers: number | null;
|
|
115
|
+
activeJobId: string | null;
|
|
116
|
+
wakeLock: boolean;
|
|
117
|
+
gpuTempC: number | null;
|
|
118
|
+
gpuPowerW: number | null;
|
|
119
|
+
gpuUtilPct: number | null;
|
|
120
|
+
freeVramMb: number;
|
|
121
|
+
slotsUsed: number;
|
|
122
|
+
};
|
|
123
|
+
export type WalletView = {
|
|
124
|
+
availableNanos: number;
|
|
125
|
+
pendingNanos: number;
|
|
126
|
+
escrowNanos: number;
|
|
127
|
+
heldNanos?: number;
|
|
128
|
+
cardFundedNanos?: number;
|
|
129
|
+
cryptoFundedNanos?: number;
|
|
130
|
+
};
|
|
131
|
+
export type AgentHost = {
|
|
132
|
+
username: string;
|
|
133
|
+
status: string;
|
|
134
|
+
kind: 'simulator' | 'live' | string;
|
|
135
|
+
gpuName: string;
|
|
136
|
+
vramMb: number;
|
|
137
|
+
hardwareVerified: boolean;
|
|
138
|
+
modelId: string;
|
|
139
|
+
modelLabel: string;
|
|
140
|
+
repo: string;
|
|
141
|
+
file: string;
|
|
142
|
+
quant: string;
|
|
143
|
+
paramB: number | null;
|
|
144
|
+
layers: number | null;
|
|
145
|
+
context: number;
|
|
146
|
+
tokPerSec: number;
|
|
147
|
+
ttftMs: number;
|
|
148
|
+
slotsUsed: number;
|
|
149
|
+
slotsTotal: number;
|
|
150
|
+
priceInNanos: number;
|
|
151
|
+
priceOutNanos: number;
|
|
152
|
+
trust: number;
|
|
153
|
+
uptimePct: number;
|
|
154
|
+
tokensServed: number;
|
|
155
|
+
flagReason: string | null;
|
|
156
|
+
verified: boolean;
|
|
157
|
+
llamaUp: boolean;
|
|
158
|
+
lastSeen: number | null;
|
|
159
|
+
activeRequest?: ActiveRequest | null;
|
|
160
|
+
};
|
|
161
|
+
export type ActiveRequest = {
|
|
162
|
+
id: string;
|
|
163
|
+
status: string;
|
|
164
|
+
modelLabel?: string | null;
|
|
165
|
+
file?: string | null;
|
|
166
|
+
hours?: number | null;
|
|
167
|
+
pricePerHourNanos?: number | null;
|
|
168
|
+
minTokS?: number | null;
|
|
169
|
+
timerStartedAt?: number | null;
|
|
170
|
+
timerEndsAt?: number | null;
|
|
171
|
+
escrowRemainingNanos?: number;
|
|
172
|
+
/** Same meaning as in the booking frame, when the server includes them. */
|
|
173
|
+
shared?: boolean | null;
|
|
174
|
+
tokS?: number | null;
|
|
175
|
+
};
|
|
176
|
+
export type AgentMe = {
|
|
177
|
+
user: {
|
|
178
|
+
id: string;
|
|
179
|
+
username: string;
|
|
180
|
+
email?: string;
|
|
181
|
+
isAdmin?: boolean;
|
|
182
|
+
};
|
|
183
|
+
wallet: WalletView;
|
|
184
|
+
host: AgentHost | null;
|
|
185
|
+
/** The newest live request of this host. */
|
|
186
|
+
activeRequest: ActiveRequest | null;
|
|
187
|
+
/** Every live request of this host, newest first. Older servers do not send it. */
|
|
188
|
+
activeRequests?: ActiveRequest[];
|
|
189
|
+
serverNow: number;
|
|
190
|
+
};
|
|
191
|
+
/** The shape of a request id the server issues. Frames and status rows with any other id are ignored. */
|
|
192
|
+
export declare const REQUEST_ID_PATTERN: RegExp;
|
|
193
|
+
export declare function isRequestId(value: unknown): value is string;
|
|
194
|
+
/** A finite number from the server or state.json, or null. Numeric strings count; NaN, Infinity, booleans and objects do not. */
|
|
195
|
+
export declare function finiteNumber(value: unknown): number | null;
|
|
196
|
+
/** A positive time in ms that `Date` can represent, or null. */
|
|
197
|
+
export declare function validTime(value: unknown): number | null;
|
|
198
|
+
/** Every live request in a /api/agent/me answer: `activeRequests`, or the single `activeRequest` from an older server. */
|
|
199
|
+
export declare function activeRequestsOf(me: Pick<AgentMe, 'activeRequest' | 'activeRequests'>): ActiveRequest[];
|
|
200
|
+
export type AgentRequest = {
|
|
201
|
+
id: string;
|
|
202
|
+
buyer?: string;
|
|
203
|
+
buyerId?: string;
|
|
204
|
+
repo?: string | null;
|
|
205
|
+
file?: string | null;
|
|
206
|
+
modelLabel?: string | null;
|
|
207
|
+
quant?: string | null;
|
|
208
|
+
paramB?: number | null;
|
|
209
|
+
layers?: number | null;
|
|
210
|
+
needsMb?: number | null;
|
|
211
|
+
hours?: number | null;
|
|
212
|
+
pricePerHourNanos?: number | null;
|
|
213
|
+
totalPayoutNanos?: number | null;
|
|
214
|
+
minContext?: number;
|
|
215
|
+
minTokS?: number;
|
|
216
|
+
status: string;
|
|
217
|
+
createdAt?: number;
|
|
218
|
+
timerStartedAt?: number | null;
|
|
219
|
+
timerEndsAt?: number | null;
|
|
220
|
+
fit?: string | null;
|
|
221
|
+
cached?: boolean;
|
|
222
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** The shape of a request id the server issues. Frames and status rows with any other id are ignored. */
|
|
2
|
+
export const REQUEST_ID_PATTERN = /^[A-Za-z0-9_-]{3,80}$/;
|
|
3
|
+
export function isRequestId(value) {
|
|
4
|
+
return typeof value === 'string' && REQUEST_ID_PATTERN.test(value);
|
|
5
|
+
}
|
|
6
|
+
/** A finite number from the server or state.json, or null. Numeric strings count; NaN, Infinity, booleans and objects do not. */
|
|
7
|
+
export function finiteNumber(value) {
|
|
8
|
+
if (typeof value !== 'number' && (typeof value !== 'string' || !value.trim()))
|
|
9
|
+
return null;
|
|
10
|
+
const number = Number(value);
|
|
11
|
+
return Number.isFinite(number) ? number : null;
|
|
12
|
+
}
|
|
13
|
+
/** A positive time in ms that `Date` can represent, or null. */
|
|
14
|
+
export function validTime(value) {
|
|
15
|
+
const time = finiteNumber(value);
|
|
16
|
+
return time != null && time > 0 && time <= 8.64e15 ? time : null;
|
|
17
|
+
}
|
|
18
|
+
const ROW_NUMBERS = ['hours', 'pricePerHourNanos', 'minTokS', 'tokS', 'escrowRemainingNanos'];
|
|
19
|
+
const ROW_TIMES = ['timerStartedAt', 'timerEndsAt'];
|
|
20
|
+
/** A status row with checked types. A field the server left out stays undefined; a broken one becomes null. */
|
|
21
|
+
function normalizeRow(raw) {
|
|
22
|
+
const row = { ...raw, status: typeof raw.status === 'string' ? raw.status : '' };
|
|
23
|
+
for (const key of ROW_NUMBERS)
|
|
24
|
+
if (raw[key] !== undefined)
|
|
25
|
+
row[key] = finiteNumber(raw[key]);
|
|
26
|
+
for (const key of ROW_TIMES)
|
|
27
|
+
if (raw[key] !== undefined)
|
|
28
|
+
row[key] = validTime(raw[key]);
|
|
29
|
+
for (const key of ['file', 'modelLabel'])
|
|
30
|
+
if (raw[key] != null && typeof raw[key] !== 'string')
|
|
31
|
+
row[key] = null;
|
|
32
|
+
if (raw.shared != null)
|
|
33
|
+
row.shared = raw.shared === true;
|
|
34
|
+
return row;
|
|
35
|
+
}
|
|
36
|
+
/** Every live request in a /api/agent/me answer: `activeRequests`, or the single `activeRequest` from an older server. */
|
|
37
|
+
export function activeRequestsOf(me) {
|
|
38
|
+
const list = Array.isArray(me.activeRequests) ? me.activeRequests : me.activeRequest ? [me.activeRequest] : [];
|
|
39
|
+
return list
|
|
40
|
+
.filter((row) => Boolean(row && typeof row === 'object' && isRequestId(row.id)))
|
|
41
|
+
.map(normalizeRow);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type ProgressOptions = {
|
|
2
|
+
label: string;
|
|
3
|
+
total: number;
|
|
4
|
+
width?: number;
|
|
5
|
+
stream?: NodeJS.WriteStream;
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
showEveryMs?: number;
|
|
8
|
+
};
|
|
9
|
+
export declare class ProgressBar {
|
|
10
|
+
private readonly stream;
|
|
11
|
+
private readonly enabled;
|
|
12
|
+
private readonly width;
|
|
13
|
+
private readonly label;
|
|
14
|
+
private total;
|
|
15
|
+
private current;
|
|
16
|
+
private startedAt;
|
|
17
|
+
private lastRender;
|
|
18
|
+
private lastLine;
|
|
19
|
+
private finished;
|
|
20
|
+
constructor(options: ProgressOptions);
|
|
21
|
+
private lastTickMs;
|
|
22
|
+
setTotal(total: number): void;
|
|
23
|
+
tick(delta: number, total?: number): void;
|
|
24
|
+
set(current: number, total?: number): void;
|
|
25
|
+
line(): string;
|
|
26
|
+
render(force: boolean): void;
|
|
27
|
+
done(message?: string): void;
|
|
28
|
+
fail(message?: string): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { formatBytes, formatBytesPerSec, formatDuration } from '../util/format.js';
|
|
2
|
+
export class ProgressBar {
|
|
3
|
+
stream;
|
|
4
|
+
enabled;
|
|
5
|
+
width;
|
|
6
|
+
label;
|
|
7
|
+
total;
|
|
8
|
+
current = 0;
|
|
9
|
+
startedAt = Date.now();
|
|
10
|
+
lastRender = 0;
|
|
11
|
+
lastLine = '';
|
|
12
|
+
finished = false;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.stream = options.stream ?? process.stderr;
|
|
15
|
+
this.enabled = options.enabled ?? Boolean(this.stream.isTTY);
|
|
16
|
+
this.width = options.width ?? 24;
|
|
17
|
+
this.label = options.label;
|
|
18
|
+
this.total = Math.max(0, options.total);
|
|
19
|
+
this.lastTickMs = options.showEveryMs ?? 100;
|
|
20
|
+
this.startedAt = Date.now();
|
|
21
|
+
}
|
|
22
|
+
lastTickMs;
|
|
23
|
+
setTotal(total) {
|
|
24
|
+
this.total = Math.max(0, total);
|
|
25
|
+
}
|
|
26
|
+
tick(delta, total) {
|
|
27
|
+
if (total != null)
|
|
28
|
+
this.total = total;
|
|
29
|
+
this.current += Math.max(0, delta);
|
|
30
|
+
this.render(false);
|
|
31
|
+
}
|
|
32
|
+
set(current, total) {
|
|
33
|
+
this.current = Math.max(0, current);
|
|
34
|
+
if (total != null)
|
|
35
|
+
this.total = Math.max(0, total);
|
|
36
|
+
this.render(false);
|
|
37
|
+
}
|
|
38
|
+
line() {
|
|
39
|
+
const ratio = this.total > 0 ? Math.min(1, this.current / this.total) : 0;
|
|
40
|
+
const filled = Math.round(ratio * this.width);
|
|
41
|
+
const bar = `${'='.repeat(filled)}${' '.repeat(this.width - filled)}`;
|
|
42
|
+
const elapsed = Math.max(0.001, (Date.now() - this.startedAt) / 1000);
|
|
43
|
+
const speed = this.current / elapsed;
|
|
44
|
+
const eta = speed > 0 && this.total > 0 ? Math.max(0, (this.total - this.current) / speed) : null;
|
|
45
|
+
const percent = this.total > 0 ? ratio * 100 : 0;
|
|
46
|
+
return `${this.label} ${percent.toFixed(0).padStart(3)}% [${bar}] ${formatBytes(this.current)}${this.total ? ` / ${formatBytes(this.total)}` : ''} ${formatBytesPerSec(speed)} ETA ${formatDuration(eta)}`;
|
|
47
|
+
}
|
|
48
|
+
render(force) {
|
|
49
|
+
if (this.finished)
|
|
50
|
+
return;
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
if (!force && now - this.lastRender < this.lastTickMs)
|
|
53
|
+
return;
|
|
54
|
+
this.lastRender = now;
|
|
55
|
+
if (!this.enabled)
|
|
56
|
+
return;
|
|
57
|
+
const line = this.line();
|
|
58
|
+
if (line === this.lastLine)
|
|
59
|
+
return;
|
|
60
|
+
this.lastLine = line;
|
|
61
|
+
this.stream.write(`\r\x1b[2K${line}`);
|
|
62
|
+
}
|
|
63
|
+
done(message) {
|
|
64
|
+
if (this.finished)
|
|
65
|
+
return;
|
|
66
|
+
this.finished = true;
|
|
67
|
+
if (this.enabled) {
|
|
68
|
+
this.stream.write(`\r\x1b[2K${this.line()}\n`);
|
|
69
|
+
if (message)
|
|
70
|
+
this.stream.write(`${message}\n`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
fail(message) {
|
|
74
|
+
if (this.finished)
|
|
75
|
+
return;
|
|
76
|
+
this.finished = true;
|
|
77
|
+
if (this.enabled) {
|
|
78
|
+
this.stream.write(`\r\x1b[2K${this.label} failed${message ? `: ${message}` : ''}\n`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=progress.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export type DashboardStatus = 'starting' | 'online' | 'working' | 'reconnecting' | 'offline' | 'auth-failed' | 'outdated' | 'paused';
|
|
2
|
+
export type DashboardJob = {
|
|
3
|
+
kind: 'request' | 'public';
|
|
4
|
+
id?: string | null;
|
|
5
|
+
model?: string | null;
|
|
6
|
+
verified?: boolean;
|
|
7
|
+
timerStartedAt?: number | null;
|
|
8
|
+
timerEndsAt?: number | null;
|
|
9
|
+
minTokS?: number | null;
|
|
10
|
+
guaranteedNanos?: number | null;
|
|
11
|
+
hours?: number | null;
|
|
12
|
+
/** A speed seat that shares the GPU. */
|
|
13
|
+
shared?: boolean;
|
|
14
|
+
/** Reserved speed of a seat; null means the full GPU. */
|
|
15
|
+
tokS?: number | null;
|
|
16
|
+
};
|
|
17
|
+
export type DashboardState = {
|
|
18
|
+
status: DashboardStatus;
|
|
19
|
+
username: string | null;
|
|
20
|
+
endpoint: string | null;
|
|
21
|
+
server: string;
|
|
22
|
+
reliability: number | null;
|
|
23
|
+
held: boolean;
|
|
24
|
+
gpuName: string | null;
|
|
25
|
+
totalVramMb: number;
|
|
26
|
+
freeVramMb: number;
|
|
27
|
+
gpuTempC: number | null;
|
|
28
|
+
gpuPowerW: number | null;
|
|
29
|
+
gpuUtilPct: number | null;
|
|
30
|
+
wakeLock: boolean;
|
|
31
|
+
llamaStatus: string;
|
|
32
|
+
/** The request in focus: the one loading or being checked, else the booking that ends first. */
|
|
33
|
+
job: DashboardJob | null;
|
|
34
|
+
/** Every booking on this host, soonest end first. */
|
|
35
|
+
bookings?: DashboardJob[];
|
|
36
|
+
liveTokPerSec: number | null;
|
|
37
|
+
/** Per-token earnings plus the time-proportional share of every booking. */
|
|
38
|
+
sessionEarnedNanos: number;
|
|
39
|
+
sessionGuaranteedNanos: number;
|
|
40
|
+
/** The per-token part of sessionEarnedNanos. */
|
|
41
|
+
tokenEarnedNanos?: number;
|
|
42
|
+
publicMode?: boolean;
|
|
43
|
+
/** Public mode prices, shown on the dashboard. */
|
|
44
|
+
priceOutUsd?: number | null;
|
|
45
|
+
priceHourUsd?: number | null;
|
|
46
|
+
walletNanos: number | null;
|
|
47
|
+
lastHeartbeatAt: number | null;
|
|
48
|
+
message: string | null;
|
|
49
|
+
showLogs?: boolean;
|
|
50
|
+
logTail?: string[];
|
|
51
|
+
keyHint?: string | null;
|
|
52
|
+
};
|
|
53
|
+
export declare function initialState(server: string): DashboardState;
|
|
54
|
+
/** "50 tok/s" for a speed seat, "full GPU" for an exclusive rental. */
|
|
55
|
+
export declare function rentalLabel(job: DashboardJob): string;
|
|
56
|
+
/** "2 rentals: 50 tok/s + full GPU"; with more than three, "4 rentals, 150 tok/s reserved". */
|
|
57
|
+
export declare function rentalsSummary(rentals: DashboardJob[]): string;
|
|
58
|
+
export declare function renderFrame(state: DashboardState, options?: {
|
|
59
|
+
width?: number;
|
|
60
|
+
color?: boolean;
|
|
61
|
+
unicode?: boolean;
|
|
62
|
+
}): string;
|
|
63
|
+
export declare class TerminalDashboard {
|
|
64
|
+
private state;
|
|
65
|
+
private readonly stream;
|
|
66
|
+
private readonly color;
|
|
67
|
+
private readonly unicode;
|
|
68
|
+
private readonly intervalMs;
|
|
69
|
+
private readonly enabled;
|
|
70
|
+
private timer;
|
|
71
|
+
private started;
|
|
72
|
+
private lastFrame;
|
|
73
|
+
private lastStatus;
|
|
74
|
+
constructor(state: DashboardState, options?: {
|
|
75
|
+
stream?: NodeJS.WriteStream;
|
|
76
|
+
color?: boolean;
|
|
77
|
+
unicode?: boolean;
|
|
78
|
+
intervalMs?: number;
|
|
79
|
+
enabled?: boolean;
|
|
80
|
+
});
|
|
81
|
+
get current(): DashboardState;
|
|
82
|
+
start(): void;
|
|
83
|
+
update(partial: Partial<DashboardState>): void;
|
|
84
|
+
setJob(job: DashboardJob | null): void;
|
|
85
|
+
private printOnce;
|
|
86
|
+
private rerender;
|
|
87
|
+
stop(): void;
|
|
88
|
+
}
|