@kenz1117/dsh-ui-usage-billing 1.4.18 → 1.4.20

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
@@ -2422,8 +2422,10 @@ function emptyUsage() {
2422
2422
  * @param pricedCost - 调用方预算好的本次费用(同一条消息折叠进多个桶时计价结果相同,
2423
2423
  * 逐桶重算会让最热路径付出 N 倍常数);提供时跳过内部的计价闸门与计算,
2424
2424
  * 调用方保证其已按 `!subscription && isPriced(key)` 判定。
2425
+ * @param unverifiedChannel - 本次调用是否来自未知通道(配置外 provider)。未知通道的
2426
+ * 支出主体无法核实,费用一律记 0(token 照常统计,issue #82)。
2425
2427
  */
2426
- function foldUsage(acc, usage, key, subscription, timeMs, official = false, pricedCost) {
2428
+ function foldUsage(acc, usage, key, subscription, timeMs, official = false, pricedCost, unverifiedChannel = false) {
2427
2429
  const cacheHit = usage.cacheReadTokens ?? 0;
2428
2430
  const cacheMiss = usage.inputTokens + (usage.cacheWriteTokens ?? 0);
2429
2431
  acc.calls += 1;
@@ -2439,7 +2441,7 @@ function foldUsage(acc, usage, key, subscription, timeMs, official = false, pric
2439
2441
  if (official) acc.officialCost += pricedCost;
2440
2442
  return;
2441
2443
  }
2442
- if (!subscription && isPriced(key)) {
2444
+ if (!subscription && !unverifiedChannel && isPriced(key)) {
2443
2445
  const thisCost = computeCostAt(modelOf(key), {
2444
2446
  input: cacheHit + cacheMiss,
2445
2447
  cacheHit,
@@ -2994,8 +2996,9 @@ function foldInto(fold, machine, events, subscriptionProviders, officialProvider
2994
2996
  }
2995
2997
  const modelKey = key;
2996
2998
  const day = dayStamp(event.time);
2997
- const priced = !subscription && isPriced(modelKey);
2998
- if (!subscription && !priced) fold.unpricedModels.add(modelKey);
2999
+ const unverifiedChannel = siteBucket === "unknown";
3000
+ const priced = !subscription && !unverifiedChannel && isPriced(modelKey);
3001
+ if (!subscription && !isPriced(modelKey)) fold.unpricedModels.add(modelKey);
2999
3002
  const entry = priced ? modelOf(modelKey) : void 0;
3000
3003
  const thisCost = entry === void 0 ? void 0 : computeCostAt(entry, {
3001
3004
  input: (usage.cacheReadTokens ?? 0) + usage.inputTokens + (usage.cacheWriteTokens ?? 0),
@@ -3003,13 +3006,13 @@ function foldInto(fold, machine, events, subscriptionProviders, officialProvider
3003
3006
  cacheMiss: usage.inputTokens + (usage.cacheWriteTokens ?? 0),
3004
3007
  output: usage.outputTokens
3005
3008
  }, event.time);
3006
- foldUsage(fold.total, usage, modelKey, subscription, event.time, official, thisCost);
3007
- foldUsage(usageCell(fold.byModel, modelKey), usage, modelKey, subscription, event.time, official, thisCost);
3008
- foldUsage(usageCell(fold.byDay, day), usage, modelKey, subscription, event.time, official, thisCost);
3009
- foldUsage(modelDayCell(fold.byDayModels, day, modelKey), usage, modelKey, subscription, event.time, official, thisCost);
3010
- foldUsage(modelDaySiteCell(fold.byDayModelsSite, day, modelKey, siteBucket), usage, modelKey, subscription, event.time, official, thisCost);
3011
- foldUsage(usageCell(fold.bySite, siteBucket), usage, modelKey, subscription, event.time, official, thisCost);
3012
- foldUsage(usageCell(fold.byTier, tierAt(event.time)), usage, modelKey, subscription, event.time, official, thisCost);
3009
+ foldUsage(fold.total, usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
3010
+ foldUsage(usageCell(fold.byModel, modelKey), usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
3011
+ foldUsage(usageCell(fold.byDay, day), usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
3012
+ foldUsage(modelDayCell(fold.byDayModels, day, modelKey), usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
3013
+ foldUsage(modelDaySiteCell(fold.byDayModelsSite, day, modelKey, siteBucket), usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
3014
+ foldUsage(usageCell(fold.bySite, siteBucket), usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
3015
+ foldUsage(usageCell(fold.byTier, tierAt(event.time)), usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
3013
3016
  if (subscription) fold.planCalls.set(modelKey, (fold.planCalls.get(modelKey) ?? 0) + 1);
3014
3017
  const turn = event.data.turn ?? -1;
3015
3018
  const state = turnState(turns, turn);
@@ -3207,7 +3210,7 @@ function createUsageAggregator(persistence, options = {}) {
3207
3210
  id,
3208
3211
  ...cwd === void 0 ? {} : { cwd },
3209
3212
  ...stamp === null ? {} : { stamp },
3210
- foldVersion: 16,
3213
+ foldVersion: 17,
3211
3214
  fingerprint: foldFingerprint,
3212
3215
  fold: serializeFold(fold)
3213
3216
  };
@@ -3304,7 +3307,7 @@ function createUsageAggregator(persistence, options = {}) {
3304
3307
  try {
3305
3308
  const cwd = meta.cwd;
3306
3309
  const ledgerRow = ledger.get(id);
3307
- if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) === 16 && ledgerRow.fingerprint === foldFingerprint) {
3310
+ if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) === 17 && ledgerRow.fingerprint === foldFingerprint) {
3308
3311
  const fold = deserializeFold(ledgerRow.fold);
3309
3312
  folds.push({
3310
3313
  id,
@@ -3386,7 +3389,7 @@ function createUsageAggregator(persistence, options = {}) {
3386
3389
  for (const entry of ledger.values()) {
3387
3390
  if (included.has(entry.id)) continue;
3388
3391
  try {
3389
- const stale = (entry.foldVersion ?? 1) < 16 || entry.fingerprint !== foldFingerprint;
3392
+ const stale = (entry.foldVersion ?? 1) < 17 || entry.fingerprint !== foldFingerprint;
3390
3393
  folds.push({
3391
3394
  id: entry.id,
3392
3395
  ...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
@@ -6409,10 +6412,12 @@ async function queryRelayQuotas(ctx, routes) {
6409
6412
  /**
6410
6413
  * usage-stats 工具开关的共享设置契约(node 与 client 两端共用)。
6411
6414
  *
6412
- * 宿主把已注册的设置命名空间服务给浏览器;node 半区在 apply 阶段读一次该值决定
6413
- * 是否向模型注入 `usage_stats` 工具(工具注入是启动期决策,改开关后重载应用生效),
6414
- * client 半区在「设置」Tab 渲染开关并写入同一命名空间。缺省的默认行为是关闭——
6415
- * 避免该工具默认占用模型每次请求的上下文(coding 场景通常在仪表盘看用量)。
6415
+ * 旧宿主(≤0.1.6)把该开关存设置命名空间;宿主 0.1.7+ 起注册面移除,改存插件
6416
+ * 条目 config 的 volatile 字段(Config schema 投影),写回经 settings.update,
6417
+ * 提交后引用原位更新、`usage_stats` 工具即时注册/注销(无需重载应用)。
6418
+ * client 半区在「设置」Tab 渲染开关并经 /api/billing/usage-tool 写入,node 半区
6419
+ * 按宿主世代选择通道。缺省的默认行为是关闭——避免该工具默认占用模型每次请求
6420
+ * 的上下文(coding 场景通常在仪表盘看用量)。
6416
6421
  */
6417
6422
  /** 设置命名空间 id(小写 kebab-case)。 */
6418
6423
  const BILLING_SETTINGS_NAMESPACE = "ui-usage-billing";
@@ -6523,6 +6528,13 @@ function validateSettingsNamespace(value) {
6523
6528
  const usageBillingSettingsNs = validateSettingsNamespace(BILLING_SETTINGS_NAMESPACE);
6524
6529
  /** 该命名空间的 wire schema:`enableUsageStatsTool` 布尔,默认关闭(issue 诉求)。 */
6525
6530
  const UsageBillingSettingsSchema = z.object({ [ENABLE_USAGE_STATS_TOOL_FIELD]: z.boolean().default(false) });
6531
+ /**
6532
+ * Loader 挂载时对条目 config 做校验的 Config 声明:`enableUsageStatsTool` 标记
6533
+ * volatile,宿主 0.1.7+ 设置系统据此把它投影为可实时编辑的开关(describe/update)。
6534
+ * schemastery 非严格 object 透传未声明键,其余 config 字段不受影响;未导出 Config
6535
+ * 的旧宿主直接把原始 config 交给 apply,普通布尔形态由读取端兼容。
6536
+ */
6537
+ const Config = z.object({ [ENABLE_USAGE_STATS_TOOL_FIELD]: z.boolean().default(false).volatile() });
6526
6538
  /** 实时定价的后台刷新间隔(毫秒):汇率/模型价低频变化,6 小时一次足够。 */
6527
6539
  const PRICING_REFRESH_INTERVAL_MS = 216e5;
6528
6540
  /** 历史回放预热的延迟:等宿主启动高峰(插件加载 / 路由挂载)过去再全量折叠,
@@ -6872,6 +6884,22 @@ function adaptSessionPersistence(raw) {
6872
6884
  };
6873
6885
  }
6874
6886
  /**
6887
+ * 读取 usage_stats 工具开关的当前生效值:宿主 0.1.7+ 经 Config 解析后是 volatile
6888
+ * 引用(.get() 即时反映设置写入,无需重载),直连 apply(测试)与未导出 Config 的
6889
+ * 旧宿主组合是普通布尔;缺省/非法值一律回 DEFAULT。
6890
+ * @param config - apply 收到的插件 config。
6891
+ * @returns 开关当前生效值。
6892
+ */
6893
+ function readUsageStatsToggle(config) {
6894
+ const field = config[ENABLE_USAGE_STATS_TOOL_FIELD];
6895
+ if (typeof field === "boolean") return field;
6896
+ if (typeof field === "object" && field !== null && typeof field.get === "function") {
6897
+ const current = field.get();
6898
+ return typeof current === "boolean" ? current : false;
6899
+ }
6900
+ return false;
6901
+ }
6902
+ /**
6875
6903
  * Host plugin body: serve real aggregated usage to the browser dashboard.
6876
6904
  * @param ctx - host context carrying webServer and sessionPersistence.
6877
6905
  * @param config - optional statsPath override.
@@ -6879,6 +6907,7 @@ function adaptSessionPersistence(raw) {
6879
6907
  function apply(ctx, config = {}) {
6880
6908
  const trustedHosts = normalizeTrustedHosts(config.trustedHosts);
6881
6909
  let usageSettingsScope;
6910
+ let liveSettings;
6882
6911
  const cwd = process.cwd();
6883
6912
  const dshHome = resolveDshHome();
6884
6913
  const snapshotPath = config.snapshotPath ?? join(dshHome, ".dsh-usage-stats.json");
@@ -7023,131 +7052,113 @@ function apply(ctx, config = {}) {
7023
7052
  }
7024
7053
  }
7025
7054
  }), "usage-billing: notify-claim route");
7026
- ctx.inject(["settings"], (sctx) => {
7027
- const legacy = sctx.settings;
7028
- if (typeof legacy.register !== "function") return;
7029
- const scope = legacy.register(usageBillingSettingsNs, UsageBillingSettingsSchema, { base: { enableUsageStatsTool: config.enableUsageStatsTool ?? false } });
7030
- usageSettingsScope = scope;
7031
- ctx.inject(["tools"], (toolsCtx) => {
7032
- if (scope.get().enableUsageStatsTool) toolsCtx.tools.register(defineTool({
7033
- name: "usage_stats",
7034
- description: "查询本机 DeepSeek Harness 的模型用量与估算费用(人民币,按官方目录价估算,非账单)。range 取值:today=今天,month=本月,session=当前会话,all=累计。",
7035
- parameters: { range: {
7036
- type: "string",
7037
- enum: [
7038
- "today",
7039
- "month",
7040
- "session",
7041
- "all",
7042
- "bySite",
7043
- "relay"
7044
- ],
7045
- required: true,
7046
- description: "统计范围:today / month / session / all / bySite / relay"
7047
- } },
7048
- output: {
7049
- schema: {
7050
- type: "object",
7051
- additionalProperties: false,
7052
- properties: {
7053
- range: {
7054
- type: "string",
7055
- required: true
7056
- },
7057
- cost: {
7058
- type: "number",
7059
- required: true,
7060
- description: "估算费用(人民币元)"
7061
- },
7062
- calls: {
7063
- type: "number",
7064
- required: true
7065
- },
7066
- input: {
7067
- type: "number",
7068
- required: true,
7069
- description: "输入 tokens"
7070
- },
7071
- output: {
7072
- type: "number",
7073
- required: true,
7074
- description: "输出 tokens"
7075
- }
7076
- }
7077
- },
7078
- render: (_args, value) => [{
7079
- type: "text",
7080
- text: `用量(${value.range}):估算费用 ${formatMoney(value.cost)},调用 ${value.calls} 次,输入 ${formatTokens(value.input)} tokens,输出 ${formatTokens(value.output)} tokens`
7081
- }]
7082
- },
7083
- async execute(args, exec) {
7084
- const stats = await aggregator.aggregate();
7085
- const zero = {
7086
- range: args.range,
7087
- cost: 0,
7088
- calls: 0,
7089
- input: 0,
7090
- output: 0
7091
- };
7092
- if (args.range === "all") return {
7093
- range: args.range,
7094
- cost: stats.total.cost,
7095
- calls: stats.total.calls,
7096
- input: stats.total.input,
7097
- output: stats.total.output
7098
- };
7099
- if (args.range === "bySite" || args.range === "relay") return {
7100
- range: args.range,
7101
- ...sumSiteBuckets(stats.bySite ?? {}, args.range)
7102
- };
7103
- if (args.range === "today") {
7104
- const day = stats.byDay[dayStamp(Date.now())];
7105
- return day === void 0 ? zero : {
7106
- range: args.range,
7107
- cost: day.cost,
7108
- calls: day.calls,
7109
- input: day.input,
7110
- output: day.output
7111
- };
7112
- }
7113
- if (args.range === "month") {
7114
- const prefix = dayStamp(Date.now()).slice(0, 7);
7115
- let cost = 0;
7116
- let calls = 0;
7117
- let input = 0;
7118
- let output = 0;
7119
- for (const [date, day] of Object.entries(stats.byDay)) {
7120
- if (!date.startsWith(prefix)) continue;
7121
- cost += day.cost;
7122
- calls += day.calls;
7123
- input += day.input;
7124
- output += day.output;
7055
+ let usageToolDisposer;
7056
+ let usageToolRegister;
7057
+ const syncUsageTool = () => {
7058
+ if (usageToolRegister === void 0) return;
7059
+ if (readUsageStatsToggle(config)) {
7060
+ if (usageToolDisposer === void 0) usageToolDisposer = usageToolRegister();
7061
+ return;
7062
+ }
7063
+ if (usageToolDisposer !== void 0) {
7064
+ usageToolDisposer();
7065
+ usageToolDisposer = void 0;
7066
+ }
7067
+ };
7068
+ ctx.inject(["tools"], (toolsCtx) => {
7069
+ usageToolRegister = () => toolsCtx.tools.register(defineTool({
7070
+ name: "usage_stats",
7071
+ description: "查询本机 DeepSeek Harness 的模型用量与估算费用(人民币,按官方目录价估算,非账单)。range 取值:today=今天,month=本月,session=当前会话,all=累计。",
7072
+ parameters: { range: {
7073
+ type: "string",
7074
+ enum: [
7075
+ "today",
7076
+ "month",
7077
+ "session",
7078
+ "all",
7079
+ "bySite",
7080
+ "relay"
7081
+ ],
7082
+ required: true,
7083
+ description: "统计范围:today / month / session / all / bySite / relay"
7084
+ } },
7085
+ output: {
7086
+ schema: {
7087
+ type: "object",
7088
+ additionalProperties: false,
7089
+ properties: {
7090
+ range: {
7091
+ type: "string",
7092
+ required: true
7093
+ },
7094
+ cost: {
7095
+ type: "number",
7096
+ required: true,
7097
+ description: "估算费用(人民币元)"
7098
+ },
7099
+ calls: {
7100
+ type: "number",
7101
+ required: true
7102
+ },
7103
+ input: {
7104
+ type: "number",
7105
+ required: true,
7106
+ description: "输入 tokens"
7107
+ },
7108
+ output: {
7109
+ type: "number",
7110
+ required: true,
7111
+ description: "输出 tokens"
7125
7112
  }
7126
- return {
7127
- range: args.range,
7128
- cost,
7129
- calls,
7130
- input,
7131
- output
7132
- };
7133
7113
  }
7134
- const sessionId = exec.agent?.id;
7135
- if (sessionId === void 0) throw new Error("usage_stats 的 session 范围需要 agent 会话上下文");
7136
- const summary = await aggregator.sessionUsageOf(String(sessionId));
7137
- if (summary !== void 0) return {
7114
+ },
7115
+ render: (_args, value) => [{
7116
+ type: "text",
7117
+ text: `用量(${value.range}):估算费用 ${formatMoney(value.cost)},调用 ${value.calls} 次,输入 ${formatTokens(value.input)} tokens,输出 ${formatTokens(value.output)} tokens`
7118
+ }]
7119
+ },
7120
+ async execute(args, exec) {
7121
+ const stats = await aggregator.aggregate();
7122
+ const zero = {
7123
+ range: args.range,
7124
+ cost: 0,
7125
+ calls: 0,
7126
+ input: 0,
7127
+ output: 0
7128
+ };
7129
+ if (args.range === "all") return {
7130
+ range: args.range,
7131
+ cost: stats.total.cost,
7132
+ calls: stats.total.calls,
7133
+ input: stats.total.input,
7134
+ output: stats.total.output
7135
+ };
7136
+ if (args.range === "bySite" || args.range === "relay") return {
7137
+ range: args.range,
7138
+ ...sumSiteBuckets(stats.bySite ?? {}, args.range)
7139
+ };
7140
+ if (args.range === "today") {
7141
+ const day = stats.byDay[dayStamp(Date.now())];
7142
+ return day === void 0 ? zero : {
7138
7143
  range: args.range,
7139
- ...summary
7144
+ cost: day.cost,
7145
+ calls: day.calls,
7146
+ input: day.input,
7147
+ output: day.output
7140
7148
  };
7149
+ }
7150
+ if (args.range === "month") {
7151
+ const prefix = dayStamp(Date.now()).slice(0, 7);
7141
7152
  let cost = 0;
7142
7153
  let calls = 0;
7143
7154
  let input = 0;
7144
7155
  let output = 0;
7145
- for (const turn of stats.byTurn ?? []) {
7146
- if (turn.sessionId !== String(sessionId)) continue;
7147
- cost += turn.cost;
7148
- calls += 1;
7149
- input += turn.input;
7150
- output += turn.output;
7156
+ for (const [date, day] of Object.entries(stats.byDay)) {
7157
+ if (!date.startsWith(prefix)) continue;
7158
+ cost += day.cost;
7159
+ calls += day.calls;
7160
+ input += day.input;
7161
+ output += day.output;
7151
7162
  }
7152
7163
  return {
7153
7164
  range: args.range,
@@ -7157,8 +7168,48 @@ function apply(ctx, config = {}) {
7157
7168
  output
7158
7169
  };
7159
7170
  }
7160
- }));
7161
- });
7171
+ const sessionId = exec.agent?.id;
7172
+ if (sessionId === void 0) throw new Error("usage_stats 的 session 范围需要 agent 会话上下文");
7173
+ const summary = await aggregator.sessionUsageOf(String(sessionId));
7174
+ if (summary !== void 0) return {
7175
+ range: args.range,
7176
+ ...summary
7177
+ };
7178
+ let cost = 0;
7179
+ let calls = 0;
7180
+ let input = 0;
7181
+ let output = 0;
7182
+ for (const turn of stats.byTurn ?? []) {
7183
+ if (turn.sessionId !== String(sessionId)) continue;
7184
+ cost += turn.cost;
7185
+ calls += 1;
7186
+ input += turn.input;
7187
+ output += turn.output;
7188
+ }
7189
+ return {
7190
+ range: args.range,
7191
+ cost,
7192
+ calls,
7193
+ input,
7194
+ output
7195
+ };
7196
+ }
7197
+ }));
7198
+ syncUsageTool();
7199
+ ctx.on("loader/volatile-update", syncUsageTool);
7200
+ });
7201
+ ctx.inject(["settings"], (sctx) => {
7202
+ const legacy = sctx.settings;
7203
+ if (typeof legacy.register === "function") {
7204
+ usageSettingsScope = legacy.register(usageBillingSettingsNs, UsageBillingSettingsSchema, { base: { enableUsageStatsTool: readUsageStatsToggle(config) } });
7205
+ return;
7206
+ }
7207
+ const forms = legacy;
7208
+ if (typeof forms.update !== "function") return;
7209
+ liveSettings = {
7210
+ update: (ns, patch) => forms.update(ns, patch),
7211
+ ns: ctx.fiber.entry?.options.id ?? usageBillingSettingsNs
7212
+ };
7162
7213
  });
7163
7214
  let live = { source: "builtin" };
7164
7215
  let pricingSyncedAt = 0;
@@ -7301,7 +7352,7 @@ function apply(ctx, config = {}) {
7301
7352
  return;
7302
7353
  }
7303
7354
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
7304
- const enabled = usageSettingsScope?.get().enableUsageStatsTool ?? false;
7355
+ const enabled = usageSettingsScope !== void 0 ? usageSettingsScope.get().enableUsageStatsTool : readUsageStatsToggle(config);
7305
7356
  if (req.method === "GET") {
7306
7357
  res.end(JSON.stringify({ enabled }));
7307
7358
  return;
@@ -7325,18 +7376,23 @@ function apply(ctx, config = {}) {
7325
7376
  return;
7326
7377
  }
7327
7378
  }
7328
- const parsed = JSON.parse(body === "" ? "{}" : body);
7329
- if (usageSettingsScope === void 0) {
7379
+ const next = JSON.parse(body === "" ? "{}" : body).enabled === true;
7380
+ if (usageSettingsScope !== void 0) {
7381
+ await usageSettingsScope.update({ enableUsageStatsTool: next });
7382
+ syncUsageTool();
7383
+ } else if (liveSettings !== void 0) {
7384
+ await liveSettings.update(liveSettings.ns, { [ENABLE_USAGE_STATS_TOOL_FIELD]: next });
7385
+ syncUsageTool();
7386
+ } else {
7330
7387
  res.end(JSON.stringify({ error: "settings unavailable" }));
7331
7388
  return;
7332
7389
  }
7333
- const next = parsed.enabled === true;
7334
- await usageSettingsScope.update({ enableUsageStatsTool: next });
7335
7390
  res.end(JSON.stringify({
7336
7391
  ok: true,
7337
7392
  enabled: next
7338
7393
  }));
7339
- } catch {
7394
+ } catch (error) {
7395
+ console.warn("[usage-billing] usage-tool toggle write-back failed:", error);
7340
7396
  res.end(JSON.stringify({ error: "invalid" }));
7341
7397
  }
7342
7398
  }
@@ -7476,4 +7532,4 @@ function apply(ctx, config = {}) {
7476
7532
  }), "usage-billing: usage-stats route");
7477
7533
  }
7478
7534
  //#endregion
7479
- export { adaptSessionPersistence, apply, createFileUsageLedgerStore, guardLoopback, inject, normalizeTrustedHosts, readPiAiProviderRoutes, resolveSubscriptionKeys };
7535
+ export { Config, adaptSessionPersistence, apply, createFileUsageLedgerStore, guardLoopback, inject, normalizeTrustedHosts, readPiAiProviderRoutes, resolveSubscriptionKeys };
@@ -174,8 +174,10 @@ export declare function emptyUsage(): ModelUsage;
174
174
  * @param pricedCost - 调用方预算好的本次费用(同一条消息折叠进多个桶时计价结果相同,
175
175
  * 逐桶重算会让最热路径付出 N 倍常数);提供时跳过内部的计价闸门与计算,
176
176
  * 调用方保证其已按 `!subscription && isPriced(key)` 判定。
177
+ * @param unverifiedChannel - 本次调用是否来自未知通道(配置外 provider)。未知通道的
178
+ * 支出主体无法核实,费用一律记 0(token 照常统计,issue #82)。
177
179
  */
178
- export declare function foldUsage(acc: ModelUsage, usage: TokenUsage, key: string, subscription: boolean, timeMs: number, official?: boolean, pricedCost?: number): void;
180
+ export declare function foldUsage(acc: ModelUsage, usage: TokenUsage, key: string, subscription: boolean, timeMs: number, official?: boolean, pricedCost?: number, unverifiedChannel?: boolean): void;
179
181
  /**
180
182
  * 联网搜索辅助请求的单次费用估算默认值(人民币元)。DeepSeek 官方对搜索请求
181
183
  * (web_search 服务端工具注入上下文)照常计费,实测每次约 0.01~0.03 元,取中值;
@@ -552,7 +554,7 @@ export interface UsageLedgerDocument {
552
554
  * 会话费用只剩最近一段,issue #29)。
553
555
  * 持久账本行据此区分新旧算法:日志已删/不可读而只能沿用旧行时,UI 标注置信度提示。
554
556
  */
555
- export declare const FOLD_VERSION = 16;
557
+ export declare const FOLD_VERSION = 17;
556
558
  /**
557
559
  * 一次性账本迁移:id 唯一,apply 在加载边界对原始文档执行,已应用过的跳过。
558
560
  * 未来账本/schema 字段变更(重命名、拆桶、语义调整)时,在此追加一条迁移并
@@ -1,5 +1,5 @@
1
1
  /** Locale dictionaries for the usage billing surface. */
2
- export type UsageBillingKey = 'title' | 'cost' | 'todayCost' | 'monthCost' | 'yearCost' | 'monthProjected' | 'liveTurn' | 'liveSession' | 'calls' | 'cacheHitRate' | 'tokens' | 'inputTokens' | 'outputTokens' | 'avgCost' | 'kpiRange' | 'avgRangeToday' | 'avgRange7d' | 'avgRangeWeek' | 'avgRangeMonth' | 'avgRangeAll' | 'trend' | 'trend7d' | 'trend30d' | 'trendMetric' | 'trendMetricCost' | 'trendMetricTokens' | 'pricingSearch' | 'bandPinHint' | 'trendEmpty' | 'trendTotal' | 'roundsUnit' | 'healthModelsLive' | 'healthVendorsDown' | 'healthVendorsUnavailable' | 'budget' | 'budgetRemain' | 'budgetAmount' | 'budgetSummary' | 'sessions' | 'sessionTitle' | 'lastActive' | 'sessionOverflow' | 'budgetTierBody' | 'models' | 'providerBilling' | 'channelUnknown' | 'directTag' | 'relayTag' | 'unknownTag' | 'planCountUnit' | 'actual' | 'pricing' | 'input' | 'output' | 'cacheHit' | 'peak' | 'offPeak' | 'flat' | 'band' | 'close' | 'footer' | 'footerCredit' | 'lastUpdated' | 'noData' | 'todayRate' | 'rateLive' | 'rateBuiltin' | 'pricingSyncedAt' | 'pricingSyncNow' | 'pricingSyncing' | 'promoBadge' | 'promoUntil' | 'promoOpenEnded' | 'pricingTip' | 'pricingUnit' | 'pricingNotes' | 'ubPeak' | 'ubOff' | 'ubStd' | 'peakBand' | 'pricingSource' | 'noteCache' | 'noteBand' | 'noteSource' | 'balance' | 'recharge' | 'rechargeHint' | 'balanceUnconfigured' | 'balanceUnauthorized' | 'balanceUnreachable' | 'uncatalogued' | 'estimatedPricing' | 'balanceDays' | 'balanceLowBody' | 'reconcileDrift' | 'reconcileDismiss' | 'subscriptionNotConfigured' | 'subscriptionUnauthorized' | 'subscriptionUnavailable' | 'subscriptionInvalid' | 'subscriptionRateLimited' | 'subscriptionSession' | 'subscriptionWeekly' | 'subscriptionMonthly' | 'subscriptionBilling' | 'subscriptionRemaining' | 'subscriptionExhausted' | 'subscriptionReset' | 'subscriptionNoApi' | 'floatWindow' | 'floatModeCombined' | 'floatModeSubscription' | 'floatMode' | 'floatTargets' | 'floatWindowHint' | 'floatPrimaryToday' | 'floatPrimaryWeek' | 'floatPrimaryMonth' | 'floatNoTargets' | 'floatNoTargetsHint' | 'cardDisplay' | 'cardDisplayHint' | 'cardMetric' | 'cardMetricMoney' | 'cardMetricTokens' | 'cardSpan' | 'cardSpanDay' | 'cardSpanWeek' | 'cardSpanMonth' | 'triggerMonthTokens' | 'triggerTodayTokens' | 'triggerWeekTokens' | 'subscriptionsStale' | 'staleLedgerNotice' | 'unreadableNotice' | 'tokenCacheWrite' | 'toolRank' | 'toolName' | 'userPrices' | 'userPricesHint' | 'userPriceSave' | 'userPriceModel' | 'userPriceSource' | 'userPriceSourceHint' | 'userPriceOffPeak' | 'userPriceNormal' | 'userPriceCurrency' | 'userPriceAdd' | 'userPriceRemoveSelected' | 'userPriceSelect' | 'sessionStaleBadge' | 'sessionUntitled' | 'heatmapLess' | 'heatmapMore' | 'currency' | 'currencyCny' | 'currencyUsd' | 'currencyEur' | 'language' | 'heatmap' | 'rounds' | 'roundsHint' | 'anomaly' | 'model' | 'thModel' | 'thInputMiss' | 'thInputHit' | 'costAbbr' | 'tabOverview' | 'tabTrends' | 'tabProviders' | 'tabPricing' | 'tabSettings' | 'budgetHint' | 'peakAlertHint' | 'peakAlertDescPeak' | 'peakAlertDescOff' | 'peakAlertDescPeakZhipu' | 'peakAlertDescOffZhipu' | 'export' | 'exportCsvDay' | 'exportCsvSession' | 'exportJson' | 'peakShare' | 'peakSharePerCall' | 'offPeakSavings' | 'perfMax' | 'weekCost' | 'roleCost' | 'roleUser' | 'roleAssistant' | 'roleTool' | 'roleHint' | 'tierPeak' | 'tierOff' | 'tierToPeak' | 'tierToOff' | 'tierAlertEnterPeak' | 'tierAlertEnterOff' | 'tierAlertEnterPeakZhipu' | 'tierAlertEnterOffZhipu' | 'peakAlertTitlePeak' | 'peakAlertTitleOff' | 'peakAlert' | 'peakAlertLeadMin' | 'peakAlertPos' | 'peakAlertMode' | 'peakAlertPosCorner' | 'peakAlertPosCenter' | 'peakAlertModePeak' | 'peakAlertModeOff' | 'peakAlertModeBoth' | 'peakAlertWebNotify' | 'peakAlertPreview' | 'planTypeCode' | 'planTypeToken' | 'subscriptionFeePerMonth' | 'triggerToday' | 'triggerMonth' | 'subscriptionTag' | 'free' | 'perfSamples' | 'perfTtft' | 'perfP50' | 'perfP90' | 'perfTps' | 'perfLatency' | 'perfEstimated' | 'perfEmpty' | 'perfTpsUnit' | 'perfTitle' | 'perfHint' | 'perfAll' | 'perfChartEmpty' | 'heatmapYear' | 'heatmapHalf' | 'heatmapMonth' | 'metricCost' | 'metricTokens' | 'heatmapTotal' | 'activeDays' | 'streakDays' | 'subscriptionAutoDetect' | 'pluginAuthor' | 'pluginRepository' | 'pluginNpm' | 'pluginLicense' | 'tabToken' | 'tokenExport' | 'tokenExportCsv' | 'tokenReasoningShare' | 'tokenReasoningShort' | 'tokenIo' | 'tokenPeak' | 'tokenDaily' | 'tokenByModel' | 'tokenMiss' | 'tokenHit' | 'tokenOutput' | 'tokenViewStructure' | 'tokenViewModel' | 'tokenHitShort' | 'tokenMissShort' | 'tokenTotal' | 'tokenShare' | 'usageStatsTool' | 'usageStatsToolHint' | 'balanceGranted' | 'balanceTopped' | 'balanceDaily' | 'balanceDaysLong' | 'balanceDaysUnit' | 'popTodayModel' | 'popNoConsumption' | 'popTitle' | 'popDirectLead' | 'popSubLead' | 'unpricedHint' | 'searchEstimateHint' | 'siteListDisplay' | 'siteListDisplayHint' | 'liveCostBar' | 'liveCostBarHint' | 'liveCostBarPosition' | 'liveCostBarPosBelow' | 'liveCostBarPosAbove' | 'liveCostBarPosToolbar' | 'exportCsvSite' | 'panelRelayQuota' | 'relayBalance' | 'relayNoQuota' | 'relayWindowUsed' | 'relayKindNewApi' | 'relayKindSub2Api' | 'relayKindUnknown' | 'expandAll' | 'collapseAll' | 'todayOnly' | 'todayNoConsumption';
2
+ export type UsageBillingKey = 'title' | 'cost' | 'todayCost' | 'monthCost' | 'yearCost' | 'monthProjected' | 'liveTurn' | 'liveSession' | 'calls' | 'cacheHitRate' | 'tokens' | 'inputTokens' | 'outputTokens' | 'avgCost' | 'kpiRange' | 'avgRangeToday' | 'avgRange7d' | 'avgRangeWeek' | 'avgRangeMonth' | 'avgRangeAll' | 'trend' | 'trend7d' | 'trend30d' | 'trendMetric' | 'trendMetricCost' | 'trendMetricTokens' | 'pricingSearch' | 'bandPinHint' | 'trendEmpty' | 'trendTotal' | 'roundsUnit' | 'healthModelsLive' | 'healthVendorsDown' | 'healthVendorsUnavailable' | 'budget' | 'budgetRemain' | 'budgetAmount' | 'budgetSummary' | 'sessions' | 'sessionTitle' | 'lastActive' | 'sessionOverflow' | 'budgetTierBody' | 'models' | 'providerBilling' | 'channelUnknown' | 'directTag' | 'relayTag' | 'unknownTag' | 'unknownChannelHint' | 'planCountUnit' | 'actual' | 'pricing' | 'input' | 'output' | 'cacheHit' | 'peak' | 'offPeak' | 'flat' | 'band' | 'close' | 'footer' | 'footerCredit' | 'lastUpdated' | 'noData' | 'todayRate' | 'rateLive' | 'rateBuiltin' | 'pricingSyncedAt' | 'pricingSyncNow' | 'pricingSyncing' | 'promoBadge' | 'promoUntil' | 'promoOpenEnded' | 'pricingTip' | 'pricingUnit' | 'pricingNotes' | 'ubPeak' | 'ubOff' | 'ubStd' | 'peakBand' | 'pricingSource' | 'noteCache' | 'noteBand' | 'noteSource' | 'balance' | 'recharge' | 'rechargeHint' | 'balanceUnconfigured' | 'balanceUnauthorized' | 'balanceUnreachable' | 'uncatalogued' | 'estimatedPricing' | 'balanceDays' | 'balanceLowBody' | 'reconcileDrift' | 'reconcileDismiss' | 'subscriptionNotConfigured' | 'subscriptionUnauthorized' | 'subscriptionUnavailable' | 'subscriptionInvalid' | 'subscriptionRateLimited' | 'subscriptionSession' | 'subscriptionWeekly' | 'subscriptionMonthly' | 'subscriptionBilling' | 'subscriptionRemaining' | 'subscriptionExhausted' | 'subscriptionReset' | 'subscriptionNoApi' | 'floatWindow' | 'floatModeCombined' | 'floatModeSubscription' | 'floatMode' | 'floatTargets' | 'floatWindowHint' | 'floatPrimaryToday' | 'floatPrimaryWeek' | 'floatPrimaryMonth' | 'floatNoTargets' | 'floatNoTargetsHint' | 'cardDisplay' | 'cardDisplayHint' | 'cardMetric' | 'cardMetricMoney' | 'cardMetricTokens' | 'cardSpan' | 'cardSpanDay' | 'cardSpanWeek' | 'cardSpanMonth' | 'triggerMonthTokens' | 'triggerTodayTokens' | 'triggerWeekTokens' | 'subscriptionsStale' | 'staleLedgerNotice' | 'unreadableNotice' | 'tokenCacheWrite' | 'toolRank' | 'toolName' | 'userPrices' | 'userPricesHint' | 'userPriceSave' | 'userPriceModel' | 'userPriceSource' | 'userPriceSourceHint' | 'userPriceOffPeak' | 'userPriceNormal' | 'userPriceCurrency' | 'userPriceAdd' | 'userPriceRemoveSelected' | 'userPriceSelect' | 'sessionStaleBadge' | 'sessionUntitled' | 'heatmapLess' | 'heatmapMore' | 'currency' | 'currencyCny' | 'currencyUsd' | 'currencyEur' | 'language' | 'heatmap' | 'rounds' | 'roundsHint' | 'anomaly' | 'model' | 'thModel' | 'thInputMiss' | 'thInputHit' | 'costAbbr' | 'tabOverview' | 'tabTrends' | 'tabProviders' | 'tabPricing' | 'tabSettings' | 'budgetHint' | 'peakAlertHint' | 'peakAlertDescPeak' | 'peakAlertDescOff' | 'peakAlertDescPeakZhipu' | 'peakAlertDescOffZhipu' | 'export' | 'exportCsvDay' | 'exportCsvSession' | 'exportJson' | 'peakShare' | 'peakSharePerCall' | 'offPeakSavings' | 'perfMax' | 'weekCost' | 'roleCost' | 'roleUser' | 'roleAssistant' | 'roleTool' | 'roleHint' | 'tierPeak' | 'tierOff' | 'tierToPeak' | 'tierToOff' | 'tierAlertEnterPeak' | 'tierAlertEnterOff' | 'tierAlertEnterPeakZhipu' | 'tierAlertEnterOffZhipu' | 'peakAlertTitlePeak' | 'peakAlertTitleOff' | 'peakAlert' | 'peakAlertLeadMin' | 'peakAlertPos' | 'peakAlertMode' | 'peakAlertPosCorner' | 'peakAlertPosCenter' | 'peakAlertModePeak' | 'peakAlertModeOff' | 'peakAlertModeBoth' | 'peakAlertWebNotify' | 'peakAlertPreview' | 'planTypeCode' | 'planTypeToken' | 'subscriptionFeePerMonth' | 'triggerToday' | 'triggerMonth' | 'subscriptionTag' | 'free' | 'perfSamples' | 'perfTtft' | 'perfP50' | 'perfP90' | 'perfTps' | 'perfLatency' | 'perfEstimated' | 'perfEmpty' | 'perfTpsUnit' | 'perfTitle' | 'perfHint' | 'perfAll' | 'perfChartEmpty' | 'heatmapYear' | 'heatmapHalf' | 'heatmapMonth' | 'metricCost' | 'metricTokens' | 'heatmapTotal' | 'activeDays' | 'streakDays' | 'subscriptionAutoDetect' | 'pluginAuthor' | 'pluginRepository' | 'pluginNpm' | 'pluginLicense' | 'tabToken' | 'tokenExport' | 'tokenExportCsv' | 'tokenReasoningShare' | 'tokenReasoningShort' | 'tokenIo' | 'tokenPeak' | 'tokenDaily' | 'tokenByModel' | 'tokenMiss' | 'tokenHit' | 'tokenOutput' | 'tokenViewStructure' | 'tokenViewModel' | 'tokenHitShort' | 'tokenMissShort' | 'tokenTotal' | 'tokenShare' | 'usageStatsTool' | 'usageStatsToolHint' | 'balanceGranted' | 'balanceTopped' | 'balanceDaily' | 'balanceDaysLong' | 'balanceDaysUnit' | 'popTodayModel' | 'popNoConsumption' | 'popTitle' | 'popDirectLead' | 'popSubLead' | 'unpricedHint' | 'searchEstimateHint' | 'siteListDisplay' | 'siteListDisplayHint' | 'liveCostBar' | 'liveCostBarHint' | 'liveCostBarPosition' | 'liveCostBarPosBelow' | 'liveCostBarPosAbove' | 'liveCostBarPosToolbar' | 'exportCsvSite' | 'panelRelayQuota' | 'relayBalance' | 'relayNoQuota' | 'relayWindowUsed' | 'relayKindNewApi' | 'relayKindSub2Api' | 'relayKindUnknown' | 'expandAll' | 'collapseAll' | 'todayOnly' | 'todayNoConsumption';
3
3
  export declare const NS = "usageBilling";
4
4
  export declare const zh: Record<UsageBillingKey, string>;
5
5
  export declare const en: Record<UsageBillingKey, string>;
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * usage-stats 工具开关的共享设置契约(node 与 client 两端共用)。
3
3
  *
4
- * 宿主把已注册的设置命名空间服务给浏览器;node 半区在 apply 阶段读一次该值决定
5
- * 是否向模型注入 `usage_stats` 工具(工具注入是启动期决策,改开关后重载应用生效),
6
- * client 半区在「设置」Tab 渲染开关并写入同一命名空间。缺省的默认行为是关闭——
7
- * 避免该工具默认占用模型每次请求的上下文(coding 场景通常在仪表盘看用量)。
4
+ * 旧宿主(≤0.1.6)把该开关存设置命名空间;宿主 0.1.7+ 起注册面移除,改存插件
5
+ * 条目 config 的 volatile 字段(Config schema 投影),写回经 settings.update,
6
+ * 提交后引用原位更新、`usage_stats` 工具即时注册/注销(无需重载应用)。
7
+ * client 半区在「设置」Tab 渲染开关并经 /api/billing/usage-tool 写入,node 半区
8
+ * 按宿主世代选择通道。缺省的默认行为是关闭——避免该工具默认占用模型每次请求
9
+ * 的上下文(coding 场景通常在仪表盘看用量)。
8
10
  */
9
11
  import type { CostCurrency, UserPriceEntry } from './pricing.ts';
10
12
  /** 设置命名空间 id(小写 kebab-case)。 */
@@ -15,6 +15,7 @@
15
15
  import type { IncomingMessage, ServerResponse } from 'node:http';
16
16
  import type { Context } from '@deepseek-ai/cordis';
17
17
  import type { CredentialProvider } from '@deepseek-ai/dsh-credentials';
18
+ import z from '@deepseek-ai/schemastery';
18
19
  import { type UsageLedgerStore, type UsagePersistence } from './aggregate.ts';
19
20
  import type { CustomBalanceConfig, DeclaredEndpointConfig, SubscriptionPlanConfig } from './pricing-shared.ts';
20
21
  import { type IdentifiedSubscriptionPlan, type SubscriptionKeys } from './subscriptions.ts';
@@ -42,6 +43,23 @@ export declare function normalizeTrustedHosts(hosts: readonly string[] | undefin
42
43
  * @returns 是否放行;false = 已拒绝并结束响应。
43
44
  */
44
45
  export declare function guardLoopback(req: IncomingMessage, res: ServerResponse, trustedHosts?: ReadonlySet<string>): boolean;
46
+ /** 宿主 0.1.7+ 经 Config schema 解析后,volatile 字段在 config 中的稳定引用形态
47
+ * (cosmokit `Volatile<T>` 的最小结构镜像,避免为类型引入直接依赖)。 */
48
+ export interface VolatileToggle {
49
+ /** @returns 当前生效的布尔快照(设置写入后原位更新,无需重载插件)。 */
50
+ get(): boolean;
51
+ }
52
+ /**
53
+ * Loader 挂载时对条目 config 做校验的 Config 声明:`enableUsageStatsTool` 标记
54
+ * volatile,宿主 0.1.7+ 设置系统据此把它投影为可实时编辑的开关(describe/update)。
55
+ * schemastery 非严格 object 透传未声明键,其余 config 字段不受影响;未导出 Config
56
+ * 的旧宿主直接把原始 config 交给 apply,普通布尔形态由读取端兼容。
57
+ */
58
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
59
+ enableUsageStatsTool: z<boolean, boolean, "volatile-defined">;
60
+ }>>, Schemastery.ObjectT<NoInfer<{
61
+ enableUsageStatsTool: z<boolean, boolean, "volatile-defined">;
62
+ }>>, "plain">;
45
63
  /** Plugin configuration. */
46
64
  export interface UsageBillingConfig {
47
65
  /** Absolute path to a `.dsh-usage-stats.json` fallback file. */
@@ -83,9 +101,10 @@ export interface UsageBillingConfig {
83
101
  * 例:`['llm.example.com']`。
84
102
  */
85
103
  trustedHosts?: string[];
86
- /** `usage_stats` 工具注入的组合 base(默认 false:不注入);与设置命名空间同字段,
87
- * 作为用户设置(设置 Tab 开关)的组合兜底。该工具占用每次请求的上下文,coding 场景多在仪表盘查看。 */
88
- enableUsageStatsTool?: boolean;
104
+ /** `usage_stats` 工具注入开关(默认 false:不注入)。宿主 0.1.7+ 经 Config
105
+ * 解析后是 volatile 引用(设置写入即时反映),直连 apply(测试)是普通布尔;
106
+ * 读取统一走 readUsageStatsToggle。该工具占用每次请求的上下文,coding 场景多在仪表盘查看。 */
107
+ enableUsageStatsTool?: boolean | VolatileToggle;
89
108
  /**
90
109
  * 联网搜索请求(`web/deepseek-search-llm-request`,日志只有请求、无用量事件)
91
110
  * 的单次费用估算(人民币元)。这类调用直连官方 api.deepseek.com,开放平台照常
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": "1.4.18",
4
+ "version": "1.4.20",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "deepseek",
@@ -119,7 +119,7 @@
119
119
  "devDependencies": {
120
120
  "@deepseek-ai/cordis": "*",
121
121
  "@deepseek-ai/cordis-plugin-include": "^1.0.7",
122
- "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
122
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.5",
123
123
  "@deepseek-ai/dsh-api-gateway": "0.1.7-rc.1",
124
124
  "@deepseek-ai/dsh-api-remotes": "0.1.7-rc.1",
125
125
  "@deepseek-ai/dsh-api-session-controller": "0.1.7-rc.1",
@@ -149,7 +149,7 @@
149
149
  "@deepseek-ai/dsh-typert-protocol": "0.1.7-rc.1",
150
150
  "@deepseek-ai/dsh-user-approval": "0.1.7-rc.1",
151
151
  "@deepseek-ai/dsh-util-workspace-path": "0.1.7-rc.1",
152
- "@deepseek-ai/schemastery": "^3.18.2",
152
+ "@deepseek-ai/schemastery": "^3.18.4",
153
153
  "@shikijs/langs": "^4.3.1",
154
154
  "@testing-library/dom": "^10.4.1",
155
155
  "@testing-library/react": "^16.1.0",