@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/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 state = turnState(turns, event.data.turn ?? -1);
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 = /(?:^|-)(?:coding|agent[-_]?plan|token[-_]?plan)(?:$|-|_)|^(?:opencode|opencode-go|kimi-coding|zai-coding|minimax|minimax-token-plan|openrouter)/i;
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 = Object.keys(injected).length === 0 ? stats : {
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 {}
@@ -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.3",
4
+ "version": "0.9.5",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },