@kenz1117/dsh-ui-usage-billing 0.9.3 → 0.9.5
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 +4 -2
- package/lib/client.js +964 -253
- package/lib/index.js +162 -6
- package/lib/types/aggregate.d.ts +60 -0
- package/lib/types/client/plan-knowledge.d.ts +22 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { readFile, stat } from "node:fs/promises";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -27,11 +28,23 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
|
27
28
|
const PLAN_KNOWLEDGE = {
|
|
28
29
|
"opencode-go": {
|
|
29
30
|
type: "code",
|
|
30
|
-
subscriptionCny: 70
|
|
31
|
+
subscriptionCny: 70,
|
|
32
|
+
tier: {
|
|
33
|
+
amount: 10,
|
|
34
|
+
currency: "USD",
|
|
35
|
+
periodDays: 7,
|
|
36
|
+
label: "周额度 $30"
|
|
37
|
+
}
|
|
31
38
|
},
|
|
32
39
|
opencode: {
|
|
33
40
|
type: "code",
|
|
34
|
-
subscriptionCny: 70
|
|
41
|
+
subscriptionCny: 70,
|
|
42
|
+
tier: {
|
|
43
|
+
amount: 10,
|
|
44
|
+
currency: "USD",
|
|
45
|
+
periodDays: 7,
|
|
46
|
+
label: "周额度 $30"
|
|
47
|
+
}
|
|
35
48
|
},
|
|
36
49
|
"kimi-coding": {
|
|
37
50
|
type: "code",
|
|
@@ -993,6 +1006,12 @@ function dayStamp(time) {
|
|
|
993
1006
|
const pad = (n) => String(n).padStart(2, "0");
|
|
994
1007
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
995
1008
|
}
|
|
1009
|
+
/** Local-time hour stamp `YYYY-MM-DDTHH` — the performance series bucket key. */
|
|
1010
|
+
function hourStamp(time) {
|
|
1011
|
+
const date = new Date(time);
|
|
1012
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1013
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}`;
|
|
1014
|
+
}
|
|
996
1015
|
/** 工作区名:取 cwd 的末级目录名;无 cwd 时返回 {@link UNKNOWN_WORKSPACE_NAME}。 */
|
|
997
1016
|
function workspaceNameOf(cwd) {
|
|
998
1017
|
if (cwd === void 0 || cwd === "") return "—";
|
|
@@ -1066,6 +1085,7 @@ function foldSession(events, subscriptionProviders, officialProviderIds) {
|
|
|
1066
1085
|
byDayModels: /* @__PURE__ */ new Map(),
|
|
1067
1086
|
planCalls: /* @__PURE__ */ new Map(),
|
|
1068
1087
|
turns: [],
|
|
1088
|
+
perf: [],
|
|
1069
1089
|
roles: {
|
|
1070
1090
|
userChars: 0,
|
|
1071
1091
|
toolChars: 0,
|
|
@@ -1078,6 +1098,8 @@ function foldSession(events, subscriptionProviders, officialProviderIds) {
|
|
|
1078
1098
|
let subscription = false;
|
|
1079
1099
|
let official = false;
|
|
1080
1100
|
const turns = /* @__PURE__ */ new Map();
|
|
1101
|
+
const steps = /* @__PURE__ */ new Map();
|
|
1102
|
+
let lastOpenStepKey;
|
|
1081
1103
|
for (const event of events) {
|
|
1082
1104
|
fold.lastActive = Math.max(fold.lastActive, event.time);
|
|
1083
1105
|
if (event.type === "session/title") {
|
|
@@ -1104,11 +1126,39 @@ function foldSession(events, subscriptionProviders, officialProviderIds) {
|
|
|
1104
1126
|
if (state !== void 0) state.endedAt = event.time;
|
|
1105
1127
|
continue;
|
|
1106
1128
|
}
|
|
1129
|
+
if (event.type === "step/start") {
|
|
1130
|
+
const turn = event.data.turn;
|
|
1131
|
+
const step = event.data.step;
|
|
1132
|
+
if (typeof turn === "number" && typeof step === "number") {
|
|
1133
|
+
const stepKey = `${turn}:${step}`;
|
|
1134
|
+
steps.set(stepKey, { startTime: event.time });
|
|
1135
|
+
lastOpenStepKey = stepKey;
|
|
1136
|
+
}
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1107
1139
|
if (event.type === "request/header") {
|
|
1108
1140
|
const { model, provider } = event.data.header.config;
|
|
1109
1141
|
key = resolveCatalogKey(model);
|
|
1110
1142
|
subscription = subscriptionProviders.has(provider);
|
|
1111
1143
|
official = officialProviderIds === void 0 ? isOfficialProvider(provider) : officialProviderIds.has(provider);
|
|
1144
|
+
if (lastOpenStepKey !== void 0) {
|
|
1145
|
+
const stepState = steps.get(lastOpenStepKey);
|
|
1146
|
+
if (stepState !== void 0 && stepState.requestTime === void 0) stepState.requestTime = event.time;
|
|
1147
|
+
}
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
if (event.type === "assistant/chunk") {
|
|
1151
|
+
const data = event.data;
|
|
1152
|
+
const turn = data.turn;
|
|
1153
|
+
const step = data.step;
|
|
1154
|
+
const chunk = data.chunk;
|
|
1155
|
+
if (typeof turn === "number" && typeof step === "number" && chunk !== void 0 && chunk.type !== "usage" && chunk.type !== "finish") {
|
|
1156
|
+
const state = steps.get(`${turn}:${step}`);
|
|
1157
|
+
if (state !== void 0) {
|
|
1158
|
+
if (state.firstContentTime === void 0) state.firstContentTime = event.time;
|
|
1159
|
+
state.lastContentTime = event.time;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1112
1162
|
continue;
|
|
1113
1163
|
}
|
|
1114
1164
|
if (event.type !== "assistant/message") continue;
|
|
@@ -1121,7 +1171,8 @@ function foldSession(events, subscriptionProviders, officialProviderIds) {
|
|
|
1121
1171
|
foldUsage(usageCell(fold.byDay, day), usage, modelKey, subscription, event.time, official);
|
|
1122
1172
|
foldUsage(modelDayCell(fold.byDayModels, day, modelKey), usage, modelKey, subscription, event.time, official);
|
|
1123
1173
|
if (subscription) fold.planCalls.set(modelKey, (fold.planCalls.get(modelKey) ?? 0) + 1);
|
|
1124
|
-
const
|
|
1174
|
+
const turn = event.data.turn ?? -1;
|
|
1175
|
+
const state = turnState(turns, turn);
|
|
1125
1176
|
state.model = modelKey;
|
|
1126
1177
|
state.input += usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
1127
1178
|
state.output += usage.outputTokens;
|
|
@@ -1146,6 +1197,15 @@ function foldSession(events, subscriptionProviders, officialProviderIds) {
|
|
|
1146
1197
|
fold.roles.inputCost += fullCost - outputCost;
|
|
1147
1198
|
}
|
|
1148
1199
|
if (state.startedAt === Number.MAX_SAFE_INTEGER) state.startedAt = event.time;
|
|
1200
|
+
const stepNum = event.data.step;
|
|
1201
|
+
if (typeof stepNum === "number") {
|
|
1202
|
+
const perfState = steps.get(`${turn}:${stepNum}`);
|
|
1203
|
+
if (perfState !== void 0) {
|
|
1204
|
+
const sample = perfSampleOf(perfState, modelKey, usage.outputTokens ?? 0, event.time);
|
|
1205
|
+
if (sample !== void 0) fold.perf.push(sample);
|
|
1206
|
+
steps.delete(`${turn}:${stepNum}`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1149
1209
|
}
|
|
1150
1210
|
fold.turns = [...turns.values()].filter((state) => state.input > 0 || state.output > 0).sort((a, b) => a.turn - b.turn).map((state) => ({
|
|
1151
1211
|
turn: state.turn,
|
|
@@ -1160,6 +1220,29 @@ function foldSession(events, subscriptionProviders, officialProviderIds) {
|
|
|
1160
1220
|
}));
|
|
1161
1221
|
return fold;
|
|
1162
1222
|
}
|
|
1223
|
+
/**
|
|
1224
|
+
* 生成一个 step 的性能样本;无效 / 超出 sane 上限(15 分钟)时返回 undefined,
|
|
1225
|
+
* 避免单条异常记录(时区错位 / 服务端抖动)拉偏均值。
|
|
1226
|
+
*/
|
|
1227
|
+
function perfSampleOf(state, model, outputTokens, endTime) {
|
|
1228
|
+
const start = state.requestTime ?? state.startTime;
|
|
1229
|
+
const first = state.firstContentTime;
|
|
1230
|
+
const last = state.lastContentTime;
|
|
1231
|
+
if (start === void 0 || first === void 0 || first < start) return void 0;
|
|
1232
|
+
const ttftMs = first - start;
|
|
1233
|
+
if (!Number.isFinite(ttftMs) || ttftMs < 0 || ttftMs > 9e5) return void 0;
|
|
1234
|
+
const genMs = last !== void 0 && last > first ? last - first : void 0;
|
|
1235
|
+
const latencyMs = endTime >= start ? endTime - start : void 0;
|
|
1236
|
+
const tps = genMs !== void 0 && genMs > 0 && outputTokens > 0 ? outputTokens / (genMs / 1e3) : void 0;
|
|
1237
|
+
return {
|
|
1238
|
+
model,
|
|
1239
|
+
hour: hourStamp(endTime),
|
|
1240
|
+
ttftMs,
|
|
1241
|
+
...tps === void 0 || !Number.isFinite(tps) || tps <= 0 ? {} : { tps },
|
|
1242
|
+
estimated: state.requestTime === void 0,
|
|
1243
|
+
...latencyMs === void 0 ? {} : { latencyMs }
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1163
1246
|
/** Accumulate one ModelUsage into another (merge step of the incremental aggregator). */
|
|
1164
1247
|
function mergeUsageInto(acc, cell) {
|
|
1165
1248
|
acc.calls += cell.calls;
|
|
@@ -1171,6 +1254,27 @@ function mergeUsageInto(acc, cell) {
|
|
|
1171
1254
|
acc.officialCalls += cell.officialCalls;
|
|
1172
1255
|
acc.officialCost += cell.officialCost;
|
|
1173
1256
|
}
|
|
1257
|
+
/** 均值(数组非空时调用;空数组按 0 兜底)。 */
|
|
1258
|
+
function mean(values) {
|
|
1259
|
+
if (values.length === 0) return 0;
|
|
1260
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
1261
|
+
}
|
|
1262
|
+
/** 分位数(0..1):先拷贝排序,再线性插值;空数组返回 0。 */
|
|
1263
|
+
function percentile(values, p) {
|
|
1264
|
+
if (values.length === 0) return 0;
|
|
1265
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1266
|
+
const idx = (sorted.length - 1) * p;
|
|
1267
|
+
const lo = Math.floor(idx);
|
|
1268
|
+
const hi = Math.ceil(idx);
|
|
1269
|
+
if (lo === hi) {
|
|
1270
|
+
const value = sorted[lo];
|
|
1271
|
+
return value === void 0 ? 0 : value;
|
|
1272
|
+
}
|
|
1273
|
+
const a = sorted[lo];
|
|
1274
|
+
const b = sorted[hi];
|
|
1275
|
+
if (a === void 0 || b === void 0) return 0;
|
|
1276
|
+
return a + (b - a) * (idx - lo);
|
|
1277
|
+
}
|
|
1174
1278
|
/**
|
|
1175
1279
|
* Create the incremental usage aggregator.
|
|
1176
1280
|
* @param persistence - the session persistence service.
|
|
@@ -1245,6 +1349,8 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
1245
1349
|
inputCost: 0,
|
|
1246
1350
|
outputCost: 0
|
|
1247
1351
|
};
|
|
1352
|
+
const perfModel = /* @__PURE__ */ new Map();
|
|
1353
|
+
const perfHour = /* @__PURE__ */ new Map();
|
|
1248
1354
|
for (const { meta, fold } of folds) {
|
|
1249
1355
|
const sessionId = String(meta.id);
|
|
1250
1356
|
mergeUsageInto(total, fold.total);
|
|
@@ -1256,6 +1362,32 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
1256
1362
|
for (const [day, cell] of fold.byDay) mergeUsageInto(usageCell(byDay, day), cell);
|
|
1257
1363
|
for (const [day, models] of fold.byDayModels) for (const [modelKey, cell] of models) mergeUsageInto(modelDayCell(byDayModels, day, modelKey), cell);
|
|
1258
1364
|
for (const [modelKey, count] of fold.planCalls) planCalls.set(modelKey, (planCalls.get(modelKey) ?? 0) + count);
|
|
1365
|
+
for (const sample of fold.perf) {
|
|
1366
|
+
let modelAccum = perfModel.get(sample.model);
|
|
1367
|
+
if (modelAccum === void 0) {
|
|
1368
|
+
modelAccum = {
|
|
1369
|
+
ttfts: [],
|
|
1370
|
+
tps: [],
|
|
1371
|
+
latencies: [],
|
|
1372
|
+
estimated: 0
|
|
1373
|
+
};
|
|
1374
|
+
perfModel.set(sample.model, modelAccum);
|
|
1375
|
+
}
|
|
1376
|
+
modelAccum.ttfts.push(sample.ttftMs);
|
|
1377
|
+
if (sample.tps !== void 0) modelAccum.tps.push(sample.tps);
|
|
1378
|
+
if (sample.latencyMs !== void 0) modelAccum.latencies.push(sample.latencyMs);
|
|
1379
|
+
if (sample.estimated) modelAccum.estimated += 1;
|
|
1380
|
+
let hourAccum = perfHour.get(sample.hour);
|
|
1381
|
+
if (hourAccum === void 0) {
|
|
1382
|
+
hourAccum = {
|
|
1383
|
+
ttfts: [],
|
|
1384
|
+
tps: []
|
|
1385
|
+
};
|
|
1386
|
+
perfHour.set(sample.hour, hourAccum);
|
|
1387
|
+
}
|
|
1388
|
+
hourAccum.ttfts.push(sample.ttftMs);
|
|
1389
|
+
if (sample.tps !== void 0) hourAccum.tps.push(sample.tps);
|
|
1390
|
+
}
|
|
1259
1391
|
for (const row of fold.turns) turnRows.push({
|
|
1260
1392
|
sessionId,
|
|
1261
1393
|
...row
|
|
@@ -1297,6 +1429,22 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
1297
1429
|
return record;
|
|
1298
1430
|
};
|
|
1299
1431
|
const toModelDayRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries(models)]));
|
|
1432
|
+
const perf = perfModel.size === 0 ? void 0 : {
|
|
1433
|
+
byModel: Object.fromEntries([...perfModel].map(([model, acc]) => [model, {
|
|
1434
|
+
samples: acc.ttfts.length,
|
|
1435
|
+
ttftAvg: mean(acc.ttfts),
|
|
1436
|
+
ttftP50: percentile(acc.ttfts, .5),
|
|
1437
|
+
ttftP90: percentile(acc.ttfts, .9),
|
|
1438
|
+
...acc.tps.length === 0 ? {} : { tpsAvg: mean(acc.tps) },
|
|
1439
|
+
latencyAvg: acc.latencies.length === 0 ? 0 : mean(acc.latencies),
|
|
1440
|
+
estimatedSamples: acc.estimated
|
|
1441
|
+
}])),
|
|
1442
|
+
byHour: Object.fromEntries([...perfHour].map(([hour, acc]) => [hour, {
|
|
1443
|
+
samples: acc.ttfts.length,
|
|
1444
|
+
ttftAvg: mean(acc.ttfts),
|
|
1445
|
+
...acc.tps.length === 0 ? {} : { tpsAvg: mean(acc.tps) }
|
|
1446
|
+
}]))
|
|
1447
|
+
};
|
|
1300
1448
|
lastDoc = {
|
|
1301
1449
|
version: 3,
|
|
1302
1450
|
updatedAt: now,
|
|
@@ -1308,6 +1456,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
1308
1456
|
bySession: sessionRows.slice(0, 100),
|
|
1309
1457
|
byTurn: turnRows.slice(0, 200),
|
|
1310
1458
|
byWorkspace: workspaces.slice(0, 100),
|
|
1459
|
+
...perf === void 0 ? {} : { perf },
|
|
1311
1460
|
byRole: (() => {
|
|
1312
1461
|
const chars = roles.userChars + roles.toolChars;
|
|
1313
1462
|
const userShare = chars > 0 ? roles.userChars / chars : .5;
|
|
@@ -1921,7 +2070,7 @@ const EMPTY_SUBSCRIPTION_KEYS = {
|
|
|
1921
2070
|
/** 订阅类 provider 的显示名(未命中的回退为 id 本身)。 */
|
|
1922
2071
|
const SUBSCRIPTION_DISPLAY_NAMES = {
|
|
1923
2072
|
"kimi-coding": "Kimi For Coding",
|
|
1924
|
-
"zai-coding-cn": "Z.ai Coding Plan",
|
|
2073
|
+
"zai-coding-cn": "Z.ai Coding Plan(国内)",
|
|
1925
2074
|
"zai-coding": "Z.ai Coding Plan",
|
|
1926
2075
|
"opencode": "OpenCode Plan",
|
|
1927
2076
|
"opencode-go": "OpenCode Go",
|
|
@@ -1941,7 +2090,7 @@ const SUBSCRIPTION_DISPLAY_NAMES = {
|
|
|
1941
2090
|
"openrouter": "OpenRouter"
|
|
1942
2091
|
};
|
|
1943
2092
|
/** 订阅类 provider id 判定:带 coding / agent-plan / token-plan 后缀,或已知订阅通道。 */
|
|
1944
|
-
const SUBSCRIPTION_ID_RE =
|
|
2093
|
+
const SUBSCRIPTION_ID_RE = /* @__PURE__ */ new RegExp("(?:^|-)(?:coding|agent[-_]?plan|token[-_]?plan)(?:$|-|_)|^(?:opencode|opencode-go|kimi-coding|zai-coding|minimax|minimax-token-plan|openrouter)", "i");
|
|
1945
2094
|
/** 是否是订阅类 provider id(如 kimi-coding、xiaomi-token-plan-cn)。 */
|
|
1946
2095
|
function isSubscriptionProviderId(providerId) {
|
|
1947
2096
|
if (SUBSCRIPTION_ID_RE.test(providerId)) return true;
|
|
@@ -2440,6 +2589,11 @@ const PRICING_REFRESH_INTERVAL_MS = 360 * 60 * 1e3;
|
|
|
2440
2589
|
const SUBSCRIPTION_CACHE_MS = 300 * 1e3;
|
|
2441
2590
|
/** DeepSeek 余额查询的默认凭据引用(与 llm-deepseek 的默认引用一致)。 */
|
|
2442
2591
|
const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
|
|
2592
|
+
/**
|
|
2593
|
+
* 本插件版本号:从包自身的 package.json 读取(单一来源),随 usage-stats
|
|
2594
|
+
* 下发,供「设置 → 插件信息卡」展示。发布版 lib/index.js 相对包根解析。
|
|
2595
|
+
*/
|
|
2596
|
+
const PACKAGE_VERSION = createRequire(import.meta.url)("../package.json").version ?? "0.0.0";
|
|
2443
2597
|
/** 统计快照的落盘节流(毫秒):前端 30 秒轮询,快照最多每 30 秒写一次。 */
|
|
2444
2598
|
const SNAPSHOT_INTERVAL_MS = 3e4;
|
|
2445
2599
|
/** Required services: the web server, the persisted session log store, and user settings. */
|
|
@@ -2771,8 +2925,9 @@ function apply(ctx, config = {}) {
|
|
|
2771
2925
|
...config.monthlyBudget === void 0 ? {} : { budget: config.monthlyBudget },
|
|
2772
2926
|
...config.lowBalanceThreshold === void 0 ? {} : { lowBalanceThreshold: config.lowBalanceThreshold }
|
|
2773
2927
|
};
|
|
2774
|
-
const payload =
|
|
2928
|
+
const payload = {
|
|
2775
2929
|
...stats,
|
|
2930
|
+
pluginVersion: PACKAGE_VERSION,
|
|
2776
2931
|
...injected
|
|
2777
2932
|
};
|
|
2778
2933
|
persistSnapshot(payload);
|
|
@@ -2786,6 +2941,7 @@ function apply(ctx, config = {}) {
|
|
|
2786
2941
|
const doc = JSON.parse(text);
|
|
2787
2942
|
if (config.monthlyBudget !== void 0) doc["budget"] = config.monthlyBudget;
|
|
2788
2943
|
if (config.lowBalanceThreshold !== void 0) doc["lowBalanceThreshold"] = config.lowBalanceThreshold;
|
|
2944
|
+
doc["pluginVersion"] = PACKAGE_VERSION;
|
|
2789
2945
|
res.end(JSON.stringify(doc));
|
|
2790
2946
|
return;
|
|
2791
2947
|
} catch {}
|
package/lib/types/aggregate.d.ts
CHANGED
|
@@ -65,6 +65,8 @@ export declare function emptyUsage(): ModelUsage;
|
|
|
65
65
|
export declare function foldUsage(acc: ModelUsage, usage: TokenUsage, key: string, subscription: boolean, timeMs: number, official?: boolean): void;
|
|
66
66
|
/** Local-time date stamp (the host runs in the user's timezone). */
|
|
67
67
|
export declare function dayStamp(time: number): string;
|
|
68
|
+
/** Local-time hour stamp `YYYY-MM-DDTHH` — the performance series bucket key. */
|
|
69
|
+
export declare function hourStamp(time: number): string;
|
|
68
70
|
/** cwd 未知时工作区聚合的占位名(UI 显示 em dash,保持语言无关)。 */
|
|
69
71
|
export declare const UNKNOWN_WORKSPACE_NAME = "\u2014";
|
|
70
72
|
/** 工作区名:取 cwd 的末级目录名;无 cwd 时返回 {@link UNKNOWN_WORKSPACE_NAME}。 */
|
|
@@ -98,6 +100,13 @@ export interface UsageStatsDocument {
|
|
|
98
100
|
* 属估算口径,UI 需标注)。旧快照可能缺失。
|
|
99
101
|
*/
|
|
100
102
|
byRole?: RoleCost;
|
|
103
|
+
/**
|
|
104
|
+
* 性能指标(TTFT / 生成速度 / 总延迟)按模型与按小时聚合;旧快照可能缺失。
|
|
105
|
+
* 口径:TTFT = request/header → 首个内容 chunk;生成速度 = 输出 token ÷ 生成时长;
|
|
106
|
+
* 总延迟 = request/header → assistant/message。工具续写步骤无独立请求头,
|
|
107
|
+
* 以 step/start 为起点估算并计 estimated。
|
|
108
|
+
*/
|
|
109
|
+
perf?: PerfStats;
|
|
101
110
|
}
|
|
102
111
|
/** 按角色费用归因:user / tool 为输入成本的启发式摊分,assistant 为输出成本实测。 */
|
|
103
112
|
export interface RoleCost {
|
|
@@ -105,6 +114,40 @@ export interface RoleCost {
|
|
|
105
114
|
assistant: number;
|
|
106
115
|
tool: number;
|
|
107
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* 性能指标(TTFT / 生成速度 / 总延迟):按模型与按小时聚合,供「性能」面板渲染。
|
|
119
|
+
* 旧快照(无 perf 字段)缺失时客户端按无数据兜底。
|
|
120
|
+
*/
|
|
121
|
+
export interface PerfStats {
|
|
122
|
+
/** 按模型聚合(键 = 计费目录键;未收录模型原样保留)。 */
|
|
123
|
+
byModel: Record<string, ModelPerf>;
|
|
124
|
+
/** 按小时聚合(键 = {@link hourStamp},北京时间)。 */
|
|
125
|
+
byHour: Record<string, HourPerf>;
|
|
126
|
+
}
|
|
127
|
+
/** 一个模型的性能统计:首字延时均值 / P50 / P90、生成速度均值、总延迟均值。 */
|
|
128
|
+
export interface ModelPerf {
|
|
129
|
+
/** 有效性能样本数(有可测 TTFT 的调用;不含损毁样本)。 */
|
|
130
|
+
samples: number;
|
|
131
|
+
/** 平均首字延时(毫秒)。 */
|
|
132
|
+
ttftAvg: number;
|
|
133
|
+
/** 首字延时 P50(毫秒)。 */
|
|
134
|
+
ttftP50: number;
|
|
135
|
+
/** 首字延时 P90(毫秒)。 */
|
|
136
|
+
ttftP90: number;
|
|
137
|
+
/** 平均生成速度(tokens/s);生成了有效输出且时长可测时存在。 */
|
|
138
|
+
tpsAvg?: number;
|
|
139
|
+
/** 平均总延迟(首次请求 → 响应完成,毫秒)。 */
|
|
140
|
+
latencyAvg: number;
|
|
141
|
+
/** 以 step/start 估算的样本数(工具续写步骤无独立 request/header)。 */
|
|
142
|
+
estimatedSamples: number;
|
|
143
|
+
}
|
|
144
|
+
/** 一个小时的性能统计(键 = {@link hourStamp})。 */
|
|
145
|
+
export interface HourPerf {
|
|
146
|
+
samples: number;
|
|
147
|
+
ttftAvg: number;
|
|
148
|
+
/** 平均生成速度(tokens/s);该小时无可测生成窗口时缺失。 */
|
|
149
|
+
tpsAvg?: number;
|
|
150
|
+
}
|
|
108
151
|
/** 会话明细行:仪表盘「会话明细」面板的数据源。 */
|
|
109
152
|
export interface SessionUsageRow {
|
|
110
153
|
/** 会话 id(字符串形式)。 */
|
|
@@ -156,6 +199,21 @@ export declare const SESSION_ROW_LIMIT = 100;
|
|
|
156
199
|
export declare const TURN_ROW_LIMIT = 200;
|
|
157
200
|
/** 聚合文档的短 TTL(毫秒):合并密集轮询,TTL 内直接复用上次的合并结果。 */
|
|
158
201
|
export declare const AGGREGATE_TTL_MS = 5000;
|
|
202
|
+
/** 单步性能样本(foldSession 的折叠产物;跨会话合并时按模型/小时再聚合)。 */
|
|
203
|
+
export interface PerfSample {
|
|
204
|
+
/** 计费目录键(模型;未收录模型原样保留)。 */
|
|
205
|
+
model: string;
|
|
206
|
+
/** 北京时间小时戳({@link hourStamp})——性能曲线的时间桶键。 */
|
|
207
|
+
hour: string;
|
|
208
|
+
/** 首字延时(毫秒);无效样本(超出 sane 上限)不入样本集。 */
|
|
209
|
+
ttftMs: number;
|
|
210
|
+
/** 生成速度(tokens/s);无有效生成窗口或无输出时缺失。 */
|
|
211
|
+
tps?: number;
|
|
212
|
+
/** 总延迟(首次请求 → 响应完成,毫秒);只测到内容但完成时刻优先于起点时缺失。 */
|
|
213
|
+
latencyMs?: number;
|
|
214
|
+
/** 无独立 request/header,以 step/start 起算(工具续写步骤)。 */
|
|
215
|
+
estimated: boolean;
|
|
216
|
+
}
|
|
159
217
|
/** One persisted session's folded usage plus drill-down metadata. */
|
|
160
218
|
interface SessionFold {
|
|
161
219
|
total: ModelUsage;
|
|
@@ -166,6 +224,8 @@ interface SessionFold {
|
|
|
166
224
|
planCalls: Map<string, number>;
|
|
167
225
|
/** 每轮费用明细(按轮次号升序,不含 sessionId);sessionId 在合并时补齐。 */
|
|
168
226
|
turns: SessionTurnRow[];
|
|
227
|
+
/** 性能样本(有可测 TTFT 的调用,按事件次序折叠)。 */
|
|
228
|
+
perf: PerfSample[];
|
|
169
229
|
/** 角色归因中间量:消息文本长度(user/tool)与输入/输出成本实测拆分。 */
|
|
170
230
|
roles: RoleFold;
|
|
171
231
|
/** 日志里最新的 session/title 文本(无标题事件时 undefined)。 */
|
|
@@ -15,11 +15,31 @@
|
|
|
15
15
|
*/
|
|
16
16
|
/** Plan type: code = subscription + quota, token = per-token usage. */
|
|
17
17
|
export type PlanType = 'code' | 'token';
|
|
18
|
+
/**
|
|
19
|
+
* 订阅档位知识(自动识别的「档位月费 + 周期额度口径」):供订阅卡片在
|
|
20
|
+
* 厂商官方未提供实时额度接口时展示参考口径。currency 为原生币种;费用与
|
|
21
|
+
* 额度按官方订阅周期(天/周/月)计量时,periodDays 表述该重置周期。
|
|
22
|
+
*/
|
|
23
|
+
export interface PlanTier {
|
|
24
|
+
/** 档位月费(原生币种值)。 */
|
|
25
|
+
amount: number;
|
|
26
|
+
currency: 'CNY' | 'USD';
|
|
27
|
+
/** 周期额度口径:每 periodDays 天重置的一个额度窗。 */
|
|
28
|
+
periodDays: number;
|
|
29
|
+
/** 周期请求额度(若有)。 */
|
|
30
|
+
requests?: number;
|
|
31
|
+
/** 周期 token 额度(若有)。 */
|
|
32
|
+
tokens?: number;
|
|
33
|
+
/** 额度口径的人话描述(官方未公布精确额度时)。 */
|
|
34
|
+
label?: string;
|
|
35
|
+
}
|
|
18
36
|
/** One plan row: provider id (after alias) → plan shape + optional subscription fee. */
|
|
19
37
|
export interface PlanKnowledgeEntry {
|
|
20
38
|
type: PlanType;
|
|
21
39
|
/** 订阅月费(人民币元);code 计划用,计入「本月预计」。 */
|
|
22
40
|
subscriptionCny?: number;
|
|
41
|
+
/** 自动识别的档位月费与周期额度口径(订阅卡片展示)。 */
|
|
42
|
+
tier?: PlanTier;
|
|
23
43
|
}
|
|
24
44
|
/**
|
|
25
45
|
* 订阅/计划 provider id → plan 知识(引用 dsh-spend 的 code/token 双口径)。
|
|
@@ -30,6 +50,8 @@ export declare const PLAN_KNOWLEDGE: Readonly<Record<string, PlanKnowledgeEntry>
|
|
|
30
50
|
export declare function planTypeOf(providerId: string): PlanType;
|
|
31
51
|
/** 订阅月费(人民币元);非 code 或未配置时为 0。 */
|
|
32
52
|
export declare function subscriptionCnyOf(providerId: string): number;
|
|
53
|
+
/** 自动识别的档位月费与周期额度口径(订阅卡片展示);无档位知识返回 undefined。 */
|
|
54
|
+
export declare function tierInfoOf(providerId: string): PlanTier | undefined;
|
|
33
55
|
export interface FallbackRate {
|
|
34
56
|
/** 归一化模型 id(计费键,与 catalogEntries 的 key 同口径)。 */
|
|
35
57
|
key: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kenz1117/dsh-ui-usage-billing",
|
|
3
3
|
"description": "Usage billing dashboard for DeepSeek Harness: sidebar cost metrics plus a full dashboard modal, priced from a current multi-provider catalog with real usage aggregated from session logs.",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.5",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|