@cassius0924/dsh-usage-dashboard 0.3.1 → 0.4.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,18 @@ 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;
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;
220
238
  onEvent(ev.time, input, output, cache, reasoning, provider, model);
239
+ result.usageRecords += 1;
221
240
  }
241
+ return result;
222
242
  }
223
- async function fetchUsage(persistence) {
243
+ async function fetchUsage(persistence, nowMs = Date.now()) {
224
244
  if (persistence === void 0) return { ok: false, error: "\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528" };
225
245
  let headers;
226
246
  try {
@@ -230,132 +250,189 @@ async function fetchUsage(persistence) {
230
250
  }
231
251
  const dayMap = /* @__PURE__ */ new Map();
232
252
  const hourMap = /* @__PURE__ */ new Map();
233
- const overall = { ...emptyBucket(), reasoning: 0, cacheSavings: 0 };
253
+ const overall = emptyOverall();
234
254
  const modelTotals = /* @__PURE__ */ new Map();
235
255
  const modelDays = /* @__PURE__ */ new Map();
236
256
  const modelHours = /* @__PURE__ */ new Map();
237
257
  const sessions = [];
258
+ const coverage = {
259
+ scope: "local-dsh-session-logs",
260
+ listedSessions: 0,
261
+ scannedSessions: 0,
262
+ usageRecords: 0,
263
+ skippedRecords: 0,
264
+ failedSessions: 0,
265
+ earliestAt: null,
266
+ latestAt: null
267
+ };
238
268
  const peakSplit = {
239
269
  peak: { total: 0, cost: 0, calls: 0 },
240
270
  offPeak: { total: 0, cost: 0, calls: 0 },
241
271
  peakEraCost: 0,
242
272
  offPeakEraCost: 0
243
273
  };
244
- const bump = (map, key, input, output, cache, cost) => {
245
- const bucket = map.get(key) ?? emptyBucket();
274
+ const now = new Date(nowMs);
275
+ const dateBefore = (days) => new Date(now.getFullYear(), now.getMonth(), now.getDate() - days);
276
+ const windowAggregates = USAGE_WINDOW_DAYS.map((days) => ({
277
+ days,
278
+ startKey: dayKeyOf(dateBefore(days - 1)),
279
+ endKey: dayKeyOf(now),
280
+ dayMap: /* @__PURE__ */ new Map(),
281
+ hourMap: /* @__PURE__ */ new Map(),
282
+ overall: emptyOverall(),
283
+ modelTotals: /* @__PURE__ */ new Map(),
284
+ modelDays: /* @__PURE__ */ new Map(),
285
+ modelHours: /* @__PURE__ */ new Map(),
286
+ sessions: []
287
+ }));
288
+ const bumpOverall = (bucket, input, output, cache, reasoning, cost, cacheSavings) => {
246
289
  bucket.input += input;
247
290
  bucket.output += output;
248
291
  bucket.cache += cache;
292
+ bucket.reasoning += reasoning;
249
293
  bucket.total += input + output + cache;
250
294
  bucket.cost += cost;
295
+ bucket.cacheSavings += cacheSavings;
251
296
  bucket.calls += 1;
252
- map.set(key, bucket);
253
297
  };
254
- for (const header of headers) {
255
- const sid = header.id ?? header.sessionId;
256
- if (sid === void 0 || sid === "") continue;
298
+ const bumpModel = (totals, days, hours, modelKey, dayKey, hourKey, input, output, cache, cost) => {
299
+ bump(totals, modelKey, input, output, cache, cost);
300
+ let dayBuckets = days.get(modelKey);
301
+ if (dayBuckets === void 0) {
302
+ dayBuckets = /* @__PURE__ */ new Map();
303
+ days.set(modelKey, dayBuckets);
304
+ }
305
+ bump(dayBuckets, dayKey, input, output, cache, cost);
306
+ let hourBuckets = hours.get(modelKey);
307
+ if (hourBuckets === void 0) {
308
+ hourBuckets = /* @__PURE__ */ new Map();
309
+ hours.set(modelKey, hourBuckets);
310
+ }
311
+ bump(hourBuckets, hourKey, input, output, cache, cost);
312
+ };
313
+ const bumpSession = (session, time, total, cost) => {
314
+ session.total += total;
315
+ session.cost += cost;
316
+ session.calls += 1;
317
+ if (time > session.lastActive) session.lastActive = time;
318
+ };
319
+ const sessionIds = headers.map((header) => header.id ?? header.sessionId).filter((sid) => sid !== void 0 && sid !== "");
320
+ coverage.listedSessions = sessionIds.length;
321
+ for (const sid of sessionIds) {
257
322
  try {
258
323
  const { events } = await persistence.readFrom(sid, 0);
324
+ coverage.scannedSessions += 1;
259
325
  const session = { id: sid, title: "", total: 0, cost: 0, calls: 0, lastActive: 0 };
260
- addUsageEvent(events, (time, input, output, cache, reasoning, provider, model) => {
326
+ const windowSessions = windowAggregates.map(() => ({ id: sid, title: "", total: 0, cost: 0, calls: 0, lastActive: 0 }));
327
+ const scanned = addUsageEvent(events, (time, input, output, cache, reasoning, provider, model) => {
328
+ if (coverage.earliestAt === null || time < coverage.earliestAt) coverage.earliestAt = time;
329
+ if (coverage.latestAt === null || time > coverage.latestAt) coverage.latestAt = time;
261
330
  const d = new Date(time);
262
331
  const dayKey = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
263
332
  const hourKey = String(d.getHours());
264
333
  const modelKey = `${provider}/${model}`;
265
334
  const cost = costOf(time, model, input, cache, output);
335
+ const cacheSavings = cacheSavingOf(time, model, cache);
336
+ const eventTotal = input + output + cache;
266
337
  bump(dayMap, dayKey, input, output, cache, cost);
267
338
  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);
339
+ bumpModel(modelTotals, modelDays, modelHours, modelKey, dayKey, hourKey, input, output, cache, cost);
340
+ bumpOverall(overall, input, output, cache, reasoning, cost, cacheSavings);
341
+ bumpSession(session, time, eventTotal, cost);
342
+ for (let i = 0; i < windowAggregates.length; i++) {
343
+ const aggregate = windowAggregates[i];
344
+ if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue;
345
+ bump(aggregate.dayMap, dayKey, input, output, cache, cost);
346
+ bump(aggregate.hourMap, hourKey, input, output, cache, cost);
347
+ bumpModel(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, modelKey, dayKey, hourKey, input, output, cache, cost);
348
+ bumpOverall(aggregate.overall, input, output, cache, reasoning, cost, cacheSavings);
349
+ bumpSession(windowSessions[i], time, eventTotal, cost);
279
350
  }
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
351
  const window = isPeak(time) ? peakSplit.peak : peakSplit.offPeak;
293
- window.total += input + output + cache;
352
+ window.total += eventTotal;
294
353
  window.cost += cost;
295
354
  window.calls += 1;
296
355
  peakSplit.peakEraCost += costUnderPeakEra(time, model, input, cache, output);
297
356
  peakSplit.offPeakEraCost += costUnderPeakEra(time, model, input, cache, output, true);
298
- overall.calls += 1;
299
357
  });
358
+ coverage.usageRecords += scanned.usageRecords;
359
+ coverage.skippedRecords += scanned.skippedRecords;
300
360
  if (session.calls > 0) {
301
- session.title = titleOf(events, sid);
361
+ const title = titleOf(events, sid);
362
+ session.title = title;
302
363
  sessions.push(session);
364
+ for (let i = 0; i < windowAggregates.length; i++) {
365
+ const windowSession = windowSessions[i];
366
+ if (windowSession.calls === 0) continue;
367
+ windowSession.title = title;
368
+ windowAggregates[i].sessions.push(windowSession);
369
+ }
303
370
  }
304
371
  } catch {
372
+ coverage.failedSessions += 1;
305
373
  }
306
374
  }
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 });
375
+ const makeDaily = (map, count) => {
376
+ const result = [];
377
+ for (let i = count - 1; i >= 0; i--) {
378
+ const key = dayKeyOf(dateBefore(i));
379
+ const b = map.get(key) ?? emptyBucket();
380
+ result.push({ date: key, input: b.input, output: b.output, cache: b.cache, total: b.total, cost: b.cost, calls: b.calls });
339
381
  }
340
- const hourly2 = [];
382
+ return result;
383
+ };
384
+ const makeHourly = (map) => {
385
+ const result = [];
341
386
  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 });
387
+ const b = map.get(String(i)) ?? emptyBucket();
388
+ result.push({ hour: i, total: b.total, cost: b.cost, calls: b.calls });
344
389
  }
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
- });
390
+ return result;
391
+ };
392
+ const makeModels = (totals, daysByModel, hoursByModel, dayCount) => {
393
+ const result = [];
394
+ for (const [modelKey, bucket] of totals) {
395
+ const slash = modelKey.indexOf("/");
396
+ const provider = slash < 0 ? "" : modelKey.slice(0, slash);
397
+ const model = slash < 0 ? modelKey : modelKey.slice(slash + 1);
398
+ const days = daysByModel.get(modelKey);
399
+ const hours = hoursByModel.get(modelKey);
400
+ const daily2 = makeDaily(days ?? /* @__PURE__ */ new Map(), dayCount).map((point) => ({ total: point.total, cost: point.cost, calls: point.calls }));
401
+ const hourly2 = makeHourly(hours ?? /* @__PURE__ */ new Map()).map((point) => ({ total: point.total, cost: point.cost, calls: point.calls }));
402
+ result.push({
403
+ provider,
404
+ model,
405
+ input: bucket.input,
406
+ output: bucket.output,
407
+ cache: bucket.cache,
408
+ total: bucket.total,
409
+ cost: bucket.cost,
410
+ calls: bucket.calls,
411
+ daily: daily2,
412
+ hourly: hourly2
413
+ });
414
+ }
415
+ result.sort((a, b) => b.total - a.total);
416
+ return result;
417
+ };
418
+ const daily = makeDaily(dayMap, 30);
419
+ const hourly = makeHourly(hourMap);
420
+ const heatmap = [];
421
+ for (let i = 363; i >= 0; i--) {
422
+ const key = dayKeyOf(dateBefore(i));
423
+ const b = dayMap.get(key) ?? emptyBucket();
424
+ heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls });
357
425
  }
