dsh-cost-meter 1.7.29 → 1.7.30
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 +7 -6
- package/README.md +7 -6
- package/lib/index.js +47 -5
- package/lib/openrouter-pricing.js +78 -0
- package/lib/pricing.js +83 -1
- package/lib/store.js +11 -2
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
Per-conversation cost · daily totals · OpenCode Go subscription quota display · budget with usage percentage · official account balance · custom provider balance · balance progress bar · history · peak/off-peak pricing hours display (peak hours UTC 01:00–04:00, 06:00–10:00; from Aug 23, 2026 weekends are billed at off-peak prices all day, shown as “Weekend — all off-peak”) · pre-switch popup & system-notification alerts for peak/off-peak changes (position / lead time / alert type configurable) · one-click price sync from the official docs · Codex-style token usage heat grid · multi-vendor model pricing (built-in 90+ model price catalog with auto-matching) · mainstream Coding Plan quota queries & display (Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow / CommandCode / SCNet) plan/API dual-track billing (subscription quota vs pay-as-you-go money separated, per-1% & full-window token/equivalent-cost estimates with daily/weekly/monthly curves) · · quota strip above the input box (budget / Go / coding-plan usage in one row, toggleable)
|
|
8
8
|
|
|
9
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
10
10
|
|
|
11
|
-
**v1.7.
|
|
11
|
+
**v1.7.30** adds OpenRouter token reference prices, a four-model offline snapshot and automatic catalog refresh. Scoped backfill preserves existing charges, and automatic updates preserve custom prices. Thanks to @GnaneshKunal for PR #156. See the [pricing guide](docs/openrouter-pricing.md#english).
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
14
14
|
[](LICENSE)
|
|
@@ -56,6 +56,7 @@ English | [中文](README.md)
|
|
|
56
56
|
| Model & Plan adaptation guide | [adaptation doc](docs/model-and-plan-adaptation.en.md) | Adaptation matrix for per-model billing and the 8 Coding Plan vendors, the auto-matching mechanism and price sources ([中文](docs/model-and-plan-adaptation.md)) |
|
|
57
57
|
| Peak/off-peak alert guide | [alert doc](docs/peak-alert.en.md) | Fully illustrated guide to the pre-switch popup and system notification: effect screenshots (EN/中文), settings reference and usage tips ([中文](docs/peak-alert.md)) |
|
|
58
58
|
| Token Plan usage stats guide | [panel doc](docs/token-plan-stats.md) | Meaning of the four columns in the per-1% & full-window panel, the end-to-end delta estimation method and precision tags, the scope boundary (dsh-made calls only) and usage curves ([中文](docs/token-plan-stats.md#中文)) |
|
|
59
|
+
| OpenRouter passthrough pricing | [pricing doc](docs/openrouter-pricing.md#english) | Token reference prices: offline snapshot, scoped zero-cost backfill, public-catalog refresh, custom-price preservation and failure fallback ([中文](docs/openrouter-pricing.md)) |
|
|
59
60
|
| Multi-provider billing | Settings / ledger | OpenAI, Anthropic, Google Gemini, Mistral and other providers with input/output, cache and reasoning-token pricing isolated by provider + model |
|
|
60
61
|
| Model-name auto-matching | Settings / ledger | Unknown model ids are matched against the price table: case/spaces/hyphens/dots and bracket annotations (e.g. (go)) are ignored — a normalized-equal or containing name hits (e.g. `gpt5.6 luna(go)`); router providers (opencode/zen etc.) search across all vendors; can be restricted to exact match, and unmatched models can be pinned to a specific entry in Settings |
|
|
61
62
|
| Extended price catalog | Settings → Extended price catalog | Built-in reference catalog grouped by vendor and model family (expandable; vendors collapsed by default); mount entries into billing with one click — mounted third-party models live inside the catalog and stay editable; a per-model “Show directly in Cost settings” toggle chooses which models (DeepSeek included) appear directly in the price table |
|
|
@@ -288,22 +289,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
|
|
|
288
289
|
dsh plugin --profile web add dsh-cost-meter
|
|
289
290
|
```
|
|
290
291
|
|
|
291
|
-
**PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.7.
|
|
292
|
+
**PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.7.30`** — review the script before running):
|
|
292
293
|
|
|
293
294
|
```powershell
|
|
294
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.7.
|
|
295
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.7.30/install.ps1 | iex
|
|
295
296
|
```
|
|
296
297
|
|
|
297
298
|
**Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
|
|
298
299
|
|
|
299
300
|
```sh
|
|
300
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.7.
|
|
301
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.7.30
|
|
301
302
|
```
|
|
302
303
|
|
|
303
304
|
Without git, use the GitHub tag archive:
|
|
304
305
|
|
|
305
306
|
```sh
|
|
306
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.7.
|
|
307
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.7.30.tar.gz
|
|
307
308
|
```
|
|
308
309
|
|
|
309
310
|
After installing, **restart** `dsh web` (plugin rows, the Typert manifest and the client bundle are all scanned at startup):
|
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
本会话费用 · 当日费用 · OpenCode Go 订阅额度显示 · 预算与已用百分比 · 官方账户余额 · 自定义 Provider 余额查询(可配任意 HTTP 端点) · 余额三段进度条 · 历史记录 · 峰谷计价时段显示(UTC 01:00–04:00、06:00–10:00 为峰时段;2026-08-23 起周末全天按谷价,显示「周末时段——全谷价」) · 峰/谷切换前弹窗与系统通知提醒(位置/提前量/提醒类型可配) · 官方价格一键同步 · 类 Codex Token 用量热图 · 多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配) · 主流 Coding Plan 额度查询与显示(Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow / CommandCode / SCNet / 火山方舟 九家,含 Volcano Ark AK/SK 签名) · Plan/API 双轨计费(订阅额度与按量金额分离统计,每 1% 额度与满窗的 token/等值金额估算及日/周/月曲线) · 输入框上方额度横条(预算/Go/Coding Plan 用量一条横排显示,可开关)
|
|
8
8
|
|
|
9
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
10
10
|
|
|
11
|
-
**v1.7.
|
|
11
|
+
**v1.7.30**:新增 OpenRouter token 价格目录与自动刷新,内置四模型离线快照;定向修复零费用旧账,保留手改价格和已记费用。感谢 @GnaneshKunal 的 PR #156。详见[定价说明](docs/openrouter-pricing.md)。
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
14
14
|
[](LICENSE)
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
| 模型与 Plan 适配说明 | [适配文档](docs/model-and-plan-adaptation.md) | 各厂商模型计费与 8 家 Coding Plan 的适配矩阵、自动匹配机制与价格来源([English](docs/model-and-plan-adaptation.en.md)) |
|
|
58
58
|
| 峰/谷切换提醒图解 | [提醒文档](docs/peak-alert.md) | 峰谷切换前弹窗与系统通知的完整图解:效果截图(中/英)、设置项说明与使用建议([English](docs/peak-alert.en.md)) |
|
|
59
59
|
| Token Plan 用量统计图解 | [面板文档](docs/token-plan-stats.md) | 每 1% 与满窗估算的四列含义、首尾差分估算方法与精度标注、口径边界(只统计 dsh 内调用)与用量曲线说明([English](docs/token-plan-stats.md#english)) |
|
|
60
|
+
| OpenRouter 透传定价 | [定价说明](docs/openrouter-pricing.md) | OpenRouter 原样透传上游价格:内置快照、旧账本自动回填、公开目录(无需认证)定时刷新合并,失败保留本地快照([English](docs/openrouter-pricing.md#english)) |
|
|
60
61
|
| 多 provider 计费 | 设置页 / 账本 | 支持 OpenAI、Anthropic、Google Gemini、Mistral 等 provider 的 input/output、缓存与 reasoning token 价格,按 provider+model 隔离计费 |
|
|
61
62
|
| 模型名自动匹配 | 设置页 / 账本 | 未知模型 id 自动匹配价格表:忽略大小写/空格/横杠/点号与括号附注,归一化等价或请求名包含表内模型名即命中(如 `gpt5.6 luna(go)`);路由 provider(opencode/zen 等)下跨厂商全库查找;可关闭为仅精确;未命中模型可手动指定计费条目 |
|
|
62
63
|
| 拓展价格表 | 设置页 → 拓展价格表 | 内置各厂商、按模型家族分类的参考价格目录(点开展开,厂商默认折叠);一键挂载参与计费,挂载的第三方模型默认收入表内可编辑;逐模型「在费用设置直接显示」开关自选哪些模型(含 DeepSeek)在「价格表」区直接显示 |
|
|
@@ -290,22 +291,22 @@ CLIProxyAPI 网关来源卡片可勾选「只显示 Gemini 额度」,仅影响
|
|
|
290
291
|
dsh plugin --profile web add dsh-cost-meter
|
|
291
292
|
```
|
|
292
293
|
|
|
293
|
-
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.7.
|
|
294
|
+
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.7.30`**,建议先下载审阅再运行):
|
|
294
295
|
|
|
295
296
|
```powershell
|
|
296
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.7.
|
|
297
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.7.30/install.ps1 | iex
|
|
297
298
|
```
|
|
298
299
|
|
|
299
300
|
**或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
|
|
300
301
|
|
|
301
302
|
```sh
|
|
302
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.7.
|
|
303
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.7.30
|
|
303
304
|
```
|
|
304
305
|
|
|
305
306
|
没有 git 时可用 GitHub tag 打包直链:
|
|
306
307
|
|
|
307
308
|
```sh
|
|
308
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.7.
|
|
309
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.7.30.tar.gz
|
|
309
310
|
```
|
|
310
311
|
|
|
311
312
|
安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
|
package/lib/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import { createUsageDeduper, USAGE_DEDUP_WINDOW_MS, usageFingerprint } from './u
|
|
|
28
28
|
import { CODING_PLAN_PROVIDERS, CODING_PLAN_PROVIDER_IDS, queryCodingPlan, scnetTokenPlanWindows, qwenTokenPlanWindows, emptyCustomBalance, queryCustomBalance, normalizeVolcengineKey } from './coding-plans.js'
|
|
29
29
|
import { recordSamples, buildPlanStats, canonicalWindowKey, periodStartOf, aggregateUsageSince, enabledPlanSetOf, billingClassOf, pruneHourBuckets, suggestPlanAutoClasses } from './plan-billing.js'
|
|
30
30
|
import { fetchWithRetry } from './net.js'
|
|
31
|
+
import { createOpenRouterPriceRefresh, repairOpenRouterLedger } from './openrouter-pricing.js'
|
|
31
32
|
import { queryGatewayQuota, emptyGatewayQuota, managementKeyVarOf, gatewaySourceFingerprint } from './gateway-quotas.js'
|
|
32
33
|
import { stateSchema, TYPERT } from './typert.host.js'
|
|
33
34
|
import { customBalanceCredentialVars } from './custom-balance.js'
|
|
@@ -94,6 +95,8 @@ const SERVER_MESSAGES = {
|
|
|
94
95
|
pricesSyncedFallback: '所选币种官方页同步失败({error}),已回退另一语言官方页,同步 {ids} 的价格;可稍后重试切换回目标币种',
|
|
95
96
|
pricesRecomputed: '价格币种已切换,历史账目按新价目重算:{days} 天 / {sessions} 个会话(日志覆盖不全的会话保持原口径)',
|
|
96
97
|
priceSyncFailed: '官方价格同步失败:{error}',
|
|
98
|
+
openrouterRefreshed: 'OpenRouter 模型价格已刷新:{count} 个模型',
|
|
99
|
+
openrouterRefreshFailed: 'OpenRouter 价格刷新失败(已保留本地快照):{error}',
|
|
97
100
|
codingPlanKeyMissing: '未找到 {provider} 的凭据。请在下方填写 API Key,或配置对应环境变量/CLI 登录态;没有订阅可关闭该家的「启用」开关。',
|
|
98
101
|
codingPlanUnauthorized: '{provider} 凭据无效或没有生效的订阅(接口返回 {code})。没有订阅可关闭该家的「启用」开关。',
|
|
99
102
|
codingPlanHttp: '{provider} 额度接口 HTTP {code}({url})',
|
|
@@ -162,6 +165,8 @@ const SERVER_MESSAGES = {
|
|
|
162
165
|
pricesSyncedFallback: 'Syncing the official page for the selected currency failed ({error}); fell back to the other language page and synced prices for {ids}. You can switch back and retry later.',
|
|
163
166
|
pricesRecomputed: 'Pricing currency switched; history has been re-costed on the new price table: {days} day(s) / {sessions} session(s) (sessions without full log coverage keep their original basis)',
|
|
164
167
|
priceSyncFailed: 'Official price sync failed: {error}',
|
|
168
|
+
openrouterRefreshed: 'OpenRouter model prices refreshed: {count} models',
|
|
169
|
+
openrouterRefreshFailed: 'OpenRouter price refresh failed (kept local snapshot): {error}',
|
|
165
170
|
codingPlanKeyMissing: 'No credentials found for {provider}. Enter the API key below, or configure the matching environment variable / CLI login; turn off the Enable switch if you have no subscription.',
|
|
166
171
|
codingPlanUnauthorized: '{provider} credentials are invalid or no active subscription was detected (API returned {code}). Turn off the Enable switch if you have no subscription.',
|
|
167
172
|
codingPlanHttp: '{provider} quota API returned HTTP {code} ({url})',
|
|
@@ -1197,6 +1202,20 @@ async function fetchPricingHtml(locale, pricingCurrency) {
|
|
|
1197
1202
|
return text
|
|
1198
1203
|
}
|
|
1199
1204
|
|
|
1205
|
+
/** fetchPrices 的 OpenRouter 附加腿:成功返回 tmsg 备注,失败返回失败备注,
|
|
1206
|
+
* 永不抛错(不翻转官方同步的 ok 状态)。 */
|
|
1207
|
+
async function openRouterRefreshNote(openRouterPrices, locale) {
|
|
1208
|
+
try {
|
|
1209
|
+
const { count } = await openRouterPrices.refresh()
|
|
1210
|
+
return tmsg(locale, 'openrouterRefreshed', { count })
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
const detail = error?.code === 'ERR_NO_MODELS'
|
|
1213
|
+
? tmsg(locale, 'noModelsParsed')
|
|
1214
|
+
: (error instanceof Error ? error.message : String(error))
|
|
1215
|
+
return tmsg(locale, 'openrouterRefreshFailed', { error: detail })
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1200
1219
|
/**
|
|
1201
1220
|
* 创建 costMeter 服务对象。手写 `typertRemote` 绑定(service/serviceKey/namespace)
|
|
1202
1221
|
* 满足 Typert 网关的 validateBinding 校验;方法按清单参数顺序位置调用。
|
|
@@ -1281,7 +1300,7 @@ async function describeGoKey(ctx, config) {
|
|
|
1281
1300
|
return legacy ? { keyConfigured: true, keySource: 'legacy' } : fallback
|
|
1282
1301
|
}
|
|
1283
1302
|
|
|
1284
|
-
function createService(ctx, ledger) {
|
|
1303
|
+
function createService(ctx, ledger, openRouterPrices) {
|
|
1285
1304
|
// 余额进程内缓存:display=off 时不清缓存但不下发;按 refreshMinutes 过期。
|
|
1286
1305
|
let balanceCache = { fetchedAt: 0, value: emptyBalance() }
|
|
1287
1306
|
// OpenCode Go 订阅额度进程内缓存(同上策略)。
|
|
@@ -2180,18 +2199,24 @@ function createService(ctx, ledger) {
|
|
|
2180
2199
|
const baseMessage = fallbackError === null
|
|
2181
2200
|
? tmsg(locale, 'pricesSynced', { ids: joined })
|
|
2182
2201
|
: tmsg(locale, 'pricesSyncedFallback', { error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError), ids: joined })
|
|
2202
|
+
// OpenRouter 附加腿(Phase2):官方页不管成败都尝试刷新,不翻转 ok。
|
|
2203
|
+
const orNote = await openRouterRefreshNote(openRouterPrices, locale)
|
|
2204
|
+
const parts = recomputeNote.length > 0 ? [baseMessage, recomputeNote, orNote] : [baseMessage, orNote]
|
|
2183
2205
|
return {
|
|
2184
2206
|
ok: true,
|
|
2185
|
-
message:
|
|
2207
|
+
message: parts.join('。'),
|
|
2186
2208
|
state: await build(false),
|
|
2187
2209
|
}
|
|
2188
2210
|
} catch (error) {
|
|
2189
2211
|
const detail = error?.code === 'ERR_NO_MODELS'
|
|
2190
2212
|
? tmsg(locale, 'noModelsParsed')
|
|
2191
2213
|
: (error instanceof Error ? error.message : String(error))
|
|
2214
|
+
// 官方页失败也不放弃 OpenRouter 腿:两路独立源,备注如实说明。
|
|
2215
|
+
const orNote = await openRouterRefreshNote(openRouterPrices, locale)
|
|
2192
2216
|
return {
|
|
2193
2217
|
ok: false,
|
|
2194
|
-
message: tmsg(locale, 'priceSyncFailed', { error: detail })
|
|
2218
|
+
message: `${tmsg(locale, 'priceSyncFailed', { error: detail })}。${orNote}`,
|
|
2219
|
+
state: await build(false),
|
|
2195
2220
|
}
|
|
2196
2221
|
}
|
|
2197
2222
|
},
|
|
@@ -2727,6 +2752,9 @@ export async function runStartupImports(ledger, sessionsRoot) {
|
|
|
2727
2752
|
// 新价格可能改变 plan 归类所需的 apiCost 口径,联动重算一次
|
|
2728
2753
|
if (repaired.recostedBuckets > 0) splitLedgerApiCost(ledger)
|
|
2729
2754
|
}
|
|
2755
|
+
// 离线时先按快照修复零费用的 OpenRouter 桶;全目录成功后才标记完成。
|
|
2756
|
+
const openRouterRepair = repairOpenRouterLedger(ledger)
|
|
2757
|
+
if (openRouterRepair.recostedBuckets > 0) ledger.scheduleWrite()
|
|
2730
2758
|
// 计费口径一致性重建(v1.6.0):桶级 apiCost 按分类回写 + 容器 = Σ桶 +
|
|
2731
2759
|
// 无明细残差归 API;修复桶/容器脱节导致的累计费用失真(用户实测:插件
|
|
2732
2760
|
// $5.26 vs 桶级分布 $12.41)。幂等,标记防重跑。v4:去掉 modlens-zen
|
|
@@ -2858,6 +2886,15 @@ export function apply(ctx) {
|
|
|
2858
2886
|
|
|
2859
2887
|
// 卸载/退出前最终落盘。
|
|
2860
2888
|
ctx.effect(() => () => ledger.close(), 'cost-meter: ledger close')
|
|
2889
|
+
const openRouterPrices = createOpenRouterPriceRefresh(ledger)
|
|
2890
|
+
let active = true
|
|
2891
|
+
ctx.effect(() => () => { active = false; openRouterPrices.dispose() }, 'cost-meter: openrouter requests')
|
|
2892
|
+
const refreshOpenRouter = () => {
|
|
2893
|
+
if (!active) return
|
|
2894
|
+
return openRouterPrices.refresh().catch(error => {
|
|
2895
|
+
if (active) console.warn(`[dsh-cost-meter] OpenRouter 价格刷新失败(保留本地价格): ${String(error?.message ?? error)}`)
|
|
2896
|
+
})
|
|
2897
|
+
}
|
|
2861
2898
|
|
|
2862
2899
|
// 历史账本按模型回填 + 首次启动自动导入安装前历史(issue #27):
|
|
2863
2900
|
// 启动后延迟执行,避免拖慢宿主启动;均幂等,只补缺失,不重复计数。
|
|
@@ -2876,13 +2913,18 @@ export function apply(ctx) {
|
|
|
2876
2913
|
} catch (error) {
|
|
2877
2914
|
console.warn(`[dsh-cost-meter] 自定义余额请求头密钥迁移失败: ${String(error?.message ?? error)}`)
|
|
2878
2915
|
}
|
|
2879
|
-
|
|
2916
|
+
if (!active) return
|
|
2917
|
+
await runStartupImports(ledger, join(resolveDshHome(), 'sessions')).catch(error => {
|
|
2880
2918
|
console.warn(`[dsh-cost-meter] 启动期历史导入失败: ${String(error?.message ?? error)}`)
|
|
2881
2919
|
})
|
|
2920
|
+
await refreshOpenRouter()
|
|
2882
2921
|
}, 3000)
|
|
2883
2922
|
backfillTimer.unref?.()
|
|
2884
2923
|
// 卸载/退出时清掉尚未触发的启动回填定时器:dispose 后不再执行导入。
|
|
2885
2924
|
ctx.effect(() => () => clearTimeout(backfillTimer), 'cost-meter: backfill timer')
|
|
2925
|
+
const openRouterTimer = setInterval(() => { void refreshOpenRouter() }, 3600_000)
|
|
2926
|
+
openRouterTimer.unref?.()
|
|
2927
|
+
ctx.effect(() => () => clearInterval(openRouterTimer), 'cost-meter: openrouter refresh timer')
|
|
2886
2928
|
|
|
2887
2929
|
// 包裹 llm/stream:捕获 usage 块(位于 finish 之前),按官方价格计入账本。
|
|
2888
2930
|
// 本插件是链尾监听者,next() 即适配器流;仅透传数据块,不改变流协议。
|
|
@@ -2924,7 +2966,7 @@ export function apply(ctx) {
|
|
|
2924
2966
|
})
|
|
2925
2967
|
|
|
2926
2968
|
// RPC 服务:客户端经 remote.costMeter.* 调用(./typert 清单由 typert-loader 注册)。
|
|
2927
|
-
ctx.provide('costMeter', createService(ctx, ledger))
|
|
2969
|
+
ctx.provide('costMeter', createService(ctx, ledger, openRouterPrices))
|
|
2928
2970
|
|
|
2929
2971
|
// dshmarket 1.47 热安装通过独立 Include 挂载 file:// 入口;它不在根 Loader
|
|
2930
2972
|
// 的包名扫描树中,typert-loader 因此漏注册 (#154)。正常包名启动仍由宿主注册。
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { DEFAULT_PROVIDER_PRICE_TABLE, OPENROUTER_MODELS_URL, normalizePrice, parseOpenRouterModels } from './pricing.js'
|
|
3
|
+
import { applyConfigPatch, repairLedgerPricing } from './store.js'
|
|
4
|
+
import { fetchWithRetry, readJsonBounded } from './net.js'
|
|
5
|
+
|
|
6
|
+
const MIGRATION = 'pricing-openrouter-v1'
|
|
7
|
+
const fingerprint = raw => {
|
|
8
|
+
const entry = normalizePrice(raw)
|
|
9
|
+
if (!entry) return null
|
|
10
|
+
const { sourceUrl, checkedAt, notes, ...rates } = entry
|
|
11
|
+
return createHash('sha256').update(JSON.stringify(rates)).digest('hex')
|
|
12
|
+
}
|
|
13
|
+
const baseline = (ledger, id) => ledger.openrouterPriceHashes?.[id]
|
|
14
|
+
?? fingerprint(DEFAULT_PROVIDER_PRICE_TABLE.openrouter.models[id])
|
|
15
|
+
const managed = (ledger, id, entry) => baseline(ledger, id) === fingerprint(entry)
|
|
16
|
+
|
|
17
|
+
/** 只估算 OpenRouter 零费用桶;已记金额、其他厂商和显式映射保持原样。 */
|
|
18
|
+
export function repairOpenRouterLedger(ledger) {
|
|
19
|
+
if (ledger.migrations.includes(MIGRATION)) return { recostedBuckets: 0 }
|
|
20
|
+
return repairLedgerPricing(ledger, (key, bucket) => {
|
|
21
|
+
const sep = key.indexOf(':')
|
|
22
|
+
if (!['openrouter', 'llm-openrouter'].includes(key.slice(0, sep)) || Number(bucket.cost) !== 0) return false
|
|
23
|
+
const id = key.slice(sep + 1), entry = ledger.config.prices?.providers?.openrouter?.models?.[id]
|
|
24
|
+
if (Object.hasOwn(ledger.config.priceOverrides ?? {}, 'openrouter:' + id)) return false
|
|
25
|
+
return !!entry && managed(ledger, id, entry)
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 每个账本共享一次在途请求;总超时覆盖响应体,卸载取消并阻止迟到写入。 */
|
|
30
|
+
export function createOpenRouterPriceRefresh(ledger, { fetchImpl, timeoutMs = 20000 } = {}) {
|
|
31
|
+
let active = true, pending = null, controller = null
|
|
32
|
+
const refresh = () => {
|
|
33
|
+
if (!active || ledger.closed) return Promise.reject(new Error('OpenRouter price refresh disposed'))
|
|
34
|
+
if (pending) return pending
|
|
35
|
+
controller = new AbortController()
|
|
36
|
+
const signal = controller.signal
|
|
37
|
+
const timer = setTimeout(() => controller?.abort(new Error('OpenRouter price refresh timed out')), timeoutMs)
|
|
38
|
+
timer.unref?.()
|
|
39
|
+
pending = (async () => {
|
|
40
|
+
const response = await fetchWithRetry(OPENROUTER_MODELS_URL, {
|
|
41
|
+
headers: { 'user-agent': 'dsh-cost-meter (DeepSeek Harness plugin)' }, redirect: 'error', signal,
|
|
42
|
+
}, { attempts: 2, fetchImpl })
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
try { await response.body?.cancel?.() } catch {}
|
|
45
|
+
throw new Error(`HTTP ${response.status}`)
|
|
46
|
+
}
|
|
47
|
+
const { models } = parseOpenRouterModels(await readJsonBounded(response, 4 * 1024 * 1024))
|
|
48
|
+
signal.throwIfAborted()
|
|
49
|
+
if (!active || ledger.closed) throw new Error('OpenRouter price refresh disposed')
|
|
50
|
+
// 网络等待后读取当前配置,保留期间的其他设置改动;手改价格不被覆盖。
|
|
51
|
+
const previous = ledger.config.prices?.providers?.openrouter?.models ?? {}
|
|
52
|
+
const merged = { ...previous }, hashes = { ...ledger.openrouterPriceHashes }
|
|
53
|
+
let count = 0
|
|
54
|
+
for (const [id, raw] of Object.entries(models)) {
|
|
55
|
+
const entry = normalizePrice(raw)
|
|
56
|
+
if (!entry) continue
|
|
57
|
+
if (Object.hasOwn(previous, id) ? !managed(ledger, id, previous[id]) : hashes[id] !== undefined) continue
|
|
58
|
+
merged[id] = { ...entry, ...(previous[id]?.notes ? { notes: previous[id].notes } : {}) }
|
|
59
|
+
hashes[id] = fingerprint(entry)
|
|
60
|
+
count++
|
|
61
|
+
}
|
|
62
|
+
const { config, errors } = applyConfigPatch(ledger.config, { prices: {
|
|
63
|
+
...ledger.config.prices,
|
|
64
|
+
providers: { ...ledger.config.prices?.providers, openrouter: { models: merged } },
|
|
65
|
+
} })
|
|
66
|
+
if (errors.length) throw new Error(errors.join(';'))
|
|
67
|
+
ledger.config = config
|
|
68
|
+
ledger.openrouterPriceHashes = hashes
|
|
69
|
+
// 离线启动只做快照范围的修复;首次目录成功后再完成迁移,避免永久漏掉其余模型。
|
|
70
|
+
const repaired = repairOpenRouterLedger(ledger)
|
|
71
|
+
if (!ledger.migrations.includes(MIGRATION)) ledger.migrations.push(MIGRATION)
|
|
72
|
+
ledger.scheduleWrite()
|
|
73
|
+
return { count, ...repaired }
|
|
74
|
+
})().finally(() => { clearTimeout(timer); pending = null; controller = null })
|
|
75
|
+
return pending
|
|
76
|
+
}
|
|
77
|
+
return { refresh, dispose() { active = false; controller?.abort(new Error('OpenRouter price refresh disposed')) } }
|
|
78
|
+
}
|
package/lib/pricing.js
CHANGED
|
@@ -280,6 +280,76 @@ export const DEFAULT_PROVIDER_PRICE_TABLE = {
|
|
|
280
280
|
'muse-spark-1.2-contributor': { input: 0.1, cachedInput: 0.002, output: 0.2, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-25', notes: '低价换取提示词/补全用于训练 Meta 模型;限地区' },
|
|
281
281
|
},
|
|
282
282
|
},
|
|
283
|
+
// OpenRouter 公开目录中的 token 参考价(https://openrouter.ai/api/v1/models)。
|
|
284
|
+
// 键为 OpenRouter 完整模型 id(含厂商前缀,如 'meta/...'),与账本
|
|
285
|
+
// 'openrouter:<id>' 精确对应;静态快照(2026-09-17),动态刷新见后续。
|
|
286
|
+
openrouter: {
|
|
287
|
+
models: {
|
|
288
|
+
'meta/muse-spark-1.3-contributor': { input: 0.1, cachedInput: 0.002, output: 0.2, billingMode: 'flat', sourceUrl: 'https://openrouter.ai/api/v1/models', checkedAt: '2026-09-17' },
|
|
289
|
+
'google/gemini-3.8-flash': { input: 0.75, cachedInput: 0.075, cacheWrite: 0.041667, output: 3.75, billingMode: 'flat', sourceUrl: 'https://openrouter.ai/api/v1/models', checkedAt: '2026-09-17' },
|
|
290
|
+
'qwen/qwen3.8-flash': { input: 0.15, cachedInput: 0.016, cacheWrite: 0.2, output: 0.47, billingMode: 'flat', sourceUrl: 'https://openrouter.ai/api/v1/models', checkedAt: '2026-09-17' },
|
|
291
|
+
'z-ai/glm-5.3-flash': { input: 0.09, cachedInput: 0.018, output: 0.3, billingMode: 'flat', sourceUrl: 'https://openrouter.ai/api/v1/models', checkedAt: '2026-09-17' },
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** OpenRouter 模型目录接口(公开,无需认证;返回 USD/token 字符串)。
|
|
297
|
+
* Phase2 实时刷新源:本地 DEFAULT_PROVIDER_PRICE_TABLE 只保留少量快照
|
|
298
|
+
* 做离线兜底,启动/手动同步/每小时刷新用它补全全量 openrouter 价。 */
|
|
299
|
+
export const OPENROUTER_MODELS_URL = 'https://openrouter.ai/api/v1/models'
|
|
300
|
+
|
|
301
|
+
/** 解析 OpenRouter /models 载荷为价目原始条目(USD/百万 token,flat 口径)。
|
|
302
|
+
* 输入:GET OPENROUTER_MODELS_URL 的 JSON({ data: [{ id, pricing }] });
|
|
303
|
+
* 输出:{ models: { '<vendor>/<model>': { input, output, cachedInput?, cacheWrite?, billingMode: 'flat', sourceUrl, checkedAt } } }。
|
|
304
|
+
* 跳过:空 id / 无 pricing / prompt 或 completion 缺失或负数/非数字
|
|
305
|
+
* (路由聚合模型如 openrouter/auto 用 prompt:"-1" 占位,无真实单价)。
|
|
306
|
+
* 可选键只在有限非负数时收录;"0"(免费模型)收录为 0。
|
|
307
|
+
* 纯函数,无网络 IO,方便 verify.mjs 单测。 */
|
|
308
|
+
export function parseOpenRouterModels(json) {
|
|
309
|
+
const list = Array.isArray(json?.data) ? json.data : null
|
|
310
|
+
if (list === null) {
|
|
311
|
+
// code 供上层按语言渲染提示(见 index.js 的 ERR_NO_MODELS 分支)。
|
|
312
|
+
const error = new Error('ERR_NO_MODELS')
|
|
313
|
+
error.code = 'ERR_NO_MODELS'
|
|
314
|
+
throw error
|
|
315
|
+
}
|
|
316
|
+
const rate = (value) => {
|
|
317
|
+
if ((typeof value !== 'string' && typeof value !== 'number')
|
|
318
|
+
|| (typeof value === 'string' && !/^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(value.trim()))) return undefined
|
|
319
|
+
const n = Number(value)
|
|
320
|
+
const scaled = n * 1e6
|
|
321
|
+
if (!Number.isFinite(scaled) || n < 0) return undefined
|
|
322
|
+
const rounded = Number(scaled.toFixed(6))
|
|
323
|
+
return rounded === 0 && scaled > 0 ? Number(scaled.toPrecision(15)) : rounded
|
|
324
|
+
}
|
|
325
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
326
|
+
// USD/token ×1e6 会带浮点尘(如 1e-7*1e6=0.09999999999999999),统一舍到
|
|
327
|
+
// 6 位小数,与本地快照写法一致,避免价目表出现长尾小数。
|
|
328
|
+
const models = {}
|
|
329
|
+
for (const item of list) {
|
|
330
|
+
if (item === null || typeof item !== 'object') continue
|
|
331
|
+
const id = typeof item.id === 'string' ? item.id.trim() : ''
|
|
332
|
+
if (id === '' || id === '__proto__' || id === 'constructor' || id === 'prototype') continue
|
|
333
|
+
const pricing = item.pricing
|
|
334
|
+
if (pricing === null || typeof pricing !== 'object' || Array.isArray(pricing)) continue
|
|
335
|
+
const prompt = rate(pricing.prompt)
|
|
336
|
+
const completion = rate(pricing.completion)
|
|
337
|
+
if (prompt === undefined || completion === undefined) continue
|
|
338
|
+
const raw = {
|
|
339
|
+
input: prompt,
|
|
340
|
+
output: completion,
|
|
341
|
+
billingMode: 'flat',
|
|
342
|
+
sourceUrl: OPENROUTER_MODELS_URL,
|
|
343
|
+
checkedAt: today,
|
|
344
|
+
}
|
|
345
|
+
const cacheRead = rate(pricing.input_cache_read)
|
|
346
|
+
if (cacheRead !== undefined) raw.cachedInput = cacheRead
|
|
347
|
+
const cacheWrite = rate(pricing.input_cache_write)
|
|
348
|
+
if (cacheWrite !== undefined) raw.cacheWrite = cacheWrite
|
|
349
|
+
models[id] = raw
|
|
350
|
+
}
|
|
351
|
+
if (Object.keys(models).length === 0) throw Object.assign(new Error('ERR_NO_MODELS'), { code: 'ERR_NO_MODELS' })
|
|
352
|
+
return { models }
|
|
283
353
|
}
|
|
284
354
|
|
|
285
355
|
/** 拓展价格表目录的模型家族分组(展示用;未列出的模型自成一家)。 */
|
|
@@ -322,6 +392,10 @@ export const PROVIDER_MODEL_FAMILIES = {
|
|
|
322
392
|
mistral: { 'mistral-large-2512': 'Mistral Large', 'mistral-medium-3.5': 'Mistral Medium', 'mistral-small-4.0': 'Mistral Small' },
|
|
323
393
|
meta: { 'muse-spark-1.2': 'Muse Spark' },
|
|
324
394
|
meituan: { 'longcat-2.0': 'LongCat' },
|
|
395
|
+
openrouter: {
|
|
396
|
+
'meta/muse-spark-1.3-contributor': 'Muse Spark', 'google/gemini-3.8-flash': 'Gemini 3.8 Flash',
|
|
397
|
+
'qwen/qwen3.8-flash': 'Qwen3.8 Flash', 'z-ai/glm-5.3-flash': 'GLM-5 Flash',
|
|
398
|
+
},
|
|
325
399
|
'opencode-go': {
|
|
326
400
|
'gpt-5.6-luna': 'GPT', 'grok-4.5': 'Grok', 'glm-5.3': 'GLM', 'glm-5.3-flash': 'GLM Flash', 'glm-5.2': 'GLM', 'glm-5.1': 'GLM',
|
|
327
401
|
'kimi-k3': 'Kimi', 'kimi-k2.7-code': 'Kimi', 'kimi-k2.6': 'Kimi',
|
|
@@ -778,6 +852,13 @@ function providerPriceEntryForNormalized(normalized, modelId, prices, options) {
|
|
|
778
852
|
if (isLocalOriginProviderOrModel(targetProvider, targetModel)) {
|
|
779
853
|
return { entry: null, billingMode: 'flat', priced: false }
|
|
780
854
|
}
|
|
855
|
+
// OpenRouter 的 :free / :online 等变体有独立价格;完整 id 精确查价,避免
|
|
856
|
+
// 新目录加入免费项后,未收录的付费模型或其他变体通过模糊匹配被记作免费。
|
|
857
|
+
if (targetProvider === 'openrouter') {
|
|
858
|
+
const models = prices?.providers?.openrouter?.models ?? {}
|
|
859
|
+
const entry = Object.hasOwn(models, targetModel) ? normalizePrice(models[targetModel]) : null
|
|
860
|
+
return { entry, billingMode: 'flat', priced: entry !== null && entry.unpriced !== true }
|
|
861
|
+
}
|
|
781
862
|
if (targetProvider === '' || targetProvider === 'deepseek' || targetProvider.includes('deepseek')) {
|
|
782
863
|
const models = prices?.models ?? {}
|
|
783
864
|
// 仅认自有属性(防原型链键);命中后统一 normalizePrice 补齐两档简写等写法,
|
|
@@ -800,6 +881,7 @@ function providerPriceEntryForNormalized(normalized, modelId, prices, options) {
|
|
|
800
881
|
let bestLen = -1
|
|
801
882
|
let bestMode = 'flat'
|
|
802
883
|
for (const [prov, table] of Object.entries(prices?.providers ?? {})) {
|
|
884
|
+
if (prov === 'openrouter') continue // 路由目录不作为其他渠道的模糊兜底价。
|
|
803
885
|
const modelsCat = table?.models ?? {}
|
|
804
886
|
const h = matchModelId(targetModel, Object.keys(modelsCat))
|
|
805
887
|
if (h === null) continue
|
|
@@ -839,7 +921,7 @@ function providerPriceEntryForNormalized(normalized, modelId, prices, options) {
|
|
|
839
921
|
let bestLen = -1
|
|
840
922
|
let bestMode = 'flat'
|
|
841
923
|
for (const [prov, table] of Object.entries(prices?.providers ?? {})) {
|
|
842
|
-
if (prov === targetProvider) continue
|
|
924
|
+
if (prov === targetProvider || prov === 'openrouter') continue
|
|
843
925
|
const models = table?.models ?? {}
|
|
844
926
|
const h = matchModelId(targetModel, Object.keys(models))
|
|
845
927
|
if (h === null) continue
|
package/lib/store.js
CHANGED
|
@@ -382,9 +382,10 @@ export function splitLedgerApiCost(ledger) {
|
|
|
382
382
|
* 逐 byProviderModel 桶按当前价格表重算(flat 模型与峰谷无关,DeepSeek
|
|
383
383
|
* 档按日中午近似,容差 10% 以内不扰动,避免峰时抖动误伤)。
|
|
384
384
|
* 同时按新 planBilling 重算 apiCost。
|
|
385
|
+
* @param accepts - 可选桶筛选器;定向迁移只修改命中的 provider:model 与费用。
|
|
385
386
|
* @returns { touchedDays, touchedSessions, recostedBuckets }
|
|
386
387
|
*/
|
|
387
|
-
export function repairLedgerPricing(ledger) {
|
|
388
|
+
export function repairLedgerPricing(ledger, accepts = () => true) {
|
|
388
389
|
const enabledPlans = enabledPlanSetOf(ledger.config)
|
|
389
390
|
const planBilling = ledger.config.planBilling
|
|
390
391
|
const prices = ledger.config.prices
|
|
@@ -402,6 +403,7 @@ export function repairLedgerPricing(ledger) {
|
|
|
402
403
|
let dayChanged = false
|
|
403
404
|
for (const [key, bucket] of Object.entries(day.byProviderModel ?? {})) {
|
|
404
405
|
if (bucket === null || typeof bucket !== 'object') continue
|
|
406
|
+
if (!accepts(key, bucket)) continue
|
|
405
407
|
const sep = key.indexOf(':')
|
|
406
408
|
const provider = sep >= 0 ? key.slice(0, sep) : 'deepseek'
|
|
407
409
|
const model = sep >= 0 ? key.slice(sep + 1) : key
|
|
@@ -445,6 +447,7 @@ export function repairLedgerPricing(ledger) {
|
|
|
445
447
|
let sessApiDelta = 0
|
|
446
448
|
let sessChanged = false
|
|
447
449
|
for (const [key, bucket] of Object.entries(session.byProviderModel ?? {})) {
|
|
450
|
+
if (bucket === null || typeof bucket !== 'object' || !accepts(key, bucket)) continue
|
|
448
451
|
const sep = key.indexOf(':')
|
|
449
452
|
const provider = sep >= 0 ? key.slice(0, sep) : 'deepseek'
|
|
450
453
|
const model = sep >= 0 ? key.slice(sep + 1) : key
|
|
@@ -1933,6 +1936,9 @@ export class Ledger {
|
|
|
1933
1936
|
// Plan 百分比采样历史与 provider×小时聚合桶(issue #64;v1.5.52 起小时桶取代环形缓冲)。
|
|
1934
1937
|
this.planSamples = extras.planSamples ?? {}
|
|
1935
1938
|
this.planHourBuckets = extras.planHourBuckets ?? {}
|
|
1939
|
+
// 自动目录上次写入的价格指纹:重启后仍能识别用户手改价格,不下发到客户端。
|
|
1940
|
+
this.openrouterPriceHashes = Object.fromEntries(Object.entries(extras.openrouterPriceHashes ?? {})
|
|
1941
|
+
.filter(([key, value]) => !['__proto__', 'constructor', 'prototype'].includes(key) && typeof value === 'string' && /^[a-f0-9]{64}$/.test(value)))
|
|
1936
1942
|
}
|
|
1937
1943
|
|
|
1938
1944
|
/** 在 $DSH_HOME 下创建/加载账本。 */
|
|
@@ -1945,6 +1951,7 @@ export class Ledger {
|
|
|
1945
1951
|
let migrations = []
|
|
1946
1952
|
let planSamples = {}
|
|
1947
1953
|
let planHourBuckets = {}
|
|
1954
|
+
let openrouterPriceHashes = {}
|
|
1948
1955
|
try {
|
|
1949
1956
|
const parsed = JSON.parse(readFileSync(path, 'utf8'))
|
|
1950
1957
|
if (parsed !== null && typeof parsed === 'object') {
|
|
@@ -1964,6 +1971,7 @@ export class Ledger {
|
|
|
1964
1971
|
}
|
|
1965
1972
|
// 新版本新增的配置键用默认值补齐;非法值清洗回落,防止击穿 strict codec。
|
|
1966
1973
|
config = sanitizeConfig(cfg)
|
|
1974
|
+
if (parsed.openrouterPriceHashes && typeof parsed.openrouterPriceHashes === 'object' && !Array.isArray(parsed.openrouterPriceHashes)) openrouterPriceHashes = parsed.openrouterPriceHashes
|
|
1967
1975
|
if (parsed.days !== null && typeof parsed.days === 'object' && !Array.isArray(parsed.days)) {
|
|
1968
1976
|
// 旧账本可能含 reasoning: null 等非法数值:清洗后再入内存,并触发回写覆盖。
|
|
1969
1977
|
days = sanitizeDays(parsed.days)
|
|
@@ -1995,7 +2003,7 @@ export class Ledger {
|
|
|
1995
2003
|
console.warn(`[dsh-cost-meter] 账本读取失败,按空账本启动: ${String(error?.message ?? error)}`)
|
|
1996
2004
|
}
|
|
1997
2005
|
}
|
|
1998
|
-
const ledger = new Ledger(config, days, path, migrations, { planSamples, planHourBuckets })
|
|
2006
|
+
const ledger = new Ledger(config, days, path, migrations, { planSamples, planHourBuckets, openrouterPriceHashes })
|
|
1999
2007
|
ledger.balanceRef = balanceRef
|
|
2000
2008
|
return ledger
|
|
2001
2009
|
}
|
|
@@ -2150,6 +2158,7 @@ export class Ledger {
|
|
|
2150
2158
|
migrations: this.migrations,
|
|
2151
2159
|
planSamples: this.planSamples ?? {},
|
|
2152
2160
|
planHourBuckets: this.planHourBuckets,
|
|
2161
|
+
openrouterPriceHashes: this.openrouterPriceHashes,
|
|
2153
2162
|
}), 'utf8')
|
|
2154
2163
|
renameSync(tmp, this.path)
|
|
2155
2164
|
this.pendingWrite = false
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cost-meter",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.30",
|
|
4
4
|
"description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(9 家,含 SCNet Token Plan 本地 Credits 计量与火山方舟 Volcano Ark Coding Plan AK/SK 签名查询)、自定义 Provider 余额查询(可配任意 HTTP 端点)与余额进度条、峰谷计价时段显示与切换前弹窗/系统通知提醒(位置/提前量/类型可配),界面中英双语。Session cost tracking plugin for DeepSeek Harness: per-conversation cost, daily totals, history and official price sync, with multi-vendor model pricing (built-in 90+ model catalog with auto-matching), Coding Plan quota queries & display for 9 vendors (incl. local credits metering for the SCNet Token Plan and Volcano Ark Coding Plan via AK/SK signature), plan/API dual-track billing with per-1% & full-window Token Plan usage estimates, custom provider balance lookup (configurable HTTP endpoint) and balance progress bar, plus peak/off-peak pricing display with pre-switch popup & system-notification alerts (position / lead time / type configurable), in a bilingual (Chinese/English) UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|