@linxin666/dsh-usage 0.3.20 → 0.3.22

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
@@ -585,22 +585,22 @@ const PEAK_WINDOWS = [{
585
585
  }];
586
586
  /** Beijing is UTC+8 year-round (no DST), so a fixed shift is exact. */
587
587
  const BEIJING_UTC_OFFSET_MS = 8 * 36e5;
588
- /** deepseek-v4-flash (and the flash-class vision experiment). */
588
+ /** deepseek-flash (DeepSeek-V4.1-Flash), including the retired flash-class ids it now serves. */
589
589
  const FLASH_PRICE = {
590
590
  cacheHit: {
591
- offPeak: .05,
592
- peak: .1
591
+ offPeak: .02,
592
+ peak: .04
593
593
  },
594
594
  inputMiss: {
595
- offPeak: 1.5,
596
- peak: 3
595
+ offPeak: 1,
596
+ peak: 2
597
597
  },
598
598
  output: {
599
- offPeak: 4.5,
600
- peak: 9
599
+ offPeak: 4,
600
+ peak: 8
601
601
  }
602
602
  };
603
- /** deepseek-v4-pro. */
603
+ /** deepseek-v4-pro (DeepSeek-V4-Pro-0813); superseded by the flash row at the retirement instant. */
604
604
  const PRO_PRICE = {
605
605
  cacheHit: {
606
606
  offPeak: .15,
@@ -615,6 +615,12 @@ const PRO_PRICE = {
615
615
  peak: 27
616
616
  }
617
617
  };
618
+ /**
619
+ * The instant DeepSeek starts serving `deepseek-v4-pro` requests from
620
+ * V4.1-Flash and billing the flash row: 2026-09-14 12:00 Beijing (UTC+8).
621
+ * Drop the pro row's time fence once V4.1 Pro ships with a published row.
622
+ */
623
+ const V4_PRO_FOLDED_INTO_FLASH_AT_MS = Date.UTC(2026, 8, 14, 4, 0);
618
624
  function withinWindow(minuteOfDay) {
619
625
  return PEAK_WINDOWS.find((window) => minuteOfDay >= window.from && minuteOfDay < window.to);
620
626
  }
@@ -650,9 +656,9 @@ function deepseekPeriodAt(ms) {
650
656
  boundaryMs: ms + 864e5
651
657
  };
652
658
  }
653
- /** The price row for a model id; unknown ids take the flash-class row (documented estimate). */
654
- function priceFor(model) {
655
- return model.includes("v4-pro") ? PRO_PRICE : FLASH_PRICE;
659
+ /** The price row for a model id at `atMs`; unknown ids take the flash-class row (documented estimate). */
660
+ function priceFor(model, atMs) {
661
+ return model.includes("v4-pro") && atMs < V4_PRO_FOLDED_INTO_FLASH_AT_MS ? PRO_PRICE : FLASH_PRICE;
656
662
  }
657
663
  /**
658
664
  * Estimate one call's DeepSeek spend in CNY from its token totals, priced in
@@ -661,7 +667,7 @@ function priceFor(model) {
661
667
  * by route family). Rounded to micro-CNY so the ledger stays readable.
662
668
  */
663
669
  function deepseekModelSpend(model, totals, atMs) {
664
- const price = priceFor(model);
670
+ const price = priceFor(model, atMs);
665
671
  const column = deepseekPeriodAt(atMs).peak ? "peak" : "offPeak";
666
672
  const spend = (totals.cacheReadTokens * price.cacheHit[column] + (totals.inputTokens + totals.cacheWriteTokens) * price.inputMiss[column] + totals.outputTokens * price.output[column]) / 1e6;
667
673
  return Math.round(spend * 1e6) / 1e6;
@@ -1008,6 +1014,8 @@ var UsageService = class {
1008
1014
  snapshotsPath;
1009
1015
  ledger = createLedgerDocument();
1010
1016
  snapshots = /* @__PURE__ */ new Map();
1017
+ /** The official DeepSeek family's real-spend watch (see SpendWatch). */
1018
+ spendWatch;
1011
1019
  /** Per-live-session route attribution (WeakMap: disposed sessions age out). */
1012
1020
  sessionRoutes = /* @__PURE__ */ new WeakMap();
1013
1021
  /** The most recent route seen this boot; the pet bubble follows it. */
@@ -1083,8 +1091,10 @@ var UsageService = class {
1083
1091
  });
1084
1092
  }
1085
1093
  providers.sort((a, b) => Number(b.supported) - Number(a.supported) || a.displayName.localeCompare(b.displayName));
1086
- const days = ledgerDayKeys(this.ledger).slice(-30);
1094
+ const allKeys = ledgerDayKeys(this.ledger);
1095
+ const days = allKeys.slice(-30);
1087
1096
  const range = summarizeDays(days.map((key) => this.ledger.days[key] ?? {}));
1097
+ const all = summarizeDays(allKeys.map((key) => this.ledger.days[key] ?? {}));
1088
1098
  return {
1089
1099
  updatedAt: Date.now(),
1090
1100
  providers,
@@ -1102,7 +1112,17 @@ var UsageService = class {
1102
1112
  to: days[days.length - 1] ?? todayKey,
1103
1113
  totals: range.totals,
1104
1114
  providers: range.providers
1105
- }
1115
+ },
1116
+ all: {
1117
+ from: allKeys[0] ?? todayKey,
1118
+ to: allKeys[allKeys.length - 1] ?? todayKey,
1119
+ totals: all.totals,
1120
+ providers: all.providers
1121
+ },
1122
+ ...this.spendWatch !== void 0 && this.spendWatch.accruedCny > 0 ? { observedSpend: {
1123
+ cny: this.spendWatch.accruedCny,
1124
+ since: this.spendWatch.since
1125
+ } } : {}
1106
1126
  }