358
- models.sort((a, b) => b.total - a.total);
426
+ const models = makeModels(modelTotals, modelDays, modelHours, 30);
427
+ const windows = windowAggregates.map((aggregate) => ({
428
+ days: aggregate.days,
429
+ daily: makeDaily(aggregate.dayMap, aggregate.days),
430
+ hourly: makeHourly(aggregate.hourMap),
431
+ totals: { ...aggregate.overall },
432
+ models: makeModels(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, aggregate.days),
433
+ sessions: [...aggregate.sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),
434
+ sessionCount: aggregate.sessions.length
435
+ }));
359
436
  return {
360
437
  ok: true,
361
438
  data: {
@@ -363,11 +440,13 @@ async function fetchUsage(persistence) {
363
440
  hourly,
364
441
  heatmap,
365
442
  models,
366
- summary: summarize(dayMap, /* @__PURE__ */ new Date()),
367
- pricing: pricingInfo(Date.now()),
443
+ summary: summarize(dayMap, new Date(nowMs)),
444
+ pricing: pricingInfo(nowMs),
368
445
  peakSplit,
369
446
  sessions: [...sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),
370
447
  sessionCount: sessions.length,
448
+ coverage,
449
+ windows,
371
450
  totals: {
372
451
  input: overall.input,
373
452
  output: overall.output,
@@ -381,6 +460,63 @@ async function fetchUsage(persistence) {
381
460
  }
382
461
  };
383
462
  }
463
+ async function fetchSessionUsage(persistence, sessionId) {
464
+ if (persistence === void 0) return { ok: false, error: "\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528" };
465
+ if (typeof sessionId !== "string" || sessionId === "") return { ok: false, error: "\u7F3A\u5C11\u4F1A\u8BDD id" };
466
+ if (!isValidSessionId(sessionId)) return { ok: false, error: "\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5" };
467
+ let events;
468
+ try {
469
+ const result = await persistence.readFrom(sessionId, 0);
470
+ events = result.events;
471
+ } catch (err) {
472
+ return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A${errorMessage(err)}` };
473
+ }
474
+ const modelTotals = /* @__PURE__ */ new Map();
475
+ let total = 0;
476
+ let cost = 0;
477
+ let calls = 0;
478
+ let firstActive = null;
479
+ let lastActive = null;
480
+ addUsageEvent(events, (time, input, output, cache, _reasoning, provider, model) => {
481
+ const modelKey = `${provider}/${model}`;
482
+ const eventCost = costOf(time, model, input, cache, output);
483
+ const eventTotal = input + output + cache;
484
+ bump(modelTotals, modelKey, input, output, cache, eventCost);
485
+ total += eventTotal;
486
+ cost += eventCost;
487
+ calls += 1;
488
+ if (firstActive === null || time < firstActive) firstActive = time;
489
+ if (lastActive === null || time > lastActive) lastActive = time;
490
+ });
491
+ const models = [...modelTotals.entries()].map(([modelKey, bucket]) => {
492
+ const slash = modelKey.indexOf("/");
493
+ const provider = slash < 0 ? "" : modelKey.slice(0, slash);
494
+ const model = slash < 0 ? modelKey : modelKey.slice(slash + 1);
495
+ return {
496
+ provider,
497
+ model,
498
+ input: bucket.input,
499
+ output: bucket.output,
500
+ cache: bucket.cache,
501
+ total: bucket.total,
502
+ cost: bucket.cost,
503
+ calls: bucket.calls
504
+ };
505
+ }).sort((a, b) => b.cost - a.cost);
506
+ return {
507
+ ok: true,
508
+ data: {
509
+ sessionId,
510
+ title: titleOf(events, sessionId),
511
+ total,
512
+ cost,
513
+ calls,
514
+ firstActive,
515
+ lastActive,
516
+ models
517
+ }
518
+ };
519
+ }
384
520
 
385
521
  // src/wire.ts
386
522
  function writeJson(res, status, body, headers) {
@@ -419,6 +555,15 @@ function apply(ctx) {
419
555
  const payload = await (force ? usageMemo.refresh() : usageMemo.get());
420
556
  const headers = payload.ok ? { "cache-control": `private, max-age=${Math.round(USAGE_TTL_MS / 1e3)}` } : { "cache-control": "no-store" };
421
557
  writeJson(res, 200, payload, headers);
558
+ } else if (pathname === "/api/dsh-usage-dashboard/session") {
559
+ const id = url.searchParams.get("id");
560
+ if (id === null || id.trim() === "" || !isValidSessionId(id)) {
561
+ const error = id === null || id.trim() === "" ? "\u7F3A\u5C11\u4F1A\u8BDD id" : "\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5";
562
+ writeJson(res, 400, { ok: false, error }, { "cache-control": "no-store" });
563
+ return;
564
+ }
565
+ const payload = await fetchSessionUsage(ctx.sessionPersistence, id);
566
+ writeJson(res, 200, payload, { "cache-control": "no-store" });
422
567
  } else {
423
568
  writeJson(res, 404, { ok: false, error: "not found" });
424
569
  }