@cassius0924/dsh-usage-dashboard 0.5.0 → 0.6.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/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
 
@@ -122,7 +157,47 @@ var pad2 = (n) => n < 10 ? `0${n}` : String(n);
122
157
  function errorMessage(err) {
123
158
  return err?.message ?? String(err);
124
159
  }
125
- async function fetchBalance(credentials) {
160
+ var ACCOUNT_CLIENT = {
161
+ version: process.env.DSH_CLIENT_VERSION ?? "0.0.0",
162
+ locale: "zh-CN",
163
+ timezoneOffsetSeconds: -(/* @__PURE__ */ new Date()).getTimezoneOffset() * 60
164
+ };
165
+ function accountBalances(toppedUpWallets, grantedWallets) {
166
+ const byCurrency = /* @__PURE__ */ new Map();
167
+ const add = (wallets, field) => {
168
+ for (const wallet of wallets) {
169
+ const currency = wallet.currency ?? "";
170
+ const amount = Number(wallet.balance ?? "0");
171
+ if (currency === "" || !Number.isFinite(amount)) continue;
172
+ const acc = byCurrency.get(currency) ?? { total: 0, granted: 0, toppedUp: 0 };
173
+ acc[field] += amount;
174
+ acc.total += amount;
175
+ byCurrency.set(currency, acc);
176
+ }
177
+ };
178
+ add(toppedUpWallets, "toppedUp");
179
+ add(grantedWallets, "granted");
180
+ return [...byCurrency].map(([currency, acc]) => ({
181
+ currency,
182
+ total: acc.total.toFixed(2),
183
+ granted: acc.granted.toFixed(2),
184
+ toppedUp: acc.toppedUp.toFixed(2)
185
+ }));
186
+ }
187
+ async function readAccountBalance(account) {
188
+ try {
189
+ const result = await account.getBalance(ACCOUNT_CLIENT);
190
+ if (result === null) return { ok: false, reason: "signed-out" };
191
+ if (result.status !== "ready") return { ok: false, reason: "failed" };
192
+ const balances = accountBalances(result.value ?? [], result.bonusWallets ?? []);
193
+ return { ok: true, data: { isAvailable: balances.length > 0, balances } };
194
+ } catch {
195
+ return { ok: false, reason: "failed" };
196
+ }
197
+ }
198
+ async function fetchBalance(credentials, account) {
199
+ const fromAccount = account === void 0 ? void 0 : await readAccountBalance(account);
200
+ if (fromAccount?.ok === true) return { ok: true, data: fromAccount.data };
126
201
  if (credentials === void 0) return { ok: false, error: "\u51ED\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528" };
127
202
  let cred;
128
203
  try {
@@ -131,7 +206,10 @@ async function fetchBalance(credentials) {
131
206
  return { ok: false, error: `\u8BFB\u53D6 API Key \u5931\u8D25\uFF1A${errorMessage(err)}` };
132
207
  }
133
208
  if (cred === void 0 || cred.value === "") {
134
- return { ok: false, error: "\u672A\u914D\u7F6E DEEPSEEK_API_KEY\uFF08\u53EF\u5728\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u4E2D\u586B\u5199\uFF09" };
209
+ return {
210
+ ok: false,
211
+ error: fromAccount?.reason === "failed" ? "\u8D26\u53F7\u4F59\u989D\u63A5\u53E3\u4E0D\u53EF\u7528\uFF0C\u4E14\u672A\u914D\u7F6E DEEPSEEK_API_KEY" : "\u672A\u914D\u7F6E DEEPSEEK_API_KEY\uFF08\u53EF\u5728\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u4E2D\u586B\u5199\uFF09"
212
+ };
135
213
  }
136
214
  let res;
137
215
  try {
@@ -175,6 +253,18 @@ function bump(map, key, input, output, cache, cost) {
175
253
  map.set(key, bucket);
176
254
  }
177
255
  var emptyOverall = () => ({ ...emptyBucket(), reasoning: 0, cacheSavings: 0 });
256
+ var emptyVision = () => ({ images: 0, imageTokens: 0, cost: 0, bytes: 0 });
257
+ function bumpVisionInto(acc, images, imageTokens, cost, bytes) {
258
+ acc.images += images;
259
+ acc.imageTokens += imageTokens;
260
+ acc.cost += cost;
261
+ acc.bytes += bytes;
262
+ }
263
+ function bumpVisionMap(map, key, images, imageTokens, cost, bytes) {
264
+ const acc = map.get(key) ?? emptyVision();
265
+ bumpVisionInto(acc, images, imageTokens, cost, bytes);
266
+ map.set(key, acc);
267
+ }
178
268
  var SESSION_TOP_N = 6;
179
269
  function titleOf(events, id) {
180
270
  let title = "";
@@ -217,6 +307,7 @@ function addUsageEvent(events, onEvent) {
217
307
  if (events === void 0) return result;
218
308
  let provider = "";
219
309
  let model = "";
310
+ const pendingImages = [];
220
311
  for (const ev of events) {
221
312
  if (ev?.type === "request/header") {
222
313
  const cfg = ev.data?.header?.config;
@@ -226,6 +317,7 @@ function addUsageEvent(events, onEvent) {
226
317
  }
227
318
  continue;
228
319
  }
320
+ pendingImages.push(...imagesInEvent(ev));
229
321
  if (ev?.type !== "assistant/message") continue;
230
322
  const usage = ev.data?.usage;
231
323
  if (usage === void 0) continue;
@@ -239,11 +331,56 @@ function addUsageEvent(events, onEvent) {
239
331
  const messageId = typeof rawMessageId === "string" && rawMessageId !== "" ? rawMessageId : void 0;
240
332
  const rawTurn = ev.data?.turn;
241
333
  const turn = typeof rawTurn === "number" && Number.isFinite(rawTurn) ? rawTurn : void 0;
242
- onEvent(ev.time, input, output, cache, reasoning, provider, model, messageId, turn);
334
+ onEvent(ev.time, input, output, cache, reasoning, provider, model, messageId, turn, pendingImages);
335
+ pendingImages.length = 0;
243
336
  result.usageRecords += 1;
244
337
  }
245
338
  return result;
246
339
  }
340
+ function imageBlocks(blocks) {
341
+ const found = [];
342
+ const walkAll = (items) => {
343
+ for (const item of items ?? []) {
344
+ if (item?.type === "image" && item.attachment !== void 0 && item.attachment !== null) {
345
+ found.push(item.attachment);
346
+ }
347
+ if (Array.isArray(item?.content)) walkAll(item.content);
348
+ }
349
+ };
350
+ walkAll(blocks);
351
+ return found;
352
+ }
353
+ function imagesInEvent(ev) {
354
+ if (ev === void 0) return [];
355
+ return [
356
+ ...imageBlocks(ev.data?.content),
357
+ ...imageBlocks(ev.data?.message?.content)
358
+ ];
359
+ }
360
+ async function readSessionEvents(persistence, id) {
361
+ if (typeof persistence.open === "function") {
362
+ const handle = await persistence.open(id, "read");
363
+ try {
364
+ const events2 = [];
365
+ for (; ; ) {
366
+ const slice = await handle.read(events2.length);
367
+ const batch = slice.events ?? [];
368
+ if (batch.length === 0) return events2;
369
+ for (const event of batch) events2.push(event);
370
+ }
371
+ } finally {
372
+ try {
373
+ await handle.close();
374
+ } catch {
375
+ }
376
+ }
377
+ }
378
+ if (persistence.readFrom === void 0) {
379
+ throw new Error("\u5BBF\u4E3B sessionPersistence \u7F3A\u5C11 open() \u4E0E readFrom()\uFF0C\u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7");
380
+ }
381
+ const { events } = await persistence.readFrom(id, 0);
382
+ return events;
383
+ }
247
384
  async function fetchUsage(persistence, nowMs = Date.now()) {
248
385
  if (persistence === void 0) return { ok: false, error: "\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528" };
249
386
  let headers;
@@ -259,6 +396,9 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
259
396
  const modelDays = /* @__PURE__ */ new Map();
260
397
  const modelHours = /* @__PURE__ */ new Map();
261
398
  const sessions = [];
399
+ const vision = emptyVision();
400
+ const visionDays = /* @__PURE__ */ new Map();
401
+ const visionSessions = /* @__PURE__ */ new Map();
262
402
  const coverage = {
263
403
  scope: "local-dsh-session-logs",
264
404
  listedSessions: 0,
@@ -287,7 +427,10 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
287
427
  modelTotals: /* @__PURE__ */ new Map(),
288
428
  modelDays: /* @__PURE__ */ new Map(),
289
429
  modelHours: /* @__PURE__ */ new Map(),
290
- sessions: []
430
+ sessions: [],
431
+ vision: emptyVision(),
432
+ visionDays: /* @__PURE__ */ new Map(),
433
+ visionSessions: /* @__PURE__ */ new Map()
291
434
  }));
292
435
  const bumpOverall = (bucket, input, output, cache, reasoning, cost, cacheSavings) => {
293
436
  bucket.input += input;
@@ -320,15 +463,17 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
320
463
  session.calls += 1;
321
464
  if (time > session.lastActive) session.lastActive = time;
322
465
  };
323
- const sessionIds = headers.map((header) => header.id ?? header.sessionId).filter((sid) => sid !== void 0 && sid !== "");
466
+ const sessionIds = headers.map((header) => header.id ?? header.sessionId ?? header.header?.id ?? header.header?.sessionId).filter((sid) => sid !== void 0 && sid !== "");
324
467
  coverage.listedSessions = sessionIds.length;
325
468
  for (const sid of sessionIds) {
326
469
  try {
327
- const { events } = await persistence.readFrom(sid, 0);
470
+ const events = await readSessionEvents(persistence, sid);
328
471
  coverage.scannedSessions += 1;
329
472
  const session = { id: sid, title: "", total: 0, cost: 0, calls: 0, lastActive: 0 };
330
473
  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) => {
474
+ const sessionVision = { ...emptyVision(), title: "" };
475
+ const windowVisionSessions = windowAggregates.map(() => ({ ...emptyVision(), title: "" }));
476
+ const scanned = addUsageEvent(events, (time, input, output, cache, reasoning, provider, model, _messageId, _turn, images) => {
332
477
  if (coverage.earliestAt === null || time < coverage.earliestAt) coverage.earliestAt = time;
333
478
  if (coverage.latestAt === null || time > coverage.latestAt) coverage.latestAt = time;
334
479
  const d = new Date(time);
@@ -343,6 +488,29 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
343
488
  bumpModel(modelTotals, modelDays, modelHours, modelKey, dayKey, hourKey, input, output, cache, cost);
344
489
  bumpOverall(overall, input, output, cache, reasoning, cost, cacheSavings);
345
490
  bumpSession(session, time, eventTotal, cost);
491
+ if (images.length > 0) {
492
+ let imageTokensSum = 0;
493
+ let imageCostSum = 0;
494
+ let imageBytesSum = 0;
495
+ for (const image of images) {
496
+ const width = typeof image.width === "number" ? image.width : 0;
497
+ const height = typeof image.height === "number" ? image.height : 0;
498
+ const tokens = estimateImageTokens(width, height);
499
+ imageTokensSum += tokens;
500
+ imageCostSum += costOf(time, model, tokens, 0, 0);
501
+ imageBytesSum += typeof image.bytes === "number" ? image.bytes : 0;
502
+ }
503
+ bumpVisionInto(vision, images.length, imageTokensSum, imageCostSum, imageBytesSum);
504
+ bumpVisionMap(visionDays, dayKey, images.length, imageTokensSum, imageCostSum, imageBytesSum);
505
+ bumpVisionInto(sessionVision, images.length, imageTokensSum, imageCostSum, imageBytesSum);
506
+ for (let i = 0; i < windowAggregates.length; i++) {
507
+ const aggregate = windowAggregates[i];
508
+ if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue;
509
+ bumpVisionInto(aggregate.vision, images.length, imageTokensSum, imageCostSum, imageBytesSum);
510
+ bumpVisionMap(aggregate.visionDays, dayKey, images.length, imageTokensSum, imageCostSum, imageBytesSum);
511
+ bumpVisionInto(windowVisionSessions[i], images.length, imageTokensSum, imageCostSum, imageBytesSum);
512
+ }
513
+ }
346
514
  for (let i = 0; i < windowAggregates.length; i++) {
347
515
  const aggregate = windowAggregates[i];
348
516
  if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue;
@@ -372,6 +540,16 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
372
540
  windowAggregates[i].sessions.push(windowSession);
373
541
  }
374
542
  }
543
+ if (sessionVision.images > 0) {
544
+ sessionVision.title = titleOf(events, sid);
545
+ visionSessions.set(sid, sessionVision);
546
+ for (let i = 0; i < windowAggregates.length; i++) {
547
+ const windowVision = windowVisionSessions[i];
548
+ if (windowVision.images === 0) continue;
549
+ windowVision.title = sessionVision.title;
550
+ windowAggregates[i].visionSessions.set(sid, windowVision);
551
+ }
552
+ }
375
553
  } catch {
376
554
  coverage.failedSessions += 1;
377
555
  }
@@ -427,6 +605,22 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
427
605
  const b = dayMap.get(key) ?? emptyBucket();
428
606
  heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls });
429
607
  }
608
+ const makeVisionDaily = (map, count) => {
609
+ const result = [];
610
+ for (let i = count - 1; i >= 0; i--) {
611
+ const key = dayKeyOf(dateBefore(i));
612
+ const b = map.get(key) ?? emptyVision();
613
+ result.push({ date: key, images: b.images, imageTokens: b.imageTokens, cost: b.cost });
614
+ }
615
+ return result;
616
+ };
617
+ const makeVisionSessions = (map) => [...map.entries()].map(([id, acc]) => ({
618
+ id,
619
+ title: acc.title !== "" ? acc.title : `\u4F1A\u8BDD ${id.slice(0, 8)}`,
620
+ images: acc.images,
621
+ imageTokens: acc.imageTokens,
622
+ cost: acc.cost
623
+ })).sort((a, b) => b.images - a.images).slice(0, SESSION_TOP_N);
430
624
  const models = makeModels(modelTotals, modelDays, modelHours, 30);
431
625
  const windows = windowAggregates.map((aggregate) => ({
432
626
  days: aggregate.days,
@@ -435,7 +629,10 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
435
629
  totals: { ...aggregate.overall },
436
630
  models: makeModels(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, aggregate.days),
437
631
  sessions: [...aggregate.sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),
438
- sessionCount: aggregate.sessions.length
632
+ sessionCount: aggregate.sessions.length,
633
+ vision: { ...aggregate.vision },
634
+ visionDaily: makeVisionDaily(aggregate.visionDays, aggregate.days),
635
+ visionSessions: makeVisionSessions(aggregate.visionSessions)
439
636
  }));
440
637
  return {
441
638
  ok: true,
@@ -451,6 +648,9 @@ async function fetchUsage(persistence, nowMs = Date.now()) {
451
648
  sessionCount: sessions.length,
452
649
  coverage,
453
650
  windows,
651
+ vision: { ...vision },
652
+ visionDaily: makeVisionDaily(visionDays, 30),
653
+ visionSessions: makeVisionSessions(visionSessions),
454
654
  totals: {
455
655
  input: overall.input,
456
656
  output: overall.output,
@@ -470,8 +670,7 @@ async function fetchSessionUsage(persistence, sessionId) {
470
670
  if (!isValidSessionId(sessionId)) return { ok: false, error: "\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5" };
471
671
  let events;
472
672
  try {
473
- const result = await persistence.readFrom(sessionId, 0);
474
- events = result.events;
673
+ events = await readSessionEvents(persistence, sessionId);
475
674
  } catch (err) {
476
675
  return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A${errorMessage(err)}` };
477
676
  }
@@ -545,7 +744,7 @@ var inject = ["webServer", "credentials", "sessionPersistence"];
545
744
  var BALANCE_TTL_MS = 6e4;
546
745
  var USAGE_TTL_MS = 5 * 6e4;
547
746
  function apply(ctx) {
548
- const balanceMemo = memoize(BALANCE_TTL_MS, () => fetchBalance(ctx.credentials));
747
+ const balanceMemo = memoize(BALANCE_TTL_MS, () => fetchBalance(ctx.credentials, ctx.get("deepseekAccount")));
549
748
  const usageMemo = memoize(USAGE_TTL_MS, () => fetchUsage(ctx.sessionPersistence));
550
749
  ctx.effect(() => ctx.webServer.register({
551
750
  kind: "prefix",