1107
1127
  };
1108
1128
  }
@@ -1128,6 +1148,33 @@ var UsageService = class {
1128
1148
  return this.daySummary(localDateKey(Date.now())).providers.find((entry) => entry.provider === provider)?.totals ?? emptyTotals();
1129
1149
  }
1130
1150
  /**
1151
+ * Accrue the official DeepSeek family's real spend from observed CNY
1152
+ * balance decreases; a balance rise is a top-up and never counts. The
1153
+ * family's route ids can alias one account, so the watch follows the
1154
+ * largest balance seen this cycle and accrues only decreases of that
1155
+ * series — a failed probe keeps the stale balance and accrues nothing
1156
+ * until the next success reports the whole drop at once.
1157
+ */
1158
+ accrueDeepSeekSpend() {
1159
+ let balanceCny;
1160
+ for (const [id, snapshot] of this.snapshots) {
1161
+ if (!isDeepSeekProviderRoute(id)) continue;
1162
+ const balance = snapshot.balance;
1163
+ if (balance === void 0 || balance.currency.toUpperCase() !== "CNY") continue;
1164
+ const value = Number(balance.totalBalance);
1165
+ if (!Number.isFinite(value)) continue;
1166
+ balanceCny = balanceCny === void 0 ? value : Math.max(balanceCny, value);
1167
+ }
1168
+ if (balanceCny === void 0) return;
1169
+ const watch = this.spendWatch ?? {
1170
+ accruedCny: 0,
1171
+ since: Date.now()
1172
+ };
1173
+ if (watch.lastBalanceCny !== void 0 && balanceCny < watch.lastBalanceCny) watch.accruedCny += watch.lastBalanceCny - balanceCny;
1174
+ watch.lastBalanceCny = balanceCny;
1175
+ this.spendWatch = watch;
1176
+ }
1177
+ /**
1131
1178
  * The display name for a route key: the LLM runtime's first, then the
1132
1179
  * adapter's, then the id itself — the snapshot may not exist for
1133
1180
  * adapter-less routes, yet the bubble still needs a legible title.
@@ -1209,6 +1256,12 @@ var UsageService = class {
1209
1256
  if (typeof parsed === "object" && parsed !== null && typeof parsed.providers === "object" && parsed.providers !== null) {
1210
1257
  for (const [provider, snapshot] of Object.entries(parsed.providers)) if (typeof snapshot === "object" && snapshot !== null && typeof snapshot.provider === "string") this.snapshots.set(provider, snapshot);
1211
1258
  }
1259
+ const watch = parsed.spendWatch;
1260
+ if (typeof watch === "object" && watch !== null && typeof watch.accruedCny === "number" && Number.isFinite(watch.accruedCny) && watch.accruedCny >= 0 && typeof watch.since === "number" && Number.isFinite(watch.since) && watch.since > 0) this.spendWatch = {
1261
+ accruedCny: watch.accruedCny,
1262
+ since: watch.since,
1263
+ ...typeof watch.lastBalanceCny === "number" && Number.isFinite(watch.lastBalanceCny) ? { lastBalanceCny: watch.lastBalanceCny } : {}
1264
+ };
1212
1265
  }
1213
1266
  } catch {} finally {
1214
1267
  this.loaded = true;
@@ -1259,7 +1312,8 @@ var UsageService = class {
1259
1312
  await mkdir(dirname(this.snapshotsPath), { recursive: true });
1260
1313
  await writeJsonAtomic(this.snapshotsPath, {
1261
1314
  version: 1,
1262
- providers: Object.fromEntries(this.snapshots)
1315
+ providers: Object.fromEntries(this.snapshots),
1316
+ ...this.spendWatch !== void 0 ? { spendWatch: this.spendWatch } : {}
1263
1317
  });
1264
1318
  } catch {}
1265
1319
  }
@@ -1294,6 +1348,7 @@ var UsageService = class {
1294
1348
  }
1295
1349
  for (const id of [...this.snapshots.keys()]) if (!seen.has(id)) this.snapshots.delete(id);
1296
1350
  if (this.disposed) return;
1351
+ this.accrueDeepSeekSpend();
1297
1352
  await this.persistSnapshots();
1298
1353
  this.announceCurrent();
1299
1354
  } finally {
@@ -1,7 +1,8 @@
1
1
  /**
2
- * The usage statistics settings section: two tabs (用量: today's usage,
3
- * balances, trend; 个人套餐: per-provider plan quota windows) plus a compact
4
- * settings row. Data comes from the host's loopback-fenced
2
+ * The usage statistics settings section: three tabs (用量: today's usage,
3
+ * balances, trend; 个人套餐: per-provider plan quota windows; Token 银行:
4
+ * the whale-yuan voucher minted from the DeepSeek official family's usage)
5
+ * plus a compact settings row. Data comes from the host's loopback-fenced
5
6
  * /api/dsh-usage/overview document; polling runs only while the section is
6
7
  * mounted and the tab is visible.
7
8
  * @module @linxin666/dsh-usage/client/UsageSectionCard
@@ -10,11 +10,13 @@ export declare const zh: {
10
10
  'usage.title': string;
11
11
  'usage.tab.usage': string;
12
12
  'usage.tab.plans': string;
13
+ 'usage.tab.bank': string;
13
14
  'usage.refresh': string;
14
15
  'usage.refreshing': string;
15
16
  'usage.updated': string;
16
17
  'usage.loading': string;
17
18
  'usage.error': string;
19
+ 'usage.disabled': string;
18
20
  'usage.current': string;
19
21
  'usage.today': string;
20
22
  'usage.today.cost': string;
@@ -48,6 +50,16 @@ export declare const zh: {
48
50
  'usage.config.bubbleMode.always': string;
49
51
  'usage.config.bubbleMode.change': string;
50
52
  'usage.config.bubbleMode.off': string;
53
+ 'usage.bank.title': string;
54
+ 'usage.bank.hint': string;
55
+ 'usage.bank.noUsage': string;
56
+ 'usage.bank.minted': string;
57
+ 'usage.bank.spend.observed': string;
58
+ 'usage.bank.spend.estimated': string;
59
+ 'usage.bank.window': string;
60
+ 'usage.bank.save': string;
61
+ 'usage.bank.share': string;
62
+ 'usage.bank.drawError': string;
51
63
  };
52
64
  /** English mirror; every zh key present. */
53
65
  export declare const en: Record<UsageKey, string>;