@emesoft/tokenhub 0.3.0 → 0.3.1
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/lib/dashboard.js +108 -30
- package/package.json +1 -1
package/lib/dashboard.js
CHANGED
|
@@ -151,13 +151,16 @@ function blockedText(report, timeZone) {
|
|
|
151
151
|
return `BLOCKED: ${period} budget reached${lifts}.`;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
const MAX_MODEL_WIDTH = 24;
|
|
155
|
+
const MIN_MODEL_WIDTH = 5;
|
|
156
|
+
|
|
154
157
|
/**
|
|
155
|
-
*
|
|
156
|
-
* @param {number}
|
|
157
|
-
* @returns {
|
|
158
|
+
* Format a request cost at the precision the pool logs it, so small amounts are never rounded away.
|
|
159
|
+
* @param {number} amount USD.
|
|
160
|
+
* @returns {string} For example `$0.0435`.
|
|
158
161
|
*/
|
|
159
|
-
function
|
|
160
|
-
return {
|
|
162
|
+
function exactMoney(amount) {
|
|
163
|
+
return `$${(Number(amount) || 0).toFixed(4)}`;
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
/**
|
|
@@ -170,37 +173,116 @@ function statusTone(status) {
|
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
/**
|
|
173
|
-
*
|
|
176
|
+
* Choose the table columns: sized to the content, then thinned to fit the terminal.
|
|
177
|
+
* Drops In/Out first, then Latency, then shrinks the model column.
|
|
178
|
+
* @param {number} columns Terminal width.
|
|
179
|
+
* @param {Record<string, any>[]} recent Requests to show (the model column fits the longest name, capped).
|
|
180
|
+
* @returns {{ id: string, title: string, width: number, right: boolean }[]} Visible columns in order.
|
|
181
|
+
*/
|
|
182
|
+
function tableColumns(columns, recent) {
|
|
183
|
+
const model = Math.min(MAX_MODEL_WIDTH, Math.max(MIN_MODEL_WIDTH, ...recent.map((request) => String(request.model ?? "-").length)));
|
|
184
|
+
let cols = [
|
|
185
|
+
{ id: "time", title: "Time", width: 8, right: false },
|
|
186
|
+
{ id: "model", title: "Model", width: model, right: false },
|
|
187
|
+
{ id: "status", title: "Status", width: 6, right: false },
|
|
188
|
+
{ id: "latency", title: "Latency", width: 7, right: true },
|
|
189
|
+
{ id: "in", title: "In", width: 6, right: true },
|
|
190
|
+
{ id: "out", title: "Out", width: 6, right: true },
|
|
191
|
+
{ id: "cost", title: "Cost", width: 9, right: true },
|
|
192
|
+
];
|
|
193
|
+
for (const drop of [["in", "out"], ["latency"]]) {
|
|
194
|
+
if (tableWidth(cols) <= columns - 1) break;
|
|
195
|
+
cols = cols.filter((col) => !drop.includes(col.id));
|
|
196
|
+
}
|
|
197
|
+
const excess = tableWidth(cols) - (columns - 1);
|
|
198
|
+
if (excess > 0) {
|
|
199
|
+
const modelCol = cols.find((col) => col.id === "model");
|
|
200
|
+
modelCol.width = Math.max(MIN_MODEL_WIDTH, modelCol.width - excess);
|
|
201
|
+
}
|
|
202
|
+
return cols;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Total width of a bordered table.
|
|
207
|
+
* @param {{ width: number }[]} cols Columns.
|
|
208
|
+
* @returns {number} Characters per line, borders and padding included.
|
|
209
|
+
*/
|
|
210
|
+
function tableWidth(cols) {
|
|
211
|
+
return cols.reduce((sum, col) => sum + col.width + 3, 1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Draw a horizontal table border.
|
|
216
|
+
* @param {{ width: number }[]} cols Columns.
|
|
217
|
+
* @param {[string, string, string]} corners Left, junction and right characters.
|
|
218
|
+
* @param {boolean} color Whether to emit colour.
|
|
219
|
+
* @returns {string} One grey line.
|
|
220
|
+
*/
|
|
221
|
+
function border(cols, [left, mid, right], color) {
|
|
222
|
+
return paint(color, "grey", `${left}${cols.map((col) => "─".repeat(col.width + 2)).join(mid)}${right}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Draw one table line from already padded cells.
|
|
227
|
+
* @param {string[]} cells Cell text, each exactly its column width (colour codes allowed).
|
|
228
|
+
* @param {boolean} color Whether to emit colour.
|
|
229
|
+
* @returns {string} One line with grey vertical borders.
|
|
230
|
+
*/
|
|
231
|
+
function tableLine(cells, color) {
|
|
232
|
+
const bar = paint(color, "grey", "│");
|
|
233
|
+
return `${bar}${cells.map((cell) => ` ${cell} `).join(bar)}${bar}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Pad text to a column width on its alignment side.
|
|
238
|
+
* @param {string} text Plain text, already no longer than the column.
|
|
239
|
+
* @param {{ width: number, right: boolean }} col Column.
|
|
240
|
+
* @returns {string} Text of exactly `col.width` characters.
|
|
241
|
+
*/
|
|
242
|
+
function fit(text, col) {
|
|
243
|
+
return col.right ? text.padStart(col.width) : text.padEnd(col.width);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Render one request as table cells.
|
|
174
248
|
* @param {Record<string, any>} request Entry from `/member/api/recent`.
|
|
175
|
-
* @param {{
|
|
176
|
-
* @param {number} modelWidth Width reserved for the model name.
|
|
249
|
+
* @param {{ id: string, width: number, right: boolean }[]} cols Visible columns.
|
|
177
250
|
* @param {boolean} color Whether to emit colour.
|
|
178
251
|
* @param {string | undefined} timeZone IANA zone.
|
|
179
|
-
* @returns {string} One
|
|
252
|
+
* @returns {string[]} One padded cell per column.
|
|
180
253
|
*/
|
|
181
|
-
function
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
254
|
+
function requestCells(request, cols, color, timeZone) {
|
|
255
|
+
const text = {
|
|
256
|
+
time: clock(request.started_at, timeZone),
|
|
257
|
+
model: String(request.model ?? "-"),
|
|
258
|
+
status: String(request.status),
|
|
259
|
+
latency: latency(request.latency_ms),
|
|
260
|
+
in: tokens(request.input_tokens),
|
|
261
|
+
out: tokens(request.output_tokens),
|
|
262
|
+
cost: exactMoney(request.usd),
|
|
263
|
+
};
|
|
264
|
+
return cols.map((col) => {
|
|
265
|
+
const cell = fit(truncate(text[col.id], col.width), col);
|
|
266
|
+
return col.id === "status" ? paint(color, statusTone(request.status), cell) : cell;
|
|
267
|
+
});
|
|
188
268
|
}
|
|
189
269
|
|
|
190
270
|
/**
|
|
191
271
|
* Render the whole dashboard as text. Pure: no I/O, so it can be tested without a terminal.
|
|
192
272
|
* @param {{ report: Record<string, any> | null, recent: Record<string, any>[], refreshedAt: number | null, error: string | null, intervalSec: number, poolHost?: string, timeZone?: string }} state Latest poll result.
|
|
193
273
|
* @param {{ columns: number, rows: number, color: boolean, now: number, fill?: boolean }} view Terminal size, colour switch, current epoch ms and whether to pad to `rows` so the footer sits on the last row (default true).
|
|
194
|
-
* @returns {string} Lines joined with `\n`, each at most `columns` wide, at most `rows` lines, no trailing newline.
|
|
274
|
+
* @returns {string} Lines joined with `\n`, each at most `columns - 1` wide (the last column is left free: some terminals clip it), at most `rows` lines, no trailing newline.
|
|
195
275
|
*/
|
|
196
276
|
export function renderFrame(state, { columns, rows, color, now, fill = true }) {
|
|
197
277
|
const { report, recent, refreshedAt, error, intervalSec, timeZone } = state;
|
|
278
|
+
const cols = tableColumns(columns, recent);
|
|
279
|
+
const width = tableWidth(cols);
|
|
198
280
|
const lines = [];
|
|
199
281
|
const stale = refreshedAt !== null && now - refreshedAt > intervalSec * 2000 + 1000;
|
|
200
282
|
const refreshed = refreshedAt === null ? "waiting for first refresh" : `refreshed ${clock(refreshedAt, timeZone)}${stale ? " (stale)" : ""}`;
|
|
201
283
|
const title = `${report ? report.member : "TokenHub"}${state.poolHost ? ` @ ${state.poolHost}` : ""}`;
|
|
202
284
|
const right = `${refreshed} · every ${intervalSec}s`;
|
|
203
|
-
lines.push(`${paint(color, "bold", title)}${" ".repeat(Math.max(2,
|
|
285
|
+
lines.push(`${paint(color, "bold", title)}${" ".repeat(Math.max(2, width - title.length - right.length))}${paint(color, "grey", right)}`);
|
|
204
286
|
lines.push("");
|
|
205
287
|
|
|
206
288
|
if (report) {
|
|
@@ -216,23 +298,19 @@ export function renderFrame(state, { columns, rows, color, now, fill = true }) {
|
|
|
216
298
|
lines.push("");
|
|
217
299
|
|
|
218
300
|
const footer = error ? paint(color, "red", `reconnecting… ${error}`) : paint(color, "grey", "q quit · r refresh now");
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
if (layout.latency) header.push("Latency".padStart(7));
|
|
224
|
-
if (layout.tokens) header.push("In".padStart(7), "Out".padStart(7));
|
|
225
|
-
header.push("$".padStart(8));
|
|
226
|
-
lines.push(paint(color, "grey", header.join(" ")));
|
|
227
|
-
const room = Math.max(0, rows - lines.length - 1);
|
|
301
|
+
lines.push(border(cols, ["┌", "┬", "┐"], color));
|
|
302
|
+
lines.push(tableLine(cols.map((col) => paint(color, "bold", fit(col.title, col))), color));
|
|
303
|
+
lines.push(border(cols, ["├", "┼", "┤"], color));
|
|
304
|
+
const room = Math.max(0, rows - lines.length - 2);
|
|
228
305
|
if (recent.length === 0) {
|
|
229
|
-
if (room > 0) lines.push(paint(color, "grey", "No requests yet."));
|
|
306
|
+
if (room > 0) lines.push(tableLine([paint(color, "grey", "No requests yet.".padEnd(width - 4))], color));
|
|
230
307
|
} else {
|
|
231
|
-
for (const request of recent.slice(0, room)) lines.push(
|
|
308
|
+
for (const request of recent.slice(0, room)) lines.push(tableLine(requestCells(request, cols, color, timeZone), color));
|
|
232
309
|
}
|
|
310
|
+
lines.push(border(cols, ["└", "┴", "┘"], color));
|
|
233
311
|
while (fill && lines.length < rows - 1) lines.push("");
|
|
234
312
|
lines.push(footer);
|
|
235
|
-
return lines.slice(-Math.max(rows, 1)).map((line) => clip(line, columns)).join("\n");
|
|
313
|
+
return lines.slice(-Math.max(rows, 1)).map((line) => clip(line, Math.max(columns - 1, 1))).join("\n");
|
|
236
314
|
}
|
|
237
315
|
|
|
238
316
|
/**
|