@cassius0924/dsh-usage-dashboard 0.4.0 → 0.6.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
@@ -60,32 +60,71 @@ function isTrustedApiRequest(req) {
60
60
 
61
61
  // src/pricing.ts
62
62
  var PEAK_PRICING_FROM_MS = Date.UTC(2026, 7, 16, 16, 0, 0);
63
+ var V41_FLASH_PRICING_FROM_MS = Date.UTC(2026, 8, 10, 4, 0, 0);
64
+ var PRO_ROUTED_TO_FLASH_FROM_MS = Date.UTC(2026, 8, 14, 4, 0, 0);
63
65
  var PEAK_WINDOWS = [[9, 12], [14, 18]];
64
66
  var BEIJING_OFFSET_MS = 8 * 36e5;
65
- var LEGACY_RATES = {
67
+ var FLAT_RATES = {
66
68
  pro: { cacheHit: 0.025, input: 3, output: 6 },
67
- flash: { cacheHit: 0.02, input: 1, output: 2 }
69
+ flash: { cacheHit: 0.02, input: 1, output: 2 },
70
+ // The vision model billed at flash rates from day one, so its pre-switch
71
+ // history is costed at the same flat prices as flash.
72
+ vision: { cacheHit: 0.02, input: 1, output: 2 }
68
73
  };
69
- var PEAK_RATES = {
74
+ var PEAK_RATES_08_17 = {
70
75
  pro: { cacheHit: 0.3, input: 9, output: 27 },
71
- flash: { cacheHit: 0.1, input: 3, output: 9 }
76
+ flash: { cacheHit: 0.1, input: 3, output: 9 },
77
+ // Official policy: the vision model is priced identically to flash; images
78
+ // cost tokens, not their own multiplier.
79
+ vision: { cacheHit: 0.1, input: 3, output: 9 }
72
80
  };
81
+ var PEAK_RATES_09_10 = {
82
+ pro: { cacheHit: 0.3, input: 9, output: 27 },
83
+ flash: { cacheHit: 0.04, input: 2, output: 8 },
84
+ vision: { cacheHit: 0.04, input: 2, output: 8 }
85
+ };
86
+ var PRICE_ERAS = [
87
+ { fromMs: PEAK_PRICING_FROM_MS, peak: PEAK_RATES_08_17, weekdaysOnly: false },
88
+ { fromMs: V41_FLASH_PRICING_FROM_MS, peak: PEAK_RATES_09_10, weekdaysOnly: true }
89
+ ];
90
+ function eraAt(timeMs) {
91
+ let era = PRICE_ERAS[0];
92
+ for (const candidate of PRICE_ERAS) if (timeMs >= candidate.fromMs) era = candidate;
93
+ return era;
94
+ }
73
95
  var halved = (rates) => ({
74
96
  cacheHit: rates.cacheHit / 2,
75
97
  input: rates.input / 2,
76
98
  output: rates.output / 2
77
99
  });
78
100
  function tierOf(model) {
79
- return model.toLowerCase().includes("flash") ? "flash" : "pro";
101
+ const name2 = model.toLowerCase();
102
+ if (name2.includes("vision")) return "vision";
103
+ if (name2.includes("flash")) return "flash";
104
+ return "pro";
105
+ }
106
+ function billedTierOf(timeMs, model) {
107
+ const tier = tierOf(model);
108
+ return tier === "pro" && timeMs >= PRO_ROUTED_TO_FLASH_FROM_MS ? "flash" : tier;
109
+ }
110
+ function isBeijingWeekday(timeMs) {
111
+ const day = new Date(timeMs + BEIJING_OFFSET_MS).getUTCDay();
112
+ return day >= 1 && day <= 5;
113
+ }
114
+ function estimateImageTokens(width, height) {
115
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return 0;
116
+ const area = width * height;
117
+ const scaledArea = Math.min(800 * 800, Math.max(384 * 384, area));
118
+ return 384 * (scaledArea / (800 * 800));
80
119
  }
81
120
  function isPeak(timeMs) {
121
+ if (eraAt(timeMs).weekdaysOnly && !isBeijingWeekday(timeMs)) return false;
82
122
  const hour = Math.floor(((timeMs + BEIJING_OFFSET_MS) % 864e5 + 864e5) % 864e5 / 36e5);
83
123
  return PEAK_WINDOWS.some(([from, to]) => hour >= from && hour < to);
84
124
  }
85
125
  function ratesAt(timeMs, model) {
86
- const tier = tierOf(model);
87
- if (timeMs < PEAK_PRICING_FROM_MS) return LEGACY_RATES[tier];
88
- const peak = PEAK_RATES[tier];
126
+ if (timeMs < PEAK_PRICING_FROM_MS) return FLAT_RATES[tierOf(model)];
127
+ const peak = eraAt(timeMs).peak[billedTierOf(timeMs, model)];
89
128
  return isPeak(timeMs) ? peak : halved(peak);
90
129
  }
91
130
  function cacheSavingOf(timeMs, model, cache) {
@@ -97,23 +136,19 @@ function costOf(timeMs, model, input, cache, output) {
97
136
  return applyRates(ratesAt(timeMs, model), input, cache, output);
98
137
  }
99
138
  function costUnderPeakEra(timeMs, model, input, cache, output, forceOffPeak = false) {
100
- const peak = PEAK_RATES[tierOf(model)];
139
+ const peak = PRICE_ERAS[PRICE_ERAS.length - 1].peak[billedTierOf(timeMs, model)];
101
140
  const rates = !forceOffPeak && isPeak(timeMs) ? peak : halved(peak);
102
141
  return applyRates(rates, input, cache, output);
103
142
  }
104
143
  function pricingInfo(nowMs) {
105
- const split = nowMs >= PEAK_PRICING_FROM_MS;
144
+ const era = eraAt(nowMs);
145
+ const row = (model, tier) => ({ model, peak: era.peak[tier], offPeak: halved(era.peak[tier]) });
106
146
  return {
107
147
  currency: "CNY",
108
148
  switchDate: "2026-08-17",
109
- splitActive: split,
110
- inPeakNow: split && isPeak(nowMs),
149
+ inPeakNow: isPeak(nowMs),
111
150
  peakWindows: ["09:00\u201312:00", "14:00\u201318:00"],
112
- tiers: ["pro", "flash"].map((tier) => ({
113
- model: tier === "pro" ? "deepseek-v4-pro" : "deepseek-v4-flash",
114
- peak: split ? PEAK_RATES[tier] : LEGACY_RATES[tier],
115
- offPeak: split ? halved(PEAK_RATES[tier]) : null
116
- }))
151
+ tiers: [row("deepseek-flash", "flash"), row("deepseek-v4-pro", "pro")]
117
152
  };
118
153
  }
119
154
 
@@ -175,6 +210,18 @@ function bump(map, key, input, output, cache, cost) {
175
210
  map.set(key, bucket);
176
211
  }
177
212
  var emptyOverall = () => ({ ...emptyBucket(), reasoning: 0, cacheSavings: 0 });
213
+ var emptyVision = () => ({ images: 0, imageTokens: 0, cost: 0, bytes: 0 });
214
+ function bumpVisionInto(acc, images, imageTokens, cost, bytes) {
215
+ acc.images += images;
216
+ acc.imageTokens += imageTokens;
217
+ acc.cost += cost;
218
+ acc.bytes += bytes;
219
+ }
220
+ function bumpVisionMap(map, key, images, imageTokens, cost, bytes) {
221
+ const acc = map.get(key) ?? emptyVision();
222
+ bumpVisionInto(acc, images, imageTokens, cost, bytes);
223
+ map.set(key, acc);
224
+ }
178
225
  var SESSION_TOP_N = 6;
179
226
  function titleOf(events, id) {
180
227
  let title = "";
@@ -217,6 +264,7 @@ function addUsageEvent(events, onEvent) {
217
264
  if (events === void 0) return result;
218
265
  let provider = "";
219
266
  let model = "";
267
+ const pendingImages = [];
220
268
  for (const ev of events) {
221
269
  if (ev?.type === "request/header") {
222
270
  const cfg = ev.data?.header?.config;
@@ -226,6 +274,7 @@ function addUsageEvent(events, onEvent) {
226
274
  }
227
275
  continue;
228
276
  }
277
+ pendingImages.push(...imagesInEvent(ev));
229
278
  if (ev?.type !== "assistant/message") continue;
230
279
  const usage = ev.data?.usage;
231
280
  if (usage === void 0) continue;
@@ -235,11 +284,60 @@ function addUsageEvent(events, onEvent) {
235
284
  continue;
236
285
  }
237
286
  const [input = 0, output = 0, cache = 0, reasoning = 0] = values;
238
- onEvent(ev.time, input, output, cache, reasoning, provider, model);
287
+ const rawMessageId = ev.data?.message?.id;
288
+ const messageId = typeof rawMessageId === "string" && rawMessageId !== "" ? rawMessageId : void 0;
289
+ const rawTurn = ev.data?.turn;
290
+ const turn = typeof rawTurn === "number" && Number.isFinite(rawTurn) ? rawTurn : void 0;
291
+ onEvent(ev.time, input, output, cache, reasoning, provider, model, messageId, turn, pendingImages);
292
+ pendingImages.length = 0;
239
293
  result.usageRecords += 1;
240
294
  }
241
295
  return result;
242
296
  }
297
+ function imageBlocks(blocks) {
298
+ const found = [];
299
+ const walkAll = (items) => {
300
+ for (const item of items ?? []) {
301
+ if (item?.type === "image" && item.attachment !== void 0 && item.attachment !== null) {
302
+ found.push(item.attachment);
303
+ }
304
+ if (Array.isArray(item?.content)) walkAll(item.content);
305
+ }
306
+ };
307
+ walkAll(blocks);
308
+ return found;
309
+ }
310
+ function imagesInEvent(ev) {
311
+ if (ev === void 0) return [];
312
+ return [
313
+ ...imageBlocks(ev.data?.content),
314
+ ...imageBlocks(ev.data?.message?.content)
315
+ ];
316
+ }
317
+ async function readSessionEvents(persistence, id) {
318
+ if (typeof persistence.open === "function") {
319
+ const handle = await persistence.open(id, "read");
320
+ try {
321
+ const events2 = [];
322
+ for (; ; ) {
323
+ const slice = await handle.read(events2.length);
324
+ const batch = slice.events ?? [];
325
+ if (batch.length === 0) return events2;
326
+ for (const event of batch) events2.push(event);
327
+ }
328
+ } finally {
329
+ try {
330
+ await handle.close();
331
+ } catch {
332
+ }
333
+ }
334
+ }
335
+ if (persistence.readFrom === void 0) {
336
+ throw new Error("\u5BBF\u4E3B sessionPersistence \u7F3A\u5C11 open() \u4E0E readFrom()\uFF0C\u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7");
337
+ }
338
+ const { events } = await persistence.readFrom(id, 0);
339
+ return events;
340
+ }
243
341
  async function fetchUsage(persistence, nowMs = Date.now()) {
244
342
  if (persistence === void 0) return { ok: false, error: "\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528" };
245
343
  let headers;
@@ -255,6 +353,9 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
255
353
  const modelDays = /* @__PURE__ */ new Map();
256
354
  const modelHours = /* @__PURE__ */ new Map();
257
355
  const sessions = [];
356
+ const vision = emptyVision();
357
+ const visionDays = /* @__PURE__ */ new Map();
358
+ const visionSessions = /* @__PURE__ */ new Map();
258
359
  const coverage = {
259
360
  scope: "local-dsh-session-logs",
260
361
  listedSessions: 0,
@@ -283,7 +384,10 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
283
384
  modelTotals: /* @__PURE__ */ new Map(),
284
385
  modelDays: /* @__PURE__ */ new Map(),
285
386
  modelHours: /* @__PURE__ */ new Map(),
286
- sessions: []
387
+ sessions: [],
388
+ vision: emptyVision(),
389
+ visionDays: /* @__PURE__ */ new Map(),
390
+ visionSessions: /* @__PURE__ */ new Map()
287
391
  }));
288
392
  const bumpOverall = (bucket, input, output, cache, reasoning, cost, cacheSavings) => {
289
393
  bucket.input += input;
@@ -316,15 +420,17 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
316
420
  session.calls += 1;
317
421
  if (time > session.lastActive) session.lastActive = time;
318
422
  };
319
- const sessionIds = headers.map((header) => header.id ?? header.sessionId).filter((sid) => sid !== void 0 && sid !== "");
423
+ const sessionIds = headers.map((header) => header.id ?? header.sessionId ?? header.header?.id ?? header.header?.sessionId).filter((sid) => sid !== void 0 && sid !== "");
320
424
  coverage.listedSessions = sessionIds.length;
321
425
  for (const sid of sessionIds) {
322
426
  try {
323
- const { events } = await persistence.readFrom(sid, 0);
427
+ const events = await readSessionEvents(persistence, sid);
324
428
  coverage.scannedSessions += 1;
325
429
  const session = { id: sid, title: "", total: 0, cost: 0, calls: 0, lastActive: 0 };
326
430
  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) => {
431
+ const sessionVision = { ...emptyVision(), title: "" };
432
+ const windowVisionSessions = windowAggregates.map(() => ({ ...emptyVision(), title: "" }));
433
+ const scanned = addUsageEvent(events, (time, input, output, cache, reasoning, provider, model, _messageId, _turn, images) => {
328
434
  if (coverage.earliestAt === null || time < coverage.earliestAt) coverage.earliestAt = time;
329
435
  if (coverage.latestAt === null || time > coverage.latestAt) coverage.latestAt = time;
330
436
  const d = new Date(time);
@@ -339,6 +445,29 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
339
445
  bumpModel(modelTotals, modelDays, modelHours, modelKey, dayKey, hourKey, input, output, cache, cost);
340
446
  bumpOverall(overall, input, output, cache, reasoning, cost, cacheSavings);
341
447
  bumpSession(session, time, eventTotal, cost);
448
+ if (images.length > 0) {
449
+ let imageTokensSum = 0;
450
+ let imageCostSum = 0;
451
+ let imageBytesSum = 0;
452
+ for (const image of images) {
453
+ const width = typeof image.width === "number" ? image.width : 0;
454
+ const height = typeof image.height === "number" ? image.height : 0;
455
+ const tokens = estimateImageTokens(width, height);
456
+ imageTokensSum += tokens;
457
+ imageCostSum += costOf(time, model, tokens, 0, 0);
458
+ imageBytesSum += typeof image.bytes === "number" ? image.bytes : 0;
459
+ }
460
+ bumpVisionInto(vision, images.length, imageTokensSum, imageCostSum, imageBytesSum);
461
+ bumpVisionMap(visionDays, dayKey, images.length, imageTokensSum, imageCostSum, imageBytesSum);
462
+ bumpVisionInto(sessionVision, images.length, imageTokensSum, imageCostSum, imageBytesSum);
463
+ for (let i = 0; i < windowAggregates.length; i++) {
464
+ const aggregate = windowAggregates[i];
465
+ if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue;
466
+ bumpVisionInto(aggregate.vision, images.length, imageTokensSum, imageCostSum, imageBytesSum);
467
+ bumpVisionMap(aggregate.visionDays, dayKey, images.length, imageTokensSum, imageCostSum, imageBytesSum);
468
+ bumpVisionInto(windowVisionSessions[i], images.length, imageTokensSum, imageCostSum, imageBytesSum);
469
+ }
470
+ }
342
471
  for (let i = 0; i < windowAggregates.length; i++) {
343
472
  const aggregate = windowAggregates[i];
344
473
  if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue;
@@ -368,6 +497,16 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
368
497
  windowAggregates[i].sessions.push(windowSession);
369
498
  }
370
499
  }
500
+ if (sessionVision.images > 0) {
501
+ sessionVision.title = titleOf(events, sid);
502
+ visionSessions.set(sid, sessionVision);
503
+ for (let i = 0; i < windowAggregates.length; i++) {
504
+ const windowVision = windowVisionSessions[i];
505
+ if (windowVision.images === 0) continue;
506
+ windowVision.title = sessionVision.title;
507
+ windowAggregates[i].visionSessions.set(sid, windowVision);
508
+ }
509
+ }
371
510
  } catch {
372
511
  coverage.failedSessions += 1;
373
512
  }
@@ -423,6 +562,22 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
423
562
  const b = dayMap.get(key) ?? emptyBucket();
424
563
  heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls });
425
564
  }
565
+ const makeVisionDaily = (map, count) => {
566
+ const result = [];
567
+ for (let i = count - 1; i >= 0; i--) {
568
+ const key = dayKeyOf(dateBefore(i));
569
+ const b = map.get(key) ?? emptyVision();
570
+ result.push({ date: key, images: b.images, imageTokens: b.imageTokens, cost: b.cost });
571
+ }
572
+ return result;
573
+ };
574
+ const makeVisionSessions = (map) => [...map.entries()].map(([id, acc]) => ({
575
+ id,
576
+ title: acc.title !== "" ? acc.title : `\u4F1A\u8BDD ${id.slice(0, 8)}`,
577
+ images: acc.images,
578
+ imageTokens: acc.imageTokens,
579
+ cost: acc.cost
580
+ })).sort((a, b) => b.images - a.images).slice(0, SESSION_TOP_N);
426
581
  const models = makeModels(modelTotals, modelDays, modelHours, 30);
427
582
  const windows = windowAggregates.map((aggregate) => ({
428
583
  days: aggregate.days,
@@ -431,7 +586,10 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
431
586
  totals: { ...aggregate.overall },
432
587
  models: makeModels(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, aggregate.days),
433
588
  sessions: [...aggregate.sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),
434
- sessionCount: aggregate.sessions.length
589
+ sessionCount: aggregate.sessions.length,
590
+ vision: { ...aggregate.vision },
591
+ visionDaily: makeVisionDaily(aggregate.visionDays, aggregate.days),
592
+ visionSessions: makeVisionSessions(aggregate.visionSessions)
435
593
  }));
436
594
  return {
437
595
  ok: true,
@@ -447,6 +605,9 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
447
605
  sessionCount: sessions.length,
448
606
  coverage,
449
607
  windows,
608
+ vision: { ...vision },
609
+ visionDaily: makeVisionDaily(visionDays, 30),
610
+ visionSessions: makeVisionSessions(visionSessions),
450
611
  totals: {
451
612
  input: overall.input,
452
613
  output: overall.output,
@@ -466,8 +627,7 @@ async function fetchSessionUsage(persistence, sessionId) {
466
627
  if (!isValidSessionId(sessionId)) return { ok: false, error: "\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5" };
467
628
  let events;
468
629
  try {
469
- const result = await persistence.readFrom(sessionId, 0);
470
- events = result.events;
630
+ events = await readSessionEvents(persistence, sessionId);
471
631
  } catch (err) {
472
632
  return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A${errorMessage(err)}` };
473
633
  }
@@ -477,7 +637,8 @@ async function fetchSessionUsage(persistence, sessionId) {
477
637
  let calls = 0;
478
638
  let firstActive = null;
479
639
  let lastActive = null;
480
- addUsageEvent(events, (time, input, output, cache, _reasoning, provider, model) => {
640
+ const turnTotals = /* @__PURE__ */ new Map();
641
+ addUsageEvent(events, (time, input, output, cache, _reasoning, provider, model, messageId, turn) => {
481
642
  const modelKey = `${provider}/${model}`;
482
643
  const eventCost = costOf(time, model, input, cache, output);
483
644
  const eventTotal = input + output + cache;
@@ -487,6 +648,15 @@ async function fetchSessionUsage(persistence, sessionId) {
487
648
  calls += 1;
488
649
  if (firstActive === null || time < firstActive) firstActive = time;
489
650
  if (lastActive === null || time > lastActive) lastActive = time;
651
+ if (messageId !== void 0) {
652
+ const key = turn === void 0 ? `message:${messageId}` : `turn:${turn}`;
653
+ const aggregate = turnTotals.get(key) ?? { messageId, total: 0, cost: 0, calls: 0 };
654
+ aggregate.messageId = messageId;
655
+ aggregate.total += eventTotal;
656
+ aggregate.cost += eventCost;
657
+ aggregate.calls += 1;
658
+ turnTotals.set(key, aggregate);
659
+ }
490
660
  });
491
661
  const models = [...modelTotals.entries()].map(([modelKey, bucket]) => {
492
662
  const slash = modelKey.indexOf("/");
@@ -513,7 +683,8 @@ async function fetchSessionUsage(persistence, sessionId) {
513
683
  calls,
514
684
  firstActive,
515
685
  lastActive,
516
- models
686
+ models,
687
+ turns: [...turnTotals.values()]
517
688
  }
518
689
  };
519
690
  }