@kenz1117/dsh-ui-usage-billing 1.4.33 → 1.4.34

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.en.md CHANGED
@@ -251,7 +251,7 @@ None. This plugin is a pure UI surface: it registers no tools, injects no system
251
251
  ## ⚠️ Known Limitations and Deferred Work
252
252
 
253
253
  - **Balance queries cover DeepSeek / Moonshot (Kimi) / StepFun / SiliconFlow / xAI / Zhipu GLM (Z.ai CN region) / Tencent Cloud TokenHub (token-plan quota)**: the first six use a standard Bearer API key; Tencent Cloud uses the cloud-API key pair (`<SecretId>:<SecretKey>`, TC3-signed management API). Other providers expose no public balance API or need non-Bearer auth (Xiaomi MiMo via console Cookie, SenseTime via AccessKey signing, MiniMax/Doubao via quota or AK/SK), so they currently show "not configured"; the extension point is `src/balance.ts` (add a querier per provider balance API).
254
- - **Relay quota depends on upstream private schemas**: New API / Sub2API interface fields are not public, so an unreadable station is labeled "no quota" rather than fabricating an amount; if a station's response fields differ, extend the parsers in `src/relay.ts`. An "unknown route" means that route no longer exists in the current provider config (renamed / deleted); historical call data is not lost — re-adding the same-named route restores attribution automatically. Cost in the unknown-route bucket is always 0 (issue #82): the spending party cannot be verified — same posture as uncatalogued models and subscription channels — while tokens are still counted, and the panel shows "—" for the group cost with an explanatory note.
254
+ - **Relay quota depends on upstream private schemas**: New API / Sub2API interface fields are not public, so an unreadable station is labeled "no quota" rather than fabricating an amount; if a station's response fields differ, extend the parsers in `src/relay.ts`. An "unknown route" means that route exists neither in the current provider config nor in the host llm registry (deleted config, logs older than the install, …); historical call data is not lost — re-adding the same-named route restores attribution automatically. Cost in the unknown-route bucket is always 0 (issue #82): the spending party cannot be verified — same posture as uncatalogued models and subscription channels — while tokens are still counted, and the panel shows "—" for the group cost with an explanatory note. Channels registered by another plugin through `ctx.llm.registerAdapter` (e.g. CommandCode) are **not** unknown routes: the plugin discovers them from the host llm registry and bills them in the `direct:<provider>` bucket at the model's official catalog price (issue #91); the registry is read through `ctx.get('llm')` (no `inject` requirement), and an unreadable or empty registry warns once (panel numbers and the unknown policy are unchanged) so this class of failure is never silent again.
255
255
  - **Overspend notifications rely on the browser Notification API**: when permission is denied or the platform lacks support, only the in-UI red-pulse fallback remains — no host-level notification channel; notifications are capped at once per day.
256
256
  - **Session rows are not navigable**: clicking a session row does not open that session (cross-plugin navigation needs a host session-selection channel); sessions are capped at 100 rows and the panel shows the top 20.
257
257
  - **Cost is a catalog estimate**: models without published per-token pricing (iFlytek, SenseTime, Xiaomi) use estimates (feature-list footnote ¹); official billing is authoritative.
package/README.md CHANGED
@@ -264,7 +264,7 @@ npm publish --access public
264
264
  ## ⚠️ Known Limitations and Deferred Work
265
265
 
266
266
  - **余额查询已接入 DeepSeek / 月之暗面(Kimi)/ 阶跃星辰(StepFun)/ 硅基流动 / xAI / 智谱 GLM(Z.ai 国内域)/ 腾讯云 TokenHub(Token Plan 余量)**:前六家用标准 Bearer API key,腾讯云用云 API 密钥对(TC3 签名)。其余厂商因无公开余额接口或需非 Bearer 鉴权(小米 MiMo 走控制台 Cookie、商汤走 AccessKey 签名、MiniMax/字节豆包走额度制或 AK/SK),暂显示「未配置」;扩展点在 `src/balance.ts`(按厂商余额 API 增加查询器)。
267
- - **中转站额度依赖上游私有 schema**:New API / Sub2API 的接口字段未公开,读不出时标「未读出额度」而非臆造金额;若某中转站响应字段不同,需按 `src/relay.ts` 的解析器扩展。未知路由表示该路由在当前 provider 配置与宿主 llm 注册表里都不存在(配置已删、日志比安装更早等),历史调用数据未丢,重新配置同名路由即可自动归位。未知路由桶的费用一律记 0(issue #82):支出主体无法核实,与未收录模型、订阅通道同姿态——token 照常统计,面板组头费用显示「—」并附说明。由独立插件经 `ctx.llm.registerAdapter` 注册的通道(如 CommandCode)不属于未知路由:插件从宿主 llm 注册表发现它们,按 `direct:<provider>` 桶以模型官方目录价计费(issue #91)。
267
+ - **中转站额度依赖上游私有 schema**:New API / Sub2API 的接口字段未公开,读不出时标「未读出额度」而非臆造金额;若某中转站响应字段不同,需按 `src/relay.ts` 的解析器扩展。未知路由表示该路由在当前 provider 配置与宿主 llm 注册表里都不存在(配置已删、日志比安装更早等),历史调用数据未丢,重新配置同名路由即可自动归位。未知路由桶的费用一律记 0(issue #82):支出主体无法核实,与未收录模型、订阅通道同姿态——token 照常统计,面板组头费用显示「—」并附说明。由独立插件经 `ctx.llm.registerAdapter` 注册的通道(如 CommandCode)不属于未知路由:插件从宿主 llm 注册表发现它们,按 `direct:<provider>` 桶以模型官方目录价计费(issue #91);注册表读取走 `ctx.get('llm')`(免 `inject` 声明),读不到/读到空集合时告警一次(面板数字不变,unknown 口径不变),避免这类失效无声无息。
268
268
  - **超支通知依赖浏览器 Notification**:权限被拒绝或平台不支持时只有界面红色脉冲兜底,没有宿主级通知通道;通知上限为每天一次。
269
269
  - **会话明细不可跳转**:点击会话行不会打开对应会话(跨插件导航需要宿主会话选择通道);会话数封顶 100 行、面板只显示前 20 行。
270
270
  - **费用为目录价估算**:讯飞 / 商汤 / 小米等未公布按量单价的模型使用估算价(特性表脚注 ¹),正式定价以厂商账单为准。
package/lib/index.js CHANGED
@@ -7063,6 +7063,53 @@ function readPiAiProviderRoutes(settings) {
7063
7063
  }
7064
7064
  }
7065
7065
  /**
7066
+ * 读取宿主 llm 注册表里「已注册 adapter」的 provider id(issue #91)。
7067
+ *
7068
+ * 必须经 `ctx.get('llm')`,**不能**用属性访问 `ctx.llm`:cordis 的服务代理要求
7069
+ * 服务名出现在插件 `inject` 里,否则抛 `cannot get property "llm" without inject`
7070
+ * ——1.4.31 把这个异常吞掉后退化成空集合,结果是「修复发版了但完全不生效」且无任何
7071
+ * 可观察信号(issue #91 follow-up)。本函数把「读不到」与「读到了空集合」都显式
7072
+ * 回报给调用方,让上层能告警而不是静默降级。
7073
+ * @param get - 宿主上下文的 `get(name)` 访问器(cordis 的免 inject 读取口)。
7074
+ * @returns `ids` = 已注册 provider id 集合(读不到时为空集);
7075
+ * `reason` = 读取失败或结果不可用的原因;成功时缺省。
7076
+ */
7077
+ function readRegisteredProviderIds(get) {
7078
+ if (typeof get !== "function") return {
7079
+ ids: /* @__PURE__ */ new Set(),
7080
+ reason: "the host context exposes no get() accessor"
7081
+ };
7082
+ try {
7083
+ const llm = get("llm");
7084
+ if (llm === void 0) return {
7085
+ ids: /* @__PURE__ */ new Set(),
7086
+ reason: "the host llm service is not registered"
7087
+ };
7088
+ if (typeof llm.listProviders !== "function") return {
7089
+ ids: /* @__PURE__ */ new Set(),
7090
+ reason: "the host llm service exposes no listProviders()"
7091
+ };
7092
+ const providers = llm.listProviders();
7093
+ if (providers.length === 0) return {
7094
+ ids: /* @__PURE__ */ new Set(),
7095
+ reason: "the host llm registry reports no registered providers"
7096
+ };
7097
+ const ids = /* @__PURE__ */ new Set();
7098
+ for (const provider of providers) if (typeof provider?.id === "string" && provider.id !== "") ids.add(provider.id);
7099
+ if (ids.size === 0) return {
7100
+ ids,
7101
+ reason: "the host llm registry returned no usable provider ids"
7102
+ };
7103
+ return { ids };
7104
+ } catch (error) {
7105
+ const detail = error instanceof Error ? error.message : String(error);
7106
+ return {
7107
+ ids: /* @__PURE__ */ new Set(),
7108
+ reason: `the host llm registry could not be read (${detail})`
7109
+ };
7110
+ }
7111
+ }
7112
+ /**
7066
7113
  * 构造「cwd → 工作区标题」解析器(host 的 `workspaceRegistry` 为可选依赖)。
7067
7114
  * 匹配与 TokenLedger 同口径:会话 cwd 等于某工作区 path、或位于其子目录时,用
7068
7115
  * 工作区标题命名该项目(子目录的会话也计入);否则返回 undefined(回退到目录名)。
@@ -7249,17 +7296,16 @@ function apply(ctx, config = {}) {
7249
7296
  applyUserModelAliases(config.modelKeyAliases);
7250
7297
  let llmRegistryWarned = false;
7251
7298
  const registeredProvidersOf = () => {
7252
- try {
7253
- const providers = (ctx.get?.("llm"))?.listProviders() ?? [];
7299
+ const { ids, reason } = readRegisteredProviderIds(ctx.get?.bind(ctx));
7300
+ if (reason === void 0) {
7254
7301
  llmRegistryWarned = false;
7255
- return new Set(providers.map((provider) => provider.id));
7256
- } catch (error) {
7257
- if (!llmRegistryWarned) {
7258
- llmRegistryWarned = true;
7259
- console.warn("[usage-billing] llm registry unavailable; plugin-registered adapter channels stay unknown (issue #91):", error);
7260
- }
7261
- return /* @__PURE__ */ new Set();
7302
+ return ids;
7303
+ }
7304
+ if (!llmRegistryWarned) {
7305
+ llmRegistryWarned = true;
7306
+ console.warn(`[usage-billing] ${reason}; plugin-registered adapter channels (e.g. commandcode) stay in the unknown bucket and are billed at 0 (issue #91). Channel grouping falls back to llm-pi-ai routes only.`);
7262
7307
  }
7308
+ return ids;
7263
7309
  };
