@lancecheney/dsh-deepseek-balance 0.1.0 → 0.1.2

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
@@ -1,6 +1,10 @@
1
1
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
2
  import { settingsNamespace } from "@deepseek-ai/dsh-settings";
3
3
  import { z } from "zod";
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { zstdDecompressSync } from "node:zlib";
4
8
 
5
9
  /**
6
10
  * @lancecheney/dsh-deepseek-balance — host half.
@@ -11,6 +15,9 @@ import { z } from "zod";
11
15
 
12
16
  const name = "deepseek-balance";
13
17
  const inject = ["webServer", "credentials", "settings"];
18
+ const Config = z.object({
19
+ tokenName: z.string().default("API key")
20
+ });
14
21
 
15
22
  const DEFAULT_BASE_URL = "https://api.deepseek.com";
16
23
  const DEFAULT_KEY_ENV = "DEEPSEEK_API_KEY";
@@ -22,6 +29,7 @@ const PRICING_FETCH_TIMEOUT_MS = 15000;
22
29
  /** Last-known-good prices: legacy + the 2026-08-17 peak/off-peak table, per currency. */
23
30
  const FALLBACK_PRICING = {
24
31
  effectiveFrom: "2026-08-17T00:00:00+08:00",
32
+ weekendFrom: "2026-08-23T00:00:00+08:00",
25
33
  currencies: {
26
34
  CNY: {
27
35
  symbol: "¥",
@@ -35,6 +43,11 @@ const FALLBACK_PRICING = {
35
43
  legacy: { hit: 0.025, miss: 3.0, output: 6.0 },
36
44
  peak: { hit: 0.30, miss: 9.0, output: 27.0 },
37
45
  offPeak: { hit: 0.15, miss: 4.5, output: 13.5 }
46
+ },
47
+ "deepseek-v4-flash-vision-exp": {
48
+ legacy: { hit: 0.02, miss: 1.0, output: 2.0 },
49
+ peak: { hit: 0.10, miss: 3.0, output: 9.0 },
50
+ offPeak: { hit: 0.05, miss: 1.5, output: 4.5 }
38
51
  }
39
52
  }
40
53
  },
@@ -50,6 +63,11 @@ const FALLBACK_PRICING = {
50
63
  legacy: { hit: 0.003625, miss: 0.435, output: 0.87 },
51
64
  peak: { hit: 0.044, miss: 1.32, output: 3.96 },
52
65
  offPeak: { hit: 0.022, miss: 0.66, output: 1.98 }
66
+ },
67
+ "deepseek-v4-flash-vision-exp": {
68
+ legacy: { hit: 0.0028, miss: 0.14, output: 0.28 },
69
+ peak: { hit: 0.014, miss: 0.44, output: 1.32 },
70
+ offPeak: { hit: 0.007, miss: 0.22, output: 0.66 }
53
71
  }
54
72
  }
55
73
  }
@@ -129,81 +147,84 @@ const number = (match, index) => {
129
147
  return Number.isFinite(value) ? value : void 0;
130
148
  };
131
149
 
