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 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
- [![version](https://img.shields.io/badge/version-1.7.29-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.7.30-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
10
10
 
11
- **v1.7.29** fixes an unavailable ledger after a marketplace hot install. Initial reads retry automatically; the empty state shows the error and a Refresh button. See the [hot-install guide](docs/market-hot-install.md#english).
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
  [![npm](https://img.shields.io/npm/v/dsh-cost-meter?label=npm)](https://www.npmjs.com/package/dsh-cost-meter)
14
14
  [![license](https://img.shields.io/badge/license-MIT-green)](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.29`** — review the script before running):
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.29/install.ps1 | iex
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.29
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.29.tar.gz
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
- [![version](https://img.shields.io/badge/version-1.7.29-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.7.30-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
10
10
 
11
- **v1.7.29**:修复插件市场热安装后“账本不可用”、必须重启才恢复的问题;首屏读取失败会自动重试,并显示错误与刷新按钮。详见[热安装说明](docs/market-hot-install.md)。
11
+ **v1.7.30**:新增 OpenRouter token 价格目录与自动刷新,内置四模型离线快照;定向修复零费用旧账,保留手改价格和已记费用。感谢 @GnaneshKunal 的 PR #156。详见[定价说明](docs/openrouter-pricing.md)。
12
12
 
13
13
  [![npm](https://img.shields.io/npm/v/dsh-cost-meter?label=npm)](https://www.npmjs.com/package/dsh-cost-meter)
14
14
  [![license](https://img.shields.io/badge/license-MIT-green)](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.29`**,建议先下载审阅再运行):
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.29/install.ps1 | iex
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.29
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.29.tar.gz
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: recomputeNote.length > 0 ? `${baseMessage}。${recomputeNote}` : baseMessage,
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
- runStartupImports(ledger, join(resolveDshHome(), 'sessions')).catch(error => {
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.29",
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",