7264
7310
  const aggregator = createUsageAggregator(adaptSessionPersistence(ctx.sessionPersistence), {
7265
7311
  ...config.subscriptionProviders === void 0 ? {} : { subscriptionProviders: config.subscriptionProviders },
@@ -7875,4 +7921,4 @@ function apply(ctx, config = {}) {
7875
7921
  }), "usage-billing: usage-stats route");
7876
7922
  }
7877
7923
  //#endregion
7878
- export { Config, adaptSessionPersistence, apply, createFileUsageLedgerStore, guardLoopback, inject, normalizeTrustedHosts, readPiAiProviderRoutes, resolveSubscriptionKeys };
7924
+ export { Config, adaptSessionPersistence, apply, createFileUsageLedgerStore, guardLoopback, inject, normalizeTrustedHosts, readPiAiProviderRoutes, readRegisteredProviderIds, resolveSubscriptionKeys };
@@ -154,6 +154,22 @@ export interface PiAiProviderRoute {
154
154
  export declare function readPiAiProviderRoutes(settings: SettingsReader): Readonly<Record<string, {
155
155
  baseURL?: string;
156
156
  }>>;
157
+ /**
158
+ * 读取宿主 llm 注册表里「已注册 adapter」的 provider id(issue #91)。
159
+ *
160
+ * 必须经 `ctx.get('llm')`,**不能**用属性访问 `ctx.llm`:cordis 的服务代理要求
161
+ * 服务名出现在插件 `inject` 里,否则抛 `cannot get property "llm" without inject`
162
+ * ——1.4.31 把这个异常吞掉后退化成空集合,结果是「修复发版了但完全不生效」且无任何
163
+ * 可观察信号(issue #91 follow-up)。本函数把「读不到」与「读到了空集合」都显式
164
+ * 回报给调用方,让上层能告警而不是静默降级。
165
+ * @param get - 宿主上下文的 `get(name)` 访问器(cordis 的免 inject 读取口)。
166
+ * @returns `ids` = 已注册 provider id 集合(读不到时为空集);
167
+ * `reason` = 读取失败或结果不可用的原因;成功时缺省。
168
+ */
169
+ export declare function readRegisteredProviderIds(get: ((name: string) => unknown) | undefined): {
170
+ ids: ReadonlySet<string>;
171
+ reason?: string;
172
+ };
157
173
  /**
158
174
  * 解析订阅适配器需要的 API Key:从 llm-pi-ai 设置的 `providers.<id>.apiKeyEnv`
159
175
  * 读引用(如 kimi-coding → KIMI_CODING_API_KEY),再经凭据 seam 解析成实际值。
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.33",
4
+ "version": "1.4.34",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "deepseek",