132
- /** Parse the zh-CN docs page: CNY prices + the effective date. */
150
+ /** Parse the zh-CN docs page: CNY peak/off-peak table (3 columns). */
133
151
  function parseCnPricing(html) {
134
152
  const text = stripTags(html);
135
- const parseModel = (id) => {
136
- const re = new RegExp(id + "\\s+空闲时段\\s+([\\d.]+)元\\s+([\\d.]+)元\\s+([\\d.]+)元\\s+高峰时段\\s+([\\d.]+)元\\s+([\\d.]+)元\\s+([\\d.]+)元");
153
+ const grab = (label) => {
154
+ const re = new RegExp(label + "\\s*空闲时段\\s*([\\d.]+)元\\s*([\\d.]+)元\\s*([\\d.]+)元\\s*高峰时段\\s*([\\d.]+)元\\s*([\\d.]+)元\\s*([\\d.]+)元");
137
155
  const m = text.match(re);
138
156
  if (!m) return void 0;
139
157
  return {
140
- offPeak: { hit: number(m, 1), miss: number(m, 2), output: number(m, 3) },
141
- peak: { hit: number(m, 4), miss: number(m, 5), output: number(m, 6) }
158
+ offPeak: [number(m, 1), number(m, 2), number(m, 3)],
159
+ peak: [number(m, 4), number(m, 5), number(m, 6)]
142
160
  };
143
161
  };
144
- const flash = parseModel("deepseek-v4-flash");
145
- const pro = parseModel("deepseek-v4-pro");
146
- if (!flash || !pro) throw new Error("CNY peak/off-peak table not found");
147
-
148
- let effectiveFrom = FALLBACK_PRICING.effectiveFrom;
149
- const dm = text.match(/北京时间\s*(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日\s*(\d{1,2}):(\d{2})/);
150
- if (dm) {
151
- const [y, mo, d, h, mi] = [1, 2, 3, 4, 5].map((i) => Number(dm[i]));
152
- effectiveFrom = `${y}-${String(mo).padStart(2, "0")}-${String(d).padStart(2, "0")}T${String(h).padStart(2, "0")}:${String(mi).padStart(2, "0")}:00+08:00`;
153
- }
154
-
155
- let legacy;
156
- const lm = text.match(/百万tokens输入(缓存命中)\s*([\d.]+)元\s*([\d.]+)元\s*百万tokens输入(缓存未命中)\s*([\d.]+)元\s*([\d.]+)元\s*百万tokens输出\s*([\d.]+)元\s*([\d.]+)元/);
157
- if (lm) {
158
- legacy = {
159
- flash: { hit: number(lm, 1), miss: number(lm, 3), output: number(lm, 5) },
160
- pro: { hit: number(lm, 2), miss: number(lm, 4), output: number(lm, 6) }
161
- };
162
- }
162
+ const hit = grab("百万tokens输入\\s*(缓存命中)");
163
+ const miss = grab("百万tokens输入\\s*(缓存未命中)");
164
+ const out = grab("百万tokens输出");
165
+ if (!hit || !miss || !out) throw new Error("CNY peak/off-peak table not found");
166
+
167
+ const mk = (i) => ({
168
+ offPeak: { hit: hit.offPeak[i], miss: miss.offPeak[i], output: out.offPeak[i] },
169
+ peak: { hit: hit.peak[i], miss: miss.peak[i], output: out.peak[i] }
170
+ });
163
171
 
164
172
  return {
165
- effectiveFrom,
166
173
  models: {
167
- "deepseek-v4-flash": { ...(legacy?.flash ? { legacy: legacy.flash } : {}), peak: flash.peak, offPeak: flash.offPeak },
168
- "deepseek-v4-pro": { ...(legacy?.pro ? { legacy: legacy.pro } : {}), peak: pro.peak, offPeak: pro.offPeak }
174
+ "deepseek-v4-flash": mk(0),
175
+ "deepseek-v4-pro": mk(1),
176
+ "deepseek-v4-flash-vision-exp": mk(2)
169
177
  }
170
178
  };
171
179
  }
172
180
 
173
- /** Parse the English docs page: USD prices. */
181
+ /** Parse the English docs page: USD peak/off-peak table (3 columns). */
174
182
  function parseUsModels(html) {
175
183
  const text = stripTags(html);
176
- const parseModel = (id) => {
177
- const re = new RegExp(id + "\\s+OFF-PEAK\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+PEAK\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)", "i");
184
+ const grab = (label) => {
185
+ const re = new RegExp(label + "\\s+OFF-PEAK\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+PEAK\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)", "i");
178
186
  const m = text.match(re);
179
187
  if (!m) return void 0;
180
188
  return {
181
- offPeak: { hit: number(m, 1), miss: number(m, 2), output: number(m, 3) },
182
- peak: { hit: number(m, 4), miss: number(m, 5), output: number(m, 6) }
189
+ offPeak: [number(m, 1), number(m, 2), number(m, 3)],
190
+ peak: [number(m, 4), number(m, 5), number(m, 6)]
183
191
  };
184
192
  };
185
- const flash = parseModel("deepseek-v4-flash");
186
- const pro = parseModel("deepseek-v4-pro");
187
- if (!flash || !pro) throw new Error("USD peak/off-peak table not found");
188
-
189
- let legacy;
190
- const lm = text.match(/1M INPUT TOKENS \(CACHE HIT\)\s+\$([\d.]+)\s+\$([\d.]+)\s+1M INPUT TOKENS \(CACHE MISS\)\s+\$([\d.]+)\s+\$([\d.]+)\s+1M OUTPUT TOKENS\s+\$([\d.]+)\s+\$([\d.]+)/i);
191
- if (lm) {
192
- legacy = {
193
- flash: { hit: number(lm, 1), miss: number(lm, 3), output: number(lm, 5) },
194
- pro: { hit: number(lm, 2), miss: number(lm, 4), output: number(lm, 6) }
195
- };
196
- }
193
+ const hit = grab("1M INPUT TOKENS\\s*\\(CACHE HIT\\)");
194
+ const miss = grab("1M INPUT TOKENS\\s*\\(CACHE MISS\\)");
195
+ const out = grab("1M OUTPUT TOKENS");
196
+ if (!hit || !miss || !out) throw new Error("USD peak/off-peak table not found");
197
+
198
+ const mk = (i) => ({
199
+ offPeak: { hit: hit.offPeak[i], miss: miss.offPeak[i], output: out.offPeak[i] },
200
+ peak: { hit: hit.peak[i], miss: miss.peak[i], output: out.peak[i] }
201
+ });
197
202
 
198
203
  return {
199
- "deepseek-v4-flash": { ...(legacy?.flash ? { legacy: legacy.flash } : {}), peak: flash.peak, offPeak: flash.offPeak },
200
- "deepseek-v4-pro": { ...(legacy?.pro ? { legacy: legacy.pro } : {}), peak: pro.peak, offPeak: pro.offPeak }
204
+ models: {
205
+ "deepseek-v4-flash": mk(0),
206
+ "deepseek-v4-pro": mk(1),
207
+ "deepseek-v4-flash-vision-exp": mk(2)
208
+ }
201
209
  };
202
210
  }
203
211
 
204
212
  const pricing = { data: FALLBACK_PRICING, fetchedAt: 0, source: "fallback" };
205
213
  let pricingRefresh = null;
206
214
 
215
+ function mergeLegacy(models, fallbackModels) {
216
+ const result = {};
217
+ for (const [id, m] of Object.entries(models)) {
218
+ const fb = fallbackModels[id];
219
+ result[id] = {
220
+ ...(fb && fb.legacy ? { legacy: fb.legacy } : {}),
221
+ peak: m.peak,
222
+ offPeak: m.offPeak
223
+ };
224
+ }
225
+ return result;
226
+ }
227
+
207
228
  async function refreshPricing(ctx) {
208
229
  if (pricingRefresh) return pricingRefresh;
209
230
  pricingRefresh = (async () => {
@@ -211,6 +232,7 @@ async function refreshPricing(ctx) {
211
232
  if (ctx?.logger?.warn) ctx.logger.warn(`deepseek-balance: ${message}`);
212
233
  };
213
234
  let effectiveFrom = FALLBACK_PRICING.effectiveFrom;
235
+ let weekendFrom = FALLBACK_PRICING.weekendFrom;
214
236
  let cnModels = FALLBACK_PRICING.currencies.CNY.models;
215
237
  let usModels = FALLBACK_PRICING.currencies.USD.models;
216
238
  try {
@@ -221,8 +243,7 @@ async function refreshPricing(ctx) {
221
243
  if (cnRes.ok) {
222
244
  try {
223
245
  const parsed = parseCnPricing(await cnRes.text());
224
- effectiveFrom = parsed.effectiveFrom;
225
- cnModels = parsed.models;
246
+ cnModels = mergeLegacy(parsed.models, FALLBACK_PRICING.currencies.CNY.models);
226
247
  } catch (error) {
227
248
  warn(`CNY pricing parse failed: ${error instanceof Error ? error.message : String(error)}`);
228
249
  }
@@ -231,7 +252,8 @@ async function refreshPricing(ctx) {
231
252
  }
232
253
  if (usRes.ok) {
233
254
  try {
234
- usModels = parseUsModels(await usRes.text());
255
+ const parsed = parseUsModels(await usRes.text());
256
+ usModels = mergeLegacy(parsed.models, FALLBACK_PRICING.currencies.USD.models);
235
257
  } catch (error) {
236
258
  warn(`USD pricing parse failed: ${error instanceof Error ? error.message : String(error)}`);
237
259
  }
@@ -240,6 +262,7 @@ async function refreshPricing(ctx) {
240
262
  }
241
263
  pricing.data = {
242
264
  effectiveFrom,
265
+ weekendFrom,
243
266
  currencies: {
244
267
  CNY: { symbol: "¥", models: cnModels },
245
268
  USD: { symbol: "$", models: usModels }
@@ -268,27 +291,6 @@ function msUntilNextHourBeijing(hour) {
268
291
  return deltaHours * 3600 * 1000 - bMinute * 60 * 1000 - bSecond * 1000 - bMs;
269
292
  }
270
293
 
271
- /** Beijing hour of a timestamp (UTC+8, no DST). */
272
- function beijingHourOf(time) {
273
- return new Date(time + 8 * 3600 * 1000).getUTCHours();
274
- }
275
-
276
- function isPeakHour(hour) {
277
- return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18);
278
- }
279
-
280
- const bucketSchema = z.object({
281
- uncachedInputTokens: z.number().int().nonnegative(),
282
- outputTokens: z.number().int().nonnegative(),
283
- cacheReadTokens: z.number().int().nonnegative(),
284
- cacheWriteTokens: z.number().int().nonnegative()
285
- });
286
-
287
- const tokenUsageByPeriodSchema = z.object({
288
- peak: bucketSchema,
289
- offPeak: bucketSchema
290
- });
291
-
292
294
  const zeroBucket = () => ({ uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
293
295
 
294
296
  const bucketsFrom = (usage) => ({
@@ -298,12 +300,6 @@ const bucketsFrom = (usage) => ({
298
300
  cacheWriteTokens: usage.cacheWriteTokens ?? 0
299
301
  });
300
302
 
301
- const bucketsEqual = (a, b) =>
302
- a.uncachedInputTokens === b.uncachedInputTokens &&
303
- a.outputTokens === b.outputTokens &&
304
- a.cacheReadTokens === b.cacheReadTokens &&
305
- a.cacheWriteTokens === b.cacheWriteTokens;
306
-
307
303
  const addBucket = (total, delta) => ({
308
304
  uncachedInputTokens: total.uncachedInputTokens + delta.uncachedInputTokens,
309
305
  outputTokens: total.outputTokens + delta.outputTokens,
@@ -311,53 +307,330 @@ const addBucket = (total, delta) => ({
311
307
  cacheWriteTokens: total.cacheWriteTokens + delta.cacheWriteTokens
312
308
  });
313
309
 
314
- const subtractBucket = (total, delta) => ({
315
- uncachedInputTokens: total.uncachedInputTokens - delta.uncachedInputTokens,
316
- outputTokens: total.outputTokens - delta.outputTokens,
317
- cacheReadTokens: total.cacheReadTokens - delta.cacheReadTokens,
318
- cacheWriteTokens: total.cacheWriteTokens - delta.cacheWriteTokens
319
- });
310
+ const ZSTD_MAGIC = 4247762216;
311
+
312
+ function scanZstdFrames(buffer) {
313
+ const frames = [];
314
+ let offset = 0;
315
+ while (offset < buffer.length) {
316
+ const start = offset;
317
+ if (buffer.length - offset < 4) break;
318
+ if (buffer.readUInt32LE(offset) !== ZSTD_MAGIC) break;
319
+ offset += 4;
320
+ const descriptor = buffer.readUInt8(offset);
321
+ offset += 1;
322
+ const contentSizeFlag = descriptor >>> 6;
323
+ const singleSegment = (descriptor & 32) !== 0;
324
+ const checksum = (descriptor & 4) !== 0;
325
+ const dictionaryFlag = descriptor & 3;
326
+ const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;
327
+ const contentSizeBytes = contentSizeFlag === 0 ? (singleSegment ? 1 : 0) : (1 << contentSizeFlag);
328
+ offset += (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;
329
+ for (;;) {
330
+ const blockHeader = buffer.readUIntLE(offset, 3);
331
+ offset += 3;
332
+ const lastBlock = (blockHeader & 1) !== 0;
333
+ const blockType = (blockHeader >>> 1) & 3;
334
+ const blockSize = blockHeader >>> 3;
335
+ offset += blockType === 1 ? 1 : blockSize;
336
+ if (lastBlock) break;
337
+ }
338
+ if (checksum) offset += 4;
339
+ frames.push({ start, end: offset });
340
+ }
341
+ return frames;
342
+ }
320
343
 
321
- /**
322
- * Whole-log token usage bucketed by Beijing-time peak/off-peak hour, so the
323
- * client can price each hour at its own rate. Replacement semantics mirror the
324
- * token-meter `tokenUsage` projection: an assistant/message finalizes the usage
325
- * for its turn/step, replacing an earlier usage chunk instead of double counting.
326
- */
327
- const tokenUsageByPeriodProjection = {
328
- key: "tokenUsageByPeriod",
329
- schema: tokenUsageByPeriodSchema,
330
- init: () => ({ totals: { peak: zeroBucket(), offPeak: zeroBucket() }, last: null }),
331
- apply: (state, event) => {
332
- let turn;
333
- let step;
334
- let usage;
335
- if (event.type === "assistant/chunk" && event.data.chunk.type === "usage") {
336
- ({ turn, step } = event.data);
337
- usage = event.data.chunk.usage;
338
- } else if (event.type === "assistant/message" && event.data.usage !== void 0) {
339
- ({ turn, step, usage } = event.data);
344
+ function dshHome() {
345
+ return process.env.DSH_HOME || join(homedir(), ".dsh");
346
+ }
347
+
348
+ function beijingDayKey(ms) {
349
+ return new Date(ms + 8 * 3600 * 1000).toISOString().slice(0, 10);
350
+ }
351
+
352
+ function periodOfUsage(time, effectiveFromMs, weekendFromMs) {
353
+ if (time < effectiveFromMs) return "flat";
354
+ const d = new Date(time + 8 * 3600 * 1000);
355
+ const day = d.getUTCDay();
356
+ if ((day === 0 || day === 6) && Number.isFinite(weekendFromMs) && time >= weekendFromMs) return "offPeak";
357
+ const hour = d.getUTCHours();
358
+ return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18) ? "peak" : "offPeak";
359
+ }
360
+
361
+ function readSessionRecords(path) {
362
+ const buf = readFileSync(path);
363
+ let text;
364
+ if (path.endsWith(".zstd")) {
365
+ const parts = [];
366
+ for (const frame of scanZstdFrames(buf)) parts.push(zstdDecompressSync(buf.subarray(frame.start, frame.end)).toString("utf8"));
367
+ text = parts.join("");
368
+ } else {
369
+ text = buf.toString("utf8");
370
+ }
371
+ const records = [];
372
+ for (const line of text.split("\n")) {
373
+ const t = line.trim();
374
+ if (t === "") continue;
375
+ try { records.push(JSON.parse(t)); } catch {}
376
+ }
377
+ return records;
378
+ }
379
+
380
+ function aggregateSessions(archivedIds = []) {
381
+ const archivedSet = new Set(archivedIds);
382
+ const root = join(dshHome(), "sessions");
383
+ const summary = {
384
+ totalTokens: 0,
385
+ peakTokens: 0,
386
+ offPeakTokens: 0,
387
+ flatTokens: 0,
388
+ dailyPeakTokens: 0,
389
+ longestChatMs: 0,
390
+ currentStreak: 0,
391
+ longestStreak: 0,
392
+ days: {},
393
+ sessions: [],
394
+ modelEffort: {},
395
+ byModel: {}
396
+ };
397
+ if (!existsSync(root)) return summary;
398
+ const effectiveFromMs = Date.parse(pricing.data.effectiveFrom || FALLBACK_PRICING.effectiveFrom);
399
+ if (!Number.isFinite(effectiveFromMs)) return summary;
400
+ const weekendFromMs = Date.parse(pricing.data.weekendFrom || FALLBACK_PRICING.weekendFrom);
401
+
402
+ let wsDirs = [];
403
+ try { wsDirs = readdirSync(root); } catch { return summary; }
404
+ for (const wsDir of wsDirs) {
405
+ const wsPath = join(root, wsDir);
406
+ let sDirs = [];
407
+ try { sDirs = readdirSync(wsPath); } catch { continue; }
408
+ for (const sDir of sDirs) {
409
+ const sPath = join(wsPath, sDir);
410
+ const zstd = join(sPath, "session.jsonl.zstd");
411
+ const plain = join(sPath, "session.jsonl");
412
+ const path = existsSync(zstd) ? zstd : existsSync(plain) ? plain : null;
413
+ if (path === null) continue;
414
+ let records;
415
+ try { records = readSessionRecords(path); } catch { continue; }
416
+ let header = null;
417
+ let title = null;
418
+ let model = null;
419
+ let effort = null;
420
+ let firstTime = null;
421
+ let lastTime = null;
422
+ let tokens = 0;
423
+ let peakTokens = 0;
424
+ let offPeakTokens = 0;
425
+ let flatTokens = 0;
426
+ const dayTokens = {};
427
+ const sessionByModel = {};
428
+ const dedupe = new Set();
429
+ {
430
+ const last = new Map();
431
+ for (const r of records) {
432
+ let key = null;
433
+ if (r.type === "assistant/message" && r.data?.usage) {
434
+ const t = r.data?.turn;
435
+ const s = r.data?.step;
436
+ if (t !== void 0 && s !== void 0) key = t + "|" + s;
437
+ } else if (r.type === "assistant/chunk" && r.data?.chunk?.type === "usage") {
438
+ const t = r.data?.turn;
439
+ const s = r.data?.step;
440
+ if (t !== void 0 && s !== void 0) key = t + "|" + s;
441
+ }
442
+ if (key !== null) last.set(key, r);
443
+ }
444
+ for (const r of last.values()) dedupe.add(r);
445
+ }
446
+ for (const r of records) {
447
+ if (r.type === "session" && header === null) { header = r; continue; }
448
+ if (r.type === "session/title" && r.data?.title) title = r.data.title;
449
+ if (r.type === "request/header") {
450
+ model = r.data?.header?.config?.model ?? model;
451
+ effort = r.data?.header?.config?.reasoningEffort ?? effort;
452
+ }
453
+ const time = typeof r.time === "number" ? r.time : null;
454
+ if (time !== null) {
455
+ if (firstTime === null || time < firstTime) firstTime = time;
456
+ if (lastTime === null || time > lastTime) lastTime = time;
457
+ }
458
+ let usage = null;
459
+ if (r.type === "assistant/message" && r.data?.usage) usage = r.data.usage;
460
+ else if (r.type === "assistant/chunk" && r.data?.chunk?.type === "usage") usage = r.data.chunk.usage;
461
+ if (usage && time !== null && dedupe.has(r)) {
462
+ const t = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0) + (usage.cacheReadTokens ?? 0);
463
+ const period = periodOfUsage(time, effectiveFromMs, weekendFromMs);
464
+ const dayKey = beijingDayKey(time);
465
+ tokens += t;
466
+ if (period === "peak") peakTokens += t;
467
+ else if (period === "offPeak") offPeakTokens += t;
468
+ else flatTokens += t;
469
+ const mkModel = model ?? "";
470
+ const b = bucketsFrom(usage);
471
+ dayTokens[dayKey] = dayTokens[dayKey] || {};
472
+ dayTokens[dayKey][mkModel] = dayTokens[dayKey][mkModel] || { flat: zeroBucket(), peak: zeroBucket(), offPeak: zeroBucket() };
473
+ dayTokens[dayKey][mkModel][period] = addBucket(dayTokens[dayKey][mkModel][period], b);
474
+ summary.byModel[mkModel] = summary.byModel[mkModel] || { flat: zeroBucket(), peak: zeroBucket(), offPeak: zeroBucket() };
475
+ summary.byModel[mkModel][period] = addBucket(summary.byModel[mkModel][period], b);
476
+ sessionByModel[mkModel] = sessionByModel[mkModel] || { flat: zeroBucket(), peak: zeroBucket(), offPeak: zeroBucket() };
477
+ sessionByModel[mkModel][period] = addBucket(sessionByModel[mkModel][period], b);
478
+ const mk = `${model ?? ""}|${effort ?? ""}`;
479
+ summary.modelEffort[mk] = summary.modelEffort[mk] || { tokens: 0 };
480
+ summary.modelEffort[mk].tokens += t;
481
+ }
482
+ }
483
+ summary.totalTokens += tokens;
484
+ summary.peakTokens += peakTokens;
485
+ summary.offPeakTokens += offPeakTokens;
486
+ summary.flatTokens += flatTokens;
487
+ for (const [dayKey, d] of Object.entries(dayTokens)) {
488
+ summary.days[dayKey] = summary.days[dayKey] || { tokens: 0, peak: 0, offPeak: 0, flat: 0, byModel: {} };
489
+ const day = summary.days[dayKey];
490
+ const byModel = day.byModel;
491
+ const bucketTotal = (bucket) => bucket.uncachedInputTokens + bucket.outputTokens + bucket.cacheReadTokens;
492
+ let dayTotal = 0;
493
+ let dayPeak = 0;
494
+ let dayOff = 0;
495
+ let dayFlat = 0;
496
+ for (const [m, b] of Object.entries(d)) {
497
+ byModel[m] = byModel[m] || { flat: zeroBucket(), peak: zeroBucket(), offPeak: zeroBucket() };
498
+ byModel[m].flat = addBucket(byModel[m].flat, b.flat);
499
+ byModel[m].peak = addBucket(byModel[m].peak, b.peak);
500
+ byModel[m].offPeak = addBucket(byModel[m].offPeak, b.offPeak);
501
+ dayPeak += bucketTotal(b.peak);
502
+ dayOff += bucketTotal(b.offPeak);
503
+ dayFlat += bucketTotal(b.flat);
504
+ dayTotal += bucketTotal(b.peak) + bucketTotal(b.offPeak) + bucketTotal(b.flat);
505
+ }
506
+ day.tokens += dayTotal;
507
+ day.peak += dayPeak;
508
+ day.offPeak += dayOff;
509
+ day.flat += dayFlat;
510
+ if (day.tokens > summary.dailyPeakTokens) summary.dailyPeakTokens = day.tokens;
511
+ }
512
+ const duration = (lastTime ?? 0) - (firstTime ?? 0);
513
+ if (duration > summary.longestChatMs) summary.longestChatMs = duration;
514
+
515
+ const sid = header?.id ?? sDir.replace(/^session-/, "");
516
+ summary.sessions.push({
517
+ id: sid,
518
+ title: title ?? header?.id ?? sDir,
519
+ cwd: header?.cwd ?? wsDir,
520
+ tokens,
521
+ peakTokens,
522
+ byModel: sessionByModel,
523
+ createdAt: header?.createdAt ?? firstTime,
524
+ endedAt: lastTime,
525
+ archived: archivedSet.has(sid)
526
+ });
527
+ }
528
+ }
529
+
530
+ summary.sessions.sort((a, b) => b.tokens - a.tokens);
531
+
532
+ const dayKeys = Object.keys(summary.days).sort();
533
+ let longest = 0;
534
+ let run = 0;
535
+ let prev = null;
536
+ for (const d of dayKeys) {
537
+ if (prev === null) run = 1;
538
+ else {
539
+ const diff = Math.round((Date.parse(d) - Date.parse(prev)) / 86400000);
540
+ run = diff === 1 ? run + 1 : 1;
541
+ }
542
+ if (run > longest) longest = run;
543
+ prev = d;
544
+ }
545
+ summary.longestStreak = longest;
546
+
547
+ const daySet = new Set(dayKeys);
548
+ let cur = 0;
549
+ let cursor = Date.parse(beijingDayKey(Date.now()));
550
+ while (daySet.has(beijingDayKey(cursor))) {
551
+ cur++;
552
+ cursor -= 86400000;
553
+ }
554
+ summary.currentStreak = cur;
555
+
556
+ return summary;
557
+ }
558
+
559
+ const DEFAULT_MODEL = "deepseek-v4-pro";
560
+
561
+ /** Compute spend (CNY/USD) from an aggregated usage summary, using the live pricing table. */
562
+ function computeSpend(summary, currency = "CNY") {
563
+ const pricingSet =
564
+ (pricing.data.currencies && pricing.data.currencies[currency]) ||
565
+ pricing.data.currencies?.CNY ||
566
+ FALLBACK_PRICING.currencies.CNY;
567
+ const models = pricingSet.models || {};
568
+ const defaultModel = models[DEFAULT_MODEL] || { legacy: null, peak: { miss: 0, hit: 0, output: 0 }, offPeak: { miss: 0, hit: 0, output: 0 } };
569
+ const priceOf = (tokens, price) => (tokens || 0) * (price || 0) / 1e6;
570
+ const bucketCost = (b, mm) => {
571
+ if (!mm) return 0;
572
+ const cost = (bucket, price) => {
573
+ if (!price) return 0;
574
+ return priceOf(bucket.uncachedInputTokens, price.miss)
575
+ + priceOf(bucket.cacheWriteTokens, price.miss)
576
+ + priceOf(bucket.cacheReadTokens, price.hit)
577
+ + priceOf(bucket.outputTokens, price.output);
578
+ };
579
+ return (mm.legacy ? cost(b.flat, mm.legacy) : 0) + cost(b.peak, mm.peak) + cost(b.offPeak, mm.offPeak);
580
+ };
581
+ let total = 0;
582
+ for (const [k, b] of Object.entries(summary.byModel || {})) {
583
+ const [m] = k.split("|");
584
+ total += bucketCost(b, models[m] || defaultModel);
585
+ }
586
+ const day = (summary.days || {})[beijingDayKey(Date.now())];
587
+ let today = null;
588
+ if (day) {
589
+ if (day.byModel) {
590
+ today = 0;
591
+ for (const [m, b] of Object.entries(day.byModel)) today += bucketCost(b, models[m] || defaultModel);
340
592
  } else {
341
- return state;
593
+ today = bucketCost(day, defaultModel);
342
594
  }
343
- const period = isPeakHour(beijingHourOf(event.time)) ? "peak" : "offPeak";
344
- const buckets = bucketsFrom(usage);
345
- const previous = state.last !== null && state.last.turn === turn && state.last.step === step ? state.last : void 0;
346
- if (previous !== void 0 && previous.period === period && bucketsEqual(previous.buckets, buckets)) return state;
347
- const totals = { peak: state.totals.peak, offPeak: state.totals.offPeak };
348
- if (previous !== void 0) totals[previous.period] = subtractBucket(totals[previous.period], previous.buckets);
349
- totals[period] = addBucket(totals[period], buckets);
350
- return { totals, last: { turn, step, period, buckets } };
351
- },
352
- view: (state) => state.totals,
353
- stateVersion: 1
354
- };
595
+ }
596
+ return { total, today, currency };
597
+ }
355
598
 
356
- function apply(ctx) {
357
- ctx.inject(["sessionProjections"], (projectionCtx) => {
358
- projectionCtx.sessionProjections.register(tokenUsageByPeriodProjection);
359
- });
599
+ const usageCache = { data: null, at: 0, currency: null };
600
+ const USAGE_CACHE_TTL_MS = 30000;
360
601
 
602
+ function tokenNameFile() {
603
+ return join(dshHome(), "deepseek-balance.json");
604
+ }
605
+
606
+ function readTokenName() {
607
+ try {
608
+ const p = tokenNameFile();
609
+ if (!existsSync(p)) return null;
610
+ const d = JSON.parse(readFileSync(p, "utf8"));
611
+ return typeof d.tokenName === "string" && d.tokenName.trim() ? d.tokenName.trim() : null;
612
+ } catch { return null; }
613
+ }
614
+
615
+ function writeTokenName(name) {
616
+ try { mkdirSync(dshHome(), { recursive: true }); } catch {}
617
+ writeFileSync(tokenNameFile(), JSON.stringify({ tokenName: name }), "utf8");
618
+ }
619
+
620
+ function resolveTokenName(config) {
621
+ return readTokenName() || config.tokenName || "API key";
622
+ }
623
+
624
+ async function maskApiKey(ctx) {
625
+ try {
626
+ const { apiKey } = await resolveDeepSeekFacts(ctx);
627
+ return apiKey.length > 12 ? `${apiKey.slice(0, 8)}*****${apiKey.slice(-4)}` : `${apiKey.slice(0, 8)}****`;
628
+ } catch {
629
+ return null;
630
+ }
631
+ }
632
+
633
+ function apply(ctx, config = {}) {
361
634
  const balanceHandler = async (req, res) => {
362
635
  if (req.method !== "GET" && req.method !== "HEAD") {
363
636
  json(res, 405, { error: "method not allowed" });
@@ -383,6 +656,7 @@ function apply(ctx) {
383
656
  json(res, upstream.status === 401 || upstream.status === 403 ? 401 : 502, { error: message });
384
657
  return;
385
658
  }
659
+ if (typeof body?.currency === "string" && body.currency.length > 0) usageCache.currency = body.currency;
386
660
  json(res, 200, body);
387
661
  } catch (error) {
388
662
  json(res, 502, { error: error instanceof Error ? error.message : String(error) });
@@ -413,6 +687,104 @@ function apply(ctx) {
413
687
  ctx.effect(() => ctx.webServer.register({ kind: "exact", path: "/api/deepseek-balance", handler: balanceHandler }), "deepseek-balance: /api/deepseek-balance route");
414
688
  ctx.effect(() => ctx.webServer.register({ kind: "exact", path: "/api/deepseek-pricing", handler: pricingHandler }), "deepseek-balance: /api/deepseek-pricing route");
415
689
 
690
+ const usageHandler = async (req, res) => {
691
+ if (req.method !== "GET" && req.method !== "HEAD") {
692
+ json(res, 405, { error: "method not allowed" });
693
+ return;
694
+ }
695
+ if (!isTrustedRead(req)) {
696
+ json(res, 403, { error: "forbidden" });
697
+ return;
698
+ }
699
+ if (usageCache.at === 0 || Date.now() - usageCache.at > USAGE_CACHE_TTL_MS) {
700
+ try {
701
+ const ws = ctx.get("workspaceRegistry");
702
+ usageCache.data = aggregateSessions(ws?.archivedSessionIds ?? []);
703
+ usageCache.data.spend = computeSpend(usageCache.data, usageCache.currency || "CNY");
704
+ usageCache.at = Date.now();
705
+ } catch (error) {
706
+ if (ctx?.logger?.warn) ctx.logger.warn(`deepseek-balance: usage aggregation failed: ${error instanceof Error ? error.message : String(error)}`);
707
+ }
708
+ }
709
+ const key = await maskApiKey(ctx);
710
+ let payload = usageCache.data;
711
+ if (!payload) {
712
+ payload = aggregateSessions();
713
+ payload.spend = computeSpend(payload, usageCache.currency || "CNY");
714
+ }
715
+ let query = null;
716
+ try { query = new URL(req.url, "http://dsh.local").searchParams; } catch {}
717
+ const sessionId = query ? query.get("session") : null;
718
+ if (sessionId && sessionId.length > 0) {
719
+ const s = (payload.sessions || []).find((x) => x.id === sessionId);
720
+ if (s) {
721
+ json(res, 200, {
722
+ id: s.id,
723
+ title: s.title,
724
+ tokens: s.tokens,
725
+ spend: computeSpend({ byModel: s.byModel || {} }, usageCache.currency || "CNY"),
726
+ apiKeyPreview: key,
727
+ tokenName: resolveTokenName(config),
728
+ effectiveFrom: pricing.data.effectiveFrom, weekendFrom: pricing.data.weekendFrom,
729
+ fetchedAt: usageCache.at,
730
+ source: pricing.source
731
+ });
732
+ return;
733
+ }
734
+ json(res, 200, {
735
+ id: sessionId,
736
+ title: sessionId,
737
+ tokens: 0,
738
+ spend: { total: 0, today: null, currency: usageCache.currency || "CNY" },
739
+ apiKeyPreview: key,
740
+ tokenName: resolveTokenName(config),
741
+ effectiveFrom: pricing.data.effectiveFrom,
742
+ fetchedAt: usageCache.at,
743
+ source: pricing.source
744
+ });
745
+ return;
746
+ }
747
+ json(res, 200, {
748
+ ...payload,
749
+ apiKeyPreview: key,
750
+ tokenName: resolveTokenName(config),
751
+ effectiveFrom: pricing.data.effectiveFrom,
752
+ fetchedAt: usageCache.at,
753
+ source: pricing.source
754
+ });
755
+ };
756
+
757
+ ctx.effect(() => ctx.webServer.register({ kind: "exact", path: "/api/deepseek-usage", handler: usageHandler }), "deepseek-balance: /api/deepseek-usage route");
758
+
759
+ const tokenNameHandler = async (req, res) => {
760
+ if (!isTrustedRead(req)) {
761
+ json(res, 403, { error: "forbidden" });
762
+ return;
763
+ }
764
+ if (req.method === "GET" || req.method === "HEAD") {
765
+ json(res, 200, { tokenName: resolveTokenName(config) });
766
+ return;
767
+ }
768
+ if (req.method === "POST") {
769
+ let raw = "";
770
+ try {
771
+ const chunks = [];
772
+ for await (const c of req) chunks.push(c);
773
+ raw = Buffer.concat(chunks).toString("utf8");
774
+ const body = raw ? JSON.parse(raw) : {};
775
+ const name = typeof body.tokenName === "string" && body.tokenName.trim() ? body.tokenName.trim().slice(0, 64) : "API key";
776
+ writeTokenName(name);
777
+ json(res, 200, { tokenName: name });
778
+ } catch (error) {
779
+ json(res, 400, { error: error instanceof Error ? error.message : String(error) });
780
+ }
781
+ return;
782
+ }
783
+ json(res, 405, { error: "method not allowed" });
784
+ };
785
+
786
+ ctx.effect(() => ctx.webServer.register({ kind: "exact", path: "/api/deepseek-balance/token-name", handler: tokenNameHandler }), "deepseek-balance: /api/deepseek-balance/token-name route");
787
+
416
788
  ctx.effect(() => {
417
789
  refreshPricing(ctx).catch(() => {});
418
790
  let timer;
@@ -428,4 +800,4 @@ function apply(ctx) {
428
800
  }, "deepseek-balance: daily 01:00 Beijing pricing refresh");
429
801
  }
430
802
 
431
- export { apply, inject, name };
803
+ export { Config, apply, inject, name };