@cassius0924/dsh-usage-dashboard 0.5.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;
@@ -239,11 +288,56 @@ function addUsageEvent(events, onEvent) {
239
288
  const messageId = typeof rawMessageId === "string" && rawMessageId !== "" ? rawMessageId : void 0;
240
289
  const rawTurn = ev.data?.turn;
241
290
  const turn = typeof rawTurn === "number" && Number.isFinite(rawTurn) ? rawTurn : void 0;
242
- onEvent(ev.time, input, output, cache, reasoning, provider, model, messageId, turn);
291
+ onEvent(ev.time, input, output, cache, reasoning, provider, model, messageId, turn, pendingImages);
292
+ pendingImages.length = 0;
243
293
  result.usageRecords += 1;
244
294
  }
245
295
  return result;
246
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
+ }
247
341
  async function fetchUsage(persistence, nowMs = Date.now()) {
248
342
  if (persistence === void 0) return { ok: false, error: "\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528" };
249
343
  let headers;
@@ -259,6 +353,9 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
259
353
  const modelDays = /* @__PURE__ */ new Map();
260
354
  const modelHours = /* @__PURE__ */ new Map();
261
355
  const sessions = [];
356
+ const vision = emptyVision();
357
+ const visionDays = /* @__PURE__ */ new Map();
358
+ const visionSessions = /* @__PURE__ */ new Map();
262
359
  const coverage = {
263
360
  scope: "local-dsh-session-logs",
264
361
  listedSessions: 0,
@@ -287,7 +384,10 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
287
384
  modelTotals: /* @__PURE__ */ new Map(),
288
385
  modelDays: /* @__PURE__ */ new Map(),
289
386
  modelHours: /* @__PURE__ */ new Map(),
290
- sessions: []
387
+ sessions: [],
388
+ vision: emptyVision(),
389
+ visionDays: /* @__PURE__ */ new Map(),
390
+ visionSessions: /* @__PURE__ */ new Map()
291
391
  }));
292
392
  const bumpOverall = (bucket, input, output, cache, reasoning, cost, cacheSavings) => {
293
393
  bucket.input += input;
@@ -320,15 +420,17 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
320
420
  session.calls += 1;
321
421
  if (time > session.lastActive) session.lastActive = time;
322
422
  };
323
- 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 !== "");
324
424
  coverage.listedSessions = sessionIds.length;
325
425
  for (const sid of sessionIds) {
326
426
  try {
327
- const { events } = await persistence.readFrom(sid, 0);
427
+ const events = await readSessionEvents(persistence, sid);
328
428
  coverage.scannedSessions += 1;
329
429
  const session = { id: sid, title: "", total: 0, cost: 0, calls: 0, lastActive: 0 };
330
430
  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) => {
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) => {
332
434
  if (coverage.earliestAt === null || time < coverage.earliestAt) coverage.earliestAt = time;
333
435
  if (coverage.latestAt === null || time > coverage.latestAt) coverage.latestAt = time;
334
436
  const d = new Date(time);
@@ -343,6 +445,29 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
343
445
  bumpModel(modelTotals, modelDays, modelHours, modelKey, dayKey, hourKey, input, output, cache, cost);
344
446
  bumpOverall(overall, input, output, cache, reasoning, cost, cacheSavings);
345
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
+ }
346
471
  for (let i = 0; i < windowAggregates.length; i++) {
347
472
  const aggregate = windowAggregates[i];
348
473
  if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue;
@@ -372,6 +497,16 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
372
497
  windowAggregates[i].sessions.push(windowSession);
373
498
  }
374
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
+ }
375
510
  } catch {
376
511
  coverage.failedSessions += 1;
377
512
  }
@@ -427,6 +562,22 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
427
562
  const b = dayMap.get(key) ?? emptyBucket();
428
563
  heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls });
429
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);
430
581
  const models = makeModels(modelTotals, modelDays, modelHours, 30);
431
582
  const windows = windowAggregates.map((aggregate) => ({
432
583
  days: aggregate.days,
@@ -435,7 +586,10 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
435
586
  totals: { ...aggregate.overall },
436
587
  models: makeModels(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, aggregate.days),
437
588
  sessions: [...aggregate.sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),
438
- 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)
439
593
  }));
440
594
  return {
441
595
  ok: true,
@@ -451,6 +605,9 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
451
605
  sessionCount: sessions.length,
452
606
  coverage,
453
607
  windows,
608
+ vision: { ...vision },
609
+ visionDaily: makeVisionDaily(visionDays, 30),
610
+ visionSessions: makeVisionSessions(visionSessions),
454
611
  totals: {
455
612
  input: overall.input,
456
613
  output: overall.output,
@@ -470,8 +627,7 @@ async function fetchSessionUsage(persistence, sessionId) {
470
627
  if (!isValidSessionId(sessionId)) return { ok: false, error: "\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5" };
471
628
  let events;
472
629
  try {
473
- const result = await persistence.readFrom(sessionId, 0);
474
- events = result.events;
630
+ events = await readSessionEvents(persistence, sessionId);
475
631
  } catch (err) {
476
632
  return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A${errorMessage(err)}` };
477
633
  }