@cassius0924/dsh-usage-dashboard 0.3.1 → 0.5.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/lib/index.js CHANGED
@@ -1,3 +1,8 @@
1
+ // src/contract.ts
2
+ var USAGE_WINDOW_DAYS = [7, 30, 90, 365];
3
+ var SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
4
+ var isValidSessionId = (id) => SESSION_ID_PATTERN.test(id);
5
+
1
6
  // src/memo.ts
2
7
  function memoize(ttlMs, compute) {
3
8
  const state = { entry: void 0, inflight: void 0 };
@@ -159,6 +164,17 @@ async function fetchBalance(credentials) {
159
164
  };
160
165
  }
161
166
  var emptyBucket = () => ({ input: 0, output: 0, cache: 0, total: 0, cost: 0, calls: 0 });
167
+ function bump(map, key, input, output, cache, cost) {
168
+ const bucket = map.get(key) ?? emptyBucket();
169
+ bucket.input += input;
170
+ bucket.output += output;
171
+ bucket.cache += cache;
172
+ bucket.total += input + output + cache;
173
+ bucket.cost += cost;
174
+ bucket.calls += 1;
175
+ map.set(key, bucket);
176
+ }
177
+ var emptyOverall = () => ({ ...emptyBucket(), reasoning: 0, cacheSavings: 0 });
162
178
  var SESSION_TOP_N = 6;
163
179
  function titleOf(events, id) {
164
180
  let title = "";
@@ -197,7 +213,8 @@ function summarize(dayMap, now) {
197
213
  };
198
214
  }
