@v1nvn/zai 0.36.0 → 0.36.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/README.md +17 -17
- package/dist/index.js +144 -108
- package/dist/index.js.map +1 -1
- package/dist/usage.mjs +451 -415
- package/dist/usage.mjs.map +1 -1
- package/package.json +3 -3
package/dist/usage.mjs
CHANGED
|
@@ -23,6 +23,400 @@ function printUsageAndExit(program) {
|
|
|
23
23
|
}
|
|
24
24
|
Number.MAX_SAFE_INTEGER;
|
|
25
25
|
//#endregion
|
|
26
|
+
//#region ../tokens/dist/text.js
|
|
27
|
+
/**
|
|
28
|
+
* Plain-text rendering primitives for the fixed-width reports. Output targets
|
|
29
|
+
* a monospace terminal — the pane — so everything here is fixed-width:
|
|
30
|
+
* padding, block-glyph bars, and compact number formatting.
|
|
31
|
+
*/
|
|
32
|
+
var MONTHS = [
|
|
33
|
+
"Jan",
|
|
34
|
+
"Feb",
|
|
35
|
+
"Mar",
|
|
36
|
+
"Apr",
|
|
37
|
+
"May",
|
|
38
|
+
"Jun",
|
|
39
|
+
"Jul",
|
|
40
|
+
"Aug",
|
|
41
|
+
"Sep",
|
|
42
|
+
"Oct",
|
|
43
|
+
"Nov",
|
|
44
|
+
"Dec"
|
|
45
|
+
];
|
|
46
|
+
var EIGHTHS = [
|
|
47
|
+
"",
|
|
48
|
+
"▏",
|
|
49
|
+
"▎",
|
|
50
|
+
"▍",
|
|
51
|
+
"▌",
|
|
52
|
+
"▋",
|
|
53
|
+
"▊",
|
|
54
|
+
"▉"
|
|
55
|
+
];
|
|
56
|
+
function rule() {
|
|
57
|
+
return "─".repeat(68);
|
|
58
|
+
}
|
|
59
|
+
function pad2(n) {
|
|
60
|
+
return String(n).padStart(2, "0");
|
|
61
|
+
}
|
|
62
|
+
function fmtTokens(n) {
|
|
63
|
+
if (n == null || Number.isNaN(n)) return "—";
|
|
64
|
+
if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
|
|
65
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
|
66
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
|
|
67
|
+
return String(n);
|
|
68
|
+
}
|
|
69
|
+
function fmtNum(n) {
|
|
70
|
+
return (n || 0).toLocaleString("en-US");
|
|
71
|
+
}
|
|
72
|
+
function padR(s, n) {
|
|
73
|
+
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
74
|
+
}
|
|
75
|
+
function padL(s, n) {
|
|
76
|
+
return s.length >= n ? s : " ".repeat(n - s.length) + s;
|
|
77
|
+
}
|
|
78
|
+
/** Fixed-width bar field (width cols): █ blocks + an eighth-fraction + trailing spaces. */
|
|
79
|
+
function barField(v, max, width) {
|
|
80
|
+
if (!v || v <= 0 || max <= 0) return " ".repeat(width);
|
|
81
|
+
const scaled = v / max * width;
|
|
82
|
+
let full = Math.floor(scaled);
|
|
83
|
+
let fi = Math.round((scaled - full) * 8);
|
|
84
|
+
if (fi === 8) {
|
|
85
|
+
full += 1;
|
|
86
|
+
fi = 0;
|
|
87
|
+
}
|
|
88
|
+
if (full === 0 && fi === 0) fi = 1;
|
|
89
|
+
let s = "█".repeat(Math.min(full, width));
|
|
90
|
+
if (full < width && fi > 0) s += EIGHTHS[fi] ?? "";
|
|
91
|
+
if (s.length < width) s += " ".repeat(width - s.length);
|
|
92
|
+
return s.slice(0, width);
|
|
93
|
+
}
|
|
94
|
+
/** Filled/empty meter: █ for used, ░ for remaining. */
|
|
95
|
+
function meter(pct, width) {
|
|
96
|
+
let filled = Math.round((pct || 0) / 100 * width);
|
|
97
|
+
filled = Math.max(0, Math.min(width, filled));
|
|
98
|
+
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/format.ts
|
|
102
|
+
/**
|
|
103
|
+
* Usage-report line model: `reportLines` lays the report out once as ink-tagged
|
|
104
|
+
* segments (the pane draws those), and `render` joins them into the CLI's
|
|
105
|
+
* monospace terminal / hook-block `reason` bytes — no markdown; alignment comes
|
|
106
|
+
* from fixed-width columns and unicode block glyphs. Input is the parsed `data`
|
|
107
|
+
* of the three ZAI/ZHIPU monitor endpoints (model-usage, tool-usage,
|
|
108
|
+
* quota/limit — the last already passed through processQuotaLimit).
|
|
109
|
+
*/
|
|
110
|
+
var VBLOCKS = "▁▂▃▄▅▆▇";
|
|
111
|
+
function parseSlot(s) {
|
|
112
|
+
const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/.exec(s ?? "");
|
|
113
|
+
if (!m) return null;
|
|
114
|
+
return {
|
|
115
|
+
h: +m[4],
|
|
116
|
+
day: `${MONTHS[+m[2] - 1]} ${m[3]}`,
|
|
117
|
+
time: `${m[4]}:${m[5]}`
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Re-express a naive "YYYY-MM-DD HH:MM" string from one UTC offset to another.
|
|
122
|
+
* Offsets are minutes east of UTC (Beijing = 480, IST = 330). Pure — does not
|
|
123
|
+
* depend on the runtime timezone, so it is deterministic under test.
|
|
124
|
+
*/
|
|
125
|
+
function shiftSlot(s, fromOffsetMin, toOffsetMin) {
|
|
126
|
+
const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/.exec(s);
|
|
127
|
+
if (!m) return null;
|
|
128
|
+
const utcMs = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]) - fromOffsetMin * 6e4;
|
|
129
|
+
const dt = new Date(utcMs + toOffsetMin * 6e4);
|
|
130
|
+
return {
|
|
131
|
+
h: dt.getUTCHours(),
|
|
132
|
+
day: `${MONTHS[dt.getUTCMonth()]} ${pad2(dt.getUTCDate())}`,
|
|
133
|
+
time: `${pad2(dt.getUTCHours())}:${pad2(dt.getUTCMinutes())}`
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function friendlyTool(code) {
|
|
137
|
+
switch ((code ?? "").toLowerCase()) {
|
|
138
|
+
case "search-prime": return "web search";
|
|
139
|
+
case "web-reader": return "web read";
|
|
140
|
+
default: return code || "other";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function idleRuns(tok) {
|
|
144
|
+
const runs = [];
|
|
145
|
+
let i = 0;
|
|
146
|
+
while (i < tok.length) if ((tok[i] || 0) === 0) {
|
|
147
|
+
let j = i;
|
|
148
|
+
while (j < tok.length && (tok[j] || 0) === 0) j++;
|
|
149
|
+
runs.push({
|
|
150
|
+
i,
|
|
151
|
+
j: j - 1,
|
|
152
|
+
len: j - i
|
|
153
|
+
});
|
|
154
|
+
i = j;
|
|
155
|
+
} else i++;
|
|
156
|
+
return runs;
|
|
157
|
+
}
|
|
158
|
+
function idleLabel(run, x, slotFn) {
|
|
159
|
+
const a = slotFn(x[run.i]);
|
|
160
|
+
const b = slotFn(x[run.j]);
|
|
161
|
+
if (!a || !b) return `idle (${run.len}h)`;
|
|
162
|
+
return `idle ${`${a.day} ${a.time}`} → ${a.day === b.day ? b.time : `${b.day} ${b.time}`} (${run.len}h)`;
|
|
163
|
+
}
|
|
164
|
+
function isPeakBucket(s) {
|
|
165
|
+
const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):/.exec(s ?? "");
|
|
166
|
+
if (!m) return false;
|
|
167
|
+
const wd = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])).getUTCDay();
|
|
168
|
+
return wd >= 1 && wd <= 5 && +m[4] >= 14 && +m[4] <= 17;
|
|
169
|
+
}
|
|
170
|
+
/** Local-time start/end of the 14:00–18:00 Beijing peak window (e.g. 11:30 / 15:30). */
|
|
171
|
+
function localPeakWindow(apiOffsetMin, toOffsetMin) {
|
|
172
|
+
const a = shiftSlot("2000-01-03 14:00", apiOffsetMin, toOffsetMin);
|
|
173
|
+
const b = shiftSlot("2000-01-03 18:00", apiOffsetMin, toOffsetMin);
|
|
174
|
+
if (!a || !b) return null;
|
|
175
|
+
return {
|
|
176
|
+
start: a.time,
|
|
177
|
+
end: b.time
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Vertical bar chart of hourly tokens. Returns an array of plain-text lines
|
|
182
|
+
* (without the section header), or null if there are fewer than 2 buckets.
|
|
183
|
+
*
|
|
184
|
+
* Bars are scaled to `maxTok` across `ROWS` rows; the topmost filled cell of a
|
|
185
|
+
* bar uses a fractional lower-block glyph (▁▂▃▄▅▆▇) for sub-row height. The
|
|
186
|
+
* y-axis labels only the peak (top) and 0 (baseline); the header window and the
|
|
187
|
+
* caller's peak annotation carry the exact values.
|
|
188
|
+
*/
|
|
189
|
+
function hourlyVerticalChart({ x, tok, nh, maxTok, peakIdx, slotFn }) {
|
|
190
|
+
const ROWS = 6;
|
|
191
|
+
if (nh < 2 || maxTok <= 0) return null;
|
|
192
|
+
const lines = [];
|
|
193
|
+
const maxLab = fmtTokens(maxTok);
|
|
194
|
+
const L = maxLab.length;
|
|
195
|
+
const AXIS = 1 + L + 1;
|
|
196
|
+
const P0 = AXIS + 2;
|
|
197
|
+
const PW = 68 - P0;
|
|
198
|
+
const slot = Math.max(1, Math.floor(PW / nh));
|
|
199
|
+
const barW = slot >= 2 ? slot - 1 : 1;
|
|
200
|
+
function barLeft(i) {
|
|
201
|
+
return P0 + i * slot;
|
|
202
|
+
}
|
|
203
|
+
function barCenter(i) {
|
|
204
|
+
return barLeft(i) + Math.floor(barW / 2);
|
|
205
|
+
}
|
|
206
|
+
const usedCols = nh * slot;
|
|
207
|
+
function blank() {
|
|
208
|
+
return Array(68).fill(" ");
|
|
209
|
+
}
|
|
210
|
+
function glyph(i, r) {
|
|
211
|
+
const v = tok[i] || 0;
|
|
212
|
+
if (v <= 0) return " ";
|
|
213
|
+
const fill = v / maxTok * ROWS - r;
|
|
214
|
+
if (fill >= 1) return "█";
|
|
215
|
+
if (fill > 0) {
|
|
216
|
+
let e = Math.round(fill * 8);
|
|
217
|
+
if (e < 1) e = 1;
|
|
218
|
+
if (e > 7) e = 7;
|
|
219
|
+
return VBLOCKS[e - 1];
|
|
220
|
+
}
|
|
221
|
+
return " ";
|
|
222
|
+
}
|
|
223
|
+
for (let r = 5; r >= 0; r--) {
|
|
224
|
+
const line = blank();
|
|
225
|
+
line[AXIS] = r === 5 ? "┤" : "│";
|
|
226
|
+
if (r === 5) for (let k = 0; k < L; k++) line[AXIS - 1 - L + k] = maxLab[k];
|
|
227
|
+
for (let i = 0; i < nh; i++) {
|
|
228
|
+
const g = glyph(i, r);
|
|
229
|
+
if (g !== " ") for (let c = 0; c < barW; c++) line[barLeft(i) + c] = g;
|
|
230
|
+
}
|
|
231
|
+
lines.push(line.join("").trimEnd());
|
|
232
|
+
}
|
|
233
|
+
function slotOf(i) {
|
|
234
|
+
return slotFn(x[i]);
|
|
235
|
+
}
|
|
236
|
+
function isTick(i) {
|
|
237
|
+
if (i === 0 || i === nh - 1) return true;
|
|
238
|
+
const s = slotOf(i);
|
|
239
|
+
return !!s && s.h % 6 === 0;
|
|
240
|
+
}
|
|
241
|
+
const base = blank();
|
|
242
|
+
base[AXIS - 2] = "0";
|
|
243
|
+
base[AXIS] = "└";
|
|
244
|
+
for (let c = AXIS + 1; c < P0 + usedCols; c++) base[c] = "─";
|
|
245
|
+
for (let i = 0; i < nh; i++) if (isTick(i)) base[barCenter(i)] = "┬";
|
|
246
|
+
lines.push(base.join("").trimEnd());
|
|
247
|
+
const hr = blank();
|
|
248
|
+
let last = -10;
|
|
249
|
+
for (let i = 0; i < nh; i++) {
|
|
250
|
+
if (!isTick(i)) continue;
|
|
251
|
+
const col = barCenter(i);
|
|
252
|
+
if (col - last < 3) continue;
|
|
253
|
+
const s = slotOf(i);
|
|
254
|
+
const txt = s ? pad2(s.h) : " ";
|
|
255
|
+
hr[col] = txt[0];
|
|
256
|
+
hr[col + 1] = txt[1];
|
|
257
|
+
last = col;
|
|
258
|
+
}
|
|
259
|
+
lines.push(hr.join("").trimEnd());
|
|
260
|
+
const day = blank();
|
|
261
|
+
let lastDay = null;
|
|
262
|
+
last = -10;
|
|
263
|
+
for (let i = 0; i < nh; i++) {
|
|
264
|
+
if (!isTick(i)) continue;
|
|
265
|
+
const s = slotOf(i);
|
|
266
|
+
if (!s || s.day === lastDay) continue;
|
|
267
|
+
const col = barCenter(i);
|
|
268
|
+
if (col - last >= s.day.length && col + s.day.length <= 68) {
|
|
269
|
+
for (let k = 0; k < s.day.length; k++) day[col + k] = s.day[k];
|
|
270
|
+
lastDay = s.day;
|
|
271
|
+
last = col + s.day.length;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (day.some((c) => c !== " ")) lines.push(day.join("").trimEnd());
|
|
275
|
+
const mark = blank();
|
|
276
|
+
for (let i = 0; i < nh; i++) if (isPeakBucket(x[i])) mark[barCenter(i)] = "↑";
|
|
277
|
+
if (peakIdx >= 0) mark[barCenter(peakIdx)] = "◂";
|
|
278
|
+
if (mark.some((c) => c !== " ")) lines.push(mark.join("").trimEnd());
|
|
279
|
+
return lines;
|
|
280
|
+
}
|
|
281
|
+
function plain(text) {
|
|
282
|
+
return { text };
|
|
283
|
+
}
|
|
284
|
+
function dim(text) {
|
|
285
|
+
return {
|
|
286
|
+
text,
|
|
287
|
+
ink: "dim"
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function bold(text) {
|
|
291
|
+
return {
|
|
292
|
+
text,
|
|
293
|
+
ink: "bold"
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function reportLines({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetMin, now = /* @__PURE__ */ new Date() }) {
|
|
297
|
+
const out = [];
|
|
298
|
+
const toOffset = localOffsetMin != null ? localOffsetMin : -(/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
299
|
+
function convertSlot(s) {
|
|
300
|
+
return shiftSlot(s, apiOffsetMin, toOffset) ?? parseSlot(s);
|
|
301
|
+
}
|
|
302
|
+
const x = model.x_time ?? [];
|
|
303
|
+
const tok = model.tokensUsage ?? [];
|
|
304
|
+
const calls = model.modelCallCount ?? [];
|
|
305
|
+
const nh = x.length;
|
|
306
|
+
const tu = model.totalUsage ?? {};
|
|
307
|
+
const total = tu.totalTokensUsage != null ? tu.totalTokensUsage : tok.reduce((a, b) => a + (b || 0), 0);
|
|
308
|
+
const totalCalls = tu.totalModelCallCount != null ? tu.totalModelCallCount : calls.reduce((a, b) => a + (b || 0), 0);
|
|
309
|
+
let peakIdx = -1;
|
|
310
|
+
let peakTok = 0;
|
|
311
|
+
for (let i = 0; i < tok.length; i++) if (tok[i] > peakTok) {
|
|
312
|
+
peakTok = tok[i];
|
|
313
|
+
peakIdx = i;
|
|
314
|
+
}
|
|
315
|
+
const peakCalls = peakIdx >= 0 ? calls[peakIdx] || 0 : 0;
|
|
316
|
+
const pctPeak = total > 0 ? peakTok / total * 100 : 0;
|
|
317
|
+
const peakSlot = peakIdx >= 0 ? convertSlot(x[peakIdx]) : null;
|
|
318
|
+
const level = quota.level ? quota.level.charAt(0).toUpperCase() + quota.level.slice(1) : "";
|
|
319
|
+
const left = ` GLM Coding Plan${level ? " · " + level : ""}`;
|
|
320
|
+
const firstSlot = convertSlot(x[0]);
|
|
321
|
+
const lastSlot = convertSlot(x[nh - 1]);
|
|
322
|
+
const win = firstSlot && lastSlot ? `${firstSlot.day} ${firstSlot.time} → ${lastSlot.day} ${lastSlot.time} · ${nh}h` : platform;
|
|
323
|
+
out.push([plain(rule())]);
|
|
324
|
+
out.push([bold(left), dim(padL(win, 68 - left.length))]);
|
|
325
|
+
out.push([plain(rule())]);
|
|
326
|
+
const head = plain(` ${fmtTokens(total)} tokens across ${fmtNum(totalCalls)} model calls`);
|
|
327
|
+
out.push([]);
|
|
328
|
+
if (peakIdx >= 0 && peakSlot) {
|
|
329
|
+
out.push([head, plain(" — ")]);
|
|
330
|
+
out.push([bold(`${Math.round(pctPeak)}%`), plain(" of it in a single hour")]);
|
|
331
|
+
out.push([plain(` (${peakSlot.day} ${peakSlot.time}, ${fmtTokens(peakTok)} tokens / ${fmtNum(peakCalls)} calls).`)]);
|
|
332
|
+
} else out.push([head, plain(".")]);
|
|
333
|
+
const activeHours = tok.filter((t) => t > 0).length;
|
|
334
|
+
const longest = idleRuns(tok).filter((r) => r.len >= 2).sort((a, b) => b.len - a.len).at(0);
|
|
335
|
+
const tt = tool.totalUsage ?? {};
|
|
336
|
+
const searchN = tt.totalNetworkSearchCount || 0;
|
|
337
|
+
const readN = tt.totalWebReadMcpCount || 0;
|
|
338
|
+
const zreadN = tt.totalZreadMcpCount || 0;
|
|
339
|
+
const toolTotal = tt.totalSearchMcpCount != null ? tt.totalSearchMcpCount : searchN + readN + zreadN;
|
|
340
|
+
let peakWinActive = 0;
|
|
341
|
+
let peakWinTokens = 0;
|
|
342
|
+
for (let k = 0; k < nh; k++) if (isPeakBucket(x[k]) && tok[k] > 0) {
|
|
343
|
+
peakWinActive++;
|
|
344
|
+
peakWinTokens += tok[k];
|
|
345
|
+
}
|
|
346
|
+
const peakWinPct = total > 0 ? peakWinTokens / total * 100 : 0;
|
|
347
|
+
const peakWin = localPeakWindow(apiOffsetMin, toOffset);
|
|
348
|
+
out.push([]);
|
|
349
|
+
out.push([plain(` Peak ${padR(peakSlot ? peakSlot.day + " " + peakSlot.time : "—", 15)}${padL(fmtTokens(peakTok), 7)} tokens · ${padL(fmtNum(peakCalls), 5)} calls`)]);
|
|
350
|
+
out.push([plain(` Active ${padR(`${activeHours} / ${nh} hours`, 15)}${longest ? idleLabel(longest, x, convertSlot) : "no idle gaps"}`)]);
|
|
351
|
+
out.push([plain(` Tools ${padR(`${toolTotal} calls`, 15)}${searchN} searches · ${readN} reads${zreadN ? ` · ${zreadN} zread` : ""}`)]);
|
|
352
|
+
if (peakWin) out.push([
|
|
353
|
+
plain(` Peak hrs Mon–Fri ${peakWin.start}–${peakWin.end} · GLM-5.2 3× · ${peakWinActive}h active · ${fmtTokens(peakWinTokens)} (`),
|
|
354
|
+
bold(`${Math.round(peakWinPct)}%`),
|
|
355
|
+
plain(")")
|
|
356
|
+
]);
|
|
357
|
+
out.push([]);
|
|
358
|
+
out.push([dim(" Hourly tokens · ↑ peak hour " + "─".repeat(Math.max(0, 39)))]);
|
|
359
|
+
const chartLines = hourlyVerticalChart({
|
|
360
|
+
x,
|
|
361
|
+
tok,
|
|
362
|
+
nh,
|
|
363
|
+
maxTok: peakTok || 1,
|
|
364
|
+
peakIdx,
|
|
365
|
+
slotFn: convertSlot
|
|
366
|
+
});
|
|
367
|
+
if (chartLines) {
|
|
368
|
+
out.push(...chartLines.map((l) => [plain(l)]));
|
|
369
|
+
if (peakIdx >= 0 && peakSlot) out.push([plain(` ◂ peak ${peakSlot.day} ${peakSlot.time} ${fmtTokens(peakTok)} tokens · ${fmtNum(peakCalls)} calls`)]);
|
|
370
|
+
} else out.push([plain(" (not enough hourly data to chart)")]);
|
|
371
|
+
out.push([]);
|
|
372
|
+
out.push([dim(" Model mix " + "─".repeat(Math.max(0, 57)))]);
|
|
373
|
+
const mixSrc = model.modelSummaryList ?? model.modelDataList ?? [];
|
|
374
|
+
const denom = mixSrc.reduce((a, m) => a + (m.totalTokens ?? 0), 0) || 1;
|
|
375
|
+
const mixSorted = [...mixSrc].sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
|
376
|
+
for (const m of mixSorted) {
|
|
377
|
+
const pct = (m.totalTokens ?? 0) / denom * 100;
|
|
378
|
+
out.push([
|
|
379
|
+
plain(` ${padR(m.modelName || "?", 11)}${padL(fmtTokens(m.totalTokens), 8)} `),
|
|
380
|
+
bold(padL(pct.toFixed(1) + "%", 6)),
|
|
381
|
+
plain(` ${barField(pct, 100, 20)}`)
|
|
382
|
+
]);
|
|
383
|
+
}
|
|
384
|
+
out.push([]);
|
|
385
|
+
out.push([dim(" Limits " + "─".repeat(Math.max(0, 60)))]);
|
|
386
|
+
const bj = new Date(now.getTime() + apiOffsetMin * 6e4);
|
|
387
|
+
const bjDay = bj.getUTCDay();
|
|
388
|
+
const elapsedMin = bjDay >= 1 && bjDay <= 5 ? Math.max(0, Math.min(240, (bj.getUTCHours() - 14) * 60 + bj.getUTCMinutes())) : 0;
|
|
389
|
+
if (peakWin) out.push([dim(` ${padR("Peak", 16)}`), plain(`${padL(peakWin.start, 5)} ${meter(Math.round(elapsedMin / 240 * 100), 22)} ${peakWin.end}`)]);
|
|
390
|
+
const limits = quota.limits ?? [];
|
|
391
|
+
const mcp = limits.find((l) => /mcp/i.test(l.type ?? ""));
|
|
392
|
+
const tok5 = limits.find((l) => /token/i.test(l.type ?? ""));
|
|
393
|
+
if (tok5) {
|
|
394
|
+
const reset = new Date(tok5.nextResetTime ?? "").toLocaleTimeString("en-IN");
|
|
395
|
+
out.push([
|
|
396
|
+
dim(` ${padR("Tokens · 5h", 16)}`),
|
|
397
|
+
bold(padL(`${tok5.percentage || 0}%`, 5)),
|
|
398
|
+
plain(` ${meter(tok5.percentage, 22)} ${reset}`)
|
|
399
|
+
]);
|
|
400
|
+
}
|
|
401
|
+
if (tok5 && mcp) out.push([]);
|
|
402
|
+
if (mcp) {
|
|
403
|
+
const reset = new Date(mcp.nextResetTime ?? "").toLocaleString("en-IN");
|
|
404
|
+
out.push([
|
|
405
|
+
dim(` ${padR("MCP · this month", 16)}`),
|
|
406
|
+
bold(padL(`${mcp.percentage || 0}%`, 5)),
|
|
407
|
+
plain(` ${meter(mcp.percentage, 22)} ${reset}`)
|
|
408
|
+
]);
|
|
409
|
+
const parts = (mcp.usageDetails ?? []).map((d) => `${friendlyTool(d.modelCode)} ${fmtNum(d.usage)}`).join(" ");
|
|
410
|
+
if (parts) out.push([plain(` ${fmtNum(mcp.currentUsage)}M / ${fmtNum(mcp.totol)}M · ${parts}`)]);
|
|
411
|
+
}
|
|
412
|
+
out.push([]);
|
|
413
|
+
out.push([plain(rule())]);
|
|
414
|
+
return out;
|
|
415
|
+
}
|
|
416
|
+
function render(input) {
|
|
417
|
+
return reportLines(input).map((line) => line.map((seg) => seg.text).join("")).join("\n");
|
|
418
|
+
}
|
|
419
|
+
//#endregion
|
|
26
420
|
//#region ../../node_modules/commander/lib/error.js
|
|
27
421
|
/**
|
|
28
422
|
* CommanderError class
|
|
@@ -2980,431 +3374,72 @@ new Command();
|
|
|
2980
3374
|
//#endregion
|
|
2981
3375
|
//#region src/resolve.ts
|
|
2982
3376
|
var DEFAULT_BASE_URL = "https://api.z.ai";
|
|
2983
|
-
var GLM_HOSTS = /* @__PURE__ */ new Set([
|
|
2984
|
-
"api.z.ai",
|
|
2985
|
-
"dev.bigmodel.cn",
|
|
2986
|
-
"open.bigmodel.cn"
|
|
2987
|
-
]);
|
|
2988
|
-
function fromEnv(value) {
|
|
2989
|
-
return value ? value : void 0;
|
|
2990
|
-
}
|
|
2991
|
-
function originOf(value) {
|
|
2992
|
-
try {
|
|
2993
|
-
const url = new URL(value);
|
|
2994
|
-
return url.protocol === "https:" || url.protocol === "http:" ? url.origin : void 0;
|
|
2995
|
-
} catch {
|
|
2996
|
-
return;
|
|
2997
|
-
}
|
|
2998
|
-
}
|
|
2999
|
-
function isGlm(origin) {
|
|
3000
|
-
return GLM_HOSTS.has(new URL(origin).hostname);
|
|
3001
|
-
}
|
|
3002
|
-
function resolveBaseUrl(flag, env) {
|
|
3003
|
-
if (flag !== void 0) {
|
|
3004
|
-
const origin = originOf(flag);
|
|
3005
|
-
if (origin === void 0) throw new Error(`invalid base URL: ${flag}`);
|
|
3006
|
-
return {
|
|
3007
|
-
glm: isGlm(origin),
|
|
3008
|
-
origin
|
|
3009
|
-
};
|
|
3010
|
-
}
|
|
3011
|
-
const inherited = fromEnv(env.ANTHROPIC_BASE_URL);
|
|
3012
|
-
const origin = inherited === void 0 ? void 0 : originOf(inherited);
|
|
3013
|
-
if (origin !== void 0 && isGlm(origin)) return {
|
|
3014
|
-
glm: true,
|
|
3015
|
-
origin
|
|
3016
|
-
};
|
|
3017
|
-
return {
|
|
3018
|
-
glm: false,
|
|
3019
|
-
origin: DEFAULT_BASE_URL
|
|
3020
|
-
};
|
|
3021
|
-
}
|
|
3022
|
-
function buildProgram() {
|
|
3023
|
-
return new Command().name("zai-usage").addOption(new Option("--auth-token <token>", "API key").env("ZAI_AUTH_TOKEN")).addOption(new Option("--base-url <url>", "base URL").env("ZAI_BASE_URL"));
|
|
3024
|
-
}
|
|
3025
|
-
function parseArgs(args) {
|
|
3026
|
-
const program = parseQuietly(buildProgram(), args);
|
|
3027
|
-
if (program === void 0) return;
|
|
3028
|
-
const { authToken, baseUrl } = program.opts();
|
|
3029
|
-
return {
|
|
3030
|
-
authToken: authToken || void 0,
|
|
3031
|
-
baseUrl: baseUrl || void 0
|
|
3032
|
-
};
|
|
3033
|
-
}
|
|
3034
|
-
function resolveConfig(env, parsed) {
|
|
3035
|
-
const base = resolveBaseUrl(parsed.baseUrl, env);
|
|
3036
|
-
const token = parsed.authToken ?? (base.glm ? fromEnv(env.ANTHROPIC_AUTH_TOKEN) : void 0);
|
|
3037
|
-
if (token === void 0) throw new Error(env.ANTHROPIC_AUTH_TOKEN ? "ANTHROPIC_AUTH_TOKEN is set, but the resolved base URL does not name a GLM host, so it is not a GLM Coding Plan token — set ZAI_AUTH_TOKEN (or --auth-token)" : "no API key: set ZAI_AUTH_TOKEN (or --auth-token)");
|
|
3038
|
-
return {
|
|
3039
|
-
token,
|
|
3040
|
-
url: base.origin
|
|
3041
|
-
};
|
|
3042
|
-
}
|
|
3043
|
-
//#endregion
|
|
3044
|
-
//#region ../tokens/dist/text.js
|
|
3045
|
-
/**
|
|
3046
|
-
* Plain-text rendering primitives for the fixed-width reports. Output targets
|
|
3047
|
-
* a monospace terminal — the pane — so everything here is fixed-width:
|
|
3048
|
-
* padding, block-glyph bars, and compact number formatting.
|
|
3049
|
-
*/
|
|
3050
|
-
var MONTHS = [
|
|
3051
|
-
"Jan",
|
|
3052
|
-
"Feb",
|
|
3053
|
-
"Mar",
|
|
3054
|
-
"Apr",
|
|
3055
|
-
"May",
|
|
3056
|
-
"Jun",
|
|
3057
|
-
"Jul",
|
|
3058
|
-
"Aug",
|
|
3059
|
-
"Sep",
|
|
3060
|
-
"Oct",
|
|
3061
|
-
"Nov",
|
|
3062
|
-
"Dec"
|
|
3063
|
-
];
|
|
3064
|
-
var EIGHTHS = [
|
|
3065
|
-
"",
|
|
3066
|
-
"▏",
|
|
3067
|
-
"▎",
|
|
3068
|
-
"▍",
|
|
3069
|
-
"▌",
|
|
3070
|
-
"▋",
|
|
3071
|
-
"▊",
|
|
3072
|
-
"▉"
|
|
3073
|
-
];
|
|
3074
|
-
function rule() {
|
|
3075
|
-
return "─".repeat(68);
|
|
3076
|
-
}
|
|
3077
|
-
function pad2(n) {
|
|
3078
|
-
return String(n).padStart(2, "0");
|
|
3079
|
-
}
|
|
3080
|
-
function fmtTokens(n) {
|
|
3081
|
-
if (n == null || Number.isNaN(n)) return "—";
|
|
3082
|
-
if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
|
|
3083
|
-
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
|
3084
|
-
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
|
|
3085
|
-
return String(n);
|
|
3086
|
-
}
|
|
3087
|
-
function fmtNum(n) {
|
|
3088
|
-
return (n || 0).toLocaleString("en-US");
|
|
3089
|
-
}
|
|
3090
|
-
function padR(s, n) {
|
|
3091
|
-
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
3092
|
-
}
|
|
3093
|
-
function padL(s, n) {
|
|
3094
|
-
return s.length >= n ? s : " ".repeat(n - s.length) + s;
|
|
3095
|
-
}
|
|
3096
|
-
/** Fixed-width bar field (width cols): █ blocks + an eighth-fraction + trailing spaces. */
|
|
3097
|
-
function barField(v, max, width) {
|
|
3098
|
-
if (!v || v <= 0 || max <= 0) return " ".repeat(width);
|
|
3099
|
-
const scaled = v / max * width;
|
|
3100
|
-
let full = Math.floor(scaled);
|
|
3101
|
-
let fi = Math.round((scaled - full) * 8);
|
|
3102
|
-
if (fi === 8) {
|
|
3103
|
-
full += 1;
|
|
3104
|
-
fi = 0;
|
|
3105
|
-
}
|
|
3106
|
-
if (full === 0 && fi === 0) fi = 1;
|
|
3107
|
-
let s = "█".repeat(Math.min(full, width));
|
|
3108
|
-
if (full < width && fi > 0) s += EIGHTHS[fi] ?? "";
|
|
3109
|
-
if (s.length < width) s += " ".repeat(width - s.length);
|
|
3110
|
-
return s.slice(0, width);
|
|
3111
|
-
}
|
|
3112
|
-
/** Filled/empty meter: █ for used, ░ for remaining. */
|
|
3113
|
-
function meter(pct, width) {
|
|
3114
|
-
let filled = Math.round((pct || 0) / 100 * width);
|
|
3115
|
-
filled = Math.max(0, Math.min(width, filled));
|
|
3116
|
-
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
3117
|
-
}
|
|
3118
|
-
//#endregion
|
|
3119
|
-
//#region src/format.ts
|
|
3120
|
-
/**
|
|
3121
|
-
* Plain-text usage report renderer.
|
|
3122
|
-
*
|
|
3123
|
-
* Rendered for a monospace terminal / hook-block `reason`, so it must NOT rely on
|
|
3124
|
-
* markdown (bold, tables, fenced code). Alignment comes from fixed-width columns
|
|
3125
|
-
* and unicode block glyphs. Input is the parsed `data` of the three ZAI/ZHIPU
|
|
3126
|
-
* monitor endpoints (model-usage, tool-usage, quota/limit — the last already
|
|
3127
|
-
* passed through processQuotaLimit).
|
|
3128
|
-
*/
|
|
3129
|
-
var VBLOCKS = "▁▂▃▄▅▆▇";
|
|
3130
|
-
function parseSlot(s) {
|
|
3131
|
-
const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/.exec(s ?? "");
|
|
3132
|
-
if (!m) return null;
|
|
3133
|
-
return {
|
|
3134
|
-
h: +m[4],
|
|
3135
|
-
day: `${MONTHS[+m[2] - 1]} ${m[3]}`,
|
|
3136
|
-
time: `${m[4]}:${m[5]}`
|
|
3137
|
-
};
|
|
3138
|
-
}
|
|
3139
|
-
/**
|
|
3140
|
-
* Re-express a naive "YYYY-MM-DD HH:MM" string from one UTC offset to another.
|
|
3141
|
-
* Offsets are minutes east of UTC (Beijing = 480, IST = 330). Pure — does not
|
|
3142
|
-
* depend on the runtime timezone, so it is deterministic under test.
|
|
3143
|
-
*/
|
|
3144
|
-
function shiftSlot(s, fromOffsetMin, toOffsetMin) {
|
|
3145
|
-
const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/.exec(s);
|
|
3146
|
-
if (!m) return null;
|
|
3147
|
-
const utcMs = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]) - fromOffsetMin * 6e4;
|
|
3148
|
-
const dt = new Date(utcMs + toOffsetMin * 6e4);
|
|
3149
|
-
return {
|
|
3150
|
-
h: dt.getUTCHours(),
|
|
3151
|
-
day: `${MONTHS[dt.getUTCMonth()]} ${pad2(dt.getUTCDate())}`,
|
|
3152
|
-
time: `${pad2(dt.getUTCHours())}:${pad2(dt.getUTCMinutes())}`
|
|
3153
|
-
};
|
|
3377
|
+
var GLM_HOSTS = /* @__PURE__ */ new Set([
|
|
3378
|
+
"api.z.ai",
|
|
3379
|
+
"dev.bigmodel.cn",
|
|
3380
|
+
"open.bigmodel.cn"
|
|
3381
|
+
]);
|
|
3382
|
+
function fromEnv(value) {
|
|
3383
|
+
return value ? value : void 0;
|
|
3154
3384
|
}
|
|
3155
|
-
function
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3385
|
+
function originOf(value) {
|
|
3386
|
+
try {
|
|
3387
|
+
const url = new URL(value);
|
|
3388
|
+
return url.protocol === "https:" || url.protocol === "http:" ? url.origin : void 0;
|
|
3389
|
+
} catch {
|
|
3390
|
+
return;
|
|
3160
3391
|
}
|
|
3161
3392
|
}
|
|
3162
|
-
function
|
|
3163
|
-
|
|
3164
|
-
let i = 0;
|
|
3165
|
-
while (i < tok.length) if ((tok[i] || 0) === 0) {
|
|
3166
|
-
let j = i;
|
|
3167
|
-
while (j < tok.length && (tok[j] || 0) === 0) j++;
|
|
3168
|
-
runs.push({
|
|
3169
|
-
i,
|
|
3170
|
-
j: j - 1,
|
|
3171
|
-
len: j - i
|
|
3172
|
-
});
|
|
3173
|
-
i = j;
|
|
3174
|
-
} else i++;
|
|
3175
|
-
return runs;
|
|
3393
|
+
function isGlm(origin) {
|
|
3394
|
+
return GLM_HOSTS.has(new URL(origin).hostname);
|
|
3176
3395
|
}
|
|
3177
|
-
function
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3396
|
+
function resolveBaseUrl(flag, env) {
|
|
3397
|
+
if (flag !== void 0) {
|
|
3398
|
+
const origin = originOf(flag);
|
|
3399
|
+
if (origin === void 0) throw new Error(`invalid base URL: ${flag}`);
|
|
3400
|
+
return {
|
|
3401
|
+
glm: isGlm(origin),
|
|
3402
|
+
origin
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
const inherited = fromEnv(env.ANTHROPIC_BASE_URL);
|
|
3406
|
+
const origin = inherited === void 0 ? void 0 : originOf(inherited);
|
|
3407
|
+
if (origin !== void 0 && isGlm(origin)) return {
|
|
3408
|
+
glm: true,
|
|
3409
|
+
origin
|
|
3410
|
+
};
|
|
3411
|
+
return {
|
|
3412
|
+
glm: false,
|
|
3413
|
+
origin: DEFAULT_BASE_URL
|
|
3414
|
+
};
|
|
3182
3415
|
}
|
|
3183
|
-
function
|
|
3184
|
-
|
|
3185
|
-
if (!m) return false;
|
|
3186
|
-
const wd = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])).getUTCDay();
|
|
3187
|
-
return wd >= 1 && wd <= 5 && +m[4] >= 14 && +m[4] <= 17;
|
|
3416
|
+
function buildProgram() {
|
|
3417
|
+
return new Command().name("zai-usage").addOption(new Option("--auth-token <token>", "API key").env("ZAI_AUTH_TOKEN")).addOption(new Option("--base-url <url>", "base URL").env("ZAI_BASE_URL")).addOption(new Option("--json", "print the report lines as JSON for the zai mod"));
|
|
3188
3418
|
}
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
const
|
|
3193
|
-
if (!a || !b) return null;
|
|
3419
|
+
function parseArgs(args) {
|
|
3420
|
+
const program = parseQuietly(buildProgram(), args);
|
|
3421
|
+
if (program === void 0) return;
|
|
3422
|
+
const { authToken, baseUrl, json } = program.opts();
|
|
3194
3423
|
return {
|
|
3195
|
-
|
|
3196
|
-
|
|
3424
|
+
authToken: authToken || void 0,
|
|
3425
|
+
baseUrl: baseUrl || void 0,
|
|
3426
|
+
json: json === true
|
|
3197
3427
|
};
|
|
3198
3428
|
}
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
*/
|
|
3208
|
-
function hourlyVerticalChart({ x, tok, nh, maxTok, peakIdx, slotFn }) {
|
|
3209
|
-
const ROWS = 6;
|
|
3210
|
-
if (nh < 2 || maxTok <= 0) return null;
|
|
3211
|
-
const lines = [];
|
|
3212
|
-
const maxLab = fmtTokens(maxTok);
|
|
3213
|
-
const L = maxLab.length;
|
|
3214
|
-
const AXIS = 1 + L + 1;
|
|
3215
|
-
const P0 = AXIS + 2;
|
|
3216
|
-
const PW = 68 - P0;
|
|
3217
|
-
const slot = Math.max(1, Math.floor(PW / nh));
|
|
3218
|
-
const barW = slot >= 2 ? slot - 1 : 1;
|
|
3219
|
-
function barLeft(i) {
|
|
3220
|
-
return P0 + i * slot;
|
|
3221
|
-
}
|
|
3222
|
-
function barCenter(i) {
|
|
3223
|
-
return barLeft(i) + Math.floor(barW / 2);
|
|
3224
|
-
}
|
|
3225
|
-
const usedCols = nh * slot;
|
|
3226
|
-
function blank() {
|
|
3227
|
-
return Array(68).fill(" ");
|
|
3228
|
-
}
|
|
3229
|
-
function glyph(i, r) {
|
|
3230
|
-
const v = tok[i] || 0;
|
|
3231
|
-
if (v <= 0) return " ";
|
|
3232
|
-
const fill = v / maxTok * ROWS - r;
|
|
3233
|
-
if (fill >= 1) return "█";
|
|
3234
|
-
if (fill > 0) {
|
|
3235
|
-
let e = Math.round(fill * 8);
|
|
3236
|
-
if (e < 1) e = 1;
|
|
3237
|
-
if (e > 7) e = 7;
|
|
3238
|
-
return VBLOCKS[e - 1];
|
|
3239
|
-
}
|
|
3240
|
-
return " ";
|
|
3241
|
-
}
|
|
3242
|
-
for (let r = 5; r >= 0; r--) {
|
|
3243
|
-
const line = blank();
|
|
3244
|
-
line[AXIS] = r === 5 ? "┤" : "│";
|
|
3245
|
-
if (r === 5) for (let k = 0; k < L; k++) line[AXIS - 1 - L + k] = maxLab[k];
|
|
3246
|
-
for (let i = 0; i < nh; i++) {
|
|
3247
|
-
const g = glyph(i, r);
|
|
3248
|
-
if (g !== " ") for (let c = 0; c < barW; c++) line[barLeft(i) + c] = g;
|
|
3249
|
-
}
|
|
3250
|
-
lines.push(line.join("").trimEnd());
|
|
3251
|
-
}
|
|
3252
|
-
function slotOf(i) {
|
|
3253
|
-
return slotFn(x[i]);
|
|
3254
|
-
}
|
|
3255
|
-
function isTick(i) {
|
|
3256
|
-
if (i === 0 || i === nh - 1) return true;
|
|
3257
|
-
const s = slotOf(i);
|
|
3258
|
-
return !!s && s.h % 6 === 0;
|
|
3259
|
-
}
|
|
3260
|
-
const base = blank();
|
|
3261
|
-
base[AXIS - 2] = "0";
|
|
3262
|
-
base[AXIS] = "└";
|
|
3263
|
-
for (let c = AXIS + 1; c < P0 + usedCols; c++) base[c] = "─";
|
|
3264
|
-
for (let i = 0; i < nh; i++) if (isTick(i)) base[barCenter(i)] = "┬";
|
|
3265
|
-
lines.push(base.join("").trimEnd());
|
|
3266
|
-
const hr = blank();
|
|
3267
|
-
let last = -10;
|
|
3268
|
-
for (let i = 0; i < nh; i++) {
|
|
3269
|
-
if (!isTick(i)) continue;
|
|
3270
|
-
const col = barCenter(i);
|
|
3271
|
-
if (col - last < 3) continue;
|
|
3272
|
-
const s = slotOf(i);
|
|
3273
|
-
const txt = s ? pad2(s.h) : " ";
|
|
3274
|
-
hr[col] = txt[0];
|
|
3275
|
-
hr[col + 1] = txt[1];
|
|
3276
|
-
last = col;
|
|
3277
|
-
}
|
|
3278
|
-
lines.push(hr.join("").trimEnd());
|
|
3279
|
-
const day = blank();
|
|
3280
|
-
let lastDay = null;
|
|
3281
|
-
last = -10;
|
|
3282
|
-
for (let i = 0; i < nh; i++) {
|
|
3283
|
-
if (!isTick(i)) continue;
|
|
3284
|
-
const s = slotOf(i);
|
|
3285
|
-
if (!s || s.day === lastDay) continue;
|
|
3286
|
-
const col = barCenter(i);
|
|
3287
|
-
if (col - last >= s.day.length && col + s.day.length <= 68) {
|
|
3288
|
-
for (let k = 0; k < s.day.length; k++) day[col + k] = s.day[k];
|
|
3289
|
-
lastDay = s.day;
|
|
3290
|
-
last = col + s.day.length;
|
|
3291
|
-
}
|
|
3292
|
-
}
|
|
3293
|
-
if (day.some((c) => c !== " ")) lines.push(day.join("").trimEnd());
|
|
3294
|
-
const mark = blank();
|
|
3295
|
-
for (let i = 0; i < nh; i++) if (isPeakBucket(x[i])) mark[barCenter(i)] = "↑";
|
|
3296
|
-
if (peakIdx >= 0) mark[barCenter(peakIdx)] = "◂";
|
|
3297
|
-
if (mark.some((c) => c !== " ")) lines.push(mark.join("").trimEnd());
|
|
3298
|
-
return lines;
|
|
3299
|
-
}
|
|
3300
|
-
function render({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetMin, now = /* @__PURE__ */ new Date() }) {
|
|
3301
|
-
const out = [];
|
|
3302
|
-
const toOffset = localOffsetMin != null ? localOffsetMin : -(/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
3303
|
-
function convertSlot(s) {
|
|
3304
|
-
return shiftSlot(s, apiOffsetMin, toOffset) ?? parseSlot(s);
|
|
3305
|
-
}
|
|
3306
|
-
const x = model.x_time ?? [];
|
|
3307
|
-
const tok = model.tokensUsage ?? [];
|
|
3308
|
-
const calls = model.modelCallCount ?? [];
|
|
3309
|
-
const nh = x.length;
|
|
3310
|
-
const tu = model.totalUsage ?? {};
|
|
3311
|
-
const total = tu.totalTokensUsage != null ? tu.totalTokensUsage : tok.reduce((a, b) => a + (b || 0), 0);
|
|
3312
|
-
const totalCalls = tu.totalModelCallCount != null ? tu.totalModelCallCount : calls.reduce((a, b) => a + (b || 0), 0);
|
|
3313
|
-
let peakIdx = -1;
|
|
3314
|
-
let peakTok = 0;
|
|
3315
|
-
for (let i = 0; i < tok.length; i++) if (tok[i] > peakTok) {
|
|
3316
|
-
peakTok = tok[i];
|
|
3317
|
-
peakIdx = i;
|
|
3318
|
-
}
|
|
3319
|
-
const peakCalls = peakIdx >= 0 ? calls[peakIdx] || 0 : 0;
|
|
3320
|
-
const pctPeak = total > 0 ? peakTok / total * 100 : 0;
|
|
3321
|
-
const peakSlot = peakIdx >= 0 ? convertSlot(x[peakIdx]) : null;
|
|
3322
|
-
const level = quota.level ? quota.level.charAt(0).toUpperCase() + quota.level.slice(1) : "";
|
|
3323
|
-
const left = ` GLM Coding Plan${level ? " · " + level : ""}`;
|
|
3324
|
-
const firstSlot = convertSlot(x[0]);
|
|
3325
|
-
const lastSlot = convertSlot(x[nh - 1]);
|
|
3326
|
-
const win = firstSlot && lastSlot ? `${firstSlot.day} ${firstSlot.time} → ${lastSlot.day} ${lastSlot.time} · ${nh}h` : platform;
|
|
3327
|
-
out.push(rule());
|
|
3328
|
-
out.push(left + padL(win, 68 - left.length));
|
|
3329
|
-
out.push(rule());
|
|
3330
|
-
let lead = ` ${fmtTokens(total)} tokens across ${fmtNum(totalCalls)} model calls`;
|
|
3331
|
-
if (peakIdx >= 0 && peakSlot) lead += ` — ${Math.round(pctPeak)}% of it in a single hour (${peakSlot.day} ${peakSlot.time}, ${fmtTokens(peakTok)} tokens / ${fmtNum(peakCalls)} calls)`;
|
|
3332
|
-
lead += ".";
|
|
3333
|
-
out.push("");
|
|
3334
|
-
out.push(lead);
|
|
3335
|
-
const activeHours = tok.filter((t) => t > 0).length;
|
|
3336
|
-
const longest = idleRuns(tok).filter((r) => r.len >= 2).sort((a, b) => b.len - a.len).at(0);
|
|
3337
|
-
const tt = tool.totalUsage ?? {};
|
|
3338
|
-
const searchN = tt.totalNetworkSearchCount || 0;
|
|
3339
|
-
const readN = tt.totalWebReadMcpCount || 0;
|
|
3340
|
-
const zreadN = tt.totalZreadMcpCount || 0;
|
|
3341
|
-
const toolTotal = tt.totalSearchMcpCount != null ? tt.totalSearchMcpCount : searchN + readN + zreadN;
|
|
3342
|
-
let peakWinActive = 0;
|
|
3343
|
-
let peakWinTokens = 0;
|
|
3344
|
-
for (let k = 0; k < nh; k++) if (isPeakBucket(x[k]) && tok[k] > 0) {
|
|
3345
|
-
peakWinActive++;
|
|
3346
|
-
peakWinTokens += tok[k];
|
|
3347
|
-
}
|
|
3348
|
-
const peakWinPct = total > 0 ? peakWinTokens / total * 100 : 0;
|
|
3349
|
-
const peakWin = localPeakWindow(apiOffsetMin, toOffset);
|
|
3350
|
-
out.push("");
|
|
3351
|
-
out.push(` Peak ${padR(peakSlot ? peakSlot.day + " " + peakSlot.time : "—", 15)}${padL(fmtTokens(peakTok), 7)} tokens · ${padL(fmtNum(peakCalls), 5)} calls`);
|
|
3352
|
-
out.push(` Active ${padR(`${activeHours} / ${nh} hours`, 15)}${longest ? idleLabel(longest, x, convertSlot) : "no idle gaps"}`);
|
|
3353
|
-
out.push(` Tools ${padR(`${toolTotal} calls`, 15)}${searchN} searches · ${readN} reads${zreadN ? ` · ${zreadN} zread` : ""}`);
|
|
3354
|
-
if (peakWin) out.push(` Peak hrs Mon–Fri ${peakWin.start}–${peakWin.end} · GLM-5.2 3× · ${peakWinActive}h active · ${fmtTokens(peakWinTokens)} (${Math.round(peakWinPct)}%)`);
|
|
3355
|
-
out.push("");
|
|
3356
|
-
out.push(" Hourly tokens · ↑ peak hour " + "─".repeat(Math.max(0, 39)));
|
|
3357
|
-
const chartLines = hourlyVerticalChart({
|
|
3358
|
-
x,
|
|
3359
|
-
tok,
|
|
3360
|
-
nh,
|
|
3361
|
-
maxTok: peakTok || 1,
|
|
3362
|
-
peakIdx,
|
|
3363
|
-
slotFn: convertSlot
|
|
3364
|
-
});
|
|
3365
|
-
if (chartLines) {
|
|
3366
|
-
out.push(...chartLines);
|
|
3367
|
-
if (peakIdx >= 0 && peakSlot) out.push(` ◂ peak ${peakSlot.day} ${peakSlot.time} ${fmtTokens(peakTok)} tokens · ${fmtNum(peakCalls)} calls`);
|
|
3368
|
-
} else out.push(" (not enough hourly data to chart)");
|
|
3369
|
-
out.push("");
|
|
3370
|
-
out.push(" Model mix " + "─".repeat(Math.max(0, 57)));
|
|
3371
|
-
const mixSrc = model.modelSummaryList ?? model.modelDataList ?? [];
|
|
3372
|
-
const denom = mixSrc.reduce((a, m) => a + (m.totalTokens ?? 0), 0) || 1;
|
|
3373
|
-
const mixSorted = [...mixSrc].sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
|
3374
|
-
for (const m of mixSorted) {
|
|
3375
|
-
const pct = (m.totalTokens ?? 0) / denom * 100;
|
|
3376
|
-
out.push(` ${padR(m.modelName || "?", 11)}${padL(fmtTokens(m.totalTokens), 8)} ${padL(pct.toFixed(1) + "%", 6)} ${barField(pct, 100, 20)}`);
|
|
3377
|
-
}
|
|
3378
|
-
out.push("");
|
|
3379
|
-
out.push(" Limits " + "─".repeat(Math.max(0, 60)));
|
|
3380
|
-
const bj = new Date(now.getTime() + apiOffsetMin * 6e4);
|
|
3381
|
-
const bjDay = bj.getUTCDay();
|
|
3382
|
-
const elapsedMin = bjDay >= 1 && bjDay <= 5 ? Math.max(0, Math.min(240, (bj.getUTCHours() - 14) * 60 + bj.getUTCMinutes())) : 0;
|
|
3383
|
-
if (peakWin) out.push(` ${padR("Peak", 16)}${padL(peakWin.start, 5)} ${meter(Math.round(elapsedMin / 240 * 100), 22)} ${peakWin.end}`);
|
|
3384
|
-
const limits = quota.limits ?? [];
|
|
3385
|
-
const mcp = limits.find((l) => /mcp/i.test(l.type ?? ""));
|
|
3386
|
-
const tok5 = limits.find((l) => /token/i.test(l.type ?? ""));
|
|
3387
|
-
if (tok5) {
|
|
3388
|
-
const reset = new Date(tok5.nextResetTime ?? "").toLocaleTimeString("en-IN");
|
|
3389
|
-
out.push(` ${padR("Tokens · 5h", 16)}${padL(`${tok5.percentage || 0}%`, 5)} ${meter(tok5.percentage, 22)} ${reset}`);
|
|
3390
|
-
}
|
|
3391
|
-
if (tok5 && mcp) out.push("");
|
|
3392
|
-
if (mcp) {
|
|
3393
|
-
const reset = new Date(mcp.nextResetTime ?? "").toLocaleString("en-IN");
|
|
3394
|
-
out.push(` ${padR("MCP · this month", 16)}${padL(`${mcp.percentage || 0}%`, 5)} ${meter(mcp.percentage, 22)} ${reset}`);
|
|
3395
|
-
const parts = (mcp.usageDetails ?? []).map((d) => `${friendlyTool(d.modelCode)} ${fmtNum(d.usage)}`).join(" ");
|
|
3396
|
-
if (parts) out.push(` ${fmtNum(mcp.currentUsage)}M / ${fmtNum(mcp.totol)}M · ${parts}`);
|
|
3397
|
-
}
|
|
3398
|
-
out.push("");
|
|
3399
|
-
out.push(rule());
|
|
3400
|
-
return out.join("\n");
|
|
3429
|
+
function resolveConfig(env, parsed) {
|
|
3430
|
+
const base = resolveBaseUrl(parsed.baseUrl, env);
|
|
3431
|
+
const token = parsed.authToken ?? (base.glm ? fromEnv(env.ANTHROPIC_AUTH_TOKEN) : void 0);
|
|
3432
|
+
if (token === void 0) throw new Error(env.ANTHROPIC_AUTH_TOKEN ? "ANTHROPIC_AUTH_TOKEN is set, but the resolved base URL does not name a GLM host, so it is not a GLM Coding Plan token — set ZAI_AUTH_TOKEN (or --auth-token)" : "no API key: set ZAI_AUTH_TOKEN (or --auth-token)");
|
|
3433
|
+
return {
|
|
3434
|
+
token,
|
|
3435
|
+
url: base.origin
|
|
3436
|
+
};
|
|
3401
3437
|
}
|
|
3402
3438
|
//#endregion
|
|
3403
3439
|
//#region src/usage.ts
|
|
3404
3440
|
/**
|
|
3405
|
-
* Usage query
|
|
3406
|
-
*
|
|
3407
|
-
* renders the plain-text report.
|
|
3441
|
+
* Usage query: the three GLM monitor endpoints of the resolved base URL,
|
|
3442
|
+
* assembled into the report input.
|
|
3408
3443
|
*/
|
|
3409
3444
|
function queryParams(now = /* @__PURE__ */ new Date()) {
|
|
3410
3445
|
const startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, now.getHours(), 0, 0, 0);
|
|
@@ -3460,12 +3495,12 @@ async function fetchReport(config) {
|
|
|
3460
3495
|
fetchJson(`${config.url}/api/monitor/usage/tool-usage`, "Tool usage", config.token, query),
|
|
3461
3496
|
fetchJson(`${config.url}/api/monitor/usage/quota/limit`, "Quota limit", config.token)
|
|
3462
3497
|
]);
|
|
3463
|
-
return
|
|
3498
|
+
return {
|
|
3464
3499
|
platform,
|
|
3465
3500
|
model,
|
|
3466
3501
|
tool,
|
|
3467
3502
|
quota: processQuotaLimit(quotaRaw)
|
|
3468
|
-
}
|
|
3503
|
+
};
|
|
3469
3504
|
} catch (e) {
|
|
3470
3505
|
throw new Error(`Request failed: ${e.message}`, { cause: e });
|
|
3471
3506
|
}
|
|
@@ -3474,7 +3509,8 @@ async function fetchReport(config) {
|
|
|
3474
3509
|
//#region src/index.ts
|
|
3475
3510
|
var parsed = parseArgs(process.argv.slice(2)) ?? printUsageAndExit(buildProgram());
|
|
3476
3511
|
try {
|
|
3477
|
-
|
|
3512
|
+
const input = await fetchReport(resolveConfig(process.env, parsed));
|
|
3513
|
+
console.log(parsed.json ? JSON.stringify({ lines: reportLines(input) }) : render(input));
|
|
3478
3514
|
} catch (e) {
|
|
3479
3515
|
console.error(e.message);
|
|
3480
3516
|
process.exit(1);
|