@kenz1117/dsh-ui-usage-billing 1.4.30 → 1.4.32
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 +1 -1
- package/lib/index.js +50 -14
- package/lib/types/aggregate.d.ts +10 -2
- package/package.json +1 -1
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
|
|
267
|
+
- **中转站额度依赖上游私有 schema**:New API / Sub2API 的接口字段未公开,读不出时标「未读出额度」而非臆造金额;若某中转站响应字段不同,需按 `src/relay.ts` 的解析器扩展。未知路由表示该路由在当前 provider 配置与宿主 llm 注册表里都不存在(配置已删、日志比安装更早等),历史调用数据未丢,重新配置同名路由即可自动归位。未知路由桶的费用一律记 0(issue #82):支出主体无法核实,与未收录模型、订阅通道同姿态——token 照常统计,面板组头费用显示「—」并附说明。由独立插件经 `ctx.llm.registerAdapter` 注册的通道(如 CommandCode)不属于未知路由:插件从宿主 llm 注册表发现它们,按 `direct:<provider>` 桶以模型官方目录价计费(issue #91)。
|
|
268
268
|
- **超支通知依赖浏览器 Notification**:权限被拒绝或平台不支持时只有界面红色脉冲兜底,没有宿主级通知通道;通知上限为每天一次。
|
|
269
269
|
- **会话明细不可跳转**:点击会话行不会打开对应会话(跨插件导航需要宿主会话选择通道);会话数封顶 100 行、面板只显示前 20 行。
|
|
270
270
|
- **费用为目录价估算**:讯飞 / 商汤 / 小米等未公布按量单价的模型使用估算价(特性表脚注 ¹),正式定价以厂商账单为准。
|
package/lib/index.js
CHANGED
|
@@ -3410,12 +3410,19 @@ function hourPerfOf(digest) {
|
|
|
3410
3410
|
* 订阅豁免),历史账本行立即失效并全量重折,配置变更即时生效(issue #37)。
|
|
3411
3411
|
* 判定函数无法序列化,统一记为 `fn`(任何函数形态互视为同一指纹)。
|
|
3412
3412
|
*/
|
|
3413
|
-
function configFingerprint(subscriptionMatcher, officialProviderIds, routeAliases, searchEstimate) {
|
|
3413
|
+
function configFingerprint(subscriptionMatcher, officialProviderIds, routeAliases, searchEstimate, modelKeyAliases = {}, registeredProviders = /* @__PURE__ */ new Set()) {
|
|
3414
|
+
const subs = typeof subscriptionMatcher === "function" ? "fn" : [...subscriptionMatcher].sort().join(",");
|
|
3415
|
+
const officials = officialProviderIds === void 0 ? "" : [...officialProviderIds].sort().join(",");
|
|
3416
|
+
const aliases = Object.keys(routeAliases).sort().map((key) => `${key}=${routeAliases[key]}`).join(",");
|
|
3417
|
+
const modelKeys = Object.keys(modelKeyAliases).sort().map((key) => `${key}=${modelKeyAliases[key]}`).join(",");
|
|
3418
|
+
const registered = [...registeredProviders].sort().join(",");
|
|
3414
3419
|
return [
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
String(searchEstimate)
|
|
3420
|
+
subs,
|
|
3421
|
+
officials,
|
|
3422
|
+
aliases,
|
|
3423
|
+
String(searchEstimate),
|
|
3424
|
+
modelKeys,
|
|
3425
|
+
registered
|
|
3419
3426
|
].join("|");
|
|
3420
3427
|
}
|
|
3421
3428
|
/** 会话格式拒读(宿主 SessionFormatUnsupportedError):日志完好,仅本构建读不了。
|
|
@@ -3446,10 +3453,21 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3446
3453
|
let ledgerAppliedMigrations;
|
|
3447
3454
|
let lastDoc;
|
|
3448
3455
|
let lastAt = 0;
|
|
3449
|
-
/** 每次聚合取最新的 provider 路由视图(中转站零配置发现);缺省按空处理(全部未知路由)。
|
|
3450
|
-
|
|
3456
|
+
/** 每次聚合取最新的 provider 路由视图(中转站零配置发现);缺省按空处理(全部未知路由)。
|
|
3457
|
+
* 叠加已注册 adapter 的通道(issue #91):宿主 llm 注册表里的 provider 合成无 baseURL
|
|
3458
|
+
* 的空路由视图——siteRefOf 的路由命中分支正是「无 origin 直连」的语义,归位
|
|
3459
|
+
* `direct:<provider>` 按官方目录价计费;llm-pi-ai 已有配置的路由优先(其 origin 可还原)。 */
|
|
3460
|
+
const routesOf = () => {
|
|
3461
|
+
const routes = { ...options.resolveRoutes?.() };
|
|
3462
|
+
const registered = options.resolveRegisteredProviders?.();
|
|
3463
|
+
if (registered !== void 0) {
|
|
3464
|
+
for (const provider of registered) if (routes[provider] === void 0) routes[provider] = {};
|
|
3465
|
+
}
|
|
3466
|
+
return routes;
|
|
3467
|
+
};
|
|
3451
3468
|
const searchEstimate = options.searchCallEstimateCny ?? .02;
|
|
3452
|
-
const
|
|
3469
|
+
const foldFingerprintOf = () => configFingerprint(subscriptionMatcher, officialProviderIds, routeAliases, searchEstimate, options.modelKeyAliases, options.resolveRegisteredProviders?.() ?? /* @__PURE__ */ new Set());
|
|
3470
|
+
let foldFingerprint = foldFingerprintOf();
|
|
3453
3471
|
const FOLD_CHUNK_EVENTS = 8e3;
|
|
3454
3472
|
/** 惰性失效(价目表时序):账本/缓存行若以「当时不可计价」的模型落盘,
|
|
3455
3473
|
* 而价目表现在能计价了,该行必须重折——否则显示层按实时价算出的
|
|
@@ -3467,7 +3485,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3467
3485
|
id,
|
|
3468
3486
|
...cwd === void 0 ? {} : { cwd },
|
|
3469
3487
|
...stamp === null ? {} : { stamp },
|
|
3470
|
-
foldVersion:
|
|
3488
|
+
foldVersion: 22,
|
|
3471
3489
|
fingerprint: foldFingerprint,
|
|
3472
3490
|
fold: serializeFold(fold)
|
|
3473
3491
|
};
|
|
@@ -3531,6 +3549,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3531
3549
|
const aggregateOnce = async () => {
|
|
3532
3550
|
const now = Date.now();
|
|
3533
3551
|
await ensureLedgerLoaded();
|
|
3552
|
+
foldFingerprint = foldFingerprintOf();
|
|
3534
3553
|
let metas;
|
|
3535
3554
|
try {
|
|
3536
3555
|
metas = await persistence.list();
|
|
@@ -3562,7 +3581,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3562
3581
|
try {
|
|
3563
3582
|
const cwd = meta.cwd;
|
|
3564
3583
|
const ledgerRow = ledger.get(id);
|
|
3565
|
-
if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) ===
|
|
3584
|
+
if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) === 22 && ledgerRow.fingerprint === foldFingerprint && !foldNeedsReprice(ledgerRow.fold)) {
|
|
3566
3585
|
const fold = deserializeFold(ledgerRow.fold);
|
|
3567
3586
|
folds.push({
|
|
3568
3587
|
id,
|
|
@@ -3573,7 +3592,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3573
3592
|
continue;
|
|
3574
3593
|
}
|
|
3575
3594
|
const hit = cache.get(id);
|
|
3576
|
-
if (hit !== void 0 && stamp !== null && hit.stamp === stamp && ![...hit.fold.unpricedModels].some((key) => isPriced(key))) {
|
|
3595
|
+
if (hit !== void 0 && stamp !== null && hit.stamp === stamp && hit.fingerprint === foldFingerprint && ![...hit.fold.unpricedModels].some((key) => isPriced(key))) {
|
|
3577
3596
|
cache.delete(id);
|
|
3578
3597
|
cache.set(id, hit);
|
|
3579
3598
|
folds.push({
|
|
@@ -3585,7 +3604,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3585
3604
|
continue;
|
|
3586
3605
|
}
|
|
3587
3606
|
const previous = cache.get(id);
|
|
3588
|
-
if (previous?.machine !== void 0 && previous.lastSeq !== void 0 && ![...previous.fold.unpricedModels].some((key) => isPriced(key))) {
|
|
3607
|
+
if (previous?.machine !== void 0 && previous.lastSeq !== void 0 && previous.fingerprint === foldFingerprint && ![...previous.fold.unpricedModels].some((key) => isPriced(key))) {
|
|
3589
3608
|
const from = previous.lastSeq + 1;
|
|
3590
3609
|
const { events, inheritedEventCount } = await persistence.readFrom(meta.id, sessionLogOffsetOf(from));
|
|
3591
3610
|
const after = await stampOf(meta);
|
|
@@ -3630,7 +3649,8 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3630
3649
|
stamp,
|
|
3631
3650
|
fold,
|
|
3632
3651
|
machine,
|
|
3633
|
-
...last !== void 0 && typeof last.seq === "number" ? { lastSeq: last.seq } : {}
|
|
3652
|
+
...last !== void 0 && typeof last.seq === "number" ? { lastSeq: last.seq } : {},
|
|
3653
|
+
fingerprint: foldFingerprint
|
|
3634
3654
|
});
|
|
3635
3655
|
folds.push({
|
|
3636
3656
|
id,
|
|
@@ -3662,7 +3682,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3662
3682
|
for (const entry of ledger.values()) {
|
|
3663
3683
|
if (included.has(entry.id)) continue;
|
|
3664
3684
|
try {
|
|
3665
|
-
const stale = (entry.foldVersion ?? 1) <
|
|
3685
|
+
const stale = (entry.foldVersion ?? 1) < 22 || entry.fingerprint !== foldFingerprint;
|
|
3666
3686
|
folds.push({
|
|
3667
3687
|
id: entry.id,
|
|
3668
3688
|
...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
|
|
@@ -7218,10 +7238,26 @@ function apply(ctx, config = {}) {
|
|
|
7218
7238
|
const workspaceTitleResolver = buildWorkspaceTitleResolver(ctx);
|
|
7219
7239
|
applyBuiltinCatalog(BUILTIN_MODEL_CATALOG, BUILTIN_MODEL_KEY_ALIASES);
|
|
7220
7240
|
applyUserModelAliases(config.modelKeyAliases);
|
|
7241
|
+
let llmRegistryWarned = false;
|
|
7242
|
+
const registeredProvidersOf = () => {
|
|
7243
|
+
try {
|
|
7244
|
+
const providers = (ctx.get?.("llm"))?.listProviders() ?? [];
|
|
7245
|
+
llmRegistryWarned = false;
|
|
7246
|
+
return new Set(providers.map((provider) => provider.id));
|
|
7247
|
+
} catch (error) {
|
|
7248
|
+
if (!llmRegistryWarned) {
|
|
7249
|
+
llmRegistryWarned = true;
|
|
7250
|
+
console.warn("[usage-billing] llm registry unavailable; plugin-registered adapter channels stay unknown (issue #91):", error);
|
|
7251
|
+
}
|
|
7252
|
+
return /* @__PURE__ */ new Set();
|
|
7253
|
+
}
|
|
7254
|
+
};
|
|
7221
7255
|
const aggregator = createUsageAggregator(adaptSessionPersistence(ctx.sessionPersistence), {
|
|
7222
7256
|
...config.subscriptionProviders === void 0 ? {} : { subscriptionProviders: config.subscriptionProviders },
|
|
7223
7257
|
...config.routeAliases === void 0 ? {} : { routeAliases: config.routeAliases },
|
|
7224
7258
|
resolveRoutes: () => readPiAiProviderRoutes(ctx.settings),
|
|
7259
|
+
resolveRegisteredProviders: registeredProvidersOf,
|
|
7260
|
+
...config.modelKeyAliases === void 0 ? {} : { modelKeyAliases: config.modelKeyAliases },
|
|
7225
7261
|
...workspaceTitleResolver === void 0 ? {} : { resolveWorkspaceTitle: workspaceTitleResolver },
|
|
7226
7262
|
...config.searchCallEstimateCny === void 0 ? {} : { searchCallEstimateCny: config.searchCallEstimateCny },
|
|
7227
7263
|
ledger: ledgerStore
|
package/lib/types/aggregate.d.ts
CHANGED
|
@@ -140,12 +140,20 @@ export interface AggregateOptions {
|
|
|
140
140
|
* 官方判定都用别名后的路由名)。
|
|
141
141
|
*/
|
|
142
142
|
routeAliases?: Readonly<Record<string, string>>;
|
|
143
|
+
/** 折叠时模型 id → 目录键的用户自定义别名(与 pricing 模块注入的同源):仅用于
|
|
144
|
+
* 折叠指纹——别名改动必须让历史行失效重折,否则 byModel 新旧 key 并存(issue #91 P2)。 */
|
|
145
|
+
modelKeyAliases?: Readonly<Record<string, string>>;
|
|
143
146
|
/** 每会话折叠缓存的上限(默认 {@link DEFAULT_MAX_CACHE_SESSIONS});
|
|
144
147
|
* 超限时按最近使用先后淘汰最久未用的会话,防长期运行内存膨胀。 */
|
|
145
148
|
maxCacheSessions?: number;
|
|
146
149
|
/** 中转站归组来源:返回当前 provider 路由视图(llm-pi-ai providers 的 baseURL)。
|
|
147
150
|
* 每次聚合时调用取最新值;缺省时全部路由按「未知路由」处理(无配置发现)。 */
|
|
148
151
|
resolveRoutes?: () => Readonly<Record<string, ProviderRouteView>>;
|
|
152
|
+
/** 已注册 adapter 的 provider 集合(宿主 llm 注册表,llm-pi-ai 之外的插件路由,
|
|
153
|
+
* issue #91):每次聚合时调用取最新值。这些通道宿主只会把请求发给已注册的
|
|
154
|
+
* adapter,身份可核实——合成无 baseURL 的路由视图归位 `direct:<provider>` 按
|
|
155
|
+
* 官方目录价计费,不再落「未知路由」记 0;缺省时保持 unknown 口径。 */
|
|
156
|
+
resolveRegisteredProviders?: () => ReadonlySet<string>;
|
|
149
157
|
/** 工作区标题解析:给定会话 cwd 返回项目显示标题;undefined = 回退到 cwd 末级目录名
|
|
150
158
|
* (host 的 workspaceRegistry 为可选依赖,缺失时不注入,行为保持不变)。 */
|
|
151
159
|
resolveWorkspaceTitle?: (cwd: string) => string | undefined;
|
|
@@ -591,7 +599,7 @@ export interface UsageLedgerDocument {
|
|
|
591
599
|
* 会话费用只剩最近一段,issue #29)。
|
|
592
600
|
* 持久账本行据此区分新旧算法:日志已删/不可读而只能沿用旧行时,UI 标注置信度提示。
|
|
593
601
|
*/
|
|
594
|
-
export declare const FOLD_VERSION =
|
|
602
|
+
export declare const FOLD_VERSION = 22;
|
|
595
603
|
/**
|
|
596
604
|
* 一次性账本迁移:id 唯一,apply 在加载边界对原始文档执行,已应用过的跳过。
|
|
597
605
|
* 未来账本/schema 字段变更(重命名、拆桶、语义调整)时,在此追加一条迁移并
|
|
@@ -687,7 +695,7 @@ export interface SessionUsageSummary {
|
|
|
687
695
|
* 订阅豁免),历史账本行立即失效并全量重折,配置变更即时生效(issue #37)。
|
|
688
696
|
* 判定函数无法序列化,统一记为 `fn`(任何函数形态互视为同一指纹)。
|
|
689
697
|
*/
|
|
690
|
-
export declare function configFingerprint(subscriptionMatcher: SubscriptionMatcher, officialProviderIds: ReadonlySet<string> | undefined, routeAliases: Readonly<Record<string, string>>, searchEstimate: number): string;
|
|
698
|
+
export declare function configFingerprint(subscriptionMatcher: SubscriptionMatcher, officialProviderIds: ReadonlySet<string> | undefined, routeAliases: Readonly<Record<string, string>>, searchEstimate: number, modelKeyAliases?: Readonly<Record<string, string>>, registeredProviders?: ReadonlySet<string>): string;
|
|
691
699
|
/**
|
|
692
700
|
* Create the incremental usage aggregator.
|
|
693
701
|
* @param persistence - the session persistence service.
|
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.
|
|
4
|
+
"version": "1.4.32",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"deepseek",
|