@kenz1117/dsh-ui-usage-billing 1.6.2 → 1.6.4
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 +58 -40
- package/README.md +36 -27
- package/lib/index.js +300 -153
- package/lib/types/balance.d.ts +4 -2
- package/lib/types/config.d.ts +21 -0
- package/lib/types/declarative.d.ts +3 -2
- package/lib/types/index.d.ts +9 -5
- package/lib/types/relay.d.ts +9 -8
- package/lib/types/route-keys.d.ts +20 -0
- package/lib/types/routes/usage-tool.d.ts +2 -0
- package/lib/types/subscriptions.d.ts +32 -5
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1311,6 +1311,18 @@ const BUILTIN_MODEL_CATALOG = [
|
|
|
1311
1311
|
output: 16.8
|
|
1312
1312
|
}
|
|
1313
1313
|
},
|
|
1314
|
+
{
|
|
1315
|
+
key: "minimax-m3.1-flash",
|
|
1316
|
+
name: "MiniMax-M3.1-Flash",
|
|
1317
|
+
provider: "MiniMax",
|
|
1318
|
+
colorVar: "ds-amber",
|
|
1319
|
+
price: {
|
|
1320
|
+
currency: "CNY",
|
|
1321
|
+
input: 1,
|
|
1322
|
+
cacheHit: .1,
|
|
1323
|
+
output: 4
|
|
1324
|
+
}
|
|
1325
|
+
},
|
|
1314
1326
|
{
|
|
1315
1327
|
key: "minimax-m3.1-flash-preview",
|
|
1316
1328
|
name: "MiniMax M3.1 Flash Preview",
|
|
@@ -2423,6 +2435,8 @@ const BUILTIN_MODEL_KEY_ALIASES = {
|
|
|
2423
2435
|
"minimax-m2-7": "minimax-m2.7",
|
|
2424
2436
|
"minimax-m2-7-highspeed": "minimax-m2.7-highspeed",
|
|
2425
2437
|
"minimax-m2-7-high-speed": "minimax-m2.7-highspeed",
|
|
2438
|
+
"minimax-m3.1-flash": "minimax-m3.1-flash",
|
|
2439
|
+
"minimax-m3-1-flash": "minimax-m3.1-flash",
|
|
2426
2440
|
"minimax-m3.1-flash-preview": "minimax-m3.1-flash-preview",
|
|
2427
2441
|
"minimax-m3-1-flash-preview": "minimax-m3.1-flash-preview",
|
|
2428
2442
|
"u2-flash": "u2-flash",
|
|
@@ -4045,6 +4059,35 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
4045
4059
|
};
|
|
4046
4060
|
}
|
|
4047
4061
|
//#endregion
|
|
4062
|
+
//#region lib/types/route-keys.js
|
|
4063
|
+
/**
|
|
4064
|
+
* 宿主 llm-pi-ai 新代际的路由 key 约定(订阅额度 / 余额 / 中转站 / 声明端点共用)。
|
|
4065
|
+
*
|
|
4066
|
+
* 新版宿主设置页把路由 key 直接存成凭据 refs(命名 = 路由 id 大写下划线 +
|
|
4067
|
+
* `_API_KEY`),providers 配置里只剩 models 列表;chat 认证走 pi-ai 目录路由的
|
|
4068
|
+
* ambient 发现,不再需要显式 `apiKeyEnv`。插件侧所有「按路由取 key」的读取口
|
|
4069
|
+
* 共用本模块的候选名推导与顺序解析——显式引用优先,约定名补后。
|
|
4070
|
+
*/
|
|
4071
|
+
/** 候选 ref 名:路由 id 约定名(`<ROUTE 大写下划线>_API_KEY`)优先,其后补宿主
|
|
4072
|
+
* pi-ai 目录的 ambient env 名(仅当与约定名不同——目录把 kimi-coding 指到
|
|
4073
|
+
* KIMI_API_KEY、opencode-go 与 opencode 同用 OPENCODE_API_KEY)。 */
|
|
4074
|
+
function routeKeyRefCandidates(route) {
|
|
4075
|
+
return [`${route.replaceAll("-", "_").toUpperCase()}_API_KEY`, ...{
|
|
4076
|
+
"kimi-coding": ["KIMI_API_KEY"],
|
|
4077
|
+
"opencode-go": ["OPENCODE_API_KEY"]
|
|
4078
|
+
}[route] ?? []];
|
|
4079
|
+
}
|
|
4080
|
+
/** 依次解析候选 ref,返回第一个解析出非空值的 ref 与值;全部落空返回 undefined。 */
|
|
4081
|
+
async function firstResolvedRef(refs, resolveRef) {
|
|
4082
|
+
for (const ref of refs) {
|
|
4083
|
+
const value = await resolveRef(ref);
|
|
4084
|
+
if (value !== void 0) return {
|
|
4085
|
+
ref,
|
|
4086
|
+
value
|
|
4087
|
+
};
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
//#endregion
|
|
4048
4091
|
//#region lib/types/resilience.js
|
|
4049
4092
|
/**
|
|
4050
4093
|
* 上游网络请求的稳定性工具:有限重试(指数退避)与每平台熔断冷却门。
|
|
@@ -4650,32 +4693,51 @@ const QUERIERS = [
|
|
|
4650
4693
|
];
|
|
4651
4694
|
/**
|
|
4652
4695
|
* Query every configured provider's account balance. A provider is queried only
|
|
4653
|
-
* when its llm-pi-ai route has an `apiKeyEnv
|
|
4654
|
-
*
|
|
4696
|
+
* when its llm-pi-ai route has a key: an explicit `apiKeyEnv`, or — on hosts
|
|
4697
|
+
* whose settings UI stores route keys as credentials refs — the route's
|
|
4698
|
+
* conventional ref (route-keys.ts). Absent routes answer `unconfigured` so the
|
|
4699
|
+
* dashboard shows a stable state instead of dropping the row.
|
|
4655
4700
|
* @param ctx - host context carrying the credentials seam.
|
|
4656
4701
|
* @param providers - the llm-pi-ai providers dict (`<route> → { apiKeyEnv? }`).
|
|
4657
4702
|
* @returns the balance rows (one per provider).
|
|
4658
4703
|
*/
|
|
4659
4704
|
async function queryBalances(ctx, providers) {
|
|
4660
|
-
const
|
|
4661
|
-
|
|
4705
|
+
const resolveRef = async (name) => {
|
|
4706
|
+
try {
|
|
4707
|
+
const hit = await ctx.credentials.resolve(credentialRef(name));
|
|
4708
|
+
return hit?.value !== void 0 && hit.value !== "" ? hit.value : void 0;
|
|
4709
|
+
} catch {
|
|
4710
|
+
return;
|
|
4711
|
+
}
|
|
4712
|
+
};
|
|
4713
|
+
const resolved = await Promise.all(QUERIERS.map(async ({ route, querier, displayName }) => {
|
|
4662
4714
|
const env = providers[route]?.apiKeyEnv;
|
|
4663
|
-
const
|
|
4664
|
-
const
|
|
4665
|
-
|
|
4666
|
-
byName.set(displayName, {
|
|
4715
|
+
const explicit = typeof env === "string" && env !== "" ? env : void 0;
|
|
4716
|
+
const hit = await firstResolvedRef([...explicit !== void 0 ? [explicit] : [], ...routeKeyRefCandidates(route)], resolveRef);
|
|
4717
|
+
return {
|
|
4667
4718
|
displayName,
|
|
4668
4719
|
querier,
|
|
4669
|
-
|
|
4720
|
+
ref: hit?.ref ?? explicit,
|
|
4721
|
+
configured: explicit !== void 0 || hit !== void 0
|
|
4722
|
+
};
|
|
4723
|
+
}));
|
|
4724
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4725
|
+
for (const item of resolved) {
|
|
4726
|
+
const existing = byName.get(item.displayName);
|
|
4727
|
+
if (existing !== void 0 && existing.ref !== void 0) continue;
|
|
4728
|
+
byName.set(item.displayName, {
|
|
4729
|
+
displayName: item.displayName,
|
|
4730
|
+
querier: item.querier,
|
|
4731
|
+
ref: item.configured ? item.ref : void 0
|
|
4670
4732
|
});
|
|
4671
4733
|
}
|
|
4672
|
-
return await Promise.all([...byName.values()].map(({
|
|
4673
|
-
if (
|
|
4734
|
+
return await Promise.all([...byName.values()].map(({ displayName, querier, ref }) => {
|
|
4735
|
+
if (ref === void 0) return Promise.resolve({
|
|
4674
4736
|
provider: displayName,
|
|
4675
4737
|
displayName,
|
|
4676
4738
|
error: "unconfigured"
|
|
4677
4739
|
});
|
|
4678
|
-
return querier(ctx,
|
|
4740
|
+
return querier(ctx, ref);
|
|
4679
4741
|
}));
|
|
4680
4742
|
}
|
|
4681
4743
|
/**
|
|
@@ -4913,8 +4975,9 @@ async function queryCustomBalances(ctx, configs) {
|
|
|
4913
4975
|
* 2. `path` 必须单斜杠绝对路径:`//evil.example/x` 是协议相对 URL,`new URL()` 会
|
|
4914
4976
|
* 把它解析到别的主机;构造后还会再校验一次 origin。
|
|
4915
4977
|
* 3. 只发 GET,无请求体,无自定义 method / headers。
|
|
4916
|
-
* 4.
|
|
4917
|
-
*
|
|
4978
|
+
* 4. 凭据仍只从匹配 provider 自己的凭据取:显式 `apiKeyEnv` 优先,缺省按路由
|
|
4979
|
+
* 约定 ref(新版宿主的存放形态,见 route-keys.ts)经同一凭据 seam 解析;声明
|
|
4980
|
+
* 不能指定任何凭据。
|
|
4918
4981
|
* 5. 跨源重定向直接失败,不跟随——那是绕过第 1 条最省事的办法。
|
|
4919
4982
|
* 6. 响应体有大小上限与共享超时,坏 / 恶意端点拖不住面板。
|
|
4920
4983
|
* 7. 声明不能覆盖内置读法:只在内置表答不上来时它才轮到。
|
|
@@ -5052,10 +5115,21 @@ function compileDeclaredEndpoint(decl) {
|
|
|
5052
5115
|
*/
|
|
5053
5116
|
async function queryDeclaredEndpoints(ctx, providers, declarations) {
|
|
5054
5117
|
const byOrigin = /* @__PURE__ */ new Map();
|
|
5055
|
-
for (const entry of Object.
|
|
5118
|
+
for (const [route, entry] of Object.entries(providers)) {
|
|
5056
5119
|
const origin = entry.baseURL === void 0 ? void 0 : normalizeDeclaredOrigin(entry.baseURL);
|
|
5057
|
-
if (origin !== void 0 && !byOrigin.has(origin)) byOrigin.set(origin,
|
|
5120
|
+
if (origin !== void 0 && !byOrigin.has(origin)) byOrigin.set(origin, {
|
|
5121
|
+
route,
|
|
5122
|
+
...entry.apiKeyEnv !== void 0 ? { apiKeyEnv: entry.apiKeyEnv } : {}
|
|
5123
|
+
});
|
|
5058
5124
|
}
|
|
5125
|
+
const resolveRef = async (name) => {
|
|
5126
|
+
try {
|
|
5127
|
+
const hit = await ctx.credentials.resolve(credentialRef(name));
|
|
5128
|
+
return hit?.value !== void 0 && hit.value !== "" ? hit.value : void 0;
|
|
5129
|
+
} catch {
|
|
5130
|
+
return;
|
|
5131
|
+
}
|
|
5132
|
+
};
|
|
5059
5133
|
return await Promise.all(declarations.map(async (decl) => {
|
|
5060
5134
|
const displayName = typeof decl.displayName === "string" && decl.displayName !== "" ? decl.displayName : "已声明";
|
|
5061
5135
|
const provider = `declared:${displayName}`;
|
|
@@ -5083,8 +5157,8 @@ async function queryDeclaredEndpoints(ctx, providers, declarations) {
|
|
|
5083
5157
|
declared: true,
|
|
5084
5158
|
error: "unreachable"
|
|
5085
5159
|
};
|
|
5086
|
-
const hit = await
|
|
5087
|
-
if (hit === void 0
|
|
5160
|
+
const hit = await firstResolvedRef([...matched.apiKeyEnv !== void 0 ? [matched.apiKeyEnv] : [], ...routeKeyRefCandidates(matched.route)], resolveRef);
|
|
5161
|
+
if (hit === void 0) return {
|
|
5088
5162
|
provider,
|
|
5089
5163
|
displayName,
|
|
5090
5164
|
declared: true,
|
|
@@ -5712,7 +5786,11 @@ function swallowed(reason, error) {
|
|
|
5712
5786
|
* other subscription providers the harness recognizes (volcengine / baidu /
|
|
5713
5787
|
* qwen / xiaomi token plans, agent plans…) are identified and listed with a
|
|
5714
5788
|
* "no quota API" marker rather than hidden. API keys come from the `llm-pi-ai`
|
|
5715
|
-
* settings namespace
|
|
5789
|
+
* settings namespace resolved through the credentials seam: the classic
|
|
5790
|
+
* `apiKeyEnv` refs, or — on hosts whose settings UI stores route keys as
|
|
5791
|
+
* credentials refs named after the route (`<ROUTE>_API_KEY`, catalog ambient
|
|
5792
|
+
* names as fallback) — the conventional refs, matched against the same
|
|
5793
|
+
* subscription-class route ids.
|
|
5716
5794
|
*/
|
|
5717
5795
|
/** 每平台熔断门:某个订阅适配器连续可重试失败(网络 / 5xx / 429)达阈值后
|
|
5718
5796
|
* 短路由不可用,避免 30 秒轮询对已故障的上游反复打满超时。鉴权失败(unauthorized)
|
|
@@ -5761,6 +5839,88 @@ const EMPTY_SUBSCRIPTION_KEYS = {
|
|
|
5761
5839
|
tencentCloudApi: "",
|
|
5762
5840
|
zaiRegion: "global"
|
|
5763
5841
|
};
|
|
5842
|
+
/** 订阅 provider id(llm-pi-ai 设置键)→ SubscriptionKeys 字段的映射。 */
|
|
5843
|
+
const SUBSCRIPTION_KEY_SOURCES = [
|
|
5844
|
+
{
|
|
5845
|
+
provider: "kimi-coding",
|
|
5846
|
+
key: "kimiApiKey"
|
|
5847
|
+
},
|
|
5848
|
+
{
|
|
5849
|
+
provider: "zai-coding-cn",
|
|
5850
|
+
key: "zaiCnApiKey"
|
|
5851
|
+
},
|
|
5852
|
+
{
|
|
5853
|
+
provider: "zai-coding",
|
|
5854
|
+
key: "zaiApiKey"
|
|
5855
|
+
},
|
|
5856
|
+
{
|
|
5857
|
+
provider: "opencode",
|
|
5858
|
+
key: "opencodeApiKey"
|
|
5859
|
+
},
|
|
5860
|
+
{
|
|
5861
|
+
provider: "opencode-go",
|
|
5862
|
+
key: "opencodeApiKey"
|
|
5863
|
+
},
|
|
5864
|
+
{
|
|
5865
|
+
provider: "minimax",
|
|
5866
|
+
key: "minmaxApiKey"
|
|
5867
|
+
},
|
|
5868
|
+
{
|
|
5869
|
+
provider: "minimax-token-plan",
|
|
5870
|
+
key: "minmaxApiKey"
|
|
5871
|
+
},
|
|
5872
|
+
{
|
|
5873
|
+
provider: "minimax-token-plan-cn",
|
|
5874
|
+
key: "minmaxApiKey"
|
|
5875
|
+
},
|
|
5876
|
+
{
|
|
5877
|
+
provider: "minimax-cn",
|
|
5878
|
+
key: "minmaxApiKey"
|
|
5879
|
+
},
|
|
5880
|
+
{
|
|
5881
|
+
provider: "openrouter",
|
|
5882
|
+
key: "openrouterApiKey"
|
|
5883
|
+
},
|
|
5884
|
+
{
|
|
5885
|
+
provider: "commandcode",
|
|
5886
|
+
key: "commandcodeApiKey"
|
|
5887
|
+
},
|
|
5888
|
+
{
|
|
5889
|
+
provider: "tencent-token-plan",
|
|
5890
|
+
key: "tencentCloudApi"
|
|
5891
|
+
}
|
|
5892
|
+
];
|
|
5893
|
+
/**
|
|
5894
|
+
* key 解析的可测核心(无文件 I/O):先显式 `apiKeyEnv`(旧口径,优先),key 仍缺
|
|
5895
|
+
* 时按约定 ref 名解析(新版宿主的存放形态,见 route-keys.ts)。`keyConfigured`
|
|
5896
|
+
* 回报每条路由的 key 是否解析成功,供识别层作为第三配置信号(refResolved)。
|
|
5897
|
+
* @param providers - llm-pi-ai 的 providers 视图。
|
|
5898
|
+
* @param resolveRef - 凭据 seam:ref 名 → 非空 key 值;未配置/失败返回 undefined。
|
|
5899
|
+
*/
|
|
5900
|
+
async function resolveSubscriptionAccess(providers, resolveRef) {
|
|
5901
|
+
const keys = { ...EMPTY_SUBSCRIPTION_KEYS };
|
|
5902
|
+
const keyConfigured = /* @__PURE__ */ new Set();
|
|
5903
|
+
for (const { provider, key } of SUBSCRIPTION_KEY_SOURCES) {
|
|
5904
|
+
const env = providers[provider]?.apiKeyEnv;
|
|
5905
|
+
if (typeof env === "string" && env !== "") {
|
|
5906
|
+
const value = await resolveRef(env);
|
|
5907
|
+
if (value !== void 0) {
|
|
5908
|
+
keys[key] = value;
|
|
5909
|
+
keyConfigured.add(provider);
|
|
5910
|
+
continue;
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
const hit = await firstResolvedRef(routeKeyRefCandidates(provider), resolveRef);
|
|
5914
|
+
if (hit !== void 0) {
|
|
5915
|
+
keys[key] = hit.value;
|
|
5916
|
+
keyConfigured.add(provider);
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
return {
|
|
5920
|
+
keys,
|
|
5921
|
+
keyConfigured
|
|
5922
|
+
};
|
|
5923
|
+
}
|
|
5764
5924
|
/** 订阅类 provider 的显示名(未命中的回退为 id 本身)。 */
|
|
5765
5925
|
const SUBSCRIPTION_DISPLAY_NAMES = {
|
|
5766
5926
|
"kimi-coding": "Kimi For Coding",
|
|
@@ -5814,14 +5974,15 @@ const SUBSCRIPTION_ADAPTERS = {
|
|
|
5814
5974
|
/** 有额度适配器的 provider id 集合(识别用)。 */
|
|
5815
5975
|
const ADAPTER_PROVIDER_IDS = new Set(Object.keys(SUBSCRIPTION_ADAPTERS));
|
|
5816
5976
|
/**
|
|
5817
|
-
* 从 llm-pi-ai 设置里识别订阅套餐:带订阅类 id
|
|
5977
|
+
* 从 llm-pi-ai 设置里识别订阅套餐:带订阅类 id 且任一配置信号成立的 provider
|
|
5978
|
+
* (显式 apiKeyEnv / models 已配置 / 约定 ref 解析出了 key)。
|
|
5818
5979
|
* @param providers - the `providers` map of the llm-pi-ai settings namespace.
|
|
5819
5980
|
* @returns identified plans in configuration order.
|
|
5820
5981
|
*/
|
|
5821
5982
|
function identifySubscriptionPlans(providers) {
|
|
5822
5983
|
const out = [];
|
|
5823
5984
|
for (const [id, config] of Object.entries(providers ?? {})) {
|
|
5824
|
-
if (typeof config?.apiKeyEnv
|
|
5985
|
+
if (!(typeof config?.apiKeyEnv === "string" && config.apiKeyEnv !== "" || config?.hasModels === true || config?.refResolved === true)) continue;
|
|
5825
5986
|
if (!isSubscriptionProviderId(id)) continue;
|
|
5826
5987
|
out.push({
|
|
5827
5988
|
provider: id,
|
|
@@ -6552,9 +6713,10 @@ async function collectSubscriptions(keys, plans = [], timeoutMs = DEFAULT_TIMEOU
|
|
|
6552
6713
|
*
|
|
6553
6714
|
* 适用场景:用户把某条 llm-pi-ai provider 路由的 `baseURL` 指向第三方中转站
|
|
6554
6715
|
* (New API / One API / VoAPI / Sub2API 等)。这类站点不卖官方余额,卖的是
|
|
6555
|
-
* 按 key 的额度(used/total)或多个滚动窗口。本模块对**配了 baseURL
|
|
6556
|
-
* apiKeyEnv
|
|
6557
|
-
*
|
|
6716
|
+
* 按 key 的额度(used/total)或多个滚动窗口。本模块对**配了 baseURL 且 key 可得**
|
|
6717
|
+
* (显式 `apiKeyEnv`,或新版宿主存进凭据 refs 的路由约定名,见 route-keys.ts)
|
|
6718
|
+
* 的路由逐个探测两个已知端点,能解析出额度就返回;解析不出的静默标记
|
|
6719
|
+
* unavailable,绝不臆造金额(与 balance/subscriptions 一致的姿态)。
|
|
6558
6720
|
*
|
|
6559
6721
|
* 探测顺序:先 Sub2API `/v1/usage`(标准化程度高),再 New API `/api/status`;
|
|
6560
6722
|
* 404 = 不是该套程序,继续试下一种;401/403 = 是但 key 不对(unauthorized);
|
|
@@ -6744,7 +6906,7 @@ async function readRelayByKind(baseURL, apiKey, kind) {
|
|
|
6744
6906
|
/**
|
|
6745
6907
|
* 查询单个中转站路由的额度。先试 Sub2API,再试 New API;任一读出额度即返回。
|
|
6746
6908
|
* @param ctx - host context carrying the credentials seam.
|
|
6747
|
-
* @param route - 待探测的路由(baseURL +
|
|
6909
|
+
* @param route - 待探测的路由(baseURL + 凭据 ref 候选)。
|
|
6748
6910
|
* @returns 该路由的一行额度结果(status 标记成败)。
|
|
6749
6911
|
*/
|
|
6750
6912
|
async function queryRelayQuota(ctx, route) {
|
|
@@ -6753,32 +6915,32 @@ async function queryRelayQuota(ctx, route) {
|
|
|
6753
6915
|
origin: originOf(route.baseURL),
|
|
6754
6916
|
displayName: route.displayName ?? route.route
|
|
6755
6917
|
};
|
|
6756
|
-
const gateKey = `${route.baseURL}::${route.
|
|
6918
|
+
const gateKey = `${route.baseURL}::${route.route}`;
|
|
6757
6919
|
if (!relayGate.check(gateKey)) return {
|
|
6758
6920
|
...base,
|
|
6759
6921
|
kind: "unknown",
|
|
6760
6922
|
status: "unavailable"
|
|
6761
6923
|
};
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
if (hit === void 0 || hit.value === "") return {
|
|
6924
|
+
const resolveRef = async (name) => {
|
|
6925
|
+
try {
|
|
6926
|
+
const hit = await ctx.credentials.resolve(credentialRef(name));
|
|
6927
|
+
return hit?.value !== void 0 && hit.value !== "" ? hit.value : void 0;
|
|
6928
|
+
} catch {
|
|
6929
|
+
return;
|
|
6930
|
+
}
|
|
6931
|
+
};
|
|
6932
|
+
const hit = await firstResolvedRef(route.apiKeyRefs, resolveRef);
|
|
6933
|
+
if (hit === void 0) return {
|
|
6773
6934
|
...base,
|
|
6774
6935
|
kind: "unknown",
|
|
6775
6936
|
status: "not-configured"
|
|
6776
6937
|
};
|
|
6938
|
+
const apiKey = hit.value;
|
|
6777
6939
|
const cached = fingerprintCache.get(base.origin);
|
|
6778
6940
|
const knownKind = cached !== void 0 && Date.now() - cached.at < FINGERPRINT_TTL_MS ? cached.kind : void 0;
|
|
6779
6941
|
try {
|
|
6780
6942
|
if (knownKind === "sub2api" || knownKind === "new-api") {
|
|
6781
|
-
const parsed = await readRelayByKind(route.baseURL,
|
|
6943
|
+
const parsed = await readRelayByKind(route.baseURL, apiKey, knownKind);
|
|
6782
6944
|
if (parsed !== null) {
|
|
6783
6945
|
relayGate.success(gateKey);
|
|
6784
6946
|
return {
|
|
@@ -6796,7 +6958,7 @@ async function queryRelayQuota(ctx, route) {
|
|
|
6796
6958
|
status: "invalid-response"
|
|
6797
6959
|
};
|
|
6798
6960
|
}
|
|
6799
|
-
const sub2 = await fetchRelayJson(endpointOf(route.baseURL, "/v1/usage"),
|
|
6961
|
+
const sub2 = await fetchRelayJson(endpointOf(route.baseURL, "/v1/usage"), apiKey);
|
|
6800
6962
|
if (sub2.ok) {
|
|
6801
6963
|
const parsed = parseSub2ApiUsage(sub2.data);
|
|
6802
6964
|
if (parsed !== null) {
|
|
@@ -6820,7 +6982,7 @@ async function queryRelayQuota(ctx, route) {
|
|
|
6820
6982
|
status: "invalid-response"
|
|
6821
6983
|
};
|
|
6822
6984
|
}
|
|
6823
|
-
const na = await fetchRelayJson(endpointOf(route.baseURL, "/api/status"),
|
|
6985
|
+
const na = await fetchRelayJson(endpointOf(route.baseURL, "/api/status"), apiKey);
|
|
6824
6986
|
if (na.ok) {
|
|
6825
6987
|
const parsed = parseNewApiStatus(na.data);
|
|
6826
6988
|
if (parsed !== null) {
|
|
@@ -6862,7 +7024,7 @@ async function queryRelayQuota(ctx, route) {
|
|
|
6862
7024
|
/**
|
|
6863
7025
|
* 批量查询多个中转站路由的额度(每个独立成败,互不影响)。
|
|
6864
7026
|
* @param ctx - host context carrying the credentials seam.
|
|
6865
|
-
* @param routes - 配了 baseURL 且
|
|
7027
|
+
* @param routes - 配了 baseURL 且 key 候选可得(显式引用存在或约定 ref 解析出值)的路由列表。
|
|
6866
7028
|
* @returns 每个路由一行的额度结果。
|
|
6867
7029
|
*/
|
|
6868
7030
|
async function queryRelayQuotas(ctx, routes) {
|
|
@@ -7142,6 +7304,8 @@ function registerBalanceRoute(ctx, deps) {
|
|
|
7142
7304
|
*
|
|
7143
7305
|
* usage_stats 工具开关:插件自带的读/写接口(不依赖宿主浏览器设置白名单)。
|
|
7144
7306
|
* GET 返回当前是否注入;POST 写设置命名空间(重启应用后工具注入随新值生效)。
|
|
7307
|
+
* 写回失败回 503 + 宿主错误文案(issue #98:此前 200 + 笼统 invalid,开关无声
|
|
7308
|
+
* 弹回且无任何诊断线索);body 非 JSON 回 400。
|
|
7145
7309
|
*
|
|
7146
7310
|
* 双世代宿主(issue #19 + #28 + v1.4.19)的接线在装配器里完成:
|
|
7147
7311
|
* - 旧宿主 ≤0.1.6 走 `settings.register` 命名空间 scope。
|
|
@@ -7174,32 +7338,43 @@ function registerUsageToolRoute(ctx, deps) {
|
|
|
7174
7338
|
return;
|
|
7175
7339
|
}
|
|
7176
7340
|
}
|
|
7177
|
-
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
7178
7341
|
if (req.method === "GET") {
|
|
7342
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
7179
7343
|
res.end(JSON.stringify({ enabled: deps.read() }));
|
|
7180
7344
|
return;
|
|
7181
7345
|
}
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
return;
|
|
7190
|
-
}
|
|
7346
|
+
let body = "";
|
|
7347
|
+
for await (const chunk of req) {
|
|
7348
|
+
body += String(chunk);
|
|
7349
|
+
if (body.length > 512) {
|
|
7350
|
+
res.writeHead(413, { "content-type": "application/json; charset=utf-8" });
|
|
7351
|
+
res.end(JSON.stringify({ error: "body too large" }));
|
|
7352
|
+
return;
|
|
7191
7353
|
}
|
|
7192
|
-
|
|
7354
|
+
}
|
|
7355
|
+
let next;
|
|
7356
|
+
try {
|
|
7357
|
+
next = JSON.parse(body === "" ? "{}" : body).enabled === true;
|
|
7358
|
+
} catch {
|
|
7359
|
+
res.writeHead(400, { "content-type": "application/json; charset=utf-8" });
|
|
7360
|
+
res.end(JSON.stringify({ error: "invalid json body" }));
|
|
7361
|
+
return;
|
|
7362
|
+
}
|
|
7363
|
+
try {
|
|
7193
7364
|
await deps.write(next);
|
|
7194
|
-
deps.syncTool();
|
|
7195
|
-
res.end(JSON.stringify({
|
|
7196
|
-
ok: true,
|
|
7197
|
-
enabled: next
|
|
7198
|
-
}));
|
|
7199
7365
|
} catch (error) {
|
|
7366
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
7200
7367
|
console.warn("[usage-billing] usage-tool toggle write-back failed:", error);
|
|
7201
|
-
res.
|
|
7368
|
+
res.writeHead(503, { "content-type": "application/json; charset=utf-8" });
|
|
7369
|
+
res.end(JSON.stringify({ error: `write failed: ${detail}` }));
|
|
7370
|
+
return;
|
|
7202
7371
|
}
|
|
7372
|
+
deps.syncTool();
|
|
7373
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
7374
|
+
res.end(JSON.stringify({
|
|
7375
|
+
ok: true,
|
|
7376
|
+
enabled: next
|
|
7377
|
+
}));
|
|
7203
7378
|
}
|
|
7204
7379
|
}), "usage-billing: usage-tool route");
|
|
7205
7380
|
}
|
|
@@ -7435,6 +7610,31 @@ const BILLING_SETTINGS_NAMESPACE = "ui-usage-billing";
|
|
|
7435
7610
|
/** 该命名空间下用户可编辑的字段名。 */
|
|
7436
7611
|
const ENABLE_USAGE_STATS_TOOL_FIELD = "enableUsageStatsTool";
|
|
7437
7612
|
//#endregion
|
|
7613
|
+
//#region lib/types/config.js
|
|
7614
|
+
/**
|
|
7615
|
+
* Plugin configuration(v1.5.2 抽出):UsageBillingConfig 原本声明在 src/index.ts
|
|
7616
|
+
* 顶部,src/index.ts 收缩为装配器后被 src/routes/usage-stats.ts 等路由文件
|
|
7617
|
+
* 引用(作为 Deps 类型签名),把它单独放这里让 route 文件能从 src/ 根 import。
|
|
7618
|
+
* 仍由 src/index.ts 的 apply(ctx, config) 消费:路由层只通过 Deps 拿到它,
|
|
7619
|
+
* 不直接构造。
|
|
7620
|
+
*
|
|
7621
|
+
* ⚠️ Config 运行时 schema 必须经 src/index.ts re-export(issue #98):宿主
|
|
7622
|
+
* dsh-settings@0.2.1-rc.2 的 settings.update 写回要求本插件条目在 loader fiber
|
|
7623
|
+
* 上有 `Config` schema(`entry.fiber.runtime.Config` 且带 toJSON),缺失时抛
|
|
7624
|
+
* `No configurable plugin entry`——v1.6.0 拆分时把 schema 退化成纯类型,「注入
|
|
7625
|
+
* 用量查询」开关随之失效(#80 方案的回归)。schemastery 非 strict object 会
|
|
7626
|
+
* merge 未声明键,其余 config 字段(statsPath / routeAliases 等)不受影响。
|
|
7627
|
+
*/
|
|
7628
|
+
/**
|
|
7629
|
+
* 宿主侧条目 config schema(#80 / #98):宿主 0.1.7+ 的设置系统据此把
|
|
7630
|
+
* `enableUsageStatsTool` 投影为 volatile 引用(设置写入即时反映 + 写回走
|
|
7631
|
+
* settings.update)。**必须**经 src/index.ts re-export 给 loader——宿主从插件
|
|
7632
|
+
* 运行时模块的 `Config` 导出发现它;只声明类型等于没有。schemastery 非 strict
|
|
7633
|
+
* object 透传未声明键,其余 config 字段不受影响;未导出 Config 的旧宿主直接
|
|
7634
|
+
* 把原始 config 交给 apply,普通布尔形态由读取端兼容。
|
|
7635
|
+
*/
|
|
7636
|
+
const Config = z.object({ [ENABLE_USAGE_STATS_TOOL_FIELD]: z.boolean().default(false).volatile() });
|
|
7637
|
+
//#endregion
|
|
7438
7638
|
//#region lib/types/index.js
|
|
7439
7639
|
/**
|
|
7440
7640
|
* Usage billing surface plugin, node half.
|
|
@@ -7584,61 +7784,6 @@ const inject = [
|
|
|
7584
7784
|
"credentials",
|
|
7585
7785
|
"settings"
|
|
7586
7786
|
];
|
|
7587
|
-
/**
|
|
7588
|
-
* 订阅 provider id(llm-pi-ai 设置键)→ billing 适配器 key 的映射。
|
|
7589
|
-
* 复用 dsh 既有的 llm-pi-ai provider 配置(apiKeyEnv 引用),不引入新配置面。
|
|
7590
|
-
*/
|
|
7591
|
-
/** key 只取字符串凭据字段:zaiRegion 是区域枚举,由下方区域逻辑单独赋值。 */
|
|
7592
|
-
const SUBSCRIPTION_KEY_SOURCES = [
|
|
7593
|
-
{
|
|
7594
|
-
provider: "kimi-coding",
|
|
7595
|
-
key: "kimiApiKey"
|
|
7596
|
-
},
|
|
7597
|
-
{
|
|
7598
|
-
provider: "zai-coding-cn",
|
|
7599
|
-
key: "zaiCnApiKey"
|
|
7600
|
-
},
|
|
7601
|
-
{
|
|
7602
|
-
provider: "zai-coding",
|
|
7603
|
-
key: "zaiApiKey"
|
|
7604
|
-
},
|
|
7605
|
-
{
|
|
7606
|
-
provider: "opencode",
|
|
7607
|
-
key: "opencodeApiKey"
|
|
7608
|
-
},
|
|
7609
|
-
{
|
|
7610
|
-
provider: "opencode-go",
|
|
7611
|
-
key: "opencodeApiKey"
|
|
7612
|
-
},
|
|
7613
|
-
{
|
|
7614
|
-
provider: "minimax",
|
|
7615
|
-
key: "minmaxApiKey"
|
|
7616
|
-
},
|
|
7617
|
-
{
|
|
7618
|
-
provider: "minimax-token-plan",
|
|
7619
|
-
key: "minmaxApiKey"
|
|
7620
|
-
},
|
|
7621
|
-
{
|
|
7622
|
-
provider: "minimax-token-plan-cn",
|
|
7623
|
-
key: "minmaxApiKey"
|
|
7624
|
-
},
|
|
7625
|
-
{
|
|
7626
|
-
provider: "minimax-cn",
|
|
7627
|
-
key: "minmaxApiKey"
|
|
7628
|
-
},
|
|
7629
|
-
{
|
|
7630
|
-
provider: "openrouter",
|
|
7631
|
-
key: "openrouterApiKey"
|
|
7632
|
-
},
|
|
7633
|
-
{
|
|
7634
|
-
provider: "commandcode",
|
|
7635
|
-
key: "commandcodeApiKey"
|
|
7636
|
-
},
|
|
7637
|
-
{
|
|
7638
|
-
provider: "tencent-token-plan",
|
|
7639
|
-
key: "tencentCloudApi"
|
|
7640
|
-
}
|
|
7641
|
-
];
|
|
7642
7787
|
/** 读 llm-pi-ai 设置的 `providers` 字典(`<route> → { apiKeyEnv?, baseURL?, displayName? }`)。
|
|
7643
7788
|
* 余额与订阅查询复用同一份来源:部署为某个 provider 配一次,多 surface 共享。
|
|
7644
7789
|
* @param settings - the settings service (reads the llm-pi-ai namespace).
|
|
@@ -7651,10 +7796,12 @@ async function readPiAiProviders(settings) {
|
|
|
7651
7796
|
for (const [route, entry] of Object.entries(providers ?? {})) {
|
|
7652
7797
|
if (entry === null || typeof entry !== "object") continue;
|
|
7653
7798
|
const { apiKeyEnv, baseURL, displayName } = entry;
|
|
7799
|
+
const models = entry.models;
|
|
7654
7800
|
out[route] = {
|
|
7655
7801
|
...typeof apiKeyEnv === "string" ? { apiKeyEnv } : {},
|
|
7656
7802
|
...typeof baseURL === "string" ? { baseURL } : {},
|
|
7657
|
-
...typeof displayName === "string" ? { displayName } : {}
|
|
7803
|
+
...typeof displayName === "string" ? { displayName } : {},
|
|
7804
|
+
...Array.isArray(models) && models.length > 0 ? { hasModels: true } : {}
|
|
7658
7805
|
};
|
|
7659
7806
|
}
|
|
7660
7807
|
return out;
|
|
@@ -7761,51 +7908,47 @@ function buildWorkspaceTitleResolver(ctx) {
|
|
|
7761
7908
|
};
|
|
7762
7909
|
}
|
|
7763
7910
|
/**
|
|
7764
|
-
*
|
|
7765
|
-
*
|
|
7766
|
-
* 同时识别出用户配置了 key 的订阅套餐(供面板只显示已识别的)。
|
|
7767
|
-
* @param settings - the settings service (reads the llm-pi-ai namespace).
|
|
7768
|
-
* @param credentials - the credentials service (resolves the env refs).
|
|
7911
|
+
* 凭据 seam 的宽容解析:ref 名 → 非空值;失败/缺失返回 undefined(多 surface 共用,
|
|
7912
|
+
* 失败按结构化静默记录)。
|
|
7769
7913
|
*/
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
let providers;
|
|
7773
|
-
try {
|
|
7774
|
-
providers = (settings.describe({ redactSecrets: true }).find((descriptor) => descriptor.ns === "llm-pi-ai")?.value)?.providers;
|
|
7775
|
-
} catch {
|
|
7776
|
-
return {
|
|
7777
|
-
keys,
|
|
7778
|
-
identified: []
|
|
7779
|
-
};
|
|
7780
|
-
}
|
|
7781
|
-
for (const { provider, key } of SUBSCRIPTION_KEY_SOURCES) {
|
|
7782
|
-
const env = providers?.[provider]?.apiKeyEnv;
|
|
7783
|
-
if (typeof env !== "string" || env === "") continue;
|
|
7914
|
+
function refResolver(credentials) {
|
|
7915
|
+
return async (name) => {
|
|
7784
7916
|
try {
|
|
7785
|
-
const resolved = await credentials.resolve(credentialRef(
|
|
7786
|
-
|
|
7917
|
+
const resolved = await credentials.resolve(credentialRef(name));
|
|
7918
|
+
return resolved?.value !== void 0 && resolved.value !== "" ? resolved.value : void 0;
|
|
7787
7919
|
} catch (error) {
|
|
7788
7920
|
swallowed("credential-resolve-failed", error);
|
|
7921
|
+
return;
|
|
7789
7922
|
}
|
|
7790
|
-
}
|
|
7791
|
-
|
|
7923
|
+
};
|
|
7924
|
+
}
|
|
7925
|
+
/**
|
|
7926
|
+
* 解析订阅适配器需要的 API Key:经典形态从 llm-pi-ai 设置的 `providers.<id>.apiKeyEnv`
|
|
7927
|
+
* 读引用(如 kimi-coding → KIMI_CODING_API_KEY);新版宿主设置页把 key 直接存成
|
|
7928
|
+
* 凭据 refs(路由约定名 `<ROUTE>_API_KEY`,pi-ai 目录 ambient 名补充),显式引用
|
|
7929
|
+
* 解析不到时按约定名找回。同时识别出已配置的订阅套餐(供面板只显示已识别的)。
|
|
7930
|
+
* @param settings - the settings service (reads the llm-pi-ai namespace).
|
|
7931
|
+
* @param credentials - the credentials service (resolves the env refs).
|
|
7932
|
+
*/
|
|
7933
|
+
async function resolveSubscriptionKeys(settings, credentials) {
|
|
7934
|
+
const providers = await readPiAiProviders(settings);
|
|
7935
|
+
const { keys, keyConfigured } = await resolveSubscriptionAccess(providers, refResolver(credentials));
|
|
7936
|
+
if (keys.zaiCnApiKey !== "") keys.zaiRegion = "bigmodel-cn";
|
|
7792
7937
|
if (keys.opencodeApiKey === "") keys.opencodeApiKey = await readOpenCodeToken();
|
|
7793
7938
|
keys.anthropicApiKey = await readClaudeOAuthToken();
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
};
|
|
7805
|
-
}
|
|
7939
|
+
const identified = identifySubscriptionPlans(Object.fromEntries(Object.entries(providers).map(([route, entry]) => [route, {
|
|
7940
|
+
...entry.apiKeyEnv !== void 0 ? { apiKeyEnv: entry.apiKeyEnv } : {},
|
|
7941
|
+
...entry.hasModels === true ? { hasModels: true } : {},
|
|
7942
|
+
...keyConfigured.has(route) ? { refResolved: true } : {}
|
|
7943
|
+
}])));
|
|
7944
|
+
if (keys.anthropicApiKey !== "" && !identified.some((plan) => plan.provider === "anthropic")) identified.push({
|
|
7945
|
+
provider: "anthropic",
|
|
7946
|
+
displayName: "Claude (Anthropic)",
|
|
7947
|
+
adapter: true
|
|
7948
|
+
});
|
|
7806
7949
|
return {
|
|
7807
7950
|
keys,
|
|
7808
|
-
identified
|
|
7951
|
+
identified
|
|
7809
7952
|
};
|
|
7810
7953
|
}
|
|
7811
7954
|
/** 读本机 OpenCode 客户端的凭据 token;取不到返回空串(安静退回)。 */
|
|
@@ -8341,14 +8484,18 @@ function apply(ctx, config = {}) {
|
|
|
8341
8484
|
};
|
|
8342
8485
|
const refreshRelay = async () => {
|
|
8343
8486
|
const providers = await readPiAiProviders(ctx.settings);
|
|
8487
|
+
const resolve = refResolver(ctx.credentials);
|
|
8344
8488
|
const routes = [];
|
|
8345
8489
|
for (const [route, entry] of Object.entries(providers)) {
|
|
8346
|
-
if (entry.baseURL === void 0
|
|
8490
|
+
if (entry.baseURL === void 0) continue;
|
|
8347
8491
|
if (isOfficialBaseUrl(entry.baseURL)) continue;
|
|
8492
|
+
const apiKeyRefs = [.../* @__PURE__ */ new Set([...entry.apiKeyEnv !== void 0 ? [entry.apiKeyEnv] : [], ...routeKeyRefCandidates(route)])];
|
|
8493
|
+
const hit = await firstResolvedRef(apiKeyRefs, resolve);
|
|
8494
|
+
if (entry.apiKeyEnv === void 0 && hit === void 0) continue;
|
|
8348
8495
|
routes.push({
|
|
8349
8496
|
route,
|
|
8350
8497
|
baseURL: entry.baseURL,
|
|
8351
|
-
|
|
8498
|
+
apiKeyRefs,
|
|
8352
8499
|
...entry.displayName === void 0 ? {} : { displayName: entry.displayName }
|
|
8353
8500
|
});
|
|
8354
8501
|
}
|
|
@@ -8382,4 +8529,4 @@ function apply(ctx, config = {}) {
|
|
|
8382
8529
|
});
|
|
8383
8530
|
}
|
|
8384
8531
|
//#endregion
|
|
8385
|
-
export { adaptSessionPersistence, apply, createFileUsageLedgerStore, guardLoopback, hostNameOf, inject, isLoopbackHost, isLoopbackOrigin, isLoopbackPeer, normalizeTrustedHosts, readPiAiProviderRoutes, readRegisteredProviderIds, resolveSubscriptionKeys };
|
|
8532
|
+
export { Config, adaptSessionPersistence, apply, createFileUsageLedgerStore, guardLoopback, hostNameOf, inject, isLoopbackHost, isLoopbackOrigin, isLoopbackPeer, normalizeTrustedHosts, readPiAiProviderRoutes, readRegisteredProviderIds, resolveSubscriptionKeys };
|