199
215
  function addUsageEvent(events, onEvent) {
200
- if (events === void 0) return;
216
+ const result = { usageRecords: 0, skippedRecords: 0 };
217
+ if (events === void 0) return result;
201
218
  let provider = "";
202
219
  let model = "";
203
220
  for (const ev of events) {
@@ -212,15 +229,22 @@ function addUsageEvent(events, onEvent) {
212
229
  if (ev?.type !== "assistant/message") continue;
213
230
  const usage = ev.data?.usage;
214
231
  if (usage === void 0) continue;
215
- const input = Number(usage.inputTokens) || 0;
216
- const output = Number(usage.outputTokens) || 0;
217
- const cache = Number(usage.cacheReadTokens) || 0;
218
- const reasoning = Number(usage.reasoningTokens) || 0;
219
- if (ev.time === void 0) continue;
220
- onEvent(ev.time, input, output, cache, reasoning, provider, model);
232
+ const values = [usage.inputTokens, usage.outputTokens, usage.cacheReadTokens, usage.reasoningTokens].map((value) => value === void 0 ? 0 : Number(value));
233
+ if (typeof ev.time !== "number" || !Number.isFinite(ev.time) || values.some((value) => !Number.isFinite(value) || value < 0)) {
234
+ result.skippedRecords += 1;
235
+ continue;
236
+ }
237
+ const [input = 0, output = 0, cache = 0, reasoning = 0] = values;
238
+ const rawMessageId = ev.data?.message?.id;
239
+ const messageId = typeof rawMessageId === "string" && rawMessageId !== "" ? rawMessageId : void 0;
240
+ const rawTurn = ev.data?.turn;
241
+ const turn = typeof rawTurn === "number" && Number.isFinite(rawTurn) ? rawTurn : void 0;
242
+ onEvent(ev.time, input, output, cache, reasoning, provider, model, messageId, turn);
243
+ result.usageRecords += 1;
221
244
  }
245
+ return result;
222
246
  }
223
- async function fetchUsage(persistence) {
247
+ async function fetchUsage(persistence, nowMs = Date.now()) {
224
248
  if (persistence === void 0) return { ok: false, error: "\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528" };
225
249
  let headers;
226
250
  try {
@@ -230,132 +254,189 @@ async function fetchUsage(persistence) {
230
254
  }
231
255
  const dayMap = /* @__PURE__ */ new Map();
232
256
  const hourMap = /* @__PURE__ */ new Map();
233
- const overall = { ...emptyBucket(), reasoning: 0, cacheSavings: 0 };
257
+ const overall = emptyOverall();
234
258
  const modelTotals = /* @__PURE__ */ new Map();
235
259
  const modelDays = /* @__PURE__ */ new Map();
236
260
  const modelHours = /* @__PURE__ */ new Map();
237
261
  const sessions = [];
262
+ const coverage = {
263
+ scope: "local-dsh-session-logs",
264
+ listedSessions: 0,
265
+ scannedSessions: 0,
266
+ usageRecords: 0,
267
+ skippedRecords: 0,
268
+ failedSessions: 0,
269
+ earliestAt: null,
270
+ latestAt: null
271
+ };
238
272
  const peakSplit = {
239
273
  peak: { total: 0, cost: 0, calls: 0 },
240
274
  offPeak: { total: 0, cost: 0, calls: 0 },
241
275
  peakEraCost: 0,
242
276
  offPeakEraCost: 0
243
277
  };
244
- const bump = (map, key, input, output, cache, cost) => {
245
- const bucket = map.get(key) ?? emptyBucket();
278
+ const now = new Date(nowMs);
279
+ const dateBefore = (days) => new Date(now.getFullYear(), now.getMonth(), now.getDate() - days);
280
+ const windowAggregates = USAGE_WINDOW_DAYS.map((days) => ({
281
+ days,
282
+ startKey: dayKeyOf(dateBefore(days - 1)),
283
+ endKey: dayKeyOf(now),
284
+ dayMap: /* @__PURE__ */ new Map(),
285
+ hourMap: /* @__PURE__ */ new Map(),
286
+ overall: emptyOverall(),
287
+ modelTotals: /* @__PURE__ */ new Map(),
288
+ modelDays: /* @__PURE__ */ new Map(),
289
+ modelHours: /* @__PURE__ */ new Map(),
290
+ sessions: []
291
+ }));
292
+ const bumpOverall = (bucket, input, output, cache, reasoning, cost, cacheSavings) => {
246
293
  bucket.input += input;
247
294
  bucket.output += output;
248
295
  bucket.cache += cache;
296
+ bucket.reasoning += reasoning;
249
297
  bucket.total += input + output + cache;
250
298
  bucket.cost += cost;
299
+ bucket.cacheSavings += cacheSavings;
251
300
  bucket.calls += 1;
252
- map.set(key, bucket);
253
301
  };
254
- for (const header of headers) {
255
- const sid = header.id ?? header.sessionId;
256
- if (sid === void 0 || sid === "") continue;
302
+ const bumpModel = (totals, days, hours, modelKey, dayKey, hourKey, input, output, cache, cost) => {
303
+ bump(totals, modelKey, input, output, cache, cost);
304
+ let dayBuckets = days.get(modelKey);
305
+ if (dayBuckets === void 0) {
306
+ dayBuckets = /* @__PURE__ */ new Map();
307
+ days.set(modelKey, dayBuckets);
308
+ }
309
+ bump(dayBuckets, dayKey, input, output, cache, cost);
310
+ let hourBuckets = hours.get(modelKey);
311
+ if (hourBuckets === void 0) {
312
+ hourBuckets = /* @__PURE__ */ new Map();
313
+ hours.set(modelKey, hourBuckets);
314
+ }
315
+ bump(hourBuckets, hourKey, input, output, cache, cost);
316
+ };
317
+ const bumpSession = (session, time, total, cost) => {
318
+ session.total += total;
319
+ session.cost += cost;
320
+ session.calls += 1;
321
+ if (time > session.lastActive) session.lastActive = time;
322
+ };
323
+ const sessionIds = headers.map((header) => header.id ?? header.sessionId).filter((sid) => sid !== void 0 && sid !== "");
324
+ coverage.listedSessions = sessionIds.length;
325
+ for (const sid of sessionIds) {
257
326
  try {
258
327
  const { events } = await persistence.readFrom(sid, 0);
328
+ coverage.scannedSessions += 1;
259
329
  const session = { id: sid, title: "", total: 0, cost: 0, calls: 0, lastActive: 0 };
260
- addUsageEvent(events, (time, input, output, cache, reasoning, provider, model) => {
330
+ const windowSessions = windowAggregates.map(() => ({ id: sid, title: "", total: 0, cost: 0, calls: 0, lastActive: 0 }));
331
+ const scanned = addUsageEvent(events, (time, input, output, cache, reasoning, provider, model) => {
332
+ if (coverage.earliestAt === null || time < coverage.earliestAt) coverage.earliestAt = time;
333
+ if (coverage.latestAt === null || time > coverage.latestAt) coverage.latestAt = time;
261
334
  const d = new Date(time);
262
335
  const dayKey = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
263
336
  const hourKey = String(d.getHours());
264
337
  const modelKey = `${provider}/${model}`;
265
338
  const cost = costOf(time, model, input, cache, output);
339
+ const cacheSavings = cacheSavingOf(time, model, cache);
340
+ const eventTotal = input + output + cache;
266
341
  bump(dayMap, dayKey, input, output, cache, cost);
267
342
  bump(hourMap, hourKey, input, output, cache, cost);
268
- bump(modelTotals, modelKey, input, output, cache, cost);
269
- let days = modelDays.get(modelKey);
270
- if (days === void 0) {
271
- days = /* @__PURE__ */ new Map();
272
- modelDays.set(modelKey, days);
273
- }
274
- bump(days, dayKey, input, output, cache, cost);
275
- let hours = modelHours.get(modelKey);
276
- if (hours === void 0) {
277
- hours = /* @__PURE__ */ new Map();
278
- modelHours.set(modelKey, hours);
343
+ bumpModel(modelTotals, modelDays, modelHours, modelKey, dayKey, hourKey, input, output, cache, cost);
344
+ bumpOverall(overall, input, output, cache, reasoning, cost, cacheSavings);
345
+ bumpSession(session, time, eventTotal, cost);
346
+ for (let i = 0; i < windowAggregates.length; i++) {
347
+ const aggregate = windowAggregates[i];
348
+ if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue;
349
+ bump(aggregate.dayMap, dayKey, input, output, cache, cost);
350
+ bump(aggregate.hourMap, hourKey, input, output, cache, cost);
351
+ bumpModel(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, modelKey, dayKey, hourKey, input, output, cache, cost);
352
+ bumpOverall(aggregate.overall, input, output, cache, reasoning, cost, cacheSavings);
353
+ bumpSession(windowSessions[i], time, eventTotal, cost);
279
354
  }
280
- bump(hours, hourKey, input, output, cache, cost);
281
- overall.input += input;
282
- overall.output += output;
283
- overall.cache += cache;
284
- overall.reasoning += reasoning;
285
- overall.total += input + output + cache;
286
- overall.cost += cost;
287
- overall.cacheSavings += cacheSavingOf(time, model, cache);
288
- session.total += input + output + cache;
289
- session.cost += cost;
290
- session.calls += 1;
291
- if (time > session.lastActive) session.lastActive = time;
292
355
  const window = isPeak(time) ? peakSplit.peak : peakSplit.offPeak;
293
- window.total += input + output + cache;
356
+ window.total += eventTotal;
294
357
  window.cost += cost;
295
358
  window.calls += 1;
296
359
  peakSplit.peakEraCost += costUnderPeakEra(time, model, input, cache, output);
297
360
  peakSplit.offPeakEraCost += costUnderPeakEra(time, model, input, cache, output, true);
298
- overall.calls += 1;
299
361
  });
362
+ coverage.usageRecords += scanned.usageRecords;
363
+ coverage.skippedRecords += scanned.skippedRecords;
300
364
  if (session.calls > 0) {
301
- session.title = titleOf(events, sid);
365
+ const title = titleOf(events, sid);
366
+ session.title = title;
302
367
  sessions.push(session);
368
+ for (let i = 0; i < windowAggregates.length; i++) {
369
+ const windowSession = windowSessions[i];
370
+ if (windowSession.calls === 0) continue;
371
+ windowSession.title = title;
372
+ windowAggregates[i].sessions.push(windowSession);
373
+ }
303
374
  }
304
375
  } catch {
376
+ coverage.failedSessions += 1;
305
377
  }
306
378
  }
307
- const daily = [];
308
- for (let i = 29; i >= 0; i--) {
309
- const d = new Date(Date.now() - i * 864e5);
310
- const key = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
311
- const b = dayMap.get(key) ?? emptyBucket();
312
- daily.push({ date: key, input: b.input, output: b.output, cache: b.cache, total: b.total, cost: b.cost, calls: b.calls });
313
- }
314
- const hourly = [];
315
- for (let i = 0; i < 24; i++) {
316
- const b = hourMap.get(String(i)) ?? emptyBucket();
317
- hourly.push({ hour: i, total: b.total, cost: b.cost, calls: b.calls });
318
- }
319
- const heatmap = [];
320
- for (let i = 83; i >= 0; i--) {
321
- const d = new Date(Date.now() - i * 864e5);
322
- const key = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
323
- const b = dayMap.get(key) ?? emptyBucket();
324
- heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls });
325
- }
326
- const models = [];
327
- for (const [modelKey, bucket] of modelTotals) {
328
- const slash = modelKey.indexOf("/");
329
- const provider = slash < 0 ? "" : modelKey.slice(0, slash);
330
- const model = slash < 0 ? modelKey : modelKey.slice(slash + 1);
331
- const days = modelDays.get(modelKey);
332
- const hours = modelHours.get(modelKey);
333
- const daily2 = [];
334
- for (let i = 29; i >= 0; i--) {
335
- const d = new Date(Date.now() - i * 864e5);
336
- const key = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
337
- const b = days?.get(key) ?? emptyBucket();
338
- daily2.push({ total: b.total, cost: b.cost, calls: b.calls });
379
+ const makeDaily = (map, count) => {
380
+ const result = [];
381
+ for (let i = count - 1; i >= 0; i--) {
382
+ const key = dayKeyOf(dateBefore(i));
383
+ const b = map.get(key) ?? emptyBucket();
384
+ result.push({ date: key, input: b.input, output: b.output, cache: b.cache, total: b.total, cost: b.cost, calls: b.calls });
339
385
  }
340
- const hourly2 = [];
386
+ return result;
387
+ };
388
+ const makeHourly = (map) => {
389
+ const result = [];
341
390
  for (let i = 0; i < 24; i++) {
342
- const b = hours?.get(String(i)) ?? emptyBucket();
343
- hourly2.push({ total: b.total, cost: b.cost, calls: b.calls });
391
+ const b = map.get(String(i)) ?? emptyBucket();
392
+ result.push({ hour: i, total: b.total, cost: b.cost, calls: b.calls });
344
393
  }
345
- models.push({
346
- provider,
347
- model,
348
- input: bucket.input,
349
- output: bucket.output,
350
- cache: bucket.cache,
351
- total: bucket.total,
352
- cost: bucket.cost,
353
- calls: bucket.calls,
354
- daily: daily2,
355
- hourly: hourly2
356
- });
394
+ return result;
395
+ };
396
+ const makeModels = (totals, daysByModel, hoursByModel, dayCount) => {
397
+ const result = [];
398
+ for (const [modelKey, bucket] of totals) {
399
+ const slash = modelKey.indexOf("/");
400
+ const provider = slash < 0 ? "" : modelKey.slice(0, slash);
401
+ const model = slash < 0 ? modelKey : modelKey.slice(slash + 1);
402
+ const days = daysByModel.get(modelKey);
403
+ const hours = hoursByModel.get(modelKey);
404
+ const daily2 = makeDaily(days ?? /* @__PURE__ */ new Map(), dayCount).map((point) => ({ total: point.total, cost: point.cost, calls: point.calls }));
405
+ const hourly2 = makeHourly(hours ?? /* @__PURE__ */ new Map()).map((point) => ({ total: point.total, cost: point.cost, calls: point.calls }));
406
+ result.push({
407
+ provider,
408
+ model,
409
+ input: bucket.input,
410
+ output: bucket.output,
411
+ cache: bucket.cache,
412
+ total: bucket.total,
413
+ cost: bucket.cost,
414
+ calls: bucket.calls,
415
+ daily: daily2,
416
+ hourly: hourly2
417
+ });
418
+ }
419
+ result.sort((a, b) => b.total - a.total);
420
+ return result;
421
+ };
422
+ const daily = makeDaily(dayMap, 30);
423
+ const hourly = makeHourly(hourMap);
424
+ const heatmap = [];
425
+ for (let i = 363; i >= 0; i--) {
426
+ const key = dayKeyOf(dateBefore(i));
427
+ const b = dayMap.get(key) ?? emptyBucket();
428
+ heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls });
357
429
  }
358
- models.sort((a, b) => b.total - a.total);
430
+ const models = makeModels(modelTotals, modelDays, modelHours, 30);
431
+ const windows = windowAggregates.map((aggregate) => ({
432
+ days: aggregate.days,
433
+ daily: makeDaily(aggregate.dayMap, aggregate.days),
434
+ hourly: makeHourly(aggregate.hourMap),
435
+ totals: { ...aggregate.overall },
436
+ models: makeModels(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, aggregate.days),
437
+ sessions: [...aggregate.sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),
438
+ sessionCount: aggregate.sessions.length
439
+ }));
359
440
  return {
360
441
  ok: true,
361
442
  data: {
@@ -363,11 +444,13 @@ async function fetchUsage(persistence) {
363
444
  hourly,
364
445
  heatmap,
365
446
  models,
366
- summary: summarize(dayMap, /* @__PURE__ */ new Date()),
367
- pricing: pricingInfo(Date.now()),
447
+ summary: summarize(dayMap, new Date(nowMs)),
448
+ pricing: pricingInfo(nowMs),
368
449
  peakSplit,
369
450
  sessions: [...sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),
370
451
  sessionCount: sessions.length,
452
+ coverage,
453
+ windows,
371
454
  totals: {
372
455
  input: overall.input,
373
456
  output: overall.output,
@@ -381,6 +464,74 @@ async function fetchUsage(persistence) {
381
464
  }
382
465
  };
383
466
  }
467
+ async function fetchSessionUsage(persistence, sessionId) {
468
+ if (persistence === void 0) return { ok: false, error: "\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528" };
469
+ if (typeof sessionId !== "string" || sessionId === "") return { ok: false, error: "\u7F3A\u5C11\u4F1A\u8BDD id" };
470
+ if (!isValidSessionId(sessionId)) return { ok: false, error: "\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5" };
471
+ let events;
472
+ try {
473
+ const result = await persistence.readFrom(sessionId, 0);
474
+ events = result.events;
475
+ } catch (err) {
476
+ return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A${errorMessage(err)}` };
477
+ }
478
+ const modelTotals = /* @__PURE__ */ new Map();
479
+ let total = 0;
480
+ let cost = 0;
481
+ let calls = 0;
482
+ let firstActive = null;
483
+ let lastActive = null;
484
+ const turnTotals = /* @__PURE__ */ new Map();
485
+ addUsageEvent(events, (time, input, output, cache, _reasoning, provider, model, messageId, turn) => {
486
+ const modelKey = `${provider}/${model}`;
487
+ const eventCost = costOf(time, model, input, cache, output);
488
+ const eventTotal = input + output + cache;
489
+ bump(modelTotals, modelKey, input, output, cache, eventCost);
490
+ total += eventTotal;
491
+ cost += eventCost;
492
+ calls += 1;
493
+ if (firstActive === null || time < firstActive) firstActive = time;
494
+ if (lastActive === null || time > lastActive) lastActive = time;
495
+ if (messageId !== void 0) {
496
+ const key = turn === void 0 ? `message:${messageId}` : `turn:${turn}`;
497
+ const aggregate = turnTotals.get(key) ?? { messageId, total: 0, cost: 0, calls: 0 };
498
+ aggregate.messageId = messageId;
499
+ aggregate.total += eventTotal;
500
+ aggregate.cost += eventCost;
501
+ aggregate.calls += 1;
502
+ turnTotals.set(key, aggregate);
503
+ }
504
+ });
505
+ const models = [...modelTotals.entries()].map(([modelKey, bucket]) => {
506
+ const slash = modelKey.indexOf("/");
507
+ const provider = slash < 0 ? "" : modelKey.slice(0, slash);
508
+ const model = slash < 0 ? modelKey : modelKey.slice(slash + 1);
509
+ return {
510
+ provider,
511
+ model,
512
+ input: bucket.input,
513
+ output: bucket.output,
514
+ cache: bucket.cache,
515
+ total: bucket.total,
516
+ cost: bucket.cost,
517
+ calls: bucket.calls
518
+ };
519
+ }).sort((a, b) => b.cost - a.cost);
520
+ return {
521
+ ok: true,
522
+ data: {
523
+ sessionId,
524
+ title: titleOf(events, sessionId),
525
+ total,
526
+ cost,
527
+ calls,
528
+ firstActive,
529
+ lastActive,
530
+ models,
531
+ turns: [...turnTotals.values()]
532
+ }
533
+ };
534
+ }
384
535
 
385
536
  // src/wire.ts
386
537
  function writeJson(res, status, body, headers) {
@@ -419,6 +570,15 @@ function apply(ctx) {
419
570
  const payload = await (force ? usageMemo.refresh() : usageMemo.get());
420
571
  const headers = payload.ok ? { "cache-control": `private, max-age=${Math.round(USAGE_TTL_MS / 1e3)}` } : { "cache-control": "no-store" };
421
572
  writeJson(res, 200, payload, headers);
573
+ } else if (pathname === "/api/dsh-usage-dashboard/session") {
574
+ const id = url.searchParams.get("id");
575
+ if (id === null || id.trim() === "" || !isValidSessionId(id)) {
576
+ const error = id === null || id.trim() === "" ? "\u7F3A\u5C11\u4F1A\u8BDD id" : "\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5";
577
+ writeJson(res, 400, { ok: false, error }, { "cache-control": "no-store" });
578
+ return;
579
+ }
580
+ const payload = await fetchSessionUsage(ctx.sessionPersistence, id);
581
+ writeJson(res, 200, payload, { "cache-control": "no-store" });
422
582
  } else {
423
583
  writeJson(res, 404, { ok: false, error: "not found" });
424
584
  }