@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/README.md +10 -0
- package/README.zh.md +10 -0
- package/lib/client.js +271 -246
- package/lib/index.js +491 -119
- package/package.json +1 -1
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
|
|
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
|
|
136
|
-
const re = new RegExp(
|
|
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:
|
|
141
|
-
peak:
|
|
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
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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":
|
|
168
|
-
"deepseek-v4-pro":
|
|
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
|
|
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
|
|
177
|
-
const re = new RegExp(
|
|
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:
|
|
182
|
-
peak:
|
|
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
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
593
|
+
today = bucketCost(day, defaultModel);
|
|
342
594
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
357
|
-
|
|
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 };
|