@kenz1117/dsh-ui-usage-billing 1.1.13 → 1.2.0
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 +42 -24
- package/README.md +37 -32
- package/lib/client.js +5 -5
- package/lib/index.js +558 -61
- package/lib/types/aggregate.d.ts +81 -28
- package/lib/types/client/PerfPanel.d.ts +10 -8
- package/lib/types/client/TokenPanel.d.ts +55 -3
- package/lib/types/client/apply.d.ts +10 -3
- package/lib/types/client/budget-store.d.ts +1 -1
- package/lib/types/client/completion-notify.d.ts +2 -2
- package/lib/types/client/live-cost.d.ts +19 -9
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/pricing.d.ts +60 -8
- package/lib/types/client/provider-display.d.ts +7 -0
- package/lib/types/client/usage-billing-settings.d.ts +42 -0
- package/lib/types/index.d.ts +24 -1
- package/lib/types/subscriptions.d.ts +2 -0
- package/lib/types/tc3.d.ts +60 -0
- package/package.json +46 -24
package/lib/index.js
CHANGED
|
@@ -2,10 +2,12 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { mkdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
+
import { SessionLogOffset } from "@deepseek-ai/dsh-session/types";
|
|
5
6
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
6
7
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
7
8
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
8
9
|
import z from "@deepseek-ai/schemastery";
|
|
10
|
+
import { createHash, createHmac } from "node:crypto";
|
|
9
11
|
//#region lib/types/client/plan-knowledge.js
|
|
10
12
|
/**
|
|
11
13
|
* Plan-knowledge reference (adapted from dsh-spend's `knowledge.js`, MIT):
|
|
@@ -192,6 +194,20 @@ let liveRate;
|
|
|
192
194
|
let livePrices;
|
|
193
195
|
let liveExtraModels;
|
|
194
196
|
/**
|
|
197
|
+
* 用户自定义模型别名(插件配置 `modelKeyAliases`,聚合启动时注入):真实日志
|
|
198
|
+
* model id → 计费目录键。优先级高于内置别名表——目录外的新模型无需等发版,
|
|
199
|
+
* 配置一条别名即完成识别与计价(键必须是 MODEL_CATALOG 的既有 key)。
|
|
200
|
+
*/
|
|
201
|
+
let userModelAliases;
|
|
202
|
+
/**
|
|
203
|
+
* 注入用户自定义模型别名(node 半区在插件启动时调用一次)。纯内存状态:
|
|
204
|
+
* 聚合折叠与客户端渲染共用同一份(两侧一致性由同一注入点保证)。
|
|
205
|
+
* @param aliases - `model id → 目录键` 映射;undefined/空 = 清除,回退内置表。
|
|
206
|
+
*/
|
|
207
|
+
function applyUserModelAliases(aliases) {
|
|
208
|
+
userModelAliases = aliases !== void 0 && Object.keys(aliases).length > 0 ? aliases : void 0;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
195
211
|
* Apply the node half's live pricing snapshot. Absent fields keep the
|
|
196
212
|
* built-in catalog and rate; callers never fabricate values.
|
|
197
213
|
* @param pricing - the `/api/billing/pricing` response.
|
|
@@ -316,6 +332,7 @@ function isBeijingWeekend(timeMs) {
|
|
|
316
332
|
*/
|
|
317
333
|
/** DeepSeek 官方高峰时段说明(峰谷分时计费目录条目共用)。 */
|
|
318
334
|
const DEEPSEEK_PEAK_HOURS = "09:00-12:00 / 14:00-18:00";
|
|
335
|
+
const GEMINI_PEAK_HOURS = "Standard / Flex";
|
|
319
336
|
const MODEL_CATALOG = [
|
|
320
337
|
{
|
|
321
338
|
key: "flash",
|
|
@@ -974,7 +991,7 @@ const MODEL_CATALOG = [
|
|
|
974
991
|
output: 6
|
|
975
992
|
}
|
|
976
993
|
},
|
|
977
|
-
peakHours:
|
|
994
|
+
peakHours: GEMINI_PEAK_HOURS,
|
|
978
995
|
tierSemantics: "latency"
|
|
979
996
|
},
|
|
980
997
|
{
|
|
@@ -993,7 +1010,7 @@ const MODEL_CATALOG = [
|
|
|
993
1010
|
output: 3.75
|
|
994
1011
|
}
|
|
995
1012
|
},
|
|
996
|
-
peakHours:
|
|
1013
|
+
peakHours: GEMINI_PEAK_HOURS,
|
|
997
1014
|
tierSemantics: "latency"
|
|
998
1015
|
},
|
|
999
1016
|
{
|
|
@@ -1405,6 +1422,7 @@ const MODEL_KEY_ALIASES = {
|
|
|
1405
1422
|
"deepseek-v4-flash": "flash",
|
|
1406
1423
|
"deepseek-v4-flash-vision-exp": "flash-vision-exp",
|
|
1407
1424
|
"deepseek-v4.1-flash": "flash",
|
|
1425
|
+
"deepseek-v4.1-flash-expires-on-0910": "flash",
|
|
1408
1426
|
"deepseek-v4-pro": "pro",
|
|
1409
1427
|
"glm-5.2": "glm",
|
|
1410
1428
|
"glm-4.5-air": "glm-4.5-air",
|
|
@@ -1433,6 +1451,7 @@ const MODEL_KEY_ALIASES = {
|
|
|
1433
1451
|
"command-a": "command-a-03-2025",
|
|
1434
1452
|
"command-r-08-2024": "command-r-08-2024",
|
|
1435
1453
|
"command-r": "command-r-08-2024",
|
|
1454
|
+
"hy3": "hunyuan",
|
|
1436
1455
|
"longcat-2.0": "longcat-2.0",
|
|
1437
1456
|
"longcat-2": "longcat-2.0",
|
|
1438
1457
|
"minicpm-v-4.5": "minicpm-v-4.5",
|
|
@@ -1521,8 +1540,49 @@ const CATALOG_CANON_INDEX = (() => {
|
|
|
1521
1540
|
* @param id - 真实模型 id(日志里出现的形式)。
|
|
1522
1541
|
* @returns 计费目录键。
|
|
1523
1542
|
*/
|
|
1543
|
+
/**
|
|
1544
|
+
* 由一个未知模型 id 派生候选 id(仅当直接查全部未命中时才尝试):
|
|
1545
|
+
* - 剥离组织前缀(`deepseek/deepseek-v4-flash` → `deepseek-v4-flash`);
|
|
1546
|
+
* - 剥离尾部纯数字段(`deepseek-v4-flash-202605` / `-0731` → `deepseek-v4-flash`,
|
|
1547
|
+
* 覆盖 TokenHub / 官方按日期滚动的快照 id);
|
|
1548
|
+
* - 两者组合派生。目录键本身(如 `mistral-large-2512`、`command-a-03-2025`)
|
|
1549
|
+
* 在直接查就已命中,永不进入派生分支,不受剥段影响。
|
|
1550
|
+
*/
|
|
1551
|
+
function derivedKeyCandidates(id) {
|
|
1552
|
+
const out = [];
|
|
1553
|
+
const push = (value) => {
|
|
1554
|
+
if (value !== "" && !out.includes(value)) out.push(value);
|
|
1555
|
+
};
|
|
1556
|
+
const stripTrailingDigits = (value) => {
|
|
1557
|
+
let base = value;
|
|
1558
|
+
for (;;) {
|
|
1559
|
+
const next = base.replace(/[-_]\d{3,}$/u, "");
|
|
1560
|
+
if (next === base || next === "") return;
|
|
1561
|
+
base = next;
|
|
1562
|
+
push(base);
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
const slash = id.lastIndexOf("/");
|
|
1566
|
+
if (slash > 0 && slash < id.length - 1) {
|
|
1567
|
+
const bare = id.slice(slash + 1);
|
|
1568
|
+
push(bare);
|
|
1569
|
+
stripTrailingDigits(bare);
|
|
1570
|
+
}
|
|
1571
|
+
stripTrailingDigits(id);
|
|
1572
|
+
return out;
|
|
1573
|
+
}
|
|
1574
|
+
/** 查一个候选 id(用户别名 → 内置别名 → 目录归一化 → models.dev 补充);未命中返回 undefined。 */
|
|
1575
|
+
function lookupCandidate(candidate) {
|
|
1576
|
+
const alias = userModelAliases?.[candidate] ?? MODEL_KEY_ALIASES[candidate];
|
|
1577
|
+
if (alias !== void 0) return alias;
|
|
1578
|
+
const canon = canonModelId(candidate);
|
|
1579
|
+
if (canon === "") return void 0;
|
|
1580
|
+
const hit = CATALOG_CANON_INDEX.get(canon);
|
|
1581
|
+
if (hit !== void 0) return hit;
|
|
1582
|
+
return (liveExtraModels ?? []).find((item) => canonModelId(item.key) === canon)?.key;
|
|
1583
|
+
}
|
|
1524
1584
|
function resolveCatalogKey(id) {
|
|
1525
|
-
const exact = MODEL_KEY_ALIASES[id] ?? id;
|
|
1585
|
+
const exact = userModelAliases?.[id] ?? MODEL_KEY_ALIASES[id] ?? id;
|
|
1526
1586
|
if (exact === id) {
|
|
1527
1587
|
const canon = canonModelId(id);
|
|
1528
1588
|
if (canon !== "") {
|
|
@@ -1531,6 +1591,10 @@ function resolveCatalogKey(id) {
|
|
|
1531
1591
|
const extraHit = (liveExtraModels ?? []).find((item) => canonModelId(item.key) === canon);
|
|
1532
1592
|
if (extraHit !== void 0) return extraHit.key;
|
|
1533
1593
|
}
|
|
1594
|
+
for (const candidate of derivedKeyCandidates(id)) {
|
|
1595
|
+
const hit = lookupCandidate(candidate);
|
|
1596
|
+
if (hit !== void 0) return hit;
|
|
1597
|
+
}
|
|
1534
1598
|
}
|
|
1535
1599
|
return exact;
|
|
1536
1600
|
}
|
|
@@ -1666,24 +1730,31 @@ function computeCost(entry, buckets, peakShare = DEFAULT_PEAK_SHARE, nowMs = Dat
|
|
|
1666
1730
|
return peak * peakShare + off * (1 - peakShare);
|
|
1667
1731
|
}
|
|
1668
1732
|
/**
|
|
1733
|
+
* v1 峰谷档判定(峰谷开闸起、周末全谷分界止):不豁免周末——该时段官方
|
|
1734
|
+
* 高峰时段为每天 9-12 / 14-18(周六日同样计峰)。仅用于历史事件计费;
|
|
1735
|
+
* 「当前时刻」的档位(提醒/时段条/费率展示)一律走 {@link tierAt} 现行规则。
|
|
1736
|
+
*/
|
|
1737
|
+
function tariffV1At(timeMs) {
|
|
1738
|
+
return isPeakHour((new Date(timeMs).getUTCHours() + 8) % 24) ? "peak" : "offPeak";
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1669
1741
|
* 按调用时刻精确判定高峰/空闲档并计价(P0-1:替代固定比例混合)。时刻未知
|
|
1670
1742
|
* (null/NaN,理论不发生在真实事件流)时回退 {@link DEFAULT_PEAK_SHARE} 混合,
|
|
1671
1743
|
* 保持旧语义不低估。平档模型(无 offPeak)两个时段同价。限时促销与峰谷档
|
|
1672
1744
|
* 同口径:按事件时刻判定该笔流量当时享受的单价。
|
|
1745
|
+
*
|
|
1746
|
+
* 历史正确性(按变更节点分段适用规则,不统一套现行价重算历史):
|
|
1747
|
+
* - 早于 {@link PEAK_ERA_START_MS} 的事件按当时官方基础价
|
|
1748
|
+
* ({@link LEGACY_DEEPSEEK_BANDS})计费;
|
|
1749
|
+
* - 峰谷开闸至 {@link WEEKEND_OFFPEAK_START_MS} 之间按 v1 规则(周末不豁免,
|
|
1750
|
+
* 周六日 9-12 / 14-18 计峰);
|
|
1751
|
+
* - 周末全谷分界起按现行规则({@link tierAt},周六日全天低谷)。
|
|
1673
1752
|
* @param entry - the catalog entry whose prices apply.
|
|
1674
1753
|
* @param buckets - token usage counts.
|
|
1675
1754
|
* @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
|
|
1676
1755
|
* @param peakShare - fallback mix used only when `timeMs` is missing.
|
|
1677
1756
|
* @returns the estimated cost in CNY(USD 计价模型已按当前汇率折算)。
|
|
1678
1757
|
*/
|
|
1679
|
-
/**
|
|
1680
|
-
* v1 峰谷档判定(峰谷开闸起、周末全谷分界止):不豁免周末——该时段官方
|
|
1681
|
-
* 高峰时段为每天 9-12 / 14-18(周六日同样计峰)。仅用于历史事件计费;
|
|
1682
|
-
* 「当前时刻」的档位(提醒/时段条/费率展示)一律走 {@link tierAt} 现行规则。
|
|
1683
|
-
*/
|
|
1684
|
-
function tariffV1At(timeMs) {
|
|
1685
|
-
return isPeakHour((new Date(timeMs).getUTCHours() + 8) % 24) ? "peak" : "offPeak";
|
|
1686
|
-
}
|
|
1687
1758
|
function computeCostAt(entry, buckets, timeMs, peakShare = DEFAULT_PEAK_SHARE) {
|
|
1688
1759
|
if (timeMs === null || timeMs === void 0 || !Number.isFinite(timeMs)) return computeCost(entry, buckets, peakShare);
|
|
1689
1760
|
const priced = applyPromo(entry, timeMs);
|
|
@@ -1737,9 +1808,9 @@ function formatTokens(value) {
|
|
|
1737
1808
|
/**
|
|
1738
1809
|
* 走订阅套餐(coding / token plan / opencode 订阅)的 provider id:这些通道的
|
|
1739
1810
|
* 调用按套餐计费,不再按 token 计费,因此即使模型 id 与计费表撞名也一律豁免。
|
|
1740
|
-
*
|
|
1741
|
-
*
|
|
1742
|
-
*
|
|
1811
|
+
* 此列表保留为显式配置的参考基线;聚合层缺省改用与订阅卡一致的
|
|
1812
|
+
* `isSubscriptionProviderId` 判定(覆盖本列表与全部 `*-token-plan` / `*-coding` 变体),
|
|
1813
|
+
* 部署仍可在 plugin config 的 `subscriptionProviders` 中显式覆盖。
|
|
1743
1814
|
*/
|
|
1744
1815
|
const DEFAULT_SUBSCRIPTION_PROVIDERS = [
|
|
1745
1816
|
"kimi-coding",
|
|
@@ -1750,7 +1821,9 @@ const DEFAULT_SUBSCRIPTION_PROVIDERS = [
|
|
|
1750
1821
|
"qwen-token-plan-cn",
|
|
1751
1822
|
"xiaomi-token-plan-ams",
|
|
1752
1823
|
"xiaomi-token-plan-cn",
|
|
1753
|
-
"xiaomi-token-plan-sgp"
|
|
1824
|
+
"xiaomi-token-plan-sgp",
|
|
1825
|
+
"tencent-token-plan",
|
|
1826
|
+
"grok"
|
|
1754
1827
|
];
|
|
1755
1828
|
/**
|
|
1756
1829
|
* 官方渠道 provider id 判定:`deepseek` 前缀(DeepSeek 官方直连)视为官方,
|
|
@@ -1760,6 +1833,26 @@ const DEFAULT_SUBSCRIPTION_PROVIDERS = [
|
|
|
1760
1833
|
function isOfficialProvider(provider) {
|
|
1761
1834
|
return /^deepseek(?:-[a-z0-9-]+)?$/i.test(provider.trim());
|
|
1762
1835
|
}
|
|
1836
|
+
/** DeepSeek 官方直连端点的归一化 origin(`siteOriginOf` 口径):有 baseURL 的路由只有它算官方。 */
|
|
1837
|
+
const OFFICIAL_DEEPSEEK_ORIGIN = "https://api.deepseek.com";
|
|
1838
|
+
/**
|
|
1839
|
+
* 官方渠道判定(通道优先):显式 `officialProviderIds` 配置最优先;否则看站点归组——
|
|
1840
|
+
* 有 baseURL 的路由只有 origin 为 DeepSeek 官方域才算官方(修复:名为 `deepseek-*`
|
|
1841
|
+
* 的中转/网关路由曾被按 id 前缀误判为官方,腾讯网关的 DeepSeek 全被计成官方渠道);
|
|
1842
|
+
* 直连路由(配置在册、无 baseURL)与路由表查不到的未知名都退回按 provider id
|
|
1843
|
+
* 前缀判定——宿主内置的官方直连不经 llm-pi-ai 路由,其 provider 名不在路由表中,
|
|
1844
|
+
* 按 unknown 一刀切会把它错杀成三方(回归修复);配置外的同名网关残留可用
|
|
1845
|
+
* `routeAliases` 显式归位到真实通道。
|
|
1846
|
+
*/
|
|
1847
|
+
function officialChannelOf(provider, ref, officialProviderIds) {
|
|
1848
|
+
if (officialProviderIds !== void 0) return officialProviderIds.has(provider);
|
|
1849
|
+
if (ref.kind === "site") return ref.origin === OFFICIAL_DEEPSEEK_ORIGIN;
|
|
1850
|
+
return isOfficialProvider(provider);
|
|
1851
|
+
}
|
|
1852
|
+
/** 一个 provider 是否走订阅套餐计费(豁免按 token 计价)。 */
|
|
1853
|
+
function isSubscriptionCall(subscription, provider) {
|
|
1854
|
+
return typeof subscription === "function" ? subscription(provider) : subscription.has(provider);
|
|
1855
|
+
}
|
|
1763
1856
|
/** 由 baseURL 归一化出站点 origin(协议 + 主机 + 端口);解析失败回退原值。 */
|
|
1764
1857
|
function siteOriginOf(baseURL) {
|
|
1765
1858
|
try {
|
|
@@ -1772,11 +1865,17 @@ function siteOriginOf(baseURL) {
|
|
|
1772
1865
|
* 把一个 provider 路由归类为站点引用。判定顺序(与路由在 provider 配置里的状态一致):
|
|
1773
1866
|
* - 路由存在于当前配置且配了 baseURL → 中转站 `site`(按 origin 归组,同站多 key 合并);
|
|
1774
1867
|
* - 路由存在于当前配置但无 baseURL → 厂商直连 `direct`;
|
|
1775
|
-
* -
|
|
1868
|
+
* - 路由不在当前配置里:内置官方直连(`deepseek` / `deepseek-*` 形态,不经 llm-pi-ai
|
|
1869
|
+
* 网关)按 `direct` 归位——unknown 一刀切会把官方直连的费用堆进「未知路由」组、
|
|
1870
|
+
* 且不算官方渠道(回归修复);订阅豁免命中的通道(显式 `subscriptionProviders`
|
|
1871
|
+
* 配置或判定函数)同样按 `direct` 归位——订阅管理类插件注册的通道不经路由表,
|
|
1872
|
+
* unknown 桶既掩盖归属又按 token 误计费(issue #37 的 grok build);其余配置外
|
|
1873
|
+
* 名称保留 unknown(无法核实通道)。
|
|
1776
1874
|
* @param provider - 会话日志里的 provider 路由名(request/header 的 `config.provider`)。
|
|
1777
1875
|
* @param routes - 当前 provider 路由视图(来自 llm-pi-ai providers)。
|
|
1876
|
+
* @param opts - `subscription`:该调用是否已被订阅豁免判定命中。
|
|
1778
1877
|
*/
|
|
1779
|
-
function siteRefOf(provider, routes) {
|
|
1878
|
+
function siteRefOf(provider, routes, opts = {}) {
|
|
1780
1879
|
const view = routes[provider];
|
|
1781
1880
|
if (view !== void 0) {
|
|
1782
1881
|
if (view.baseURL !== void 0) return {
|
|
@@ -1789,6 +1888,14 @@ function siteRefOf(provider, routes) {
|
|
|
1789
1888
|
provider
|
|
1790
1889
|
};
|
|
1791
1890
|
}
|
|
1891
|
+
if (isOfficialProvider(provider)) return {
|
|
1892
|
+
kind: "direct",
|
|
1893
|
+
provider
|
|
1894
|
+
};
|
|
1895
|
+
if (opts.subscription === true) return {
|
|
1896
|
+
kind: "direct",
|
|
1897
|
+
provider
|
|
1898
|
+
};
|
|
1792
1899
|
return {
|
|
1793
1900
|
kind: "unknown",
|
|
1794
1901
|
provider
|
|
@@ -1955,7 +2062,8 @@ function serializeFold(fold) {
|
|
|
1955
2062
|
turns: fold.turns,
|
|
1956
2063
|
perf: fold.perf,
|
|
1957
2064
|
roles: fold.roles,
|
|
1958
|
-
lastActive: fold.lastActive
|
|
2065
|
+
lastActive: fold.lastActive,
|
|
2066
|
+
...fold.title !== void 0 ? { title: fold.title } : {}
|
|
1959
2067
|
};
|
|
1960
2068
|
}
|
|
1961
2069
|
/** Restore a JSON-safe ledger fold into the in-memory Map/Set representation. */
|
|
@@ -1974,7 +2082,8 @@ function deserializeFold(fold) {
|
|
|
1974
2082
|
turns: fold.turns,
|
|
1975
2083
|
perf: fold.perf,
|
|
1976
2084
|
roles: fold.roles,
|
|
1977
|
-
lastActive: fold.lastActive
|
|
2085
|
+
lastActive: fold.lastActive,
|
|
2086
|
+
...fold.title !== void 0 ? { title: fold.title } : {}
|
|
1978
2087
|
};
|
|
1979
2088
|
}
|
|
1980
2089
|
/** Runtime boundary for a user-editable/corrupt ledger file. Invalid rows are ignored. */
|
|
@@ -2097,13 +2206,16 @@ function freshFold() {
|
|
|
2097
2206
|
* @param fold - 累计目标(原地修改)。
|
|
2098
2207
|
* @param machine - 折叠状态机:进入时为上一批次末态,返回时为本批次末态。
|
|
2099
2208
|
* @param events - 本批次事件(日志顺序)。
|
|
2100
|
-
* @param subscriptionProviders -
|
|
2101
|
-
* @param officialProviderIds - 官方直连 provider 集合(undefined = 按
|
|
2209
|
+
* @param subscriptionProviders - 订阅套餐匹配器(显式集合或判定函数)。
|
|
2210
|
+
* @param officialProviderIds - 官方直连 provider 集合(undefined = 按 {@link officialChannelOf} 通道判定)。
|
|
2102
2211
|
* @param routes - 当前 provider 路由视图(中转站归组)。
|
|
2103
2212
|
* @param searchCallEstimateCny - 联网搜索请求的单次费用估算(人民币元)。
|
|
2104
|
-
* @param seedLength - fork 血缘边界(seq
|
|
2213
|
+
* @param seedLength - fork 血缘边界(seq 低于它的事件是父会话种子,跳过);
|
|
2214
|
+
* 来源为存储元数据的 `inheritedEventCount`。
|
|
2215
|
+
* @param routeAliases - 历史路由别名(旧路由名 → 当前路由名),改名/删除路由的
|
|
2216
|
+
* 历史用量按别名归位,不再落「未知路由」桶。
|
|
2105
2217
|
*/
|
|
2106
|
-
function foldInto(fold, machine, events, subscriptionProviders, officialProviderIds, routes, searchCallEstimateCny, seedLength) {
|
|
2218
|
+
function foldInto(fold, machine, events, subscriptionProviders, officialProviderIds, routes, searchCallEstimateCny, seedLength, routeAliases = {}) {
|
|
2107
2219
|
let key = machine.key;
|
|
2108
2220
|
let subscription = machine.subscription;
|
|
2109
2221
|
let official = machine.official;
|
|
@@ -2150,11 +2262,13 @@ function foldInto(fold, machine, events, subscriptionProviders, officialProvider
|
|
|
2150
2262
|
continue;
|
|
2151
2263
|
}
|
|
2152
2264
|
if (event.type === "request/header") {
|
|
2153
|
-
const { model, provider } = event.data.header.config;
|
|
2265
|
+
const { model, provider: rawProvider } = event.data.header.config;
|
|
2266
|
+
const provider = routeAliases[rawProvider] ?? rawProvider;
|
|
2154
2267
|
key = resolveCatalogKey(model);
|
|
2155
|
-
subscription = subscriptionProviders
|
|
2156
|
-
|
|
2157
|
-
|
|
2268
|
+
subscription = isSubscriptionCall(subscriptionProviders, provider);
|
|
2269
|
+
const siteRef = siteRefOf(provider, routes, { subscription });
|
|
2270
|
+
official = officialChannelOf(provider, siteRef, officialProviderIds);
|
|
2271
|
+
siteBucket = siteBucketKey(siteRef);
|
|
2158
2272
|
if (lastOpenStepKey !== void 0) {
|
|
2159
2273
|
const stepState = steps.get(lastOpenStepKey);
|
|
2160
2274
|
if (stepState !== void 0 && stepState.requestTime === void 0) stepState.requestTime = event.time;
|
|
@@ -2207,10 +2321,12 @@ function foldInto(fold, machine, events, subscriptionProviders, officialProvider
|
|
|
2207
2321
|
if (usage === void 0) continue;
|
|
2208
2322
|
const source = event.data.message?.source;
|
|
2209
2323
|
if (source?.kind === "model" && typeof source.provider === "string" && typeof source.model === "string") {
|
|
2324
|
+
const provider = routeAliases[source.provider] ?? source.provider;
|
|
2210
2325
|
key = resolveCatalogKey(source.model);
|
|
2211
|
-
subscription = subscriptionProviders
|
|
2212
|
-
|
|
2213
|
-
|
|
2326
|
+
subscription = isSubscriptionCall(subscriptionProviders, provider);
|
|
2327
|
+
const siteRef = siteRefOf(provider, routes, { subscription });
|
|
2328
|
+
official = officialChannelOf(provider, siteRef, officialProviderIds);
|
|
2329
|
+
siteBucket = siteBucketKey(siteRef);
|
|
2214
2330
|
}
|
|
2215
2331
|
const modelKey = key;
|
|
2216
2332
|
const day = dayStamp(event.time);
|
|
@@ -2338,14 +2454,29 @@ function percentile(values, p) {
|
|
|
2338
2454
|
return a + (b - a) * (idx - lo);
|
|
2339
2455
|
}
|
|
2340
2456
|
/**
|
|
2457
|
+
* 聚合配置指纹:影响折叠语义的全部配置(订阅豁免、官方名单、路由别名、搜索估值)
|
|
2458
|
+
* 的稳定序列化。账本行的复用判定携带该指纹——用户改配置后(如为 grok build 加
|
|
2459
|
+
* 订阅豁免),历史账本行立即失效并全量重折,配置变更即时生效(issue #37)。
|
|
2460
|
+
* 判定函数无法序列化,统一记为 `fn`(任何函数形态互视为同一指纹)。
|
|
2461
|
+
*/
|
|
2462
|
+
function configFingerprint(subscriptionMatcher, officialProviderIds, routeAliases, searchEstimate) {
|
|
2463
|
+
return [
|
|
2464
|
+
typeof subscriptionMatcher === "function" ? "fn" : [...subscriptionMatcher].sort().join(","),
|
|
2465
|
+
officialProviderIds === void 0 ? "" : [...officialProviderIds].sort().join(","),
|
|
2466
|
+
Object.keys(routeAliases).sort().map((key) => `${key}=${routeAliases[key]}`).join(","),
|
|
2467
|
+
String(searchEstimate)
|
|
2468
|
+
].join("|");
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2341
2471
|
* Create the incremental usage aggregator.
|
|
2342
2472
|
* @param persistence - the session persistence service.
|
|
2343
2473
|
* @param options - aggregation tuning (e.g. subscription-plan providers).
|
|
2344
2474
|
* @returns the aggregator holding the per-session fold cache.
|
|
2345
2475
|
*/
|
|
2346
2476
|
function createUsageAggregator(persistence, options = {}) {
|
|
2347
|
-
const
|
|
2477
|
+
const subscriptionMatcher = options.subscriptionProviders === void 0 ? new Set(DEFAULT_SUBSCRIPTION_PROVIDERS) : new Set(options.subscriptionProviders);
|
|
2348
2478
|
const officialProviderIds = options.officialProviderIds === void 0 ? void 0 : new Set(options.officialProviderIds);
|
|
2479
|
+
const routeAliases = options.routeAliases ?? {};
|
|
2349
2480
|
const maxCacheSessions = options.maxCacheSessions ?? 400;
|
|
2350
2481
|
const cache = /* @__PURE__ */ new Map();
|
|
2351
2482
|
const ledger = /* @__PURE__ */ new Map();
|
|
@@ -2360,6 +2491,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2360
2491
|
/** 每次聚合取最新的 provider 路由视图(中转站零配置发现);缺省按空处理(全部未知路由)。 */
|
|
2361
2492
|
const routesOf = () => options.resolveRoutes?.() ?? {};
|
|
2362
2493
|
const searchEstimate = options.searchCallEstimateCny ?? .02;
|
|
2494
|
+
const foldFingerprint = configFingerprint(subscriptionMatcher, officialProviderIds, routeAliases, searchEstimate);
|
|
2363
2495
|
const FOLD_CHUNK_EVENTS = 8e3;
|
|
2364
2496
|
/** 把一份(全量或增量)折叠结果登记进 durable ledger;内容无变化时不落盘。 */
|
|
2365
2497
|
const recordLedger = (id, cwd, stamp, fold) => {
|
|
@@ -2368,11 +2500,12 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2368
2500
|
id,
|
|
2369
2501
|
...cwd === void 0 ? {} : { cwd },
|
|
2370
2502
|
...stamp === null ? {} : { stamp },
|
|
2371
|
-
foldVersion:
|
|
2503
|
+
foldVersion: 13,
|
|
2504
|
+
fingerprint: foldFingerprint,
|
|
2372
2505
|
fold: serializeFold(fold)
|
|
2373
2506
|
};
|
|
2374
2507
|
const row = ledger.get(id);
|
|
2375
|
-
if (row === void 0 || row.stamp !== entry.stamp || row.cwd !== entry.cwd || row.foldVersion !== entry.foldVersion || stamp === null && JSON.stringify(row.fold) !== JSON.stringify(entry.fold)) {
|
|
2508
|
+
if (row === void 0 || row.stamp !== entry.stamp || row.cwd !== entry.cwd || row.foldVersion !== entry.foldVersion || row.fingerprint !== entry.fingerprint || stamp === null && JSON.stringify(row.fold) !== JSON.stringify(entry.fold)) {
|
|
2376
2509
|
ledger.set(id, entry);
|
|
2377
2510
|
ledgerNeedsSave = true;
|
|
2378
2511
|
}
|
|
@@ -2392,9 +2525,15 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2392
2525
|
console.warn("[usage-billing] failed to load durable usage ledger; rebuilding from current sessions:", error);
|
|
2393
2526
|
}
|
|
2394
2527
|
};
|
|
2395
|
-
/**
|
|
2396
|
-
*
|
|
2528
|
+
/** 失效键:宿主 0.1.3+ 走 adapter 暴露的 revision 令牌;0.1.2 形状退回
|
|
2529
|
+
* 日志文件 mtime+size(经 locate,拿不到或抛错时返回 null 让调用方重折)。
|
|
2530
|
+
* locate/stat 调用纳入 try,避免单个会话的异常把整份聚合拖垮。 */
|
|
2397
2531
|
const stampOf = async (meta) => {
|
|
2532
|
+
if (persistence.stampOf !== void 0) try {
|
|
2533
|
+
return await persistence.stampOf(meta.id);
|
|
2534
|
+
} catch {
|
|
2535
|
+
return null;
|
|
2536
|
+
}
|
|
2398
2537
|
try {
|
|
2399
2538
|
const location = persistence.locate?.(meta);
|
|
2400
2539
|
if (location === void 0) return null;
|
|
@@ -2458,7 +2597,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2458
2597
|
try {
|
|
2459
2598
|
const cwd = meta.cwd;
|
|
2460
2599
|
const ledgerRow = ledger.get(id);
|
|
2461
|
-
if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) ===
|
|
2600
|
+
if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) === 13 && ledgerRow.fingerprint === foldFingerprint) {
|
|
2462
2601
|
const fold = deserializeFold(ledgerRow.fold);
|
|
2463
2602
|
folds.push({
|
|
2464
2603
|
id,
|
|
@@ -2471,7 +2610,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2471
2610
|
const previous = cache.get(id);
|
|
2472
2611
|
if (previous?.machine !== void 0 && previous.lastSeq !== void 0) {
|
|
2473
2612
|
const from = previous.lastSeq + 1;
|
|
2474
|
-
const { events } = await persistence.readFrom(meta.id, from);
|
|
2613
|
+
const { events, inheritedEventCount } = await persistence.readFrom(meta.id, SessionLogOffset(from));
|
|
2475
2614
|
const after = await stampOf(meta);
|
|
2476
2615
|
if (stamp !== null && after !== stamp) {
|
|
2477
2616
|
cache.delete(id);
|
|
@@ -2482,10 +2621,9 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2482
2621
|
cache.delete(id);
|
|
2483
2622
|
continue;
|
|
2484
2623
|
}
|
|
2485
|
-
foldInto(previous.fold, previous.machine, events,
|
|
2624
|
+
foldInto(previous.fold, previous.machine, events, subscriptionMatcher, officialProviderIds, routesOf(), searchEstimate, inheritedEventCount, routeAliases);
|
|
2486
2625
|
refreshTurns(previous.fold, previous.machine);
|
|
2487
|
-
|
|
2488
|
-
previous.lastSeq = typeof last.seq === "number" ? last.seq : previous.lastSeq;
|
|
2626
|
+
previous.lastSeq = events[events.length - 1]?.seq ?? previous.lastSeq;
|
|
2489
2627
|
previous.stamp = stamp;
|
|
2490
2628
|
cache.delete(id);
|
|
2491
2629
|
cache.set(id, previous);
|
|
@@ -2498,13 +2636,13 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2498
2636
|
recordLedger(id, cwd, stamp, previous.fold);
|
|
2499
2637
|
continue;
|
|
2500
2638
|
}
|
|
2501
|
-
const { events } = await persistence.readFrom(meta.id, 0);
|
|
2639
|
+
const { events, inheritedEventCount } = await persistence.readFrom(meta.id, SessionLogOffset(0));
|
|
2502
2640
|
const after = await stampOf(meta);
|
|
2503
2641
|
if (stamp !== null && after !== stamp) continue;
|
|
2504
2642
|
const fold = freshFold();
|
|
2505
2643
|
const machine = freshMachine();
|
|
2506
2644
|
for (let start = 0; start < events.length; start += FOLD_CHUNK_EVENTS) {
|
|
2507
|
-
foldInto(fold, machine, events.slice(start, start + FOLD_CHUNK_EVENTS),
|
|
2645
|
+
foldInto(fold, machine, events.slice(start, start + FOLD_CHUNK_EVENTS), subscriptionMatcher, officialProviderIds, routesOf(), searchEstimate, inheritedEventCount, routeAliases);
|
|
2508
2646
|
if (start + FOLD_CHUNK_EVENTS < events.length) await new Promise((resolve) => {
|
|
2509
2647
|
setImmediate(resolve);
|
|
2510
2648
|
});
|
|
@@ -2541,7 +2679,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2541
2679
|
for (const entry of ledger.values()) {
|
|
2542
2680
|
if (included.has(entry.id)) continue;
|
|
2543
2681
|
try {
|
|
2544
|
-
const stale = (entry.foldVersion ?? 1) <
|
|
2682
|
+
const stale = (entry.foldVersion ?? 1) < 13 || entry.fingerprint !== foldFingerprint;
|
|
2545
2683
|
folds.push({
|
|
2546
2684
|
id: entry.id,
|
|
2547
2685
|
...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
|
|
@@ -2577,7 +2715,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2577
2715
|
outputCost: 0
|
|
2578
2716
|
};
|
|
2579
2717
|
const perfModel = /* @__PURE__ */ new Map();
|
|
2580
|
-
const
|
|
2718
|
+
const perfHourModel = /* @__PURE__ */ new Map();
|
|
2581
2719
|
for (const { id: sessionId, cwd, fold, staleLedger } of folds) {
|
|
2582
2720
|
mergeUsageInto(total, fold.total);
|
|
2583
2721
|
roles.userChars += fold.roles.userChars;
|
|
@@ -2608,13 +2746,18 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2608
2746
|
if (sample.tps !== void 0) modelAccum.tps.push(sample.tps);
|
|
2609
2747
|
if (sample.latencyMs !== void 0) modelAccum.latencies.push(sample.latencyMs);
|
|
2610
2748
|
if (sample.estimated) modelAccum.estimated += 1;
|
|
2611
|
-
let
|
|
2749
|
+
let hourModels = perfHourModel.get(sample.hour);
|
|
2750
|
+
if (hourModels === void 0) {
|
|
2751
|
+
hourModels = /* @__PURE__ */ new Map();
|
|
2752
|
+
perfHourModel.set(sample.hour, hourModels);
|
|
2753
|
+
}
|
|
2754
|
+
let hourAccum = hourModels.get(sample.model);
|
|
2612
2755
|
if (hourAccum === void 0) {
|
|
2613
2756
|
hourAccum = {
|
|
2614
2757
|
ttfts: [],
|
|
2615
2758
|
tps: []
|
|
2616
2759
|
};
|
|
2617
|
-
|
|
2760
|
+
hourModels.set(sample.model, hourAccum);
|
|
2618
2761
|
}
|
|
2619
2762
|
hourAccum.ttfts.push(sample.ttftMs);
|
|
2620
2763
|
if (sample.tps !== void 0) hourAccum.tps.push(sample.tps);
|
|
@@ -2674,14 +2817,14 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2674
2817
|
latencyAvg: acc.latencies.length === 0 ? 0 : mean(acc.latencies),
|
|
2675
2818
|
estimatedSamples: acc.estimated
|
|
2676
2819
|
}])),
|
|
2677
|
-
|
|
2820
|
+
byHourModel: Object.fromEntries([...perfHourModel].map(([hour, models]) => [hour, Object.fromEntries([...models].map(([model, acc]) => [model, {
|
|
2678
2821
|
samples: acc.ttfts.length,
|
|
2679
2822
|
ttftAvg: mean(acc.ttfts),
|
|
2680
2823
|
...acc.tps.length === 0 ? {} : { tpsAvg: mean(acc.tps) }
|
|
2681
|
-
}]))
|
|
2824
|
+
}]))]))
|
|
2682
2825
|
};
|
|
2683
2826
|
lastDoc = {
|
|
2684
|
-
version:
|
|
2827
|
+
version: 4,
|
|
2685
2828
|
updatedAt: now,
|
|
2686
2829
|
source: "session-logs",
|
|
2687
2830
|
timezone: hostTimeZone(),
|
|
@@ -2804,6 +2947,186 @@ function createCooldownGate(options = {}) {
|
|
|
2804
2947
|
};
|
|
2805
2948
|
}
|
|
2806
2949
|
//#endregion
|
|
2950
|
+
//#region lib/types/tc3.js
|
|
2951
|
+
/**
|
|
2952
|
+
* Tencent Cloud TokenHub control-plane access (cloud API 3.0, TC3-HMAC-SHA256).
|
|
2953
|
+
*
|
|
2954
|
+
* Shared by two surfaces that read the same Token Plan:
|
|
2955
|
+
* - `balance.ts` — the balance row (absolute remaining quota);
|
|
2956
|
+
* - `subscriptions.ts` — the subscription card (monthly window + plan name).
|
|
2957
|
+
*
|
|
2958
|
+
* Credentials are a cloud API key pair (`<SecretId>:<SecretKey>`), NOT the
|
|
2959
|
+
* TokenHub inference key. Field names on the control plane are not fully
|
|
2960
|
+
* stable, so parsing stays defensive (semantic-key scanning).
|
|
2961
|
+
*/
|
|
2962
|
+
/** TokenHub 管控面 API 端点(cloud.tencent.cn/document/api/1823/132270)。 */
|
|
2963
|
+
const TOKENHUB_HOST = "tokenhub.tencentcloudapi.com";
|
|
2964
|
+
/** 云 API 3.0 产品名与版本(签名的 service 段与请求头都必须一致)。 */
|
|
2965
|
+
const TOKENHUB_SERVICE = "tokenhub";
|
|
2966
|
+
const TOKENHUB_VERSION = "2026-03-22";
|
|
2967
|
+
/** 请求地域:管控面对地域不敏感,取默认国内地域(文档地域列表含 ap-guangzhou)。 */
|
|
2968
|
+
const TOKENHUB_REGION = "ap-guangzhou";
|
|
2969
|
+
/** 管控面调用超时(毫秒):余额与订阅面板共用同一预算。 */
|
|
2970
|
+
const TOKENHUB_TIMEOUT_MS = 8e3;
|
|
2971
|
+
/** 腾讯云凭据引用值格式:`<SecretId>:<SecretKey>`(分隔符取首个冒号)。 */
|
|
2972
|
+
function parseTencentCredential(value) {
|
|
2973
|
+
const sep = value.indexOf(":");
|
|
2974
|
+
if (sep === -1) return void 0;
|
|
2975
|
+
const secretId = value.slice(0, sep).trim();
|
|
2976
|
+
const secretKey = value.slice(sep + 1).trim();
|
|
2977
|
+
if (secretId === "" || secretKey === "") return void 0;
|
|
2978
|
+
return {
|
|
2979
|
+
secretId,
|
|
2980
|
+
secretKey
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
/**
|
|
2984
|
+
* 构造云 API 3.0 TC3-HMAC-SHA256 签名(官方签名方法 v3)。导出供测试:纯函数,
|
|
2985
|
+
* 输入确定则签名确定。Action 不参与签名——它走 `X-TC-Action` 请求头。
|
|
2986
|
+
* @param secretId - 云 API SecretId。
|
|
2987
|
+
* @param secretKey - 云 API SecretKey。
|
|
2988
|
+
* @param payload - 已序列化的请求体(含 Action/Version/Region 公共参数)。
|
|
2989
|
+
* @param timestamp - 签名时间戳(秒)。
|
|
2990
|
+
* @returns Authorization 头的值。
|
|
2991
|
+
*/
|
|
2992
|
+
function tc3Authorization(secretId, secretKey, payload, timestamp) {
|
|
2993
|
+
const date = (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString().slice(0, 10);
|
|
2994
|
+
const canonicalRequest = `POST\n/\n\n${`content-type:application/json; charset=utf-8\nhost:${TOKENHUB_HOST}\n`}\ncontent-type;host\n${createHash("sha256").update(payload).digest("hex")}`;
|
|
2995
|
+
const hashedCanonical = createHash("sha256").update(canonicalRequest).digest("hex");
|
|
2996
|
+
const stringToSign = `TC3-HMAC-SHA256\n${String(timestamp)}\n${date}/${TOKENHUB_SERVICE}/tc3_request\n${hashedCanonical}`;
|
|
2997
|
+
const kDate = createHmac("sha256", date).update(secretKey).digest();
|
|
2998
|
+
const kService = createHmac("sha256", kDate).update(TOKENHUB_SERVICE).digest();
|
|
2999
|
+
const kSigning = createHmac("sha256", kService).update("tc3_request").digest();
|
|
3000
|
+
const signature = createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
3001
|
+
return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${signature}`;
|
|
3002
|
+
}
|
|
3003
|
+
/**
|
|
3004
|
+
* 调用一次 TokenHub 管控面接口:TC3 签名 + 超时保护,返回响应 JSON 的 `Response`。
|
|
3005
|
+
* 业务错误(Response.Error)与 HTTP 层错误都带 `code` 抛出,调用方按
|
|
3006
|
+
* unauthorized / unreachable / invalid 归类。
|
|
3007
|
+
*/
|
|
3008
|
+
async function callTokenHub(secretId, secretKey, action, params) {
|
|
3009
|
+
const payload = JSON.stringify({
|
|
3010
|
+
Action: action,
|
|
3011
|
+
Version: TOKENHUB_VERSION,
|
|
3012
|
+
Region: TOKENHUB_REGION,
|
|
3013
|
+
...params
|
|
3014
|
+
});
|
|
3015
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
3016
|
+
const controller = new AbortController();
|
|
3017
|
+
const timer = setTimeout(() => controller.abort(), TOKENHUB_TIMEOUT_MS);
|
|
3018
|
+
try {
|
|
3019
|
+
const response = await fetch(`https://${TOKENHUB_HOST}/`, {
|
|
3020
|
+
method: "POST",
|
|
3021
|
+
headers: {
|
|
3022
|
+
"content-type": "application/json; charset=utf-8",
|
|
3023
|
+
host: TOKENHUB_HOST,
|
|
3024
|
+
"x-tc-action": action.toLowerCase(),
|
|
3025
|
+
"x-tc-version": TOKENHUB_VERSION,
|
|
3026
|
+
"x-tc-region": TOKENHUB_REGION,
|
|
3027
|
+
"x-tc-timestamp": String(timestamp),
|
|
3028
|
+
authorization: tc3Authorization(secretId, secretKey, payload, timestamp)
|
|
3029
|
+
},
|
|
3030
|
+
body: payload,
|
|
3031
|
+
signal: controller.signal
|
|
3032
|
+
});
|
|
3033
|
+
if (response.status === 401 || response.status === 403) throw Object.assign(/* @__PURE__ */ new Error("unauthorized"), { code: "unauthorized" });
|
|
3034
|
+
if (!response.ok) throw Object.assign(/* @__PURE__ */ new Error(`HTTP ${String(response.status)}`), {
|
|
3035
|
+
httpStatus: response.status,
|
|
3036
|
+
code: "unreachable"
|
|
3037
|
+
});
|
|
3038
|
+
const inner = (await response.json()).Response;
|
|
3039
|
+
if (inner === void 0) throw Object.assign(/* @__PURE__ */ new Error("no Response envelope"), { code: "invalid" });
|
|
3040
|
+
if (inner.Error !== void 0 && inner.Error !== null) {
|
|
3041
|
+
const err = inner.Error;
|
|
3042
|
+
const code = err.Code === "AuthFailure.SignatureFailure" || err.Code === "AuthFailure.SecretIdNotFound" ? "unauthorized" : "unreachable";
|
|
3043
|
+
throw Object.assign(new Error(String(err.Code ?? "api-error")), { code });
|
|
3044
|
+
}
|
|
3045
|
+
return inner;
|
|
3046
|
+
} finally {
|
|
3047
|
+
clearTimeout(timer);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
/** 数字归一化:上游可能给字符串数字;非有限数返回 undefined。 */
|
|
3051
|
+
function toNumber$1(value) {
|
|
3052
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
3053
|
+
if (typeof value === "string") {
|
|
3054
|
+
const parsed = Number(value);
|
|
3055
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
/**
|
|
3059
|
+
* 在套餐余量对象里防御性提取「剩余额度」:官方 SubPackageBalance/PackageInfo
|
|
3060
|
+
* 的字段名未稳定公开(issue #18 调研期),按语义键名扫描——命中 remaining /
|
|
3061
|
+
* balance / left 语义键直接用;命中 total 与 used 则相减推导。数字一律经
|
|
3062
|
+
* {@link toNumber} 归一化(上游可能给字符串)。
|
|
3063
|
+
* 导出供测试:纯函数。
|
|
3064
|
+
* @param source - 套餐详情里的余量对象(PackageInfo / SubPackageBalance 等)。
|
|
3065
|
+
* @returns 剩余额度(上游单位,通常为 token 数或元);提取不到返回 undefined。
|
|
3066
|
+
*/
|
|
3067
|
+
function pickRemainingQuota(source) {
|
|
3068
|
+
if (source === null || typeof source !== "object") return void 0;
|
|
3069
|
+
const numeric = Object.entries(source).filter(([, v]) => v !== null && toNumber$1(v) !== void 0);
|
|
3070
|
+
const byKey = (needles) => {
|
|
3071
|
+
for (const [key, value] of numeric) {
|
|
3072
|
+
const lower = key.toLowerCase();
|
|
3073
|
+
if (needles.some((n) => lower.includes(n))) {
|
|
3074
|
+
const num = toNumber$1(value);
|
|
3075
|
+
if (num !== void 0 && num >= 0) return num;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
};
|
|
3079
|
+
const remaining = byKey([
|
|
3080
|
+
"remain",
|
|
3081
|
+
"balance",
|
|
3082
|
+
"left",
|
|
3083
|
+
"available"
|
|
3084
|
+
]);
|
|
3085
|
+
if (remaining !== void 0) return remaining;
|
|
3086
|
+
const total = byKey(["total"]);
|
|
3087
|
+
const used = byKey(["used", "consume"]);
|
|
3088
|
+
if (total !== void 0 && used !== void 0) return Math.max(0, total - used);
|
|
3089
|
+
}
|
|
3090
|
+
/**
|
|
3091
|
+
* 防御性提取「总额度」:语义键 total / limit / quota。与
|
|
3092
|
+
* {@link pickRemainingQuota} 配对使用——两者都能取到时订阅卡才能算出
|
|
3093
|
+
* 已用百分比窗口;只有剩余值时窗口保持为空(绝不猜总额度)。
|
|
3094
|
+
* 注意排除键名:`RemainingQuota` / `UsedQuota` 这类键同样含 quota 子串,
|
|
3095
|
+
* 必须先剔除剩余 / 已用语义,否则会把剩余值误当总额度(有单测守卫)。
|
|
3096
|
+
* @param source - 套餐详情里的额度对象(PackageInfo / SubPackageBalance 等)。
|
|
3097
|
+
*/
|
|
3098
|
+
function pickTotalQuota(source) {
|
|
3099
|
+
if (source === null || typeof source !== "object") return void 0;
|
|
3100
|
+
const forbidden = [
|
|
3101
|
+
"remain",
|
|
3102
|
+
"used",
|
|
3103
|
+
"consume",
|
|
3104
|
+
"left",
|
|
3105
|
+
"available",
|
|
3106
|
+
"balance"
|
|
3107
|
+
];
|
|
3108
|
+
for (const [key, value] of Object.entries(source)) {
|
|
3109
|
+
const lower = key.toLowerCase();
|
|
3110
|
+
if (!lower.includes("total") && !lower.includes("limit") && !lower.includes("quota")) continue;
|
|
3111
|
+
if (forbidden.some((marker) => lower.includes(marker))) continue;
|
|
3112
|
+
const num = toNumber$1(value);
|
|
3113
|
+
if (num !== void 0 && num > 0) return num;
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
/** 套餐列表里提取第一个启用套餐的 TeamId:集合字段名做候选兼容。 */
|
|
3117
|
+
function firstEnabledTeamId(inner) {
|
|
3118
|
+
const candidates = inner.TeamSet ?? inner.TokenPlanSet ?? inner.PlanSet;
|
|
3119
|
+
if (!Array.isArray(candidates)) return void 0;
|
|
3120
|
+
for (const item of candidates) {
|
|
3121
|
+
if (item === null || typeof item !== "object") continue;
|
|
3122
|
+
const row = item;
|
|
3123
|
+
if (row.TeamId === void 0 && row.PlanId === void 0) continue;
|
|
3124
|
+
if (row.Status !== void 0 && row.Status !== "enable") continue;
|
|
3125
|
+
const id = row.TeamId ?? row.PlanId;
|
|
3126
|
+
if (typeof id === "string" && id !== "") return id;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
//#endregion
|
|
2807
3130
|
//#region lib/types/balance.js
|
|
2808
3131
|
/**
|
|
2809
3132
|
* Account-balance queries for the billing dashboard.
|
|
@@ -2848,13 +3171,6 @@ const XAI_CREDITS_URL = "https://api.x.ai/v1/billing/credits";
|
|
|
2848
3171
|
/** 智谱 GLM(大模型国内域)官方余额接口(open.bigmodel.cn/api/paas/v4/balance)。 */
|
|
2849
3172
|
const ZHIPU_BALANCE_URL = "https://open.bigmodel.cn/api/paas/v4/balance";
|
|
2850
3173
|
/** 数字归一化:接口返回的余额是字符串(如 `"110.00"`),统一转 number。 */
|
|
2851
|
-
function toNumber$1(value) {
|
|
2852
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2853
|
-
if (typeof value === "string") {
|
|
2854
|
-
const parsed = Number(value);
|
|
2855
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
2858
3174
|
/**
|
|
2859
3175
|
* Fetch a Bearer-protected balance endpoint and normalize the HTTP outcome into
|
|
2860
3176
|
* a shared {@link ProviderBalance} row. Each provider supplies its own
|
|
@@ -3139,6 +3455,26 @@ const QUERIERS = [
|
|
|
3139
3455
|
route: "tokendance-space",
|
|
3140
3456
|
displayName: "TokenDance",
|
|
3141
3457
|
querier: queryTokenDance
|
|
3458
|
+
},
|
|
3459
|
+
{
|
|
3460
|
+
route: "tencent-tokenhub",
|
|
3461
|
+
displayName: "腾讯云 TokenHub",
|
|
3462
|
+
querier: queryTencentTokenPlan
|
|
3463
|
+
},
|
|
3464
|
+
{
|
|
3465
|
+
route: "tokenhub",
|
|
3466
|
+
displayName: "腾讯云 TokenHub",
|
|
3467
|
+
querier: queryTencentTokenPlan
|
|
3468
|
+
},
|
|
3469
|
+
{
|
|
3470
|
+
route: "tencent",
|
|
3471
|
+
displayName: "腾讯云 TokenHub",
|
|
3472
|
+
querier: queryTencentTokenPlan
|
|
3473
|
+
},
|
|
3474
|
+
{
|
|
3475
|
+
route: "tencentcloud",
|
|
3476
|
+
displayName: "腾讯云 TokenHub",
|
|
3477
|
+
querier: queryTencentTokenPlan
|
|
3142
3478
|
}
|
|
3143
3479
|
];
|
|
3144
3480
|
/**
|
|
@@ -3171,6 +3507,77 @@ async function queryBalances(ctx, providers) {
|
|
|
3171
3507
|
return querier(ctx, env);
|
|
3172
3508
|
}));
|
|
3173
3509
|
}
|
|
3510
|
+
/**
|
|
3511
|
+
* 查询腾讯云 TokenHub Token Plan 套餐余量。凭据值格式 `<SecretId>:<SecretKey>`
|
|
3512
|
+
* (云 API 密钥,非 TokenHub 推理 key)。链路:套餐列表取 TeamId → 套餐详情读
|
|
3513
|
+
* 主额度包余量。管控面字段名未完全稳定,解析按语义键防御提取。
|
|
3514
|
+
* @param ctx - host context carrying the credentials seam.
|
|
3515
|
+
* @param apiKeyEnv - credential reference resolving the `<SecretId>:<SecretKey>` pair.
|
|
3516
|
+
*/
|
|
3517
|
+
async function queryTencentTokenPlan(ctx, apiKeyEnv) {
|
|
3518
|
+
const provider = "腾讯云 TokenHub";
|
|
3519
|
+
const hit = await ctx.credentials.resolve(credentialRef(apiKeyEnv));
|
|
3520
|
+
if (hit === void 0) return {
|
|
3521
|
+
provider,
|
|
3522
|
+
displayName: provider,
|
|
3523
|
+
error: "unconfigured"
|
|
3524
|
+
};
|
|
3525
|
+
if (!balanceGate.check(provider)) return {
|
|
3526
|
+
provider,
|
|
3527
|
+
displayName: provider,
|
|
3528
|
+
error: "unreachable"
|
|
3529
|
+
};
|
|
3530
|
+
const credential = parseTencentCredential(hit.value);
|
|
3531
|
+
if (credential === void 0) return {
|
|
3532
|
+
provider,
|
|
3533
|
+
displayName: provider,
|
|
3534
|
+
error: "unauthorized"
|
|
3535
|
+
};
|
|
3536
|
+
const doRequest = async () => {
|
|
3537
|
+
const teamId = firstEnabledTeamId(await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlanList", {}));
|
|
3538
|
+
if (teamId === void 0) return {
|
|
3539
|
+
provider,
|
|
3540
|
+
displayName: provider,
|
|
3541
|
+
error: "invalid"
|
|
3542
|
+
};
|
|
3543
|
+
const detail = await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlan", { TeamId: teamId });
|
|
3544
|
+
const remaining = pickRemainingQuota(detail.PackageInfo) ?? pickRemainingQuota(detail);
|
|
3545
|
+
if (remaining === void 0) {
|
|
3546
|
+
console.warn(`[usage-billing] balance response drifted for ${provider}: no remaining-quota field parsed`);
|
|
3547
|
+
return {
|
|
3548
|
+
provider,
|
|
3549
|
+
displayName: provider,
|
|
3550
|
+
error: "invalid"
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
const plan = typeof detail.Name === "string" ? detail.Name : void 0;
|
|
3554
|
+
const exhausted = detail.StopReason === "EXHAUSTED";
|
|
3555
|
+
return {
|
|
3556
|
+
provider,
|
|
3557
|
+
displayName: provider,
|
|
3558
|
+
currency: "CNY",
|
|
3559
|
+
totalBalance: remaining,
|
|
3560
|
+
...plan !== void 0 ? { plan } : {},
|
|
3561
|
+
...exhausted ? { isAvailable: false } : {}
|
|
3562
|
+
};
|
|
3563
|
+
};
|
|
3564
|
+
try {
|
|
3565
|
+
const row = await withRetry(doRequest, {
|
|
3566
|
+
retries: 1,
|
|
3567
|
+
baseDelayMs: 250,
|
|
3568
|
+
maxDelayMs: 2e3
|
|
3569
|
+
});
|
|
3570
|
+
balanceGate.success(provider);
|
|
3571
|
+
return row;
|
|
3572
|
+
} catch (error) {
|
|
3573
|
+
balanceGate.fail(provider);
|
|
3574
|
+
return {
|
|
3575
|
+
provider,
|
|
3576
|
+
displayName: provider,
|
|
3577
|
+
error: error.code === "unauthorized" ? "unauthorized" : "unreachable"
|
|
3578
|
+
};
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3174
3581
|
/** 点路径取值:`data.total_available` → 逐层下钻;任一缺失返回 undefined。 */
|
|
3175
3582
|
function getPath(data, path) {
|
|
3176
3583
|
let cursor = data;
|
|
@@ -3987,6 +4394,7 @@ const EMPTY_SUBSCRIPTION_KEYS = {
|
|
|
3987
4394
|
opencodeApiKey: "",
|
|
3988
4395
|
minmaxApiKey: "",
|
|
3989
4396
|
openrouterApiKey: "",
|
|
4397
|
+
tencentCloudApi: "",
|
|
3990
4398
|
zaiRegion: "global"
|
|
3991
4399
|
};
|
|
3992
4400
|
/** 订阅类 provider 的显示名(未命中的回退为 id 本身)。 */
|
|
@@ -4011,10 +4419,12 @@ const SUBSCRIPTION_DISPLAY_NAMES = {
|
|
|
4011
4419
|
"minimax-token-plan": "MiniMax Token Plan",
|
|
4012
4420
|
"minimax-token-plan-cn": "MiniMax Token Plan(国内)",
|
|
4013
4421
|
"minimax-cn": "MiniMax Token Plan(国内)",
|
|
4014
|
-
"openrouter": "OpenRouter"
|
|
4422
|
+
"openrouter": "OpenRouter",
|
|
4423
|
+
"tencent-token-plan": "腾讯云 Token Plan",
|
|
4424
|
+
"grok": "Grok(X Premium)"
|
|
4015
4425
|
};
|
|
4016
4426
|
/** 订阅类 provider id 判定:带 coding / agent-plan / token-plan 后缀,或已知订阅通道。 */
|
|
4017
|
-
const SUBSCRIPTION_ID_RE = /* @__PURE__ */ new RegExp("(?:^|-)(?:coding|agent[-_]?plan|token[-_]?plan)(?:$|-|_)|^(?:opencode|opencode-go|kimi-coding|zai-coding|minimax|minimax-cn|minimax-token-plan|minimax-token-plan-cn|openrouter)", "i");
|
|
4427
|
+
const SUBSCRIPTION_ID_RE = /* @__PURE__ */ new RegExp("(?:^|-)(?:coding|agent[-_]?plan|token[-_]?plan)(?:$|-|_)|^(?:opencode|opencode-go|kimi-coding|zai-coding|minimax|minimax-cn|minimax-token-plan|minimax-token-plan-cn|openrouter|grok)", "i");
|
|
4018
4428
|
/** 是否是订阅类 provider id(如 kimi-coding、xiaomi-token-plan-cn)。 */
|
|
4019
4429
|
function isSubscriptionProviderId(providerId) {
|
|
4020
4430
|
if (SUBSCRIPTION_ID_RE.test(providerId)) return true;
|
|
@@ -4030,7 +4440,8 @@ const SUBSCRIPTION_ADAPTERS = {
|
|
|
4030
4440
|
"minimax-cn": { collect: collectMiniMax },
|
|
4031
4441
|
"minimax-token-plan": { collect: collectMiniMax },
|
|
4032
4442
|
"minimax-token-plan-cn": { collect: collectMiniMax },
|
|
4033
|
-
"openrouter": { collect: collectOpenRouter }
|
|
4443
|
+
"openrouter": { collect: collectOpenRouter },
|
|
4444
|
+
"tencent-token-plan": { collect: collectTencentTokenPlan }
|
|
4034
4445
|
};
|
|
4035
4446
|
/** 有额度适配器的 provider id 集合(识别用)。 */
|
|
4036
4447
|
const ADAPTER_PROVIDER_IDS = new Set(Object.keys(SUBSCRIPTION_ADAPTERS));
|
|
@@ -4527,6 +4938,48 @@ async function collectOpenRouter(keys, config, timeoutMs) {
|
|
|
4527
4938
|
}
|
|
4528
4939
|
}
|
|
4529
4940
|
/**
|
|
4941
|
+
* 腾讯云 Token Plan(TokenHub 管控面)订阅额度。凭据是云 API 密钥对
|
|
4942
|
+
* `<SecretId>:<SecretKey>`(与余额面板同源、同格式),链路与 balance.ts 的
|
|
4943
|
+
* TokenHub 查询一致:DescribeTokenPlanList 取 TeamId → DescribeTokenPlan 读
|
|
4944
|
+
* 主额度包。管控面字段名未完全稳定:剩余与总额度都能取到才产出百分比窗口,
|
|
4945
|
+
* 只有剩余值时窗口留空(绝不猜总额度),plan 名照常展示。
|
|
4946
|
+
*/
|
|
4947
|
+
async function collectTencentTokenPlan(keys, _config, _timeoutMs) {
|
|
4948
|
+
const provider = "tencent-token-plan";
|
|
4949
|
+
const displayName = SUBSCRIPTION_DISPLAY_NAMES[provider] ?? provider;
|
|
4950
|
+
const hint = "凭据需为腾讯云云 API 密钥对(SecretId:SecretKey,控制台访问管理获取),非 TokenHub 推理 key";
|
|
4951
|
+
const quota = (partial) => ({
|
|
4952
|
+
provider,
|
|
4953
|
+
displayName,
|
|
4954
|
+
windows: [],
|
|
4955
|
+
hint,
|
|
4956
|
+
...partial
|
|
4957
|
+
});
|
|
4958
|
+
if (keys.tencentCloudApi === "") return quota({ status: "not-configured" });
|
|
4959
|
+
const credential = parseTencentCredential(keys.tencentCloudApi);
|
|
4960
|
+
if (credential === void 0) return quota({ status: "unauthorized" });
|
|
4961
|
+
try {
|
|
4962
|
+
const teamId = firstEnabledTeamId(await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlanList", {}));
|
|
4963
|
+
if (teamId === void 0) return quota({ status: "invalid-response" });
|
|
4964
|
+
const detail = await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlan", { TeamId: teamId });
|
|
4965
|
+
const remaining = pickRemainingQuota(detail.PackageInfo) ?? pickRemainingQuota(detail);
|
|
4966
|
+
const total = pickTotalQuota(detail.PackageInfo) ?? pickTotalQuota(detail);
|
|
4967
|
+
const plan = typeof detail.Name === "string" ? detail.Name : void 0;
|
|
4968
|
+
return quota({
|
|
4969
|
+
status: "ok",
|
|
4970
|
+
windows: remaining !== void 0 && total !== void 0 && total > 0 ? [{
|
|
4971
|
+
kind: "monthly",
|
|
4972
|
+
usedPercent: round1$1(clampPercent$1((1 - remaining / total) * 100) ?? 0),
|
|
4973
|
+
remainingPercent: round1$1(clampPercent$1(remaining / total * 100) ?? 0),
|
|
4974
|
+
remaining
|
|
4975
|
+
}] : [],
|
|
4976
|
+
...plan !== void 0 ? { plan } : {}
|
|
4977
|
+
});
|
|
4978
|
+
} catch (error) {
|
|
4979
|
+
return quota({ status: statusOf$1(error) });
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
/**
|
|
4530
4983
|
* Collect quota for the given plans concurrently (adapter-backed plans only;
|
|
4531
4984
|
* identified plans without an adapter are surfaced by the caller as "no
|
|
4532
4985
|
* quota API" rows).
|
|
@@ -5085,6 +5538,10 @@ const SUBSCRIPTION_KEY_SOURCES = [
|
|
|
5085
5538
|
{
|
|
5086
5539
|
provider: "openrouter",
|
|
5087
5540
|
key: "openrouterApiKey"
|
|
5541
|
+
},
|
|
5542
|
+
{
|
|
5543
|
+
provider: "tencent-token-plan",
|
|
5544
|
+
key: "tencentCloudApi"
|
|
5088
5545
|
}
|
|
5089
5546
|
];
|
|
5090
5547
|
/** 读 llm-pi-ai 设置的 `providers` 字典(`<route> → { apiKeyEnv?, baseURL?, displayName? }`)。
|
|
@@ -5208,6 +5665,44 @@ async function readOpenCodeToken() {
|
|
|
5208
5665
|
return "";
|
|
5209
5666
|
}
|
|
5210
5667
|
/**
|
|
5668
|
+
* 宿主 persistence 形状适配。宿主 0.1.3 起 SessionPersistence 改为
|
|
5669
|
+
* SessionHandle 模型(open(id,'read') 后经 handle.read(offset) 读,fork 边界
|
|
5670
|
+
* 挂在 handle.inheritedEventCount,list 返回 {header, revision} 快照行,
|
|
5671
|
+
* 0.1.2 的 readFrom/locate 消失)。这里按结构探测把两种宿主形状都收敛为
|
|
5672
|
+
* 聚合层期望的 0.1.2 面貌:0.1.2 直接带 readFrom 的原样直通;0.1.3 的
|
|
5673
|
+
* 读取转为 open → handle.read,revision 令牌经 stampOf 暴露给增量缓存。
|
|
5674
|
+
* 候选时刻的运行时对象是宿主注入的外部形状,结构断言即 durable 收窄点。
|
|
5675
|
+
* 导出供测试:纯函数(不触 ctx)。
|
|
5676
|
+
*/
|
|
5677
|
+
function adaptSessionPersistence(raw) {
|
|
5678
|
+
if (typeof raw?.readFrom === "function") return raw;
|
|
5679
|
+
const host = raw;
|
|
5680
|
+
const revisions = /* @__PURE__ */ new Map();
|
|
5681
|
+
return {
|
|
5682
|
+
list: async () => {
|
|
5683
|
+
const snapshots = await host.list();
|
|
5684
|
+
revisions.clear();
|
|
5685
|
+
for (const snapshot of snapshots) revisions.set(String(snapshot.header.id), String(snapshot.revision));
|
|
5686
|
+
return snapshots.map((snapshot) => snapshot.header);
|
|
5687
|
+
},
|
|
5688
|
+
readFrom: async (id, fromSeq) => {
|
|
5689
|
+
const handle = await host.open(id, "read");
|
|
5690
|
+
try {
|
|
5691
|
+
const events = await handle.read(fromSeq);
|
|
5692
|
+
return {
|
|
5693
|
+
meta: handle.header,
|
|
5694
|
+
fromSeq: SessionLogOffset(fromSeq),
|
|
5695
|
+
inheritedEventCount: SessionLogOffset(handle.inheritedEventCount),
|
|
5696
|
+
events
|
|
5697
|
+
};
|
|
5698
|
+
} finally {
|
|
5699
|
+
await handle[Symbol.asyncDispose]?.();
|
|
5700
|
+
}
|
|
5701
|
+
},
|
|
5702
|
+
stampOf: async (id) => revisions.get(String(id)) ?? null
|
|
5703
|
+
};
|
|
5704
|
+
}
|
|
5705
|
+
/**
|
|
5211
5706
|
* Host plugin body: serve real aggregated usage to the browser dashboard.
|
|
5212
5707
|
* @param ctx - host context carrying webServer and sessionPersistence.
|
|
5213
5708
|
* @param config - optional statsPath override.
|
|
@@ -5237,8 +5732,10 @@ function apply(ctx, config = {}) {
|
|
|
5237
5732
|
}).catch(() => {});
|
|
5238
5733
|
};
|
|
5239
5734
|
const workspaceTitleResolver = buildWorkspaceTitleResolver(ctx);
|
|
5240
|
-
|
|
5735
|
+
applyUserModelAliases(config.modelKeyAliases);
|
|
5736
|
+
const aggregator = createUsageAggregator(adaptSessionPersistence(ctx.sessionPersistence), {
|
|
5241
5737
|
...config.subscriptionProviders === void 0 ? {} : { subscriptionProviders: config.subscriptionProviders },
|
|
5738
|
+
...config.routeAliases === void 0 ? {} : { routeAliases: config.routeAliases },
|
|
5242
5739
|
resolveRoutes: () => readPiAiProviderRoutes(ctx.settings),
|
|
5243
5740
|
...workspaceTitleResolver === void 0 ? {} : { resolveWorkspaceTitle: workspaceTitleResolver },
|
|
5244
5741
|
...config.searchCallEstimateCny === void 0 ? {} : { searchCallEstimateCny: config.searchCallEstimateCny },
|
|
@@ -5764,4 +6261,4 @@ function apply(ctx, config = {}) {
|
|
|
5764
6261
|
}), "usage-billing: usage-stats route");
|
|
5765
6262
|
}
|
|
5766
6263
|
//#endregion
|
|
5767
|
-
export { apply, createFileUsageLedgerStore, guardLoopback, inject, readPiAiProviderRoutes, resolveSubscriptionKeys };
|
|
6264
|
+
export { adaptSessionPersistence, apply, createFileUsageLedgerStore, guardLoopback, inject, readPiAiProviderRoutes, resolveSubscriptionKeys };
|