dsh-key-usage 0.1.2 → 0.1.4
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 +115 -24
- package/client/client.js +2793 -419
- package/client/client.js.map +1 -1
- package/docs/images/usage-daily.png +0 -0
- package/docs/images/usage-overview.png +0 -0
- package/docs/images/usage-quota.png +0 -0
- package/docs/images//346/214/211/346/250/241/345/236/213.png +0 -0
- package/docs/images//351/241/271/347/233/256/346/200/273/351/207/217.png +0 -0
- package/lib/index.js +73 -4
- package/lib/live-quota.js +96 -30
- package/lib/merge.js +8 -1
- package/lib/pricing-catalog.js +349 -0
- package/lib/pricing.js +500 -0
- package/lib/quota-snapshots.js +68 -0
- package/lib/routes.js +275 -28
- package/lib/session-usage.js +67 -46
- package/lib/sessions.js +98 -8
- package/package.json +18 -9
package/lib/sessions.js
CHANGED
|
@@ -13,6 +13,15 @@ import { normalizeSessionId } from './session-meta.js';
|
|
|
13
13
|
* 只有首次请求(或会话变化)才重新读取事件。
|
|
14
14
|
*/
|
|
15
15
|
const MAX_SESSIONS = 200;
|
|
16
|
+
/**
|
|
17
|
+
* 同时在读的会话数:宿主的 `open(id,"read")` 与随后的 `read(0)` 共用一条容量 2 的解析
|
|
18
|
+
* memo(后端注释写明「只装处于两步之间的会话」),2 路正好把两次「找路径 + 读文件」的
|
|
19
|
+
* 等待叠起来;3 路会把先到的会话挤出 memo、逼它整段重解压。
|
|
20
|
+
* 本机实测(133 会话 / 119 MB):串行 7.4~7.9 s,2 路 6.0~6.4 s,3 路反而 7.8~8.2 s。
|
|
21
|
+
*/
|
|
22
|
+
const READ_CONCURRENCY = 2;
|
|
23
|
+
/** 超过这个体积的会话改为串行:它们的耗时主要在主线程解压,叠不出等待,却会放大 memo 被第三方挤掉时的重解压代价。 */
|
|
24
|
+
const SERIAL_SESSION_BYTES = 1024 * 1024;
|
|
16
25
|
function firstString(...values) {
|
|
17
26
|
for (const value of values) {
|
|
18
27
|
if (typeof value === 'string' && value.trim() !== '')
|
|
@@ -49,7 +58,9 @@ export function createSessionSource(host) {
|
|
|
49
58
|
.sort((first, second) => second.order - first.order)
|
|
50
59
|
.slice(0, MAX_SESSIONS);
|
|
51
60
|
const present = new Set();
|
|
52
|
-
|
|
61
|
+
// 小会话两路并发、大会话串行:两条队列都不改变最终 records 的顺序(顺序由 ordered 决定)
|
|
62
|
+
const shared = [];
|
|
63
|
+
const alone = [];
|
|
53
64
|
for (const { snapshot } of ordered) {
|
|
54
65
|
const id = firstString(snapshot.header.id);
|
|
55
66
|
if (id === null)
|
|
@@ -60,23 +71,36 @@ export function createSessionSource(host) {
|
|
|
60
71
|
const cached = cache.get(id);
|
|
61
72
|
if (cached !== undefined && cached.revision === revision && cached.eventCount === eventCount)
|
|
62
73
|
continue;
|
|
74
|
+
const size = typeof snapshot.sizeBytes === 'number' ? snapshot.sizeBytes : Number.POSITIVE_INFINITY;
|
|
75
|
+
const pending = { id, header: snapshot.header, revision, eventCount };
|
|
76
|
+
if (size <= SERIAL_SESSION_BYTES)
|
|
77
|
+
shared.push(pending);
|
|
78
|
+
else
|
|
79
|
+
alone.push(pending);
|
|
80
|
+
}
|
|
81
|
+
let skipped = 0;
|
|
82
|
+
const readSession = async ({ id, header, revision, eventCount }) => {
|
|
63
83
|
let handle;
|
|
64
84
|
try {
|
|
65
85
|
handle = await persistence.open?.(id, 'read');
|
|
66
86
|
if (handle === undefined) {
|
|
67
87
|
skipped += 1;
|
|
68
|
-
|
|
88
|
+
return;
|
|
69
89
|
}
|
|
70
90
|
const { events } = await handle.read(0);
|
|
71
91
|
const inherited = typeof handle.inheritedEventCount === 'number' ? handle.inheritedEventCount : 0;
|
|
72
92
|
if (!Number.isSafeInteger(inherited) || inherited < 0 || inherited > events.length) {
|
|
73
93
|
skipped += 1;
|
|
74
|
-
|
|
94
|
+
return;
|
|
75
95
|
}
|
|
76
|
-
//
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
96
|
+
// 继承前缀只用于建立模型归属,不计入本次用量;无继承时直接用原数组,省一次整段拷贝
|
|
97
|
+
const fold = inherited === 0
|
|
98
|
+
? foldSessionEvents(events)
|
|
99
|
+
: foldSessionEvents([
|
|
100
|
+
...events.slice(0, inherited).filter((event) => event.type === 'request/header'),
|
|
101
|
+
...events.slice(inherited),
|
|
102
|
+
]);
|
|
103
|
+
const metadata = metadataOf(header);
|
|
80
104
|
cache.set(id, {
|
|
81
105
|
revision,
|
|
82
106
|
eventCount,
|
|
@@ -97,7 +121,9 @@ export function createSessionSource(host) {
|
|
|
97
121
|
}
|
|
98
122
|
}
|
|
99
123
|
}
|
|
100
|
-
}
|
|
124
|
+
};
|
|
125
|
+
await forEachWithLimit(shared, READ_CONCURRENCY, readSession);
|
|
126
|
+
await forEachWithLimit(alone, 1, readSession);
|
|
101
127
|
for (const id of [...cache.keys()])
|
|
102
128
|
if (!present.has(id))
|
|
103
129
|
cache.delete(id);
|
|
@@ -170,6 +196,23 @@ export function createSessionSource(host) {
|
|
|
170
196
|
}
|
|
171
197
|
return { collect };
|
|
172
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* 有界并发跑任务:worker 数即并发上限,按游标顺序取活,不预先建满 promise 列表。
|
|
201
|
+
* worker 自己吞掉异常(读取失败只影响该会话),所以这里不会提前中断其它会话。
|
|
202
|
+
*/
|
|
203
|
+
async function forEachWithLimit(items, limit, worker) {
|
|
204
|
+
let cursor = 0;
|
|
205
|
+
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
206
|
+
for (;;) {
|
|
207
|
+
const index = cursor;
|
|
208
|
+
cursor += 1;
|
|
209
|
+
if (index >= items.length)
|
|
210
|
+
return;
|
|
211
|
+
await worker(items[index]);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
await Promise.all(runners);
|
|
215
|
+
}
|
|
173
216
|
function coverageOf(totalSessions, countedSessions, skippedSessions) {
|
|
174
217
|
if (countedSessions === 0 && totalSessions > 0) {
|
|
175
218
|
return { status: 'unavailable', totalSessions, countedSessions, skippedSessions };
|
|
@@ -214,6 +257,53 @@ export function sessionRowsOf(records, knownProviders, meta = {}) {
|
|
|
214
257
|
}))
|
|
215
258
|
.sort((first, second) => (second.lastTime ?? 0) - (first.lastTime ?? 0));
|
|
216
259
|
}
|
|
260
|
+
export function modelRowsOf(records, knownProviders) {
|
|
261
|
+
const byModel = new Map();
|
|
262
|
+
for (const record of records) {
|
|
263
|
+
const sessionLastTime = record.summary.lastTime;
|
|
264
|
+
for (const model of attributedModels(record.summary.models, knownProviders)) {
|
|
265
|
+
const key = model.provider + '|' + model.model;
|
|
266
|
+
let accumulator = byModel.get(key);
|
|
267
|
+
if (accumulator === undefined) {
|
|
268
|
+
accumulator = {
|
|
269
|
+
provider: model.provider, model: model.model, calls: 0,
|
|
270
|
+
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, sessionCount: 0, lastTime: null,
|
|
271
|
+
};
|
|
272
|
+
byModel.set(key, accumulator);
|
|
273
|
+
}
|
|
274
|
+
accumulator.calls += model.calls;
|
|
275
|
+
accumulator.inputTokens += model.buckets.inputTokens;
|
|
276
|
+
accumulator.outputTokens += model.buckets.outputTokens;
|
|
277
|
+
accumulator.cacheReadTokens += model.buckets.cacheReadTokens;
|
|
278
|
+
accumulator.cacheWriteTokens += model.buckets.cacheWriteTokens;
|
|
279
|
+
accumulator.sessionCount += 1;
|
|
280
|
+
// 会话事件只有会话级时间戳,「最近活跃」只能取用到该模型的最晚会话时间,是近似值
|
|
281
|
+
if (sessionLastTime !== null && (accumulator.lastTime === null || sessionLastTime > accumulator.lastTime)) {
|
|
282
|
+
accumulator.lastTime = sessionLastTime;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return [...byModel.values()]
|
|
287
|
+
.map((accumulator) => {
|
|
288
|
+
const displayTokens = accumulator.inputTokens + accumulator.outputTokens + accumulator.cacheReadTokens;
|
|
289
|
+
return {
|
|
290
|
+
provider: accumulator.provider,
|
|
291
|
+
model: accumulator.model,
|
|
292
|
+
calls: accumulator.calls,
|
|
293
|
+
tokens: displayTokens,
|
|
294
|
+
cacheHitRate: displayTokens > 0 ? Math.round(accumulator.cacheReadTokens / displayTokens * 1000) / 10 : null,
|
|
295
|
+
sessionCount: accumulator.sessionCount,
|
|
296
|
+
lastTime: accumulator.lastTime,
|
|
297
|
+
buckets: {
|
|
298
|
+
inputTokens: accumulator.inputTokens,
|
|
299
|
+
outputTokens: accumulator.outputTokens,
|
|
300
|
+
cacheReadTokens: accumulator.cacheReadTokens,
|
|
301
|
+
cacheWriteTokens: accumulator.cacheWriteTokens,
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
})
|
|
305
|
+
.sort((first, second) => second.tokens - first.tokens);
|
|
306
|
+
}
|
|
217
307
|
export function projectRowsOf(records, knownProviders, meta = {}) {
|
|
218
308
|
const archived = meta.archived ?? new Set();
|
|
219
309
|
const byWorkspace = new Map();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-key-usage",
|
|
3
3
|
"description": "DeepSeek Harness 侧边栏「使用情况」:按通道展示用量明细、活跃热力图、项目/会话用量与官方余额/配额(实时优先,失败回落快照)",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.4",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"keywords": [
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"LICENSE",
|
|
28
28
|
"docs/images/usage-overview.png",
|
|
29
29
|
"docs/images/usage-daily.png",
|
|
30
|
+
"docs/images/按模型.png",
|
|
31
|
+
"docs/images/项目总量.png",
|
|
30
32
|
"docs/images/usage-quota.png"
|
|
31
33
|
],
|
|
32
34
|
"repository": {
|
|
@@ -44,13 +46,9 @@
|
|
|
44
46
|
"access": "public",
|
|
45
47
|
"registry": "https://registry.npmjs.org/"
|
|
46
48
|
},
|
|
47
|
-
"scripts": {
|
|
48
|
-
"build:client": "tsdown && node scripts/normalize-client-banner.mjs",
|
|
49
|
-
"check": "node --check lib/index.js && node --check lib/ledger.js && node --check lib/collect.js && node --check lib/routes.js && node --check lib/sessions.js && node --check lib/session-usage.js && node --check lib/session-meta.js && node --check lib/merge.js && node --check lib/live-quota.js",
|
|
50
|
-
"test": "node test/ledger.test.mjs && node test/collect.test.mjs && node test/channels.test.mjs && node test/session-usage.test.mjs && node test/session-meta.test.mjs && node test/sessions.test.mjs && node test/merge.test.mjs && node test/live-quota.test.mjs && node test/routes.test.mjs"
|
|
51
|
-
},
|
|
52
49
|
"peerDependencies": {
|
|
53
|
-
"@deepseek-ai/cordis": "^4.0.1"
|
|
50
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
51
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
54
52
|
},
|
|
55
53
|
"peerDependenciesMeta": {
|
|
56
54
|
"@deepseek-ai/cordis": {
|
|
@@ -65,7 +63,8 @@
|
|
|
65
63
|
"platform": "web",
|
|
66
64
|
"inject": [
|
|
67
65
|
"@deepseek-ai/dsh-client-locale",
|
|
68
|
-
"@deepseek-ai/dsh-client-ui-slots"
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
69
68
|
]
|
|
70
69
|
}
|
|
71
70
|
},
|
|
@@ -76,5 +75,15 @@
|
|
|
76
75
|
"react": "18.3.1",
|
|
77
76
|
"tsdown": "0.22.14",
|
|
78
77
|
"typescript": "7.0.2"
|
|
78
|
+
},
|
|
79
|
+
"scripts": {
|
|
80
|
+
"build": "tsc -p tsconfig.server.json && tsdown && node scripts/normalize-client-banner.mjs",
|
|
81
|
+
"build:client": "tsdown && node scripts/normalize-client-banner.mjs",
|
|
82
|
+
"build:server": "tsc -p tsconfig.server.json",
|
|
83
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
84
|
+
"check": "node --check lib/index.js && node --check lib/ledger.js && node --check lib/collect.js && node --check lib/routes.js && node --check lib/sessions.js && node --check lib/session-usage.js && node --check lib/session-meta.js && node --check lib/quota-snapshots.js && node --check lib/merge.js && node --check lib/live-quota.js && node --check lib/pricing.js && node --check lib/pricing-catalog.js",
|
|
85
|
+
"test": "node test/ledger.test.mjs && node test/collect.test.mjs && node test/channels.test.mjs && node test/session-usage.test.mjs && node test/session-meta.test.mjs && node test/sessions.test.mjs && node test/merge.test.mjs && node test/live-quota.test.mjs && node test/pricing.test.mjs && node test/routes.test.mjs",
|
|
86
|
+
"verify": "npm run typecheck && npm run build && npm run check && npm run test",
|
|
87
|
+
"pack:dry": "npm pack --dry-run"
|
|
79
88
|
}
|
|
80
|
-
}
|
|
89
|
+
}
|