balance-dsh 1.0.10 → 1.1.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/client.js CHANGED
@@ -111,12 +111,13 @@ window.__ModuleLoader__.load({
111
111
  }
112
112
 
113
113
  // ─── 价格(与服务端默认保持一致;服务端 /balance-api 返回后会覆盖) ───
114
- // 2026-08-17 生效的 DeepSeek V4-Flash 空闲时段价(元/百万 tokens)
114
+ // 北京时间 2026-09-10 12:00 起生效的 DeepSeek V4-Flash 空闲时段价(元/百万 tokens)
115
+ // 注:历史消耗由服务端按当时价格回传 cost;这里的默认值只用于拉不到服务端分桶时的本地估算。
115
116
  const DEFAULT_PRICING = {
116
- inputCnyPerMillion: 1.5, // 输入(缓存未命中)
117
- cacheReadCnyPerMillion: 0.05, // 输入(缓存命中)
117
+ inputCnyPerMillion: 1, // 输入(缓存未命中)
118
+ cacheReadCnyPerMillion: 0.02, // 输入(缓存命中)
118
119
  cacheWriteCnyPerMillion: 0, // DeepSeek 无缓存写入计费
119
- outputCnyPerMillion: 4.5, // 输出
120
+ outputCnyPerMillion: 4, // 输出
120
121
  };
121
122
 
122
123
  let pricing = DEFAULT_PRICING;
@@ -808,9 +809,12 @@ function UsageCostLine({ useProjection, sessionId }) {
808
809
 
809
810
  // 金额:优先用服务端分时段 usage(高峰桶×2+空闲桶);未就绪时按当前时段价估算
810
811
  const split = splitReady && splitUsage ? splitUsage : null;
811
- const cost = split ? computeSplitCost(split, p) : computeCost(usage, periodAdjustedPricing(p));
812
- const peakTokens = split ? split.peak.inputTokens + split.peak.cacheReadTokens + split.peak.cacheWriteTokens + split.peak.outputTokens : 0;
813
- const offpeakTokens = split ? split.offpeak.inputTokens + split.offpeak.cacheReadTokens + split.offpeak.cacheWriteTokens + split.offpeak.outputTokens : 0;
812
+ // 服务端分桶可能"非 null 但为空"(拿不到该会话事件),此时必须回退本地投影估算,否则金额会显示 ¥0.0000
813
+ const splitUsable =
814
+ split != null && ((Array.isArray(split.byModel) && split.byModel.length > 0) || (split.totalTokens ?? 0) > 0);
815
+ const cost = splitUsable ? computeSplitCost(split, p) : computeCost(usage, periodAdjustedPricing(p));
816
+ const peakTokens = splitUsable ? split.peak.inputTokens + split.peak.cacheReadTokens + split.peak.cacheWriteTokens + split.peak.outputTokens : 0;
817
+ const offpeakTokens = splitUsable ? split.offpeak.inputTokens + split.offpeak.cacheReadTokens + split.offpeak.cacheWriteTokens + split.offpeak.outputTokens : 0;
814
818
 
815
819
  // 子代理消耗(与主会话分开,同样跨时段精确计费)
816
820
  const hasSubagent = splitReady && subagentUsage && subagentUsage.totalTokens > 0;
@@ -825,7 +829,7 @@ function UsageCostLine({ useProjection, sessionId }) {
825
829
  "新增:" + formatTokens(newTokens) + " tokens(未缓存输入 " + formatTokens(uncachedInput) + " + 输出 " + formatTokens(output) + ")",
826
830
  "计费:" + formatTokens(billedTotal) + " tokens(含缓存重读 " + formatTokens(cacheRead) + "、缓存写入 " + formatTokens(cacheWrite) + ")",
827
831
  ];
828
- if (split) {
832
+ if (splitUsable) {
829
833
  detailLines.push(
830
834
  "时段拆分:高峰 " + formatTokens(peakTokens) + " tokens + 空闲 " + formatTokens(offpeakTokens) + " tokens",
831
835
  "估算金额:¥" + cost.toFixed(4) + "(高峰桶按目录价×2、空闲桶按目录价)",
package/lib/index.js CHANGED
@@ -55,10 +55,10 @@ const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
55
55
  // DeepSeek 目录价(人民币/百万 tokens),按模型区分。默认取 2026-08-17 生效的
56
56
  // V4 峰谷定价之空闲时段价;高峰时段(每日 9:00 / 14:00)价格为下表 2 倍,
57
57
  // 可在 config.peak: true 时统一翻倍,或在 config.price 中逐项覆盖。
58
- // V4-Flash 空闲:输入未命中 1.5 / 命中 0.05 / 输出 4.5
59
- // V4-Pro 空闲:输入未命中 4.5 / 命中 0.15 / 输出 13.5
58
+ // V4-Flash 空闲:输入未命中 1 / 命中 0.02 / 输出 4(北京时间 2026-09-10 12:00 起的新价)
59
+ // V4-Pro 空闲:输入未命中 4.5 / 命中 0.15 / 输出 13.5(官方 2026-09-14 后继续提供,计费不变)
60
60
  const MODEL_PRICING = {
61
- "deepseek-v4-flash": { inputCnyPerMillion: 1.5, cacheReadCnyPerMillion: 0.05, cacheWriteCnyPerMillion: 0, outputCnyPerMillion: 4.5 },
61
+ "deepseek-v4-flash": { inputCnyPerMillion: 1, cacheReadCnyPerMillion: 0.02, cacheWriteCnyPerMillion: 0, outputCnyPerMillion: 4 },
62
62
  "deepseek-v4-pro": { inputCnyPerMillion: 4.5, cacheReadCnyPerMillion: 0.15, cacheWriteCnyPerMillion: 0, outputCnyPerMillion: 13.5 },
63
63
  // 旧模型兜底(V3.2 时代目录价)
64
64
  "deepseek-chat": { inputCnyPerMillion: 2, cacheReadCnyPerMillion: 0.5, cacheWriteCnyPerMillion: 0, outputCnyPerMillion: 3 },
@@ -66,6 +66,18 @@ const MODEL_PRICING = {
66
66
  };
67
67
  const FALLBACK_PRICING = MODEL_PRICING["deepseek-v4-flash"];
68
68
 
69
+ // 定价历史:官方自北京时间 2026-09-10 12:00 起下调 flash 系列价格。
70
+ // 调价前产生的 token 仍按当时旧价计算(历史消耗金额不因后续调价而改变),
71
+ // 之后的消耗用 MODEL_PRICING(新价)。按每条 usage 的事件时间戳自动选择。
72
+ const FLASH_REPRICED_AT = Date.parse("2026-09-10T12:00:00+08:00");
73
+ const LEGACY_MODEL_PRICING = {
74
+ "deepseek-v4-flash": { inputCnyPerMillion: 1.5, cacheReadCnyPerMillion: 0.05, cacheWriteCnyPerMillion: 0, outputCnyPerMillion: 4.5 },
75
+ };
76
+ /** 事件时间在调价前返回 "legacy"(旧价),之后返回 "current"(新价)。 */
77
+ function pricingEraAt(time) {
78
+ return Number(time) < FLASH_REPRICED_AT ? "legacy" : "current";
79
+ }
80
+
69
81
  // 高峰/空闲时段(V4 峰谷定价,2026-08-17 生效):高峰价 = 空闲价 × 2。
70
82
  // 官方高峰窗口(北京时间)为 周一至周五 09:00-12:00、14:00-18:00;周末全天空闲。
71
83
  // 可用 config.peakWindows 覆盖小时窗口(如 [{start:9,end:12},{start:14,end:18}]),
@@ -91,19 +103,23 @@ function isPeakAt(timeMs, config) {
91
103
  return weekdays.includes(weekday) && windows.some((w) => hour >= w.start && hour < w.end);
92
104
  }
93
105
 
94
- /** 把模型字符串规整到定价表键;未识别返回 ""(用 Flash 价兜底)。 */
106
+ /** 把模型字符串规整到定价表键;未识别返回 ""(用 Flash 价兜底)。
107
+ * 注意:官方已把 Flash 主名改为 `deepseek-flash`,旧名 `deepseek-v4-flash` /
108
+ * `deepseek-v4-flash-vision-exp` 虽已退役但仍按 Flash 价计费,因此统一按 "flash" 匹配。 */
95
109
  function modelKey(model) {
96
110
  const m = String(model || "").toLowerCase();
97
- if (m.includes("v4-pro") || m.includes("v4pro")) return "deepseek-v4-pro";
98
- if (m.includes("v4-flash") || m.includes("v4flash")) return "deepseek-v4-flash";
111
+ if (m.includes("v4-pro") || m.includes("v4pro") || m.includes("-pro")) return "deepseek-v4-pro";
112
+ if (m.includes("flash")) return "deepseek-v4-flash";
99
113
  if (m.includes("reasoner")) return "deepseek-reasoner";
100
114
  if (m.includes("chat")) return "deepseek-chat";
101
115
  return "";
102
116
  }
103
117
 
104
- /** 解析价格:模型基价 + config.price 覆盖,config.peak: true 时按高峰价翻倍。 */
105
- function resolvePricing(config, model = "") {
106
- const base = MODEL_PRICING[modelKey(model)] ?? FALLBACK_PRICING;
118
+ /** 解析价格:模型基价 + config.price 覆盖,config.peak: true 时按高峰价翻倍。
119
+ * era = "legacy" 时取调价前的历史价(目前仅 flash 系列与现价不同)。 */
120
+ function resolvePricing(config, model = "", era = "current") {
121
+ const key = modelKey(model);
122
+ const base = (era === "legacy" && LEGACY_MODEL_PRICING[key]) || MODEL_PRICING[key] || FALLBACK_PRICING;
107
123
  const price = config?.price ?? {};
108
124
  const multiplier = config?.peak === true ? 2 : 1;
109
125
  const pick = (name, fallback) => Number(price[name] ?? fallback) * multiplier;
@@ -115,9 +131,25 @@ function resolvePricing(config, model = "") {
115
131
  };
116
132
  }
117
133
 
134
+ /** 取会话事件数组。DSH 2.0.4 起 live session **不再有 `.events` 属性**,改用 `snapshotEvents()` 方法
135
+ * (签名 `snapshotEvents(fromSeq = 0, toSeqExclusive = seq)`,可无参调用取全部事件);
136
+ * 旧版仍读 `.events` 属性。两者都拿不到时返回空数组(由调用方降级)。 */
137
+ function sessionEvents(session) {
138
+ if (!session) return [];
139
+ if (typeof session.snapshotEvents === "function") {
140
+ try {
141
+ const events = session.snapshotEvents();
142
+ if (Array.isArray(events)) return events;
143
+ } catch {
144
+ // 落到旧属性兜底
145
+ }
146
+ }
147
+ return Array.isArray(session.events) ? session.events : [];
148
+ }
149
+
118
150
  /** 从会话中识别实际使用的模型。
119
151
  * 优先用 DSH 会话的 requestHeader()/requestContext() 折叠结果(即官方 session.models 取 current 的同一来源),
120
- * 取不到再回退遍历 session.events 找 request/header / request/context 事件。 */
152
+ * 取不到再回退遍历会话事件找 request/header / request/context 事件。 */
121
153
  function detectModel(session) {
122
154
  if (!session) return "";
123
155
  // 1) DSH 官方折叠 API:requestHeader().config.model 或 requestContext().model
@@ -133,7 +165,7 @@ function detectModel(session) {
133
165
  }
134
166
  // 2) 兜底:遍历事件
135
167
  let model = "";
136
- for (const event of session?.events ?? []) {
168
+ for (const event of sessionEvents(session)) {
137
169
  if (event.type === "request/header") {
138
170
  const m = event.data?.header?.config?.model;
139
171
  if (typeof m === "string" && m !== "") model = m;
@@ -272,11 +304,14 @@ function sumEventsUsage(events, config, fallbackModel = "") {
272
304
  bucket.cacheWriteTokens += usage.cacheWriteTokens ?? 0;
273
305
  };
274
306
  const empty = () => ({ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
275
- const byModel = new Map(); // model -> { model, peak, offpeak }
307
+ const byModel = new Map(); // `${model}\0${era}` -> { model, era, peak, offpeak }
276
308
  for (const { usage, time } of latestByStep.values()) {
277
309
  const model = modelAtTime(switches, time) || fallback;
278
- let e = byModel.get(model);
279
- if (!e) { e = { model, peak: empty(), offpeak: empty() }; byModel.set(model, e); }
310
+ // 同一模型在调价前后的 token 分开分桶,各自用当时的价格计算(历史金额不变)。
311
+ const era = pricingEraAt(time);
312
+ const key = `${model}\u0000${era}`;
313
+ let e = byModel.get(key);
314
+ if (!e) { e = { model, era, peak: empty(), offpeak: empty() }; byModel.set(key, e); }
280
315
  add(isPeakAt(time, config) ? e.peak : e.offpeak, usage);
281
316
  }
282
317
  // 聚合 + 转换 byModel(按金额未在前面算,这里只分桶 + 汇总字段)
@@ -284,7 +319,7 @@ function sumEventsUsage(events, config, fallbackModel = "") {
284
319
  const offpeak = empty();
285
320
  const modelList = [];
286
321
  for (const e of byModel.values()) {
287
- modelList.push({ model: e.model, peak: e.peak, offpeak: e.offpeak });
322
+ modelList.push({ model: e.model, era: e.era, peak: e.peak, offpeak: e.offpeak, cost: estimateCost(e, resolvePricing(config, e.model, e.era)) });
288
323
  peak.inputTokens += e.peak.inputTokens;
289
324
  peak.outputTokens += e.peak.outputTokens;
290
325
  peak.cacheReadTokens += e.peak.cacheReadTokens;
@@ -315,7 +350,7 @@ function sumEventsUsage(events, config, fallbackModel = "") {
315
350
  function sumSessionUsage(session, config) {
316
351
  if (session == null) return null;
317
352
  // fallbackModel = detectModel(session):当 message 早于所有模型切换点时兜底,避免落到默认 flash 价。
318
- return sumEventsUsage(session.events ?? [], config, detectModel(session));
353
+ return sumEventsUsage(sessionEvents(session), config, detectModel(session));
319
354
  }
320
355
 
321
356
  function estimateCost(usage, pricing) {
@@ -385,7 +420,7 @@ function collectDailyUsage(events, config, fallbackModel, cutoffMs, days, sessio
385
420
  const model = modelAtTime(switches, time) || fallback;
386
421
  const bucket = { peak: emptyTokenBucket(), offpeak: emptyTokenBucket() };
387
422
  addTokenUsage(isPeakAt(time, config) ? bucket.peak : bucket.offpeak, usage);
388
- day.cost += estimateCost(bucket, resolvePricing(config, model));
423
+ day.cost += estimateCost(bucket, resolvePricing(config, model, pricingEraAt(time)));
389
424
  }
390
425
  }
391
426
 
@@ -583,7 +618,15 @@ async function sumSubagentUsage(ctx, sessionId, config) {
583
618
  }
584
619
 
585
620
  function formatUsage(usage, pricing, model = "") {
586
- const cost = estimateCost(usage, pricing);
621
+ // 优先按 byModel 逐段累加(每段用各自模型 + 价格时代的价格,历史消耗不因调价而变);
622
+ // 无 byModel 时回退单一价格估算(兼容旧数据)。
623
+ const cost =
624
+ Array.isArray(usage.byModel) && usage.byModel.length > 0
625
+ ? usage.byModel.reduce(
626
+ (sum, b) => sum + (typeof b.cost === "number" && Number.isFinite(b.cost) ? b.cost : estimateCost(b, pricing)),
627
+ 0,
628
+ )
629
+ : estimateCost(usage, pricing);
587
630
  const newTokens = usage.inputTokens + usage.outputTokens;
588
631
  const peakTokens = usage.peak.inputTokens + usage.peak.cacheReadTokens + usage.peak.cacheWriteTokens + usage.peak.outputTokens;
589
632
  const offpeakTokens = usage.offpeak.inputTokens + usage.offpeak.cacheReadTokens + usage.offpeak.cacheWriteTokens + usage.offpeak.outputTokens;
@@ -729,6 +772,27 @@ export function apply(ctx, config = {}) {
729
772
  usage = sumSessionUsage(session, config);
730
773
  detectedModel = detectModel(session);
731
774
  }
775
+ // DSH 2.0.4 起 live session(sessions.get)可能拿不到 events,导致 usage 全 0。
776
+ // 此时回退从持久化读事件,避免客户端拿到"空但非 null"的 usage 而把金额显示成 ¥0.0000。
777
+ if (usage == null || usage.totalTokens === 0) {
778
+ try {
779
+ const persisted = await inspectPersistedSession(ctx, sessionParam);
780
+ if (Array.isArray(persisted?.events) && persisted.events.length > 0) {
781
+ const persistedUsage = sumEventsUsage(persisted.events, config, detectedModel);
782
+ if (persistedUsage.totalTokens > 0) {
783
+ usage = persistedUsage;
784
+ if (detectedModel === "") {
785
+ const switches = collectModelSwitches(persisted.events);
786
+ if (switches.length > 0) detectedModel = switches[switches.length - 1].model;
787
+ }
788
+ }
789
+ }
790
+ } catch {
791
+ // 持久化读取不可用:忽略,走下方统一降级
792
+ }
793
+ }
794
+ // 仍无任何 token 就返回 null,让客户端回退本地投影估算(而不是当成 0 消耗显示 ¥0.0000)
795
+ if (usage != null && usage.totalTokens === 0) usage = null;
732
796
  // 子代理枚举走持久化目录(不依赖 live 会话),即使父会话不在内存也能拿到
733
797
  const subRes = await sumSubagentUsage(httpCtx, sessionParam, config);
734
798
  subagentUsage = subRes.usage;
@@ -741,10 +805,10 @@ export function apply(ctx, config = {}) {
741
805
  const effectiveModel = model || detectedModel;
742
806
  // 逐模型标注 cost(高峰期×2、空闲期×1),客户端直接按各模型 cost 累加,无需自己定价。
743
807
  if (usage && Array.isArray(usage.byModel)) {
744
- usage.byModel = usage.byModel.map((b) => ({ ...b, cost: estimateCost(b, resolvePricing(config, b.model)) }));
808
+ usage.byModel = usage.byModel.map((b) => ({ ...b, cost: estimateCost(b, resolvePricing(config, b.model, b.era)) }));
745
809
  }
746
810
  if (subagentUsage && Array.isArray(subagentUsage.byModel)) {
747
- subagentUsage.byModel = subagentUsage.byModel.map((b) => ({ ...b, cost: estimateCost(b, resolvePricing(config, b.model)) }));
811
+ subagentUsage.byModel = subagentUsage.byModel.map((b) => ({ ...b, cost: estimateCost(b, resolvePricing(config, b.model, b.era)) }));
748
812
  }
749
813
  try {
750
814
  const data = await fetchBalance(apiKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "balance-dsh",
3
- "version": "1.0.10",
3
+ "version": "1.1.0",
4
4
  "description": "查询 DeepSeek 余额并显示当前会话 token 消耗。DeepSeek 账户余额 + 会话 token/费用显示,含高峰/空闲峰谷计费。",
5
5
  "author": "Rannist",
6
6
  "type": "module",