dsh-cost-meter 1.5.37 → 1.5.42
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 +13 -11
- package/README.md +13 -11
- package/lib/backfill.js +126 -15
- package/lib/billing-stream.js +63 -0
- package/lib/client.js +224 -68
- package/lib/coding-plans.js +81 -12
- package/lib/index.js +61 -37
- package/lib/pricing.js +204 -44
- package/lib/store.js +38 -1
- package/lib/typert.host.js +7 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
**Session cost tracking plugin for the DeepSeek Harness web GUI (bilingual UI)**
|
|
6
6
|
|
|
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) · 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) · quota strip above the input box (budget / Go / coding-plan usage in one row, toggleable)
|
|
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) · 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
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -29,8 +29,8 @@ English | [中文](README.md)
|
|
|
29
29
|
| Official balance | Sidebar top / Settings page (configurable) | Total / granted / topped-up balance, auto-refresh + manual refresh; optional three-segment progress bar (blue/orange/gray), whose today segment only counts official-channel spend (coding plans / custom providers excluded) |
|
|
30
30
|
| Custom provider balance | Sidebar / Settings page (configurable) | Configurable HTTP balance lookup (e.g. LiteLLM); bilingual labels, currency, extract rules (dot path / number / add / subtract / divide — use divide for NewApi-style quota endpoints, see [example](#custom-provider-balance-example-newapi-template)); collapsible panel alongside Coding Plan quotas |
|
|
31
31
|
| OpenCode Go quota | Sidebar / Settings / bottom-right dock (configurable) | Rolling-5h / weekly / monthly usage percent and reset times, each window toggleable independently, budget used % can show alongside; key auto-discovered (DSH credential store OPENCODE_GO_API_KEY / env / opencode login) or entered manually |
|
|
32
|
-
| Coding plan quotas | Sidebar / Settings page (per vendor) | Multi-vendor coding-plan quota queries (Anthropic Claude Pro/Max, Z.ai / Zhipu GLM Coding Plan, MiniMax Token Plan, Kimi
|
|
33
|
-
| Quota strip | Above the input box (toggle in Display settings) | One compact chip row for budget used % / the Go main window / each enabled coding-plan usage window (short label + mini progress bar, ≥80% warn, ≥100% over, hover for reset times); a first-run guide card lets you decide whether to enable it; hides itself when there is no quota data |
|
|
32
|
+
| Coding plan quotas | Sidebar / Settings page (per vendor) | Multi-vendor coding-plan quota queries (Anthropic Claude Pro/Max, Z.ai / Zhipu GLM Coding Plan, MiniMax Token Plan, Kimi Code weekly + 5-hour quotas with PAYG balance fallback when no subscription key, OpenRouter credits, SiliconFlow balance, CommandCode 5h/weekly windows + monthly credits balance); per-vendor enable switch, key, display position and refresh interval (sidebar card in the same box style as the Go quota; the collapsed rail shows percentages), credentials only sent to official endpoints; neutral hints when no credentials/subscription; SCNet Token Plan has no quota API — monthly usage is estimated from the local ledger via the official credits deduction table (no credentials needed) |
|
|
33
|
+
| Quota strip | Above the input box (toggle in Display settings) | One compact chip row for budget used % / the Go main window / each enabled coding-plan usage window (short label + mini progress bar, ≥80% warn, ≥100% over, hover for reset times); click any chip to refresh its data source (budget → state, Go → Go quota, vendor → all its windows); multiple windows of one vendor merge into a single segmented chip; a first-run guide card lets you decide whether to enable it; hides itself when there is no quota data |
|
|
34
34
|
| Click to refresh | Sidebar balance/quota boxes | Click the official balance / custom balance / coding-plan box (collapsed rail included) to fetch the latest data immediately; the box pulses while refreshing, failures keep the previous value and surface the reason in the hover tooltip; keyboard Enter/Space also triggers; a one-time guide card appears after the update |
|
|
35
35
|
| Today's cost | Sidebar bottom (above the settings button) | “Today ¥x”, hover for call count and token details |
|
|
36
36
|
| Budget box | Sidebar bottom (between the balance row and the settings button) | Rounded-square frame: budget, used %, progress bar, today's cost & share of budget, used/limit; ≥80% warning, ≥100% over-budget |
|
|
@@ -41,10 +41,11 @@ English | [中文](README.md)
|
|
|
41
41
|
| Pre-install history import | Automatic on first launch | After install/upgrade, the first launch automatically replays all host session logs to import conversations from before the plugin was installed (missing dates are rebuilt whole; existing dates only gain previously unknown sessions; idempotent and never double-counts live metering; costs priced at per-event historical rates); a manual re-run entry remains in Settings |
|
|
42
42
|
| Budget settings | Settings page, top | Limit, period (today / month / cumulative / custom date range), used % |
|
|
43
43
|
| Price table | Settings page | Per-model off-peak / peak prices (input/output shorthand supported; cache prices derived automatically); fully editable |
|
|
44
|
-
| Peak/off-peak hours display | Settings / budget / today | Shows UTC peak hours 01:00–04:00 and 06:00–10:00 with the current tier; expanded view shows a peak/off-peak period strip (current period + countdown), collapsed (rail) view shows a vertical peak/off-peak progress bar; independently toggleable |
|
|
44
|
+
| Peak/off-peak hours display | Settings / budget / today | Shows UTC peak hours 01:00–04:00 and 06:00–10:00 with the current tier; from Aug 23, 2026 weekends (Sat & Sun, Beijing time) are billed at off-peak prices all day and shown as “Weekend — all off-peak”; expanded view shows a peak/off-peak period strip (current period + countdown), collapsed (rail) view shows a vertical peak/off-peak progress bar; independently toggleable |
|
|
45
45
|
| Peak/off-peak switch popup alert | Global overlay | A full-width bracketed popup appears when the next tier switch is within the configured lead time (default 2 minutes, 1–30), with an alert-colored badge distinguishing entering peak vs off-peak; position selectable (**bottom-right / screen center**), alert type selectable (entering peak / entering off-peak / both), one alert per switch point; optionally **sends a browser (system) notification** (so you still get alerted when the page is backgrounded; requires granting notification permission); configured in the peak pricing panel in Settings, with a **one-click popup preview** (rendered by the real component — copy, position and notifications exactly as they will fire) |
|
|
46
|
-
| Official price sync | Settings page | Fetches and parses the official pricing page, applies with one click |
|
|
46
|
+
| Official price sync | Settings page | Fetches and parses the official pricing page, applies with one click; the **official price currency** is selectable (USD · English page / CNY · Chinese page) — CNY prices are booked at the display exchange rate and match the official CNY bill when displayed in CNY |
|
|
47
47
|
| UI language | Settings → Display settings | Simplified Chinese / English / Follow browser (auto); switches instantly and auto-saves |
|
|
48
|
+
| Hide official balance / hide today's cost | Settings → Display settings | Two independent toggles: when on, the matching UI blocks (sidebar balance row & panels / today's cost row, budget details, overview today card) **are not rendered at all**; token and call-count stats stay visible — safe for screen sharing and screenshots |
|
|
48
49
|
| AI price sync | [prompt](docs/AI-PRICE-SYNC-PROMPT.en.md) | DeepSeek official sync; other providers use the verified official price catalog and manual configuration |
|
|
49
50
|
| 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)) |
|
|
50
51
|
| 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)) |
|
|
@@ -228,22 +229,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
|
|
|
228
229
|
dsh plugin --profile web add dsh-cost-meter
|
|
229
230
|
```
|
|
230
231
|
|
|
231
|
-
**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.5.
|
|
232
|
+
**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.5.42`** — review the script before running):
|
|
232
233
|
|
|
233
234
|
```powershell
|
|
234
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
235
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.42/install.ps1 | iex
|
|
235
236
|
```
|
|
236
237
|
|
|
237
238
|
**Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
|
|
238
239
|
|
|
239
240
|
```sh
|
|
240
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
241
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.42
|
|
241
242
|
```
|
|
242
243
|
|
|
243
244
|
Without git, use the GitHub tag archive:
|
|
244
245
|
|
|
245
246
|
```sh
|
|
246
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
247
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.42.tar.gz
|
|
247
248
|
```
|
|
248
249
|
|
|
249
250
|
After installing, **restart** `dsh web` (plugin rows, the Typert manifest and the client bundle are all scanned at startup):
|
|
@@ -274,6 +275,7 @@ dsh plugin --profile web add link:./dsh-cost-meter # symlink; edit lib/client.j
|
|
|
274
275
|
- Price units match the official docs: **USD / 1M tokens**;
|
|
275
276
|
- cost = cache-missed input × cache-miss + output × output + (cache read + cache write) × cache-hit (cache writes follow the legacy official rule and are billed at the hit price);
|
|
276
277
|
- **Pure two-tier peak/off-peak pricing** (the official scheme since 2026-08): peak hours (01:00–04:00, 06:00–10:00 UTC) bill at the peak price and all other hours at the off-peak price (off-peak = half of peak). The base tier equals the off-peak tier, and billing falls back to off-peak when peak/off-peak is disabled; the Settings page shows the live tier (peak / off-peak); the budget/today's cost area shows a peak/off-peak period strip (current/next period with countdown), and the collapsed rail shows a vertical peak/off-peak progress bar;
|
|
278
|
+
- **Weekend all-off-peak rule** (official notice, effective 2026-08-23 00:00 Beijing time): weekends (Saturday & Sunday by the Beijing calendar) no longer differentiate peak/off-peak and are billed entirely at off-peak prices; the period strip shows “Weekend — all off-peak” with the countdown pointing to Monday's first peak window. Charges before that moment still follow the previous rules (the first affected weekend covers Sunday only);
|
|
277
279
|
- **Historical billing correctness**: calls before 2026-08-16 16:00 UTC (the peak-era boundary) are billed at the base prices of that time, and later calls at the two-tier scheme;
|
|
278
280
|
- The ledger always stores amounts in **USD**; currency and FX rate only affect display (default 1 USD = 7.2 CNY, configurable);
|
|
279
281
|
- The session badge is **billed exactly** at the moment each call is made (host-exported per-call cost), just like daily/monthly/cumulative totals and the budget;
|
|
@@ -316,7 +318,7 @@ The plugin never imports cordis/dsh Service/Context runtime classes (only Node b
|
|
|
316
318
|
|
|
317
319
|
## How official price sync works
|
|
318
320
|
|
|
319
|
-
`fetchPrices` fetches the official pricing page (Docusaurus server-side pre-rendered) and parses:
|
|
321
|
+
`fetchPrices` fetches the official pricing page (Docusaurus server-side pre-rendered; the English page lists USD prices and the Chinese page CNY prices, selected by the "official price currency" setting — currency is auto-detected from the money symbols, and peak windows on the Chinese page are converted from Beijing time to UTC by −8h) and parses:
|
|
320
322
|
|
|
321
323
|
1. the base price table (transposed layout: first row MODEL + model ids, price labels followed by the prices);
|
|
322
324
|
2. the peak/off-peak price table (two rows per model: OFF-PEAK / PEAK);
|
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
**DeepSeek Harness 会话费用统计插件(界面中英双语)**
|
|
6
6
|
|
|
7
|
-
本会话费用 · 当日费用 · OpenCode Go 订阅额度显示 · 预算与已用百分比 · 官方账户余额 · 自定义 Provider 余额查询(可配任意 HTTP 端点) · 余额三段进度条 · 历史记录 · 峰谷计价时段显示(UTC 01:00–04:00、06:00–10:00
|
|
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 八家) · 输入框上方额度横条(预算/Go/Coding Plan 用量一条横排显示,可开关)
|
|
8
8
|
|
|
9
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
| 官方余额 | 侧边栏顶部 / 设置页(可配) | 总余额 / 赠送 / 充值,自动刷新 + 手动刷新;可选三段进度条(蓝/橙/灰),当日段只统计官方渠道费用(不含 Coding Plan / 自定义 Provider) |
|
|
30
30
|
| 自定义 Provider 余额 | 侧边栏 / 设置页(可配) | 可配置 HTTP 查询任意 Provider 余额(LiteLLM 等);中/英名称、币种、extract 规则(点路径 / 数字常量 / add / subtract / divide,divide 适配 NewApi 等 quota 端点,见下方[示例](#自定义-provider-余额配置示例newapi-模板));与 Coding Plan 同区可折叠配置 |
|
|
31
31
|
| OpenCode Go 额度 | 侧边栏 / 设置页 / 右下角(dock,可配) | 滚动 5 小时 / 本周 / 本月用量百分比与重置时间,三档可分别开关,可同时显示预算已用%;Key 自动发现(DSH 凭据库 OPENCODE_GO_API_KEY / 环境变量 / opencode 登录态)或手动填写 |
|
|
32
|
-
| Coding Plan 额度 | 侧边栏 / 设置页(每家可配) | 多厂商 coding plan 订阅额度查询(Anthropic Claude Pro/Max、Z.ai/智谱 GLM、MiniMax Token Plan、Kimi
|
|
33
|
-
| 额度横条 | 输入框上方(显示设置可开关) | 一条横排 chips 实时显示预算已用% / Go 主窗口 / 各已启用 Coding Plan 用量窗口(短标签+迷你进度条,≥80% 预警、≥100% 超支,悬停见重置时刻)
|
|
32
|
+
| Coding Plan 额度 | 侧边栏 / 设置页(每家可配) | 多厂商 coding plan 订阅额度查询(Anthropic Claude Pro/Max、Z.ai/智谱 GLM、MiniMax Token Plan、Kimi Code 本周/5 小时配额(无订阅 Key 时回落 PAYG 余额)、OpenRouter credits、SiliconFlow 余额、CommandCode 5h/周窗口与月度 Credits 余额),各家独立启用开关、Key、显示位置与刷新间隔(侧边栏卡片与 Go 额度同款,收起窄栏显示百分比),凭据只发往官方端点;无凭据/无订阅为中性提示;SCNet 超算互联网 Token Plan 无 API 额度端点,按官方 Credits 抵扣表由本地账本估算月度用量(无需凭据) |
|
|
33
|
+
| 额度横条 | 输入框上方(显示设置可开关) | 一条横排 chips 实时显示预算已用% / Go 主窗口 / 各已启用 Coding Plan 用量窗口(短标签+迷你进度条,≥80% 预警、≥100% 超支,悬停见重置时刻);点击任意 chip 即刷新对应数据源(budget→状态、Go→Go 额度、厂商→该家全部窗口),同一厂商多窗口融合为一条 chip 分段显示;首次更新弹引导卡由用户自主决定开关;无可用数据自动隐藏 |
|
|
34
34
|
| 点击立即刷新 | 侧边栏余额/额度图框 | 官方余额 / 自定义余额 / Coding Plan 图框(含窄栏收起态)点击即触发一次查询,刷新中呼吸闪烁,失败保持原值并在悬停提示说明;键盘 Enter/Space 可触发;更新后首次进入有引导提示 |
|
|
35
35
|
| 当日费用 | 侧边栏底部(设置按钮上方) | 「今日 ¥x」,悬停见调用次数与 token 明细 |
|
|
36
36
|
| 预算图框 | 侧边栏底部(余额行与设置按钮之间) | 圆角方形图框:预算、已用%、进度条、今日费用与占预算%、已用/额度,≥80% 预警、≥100% 超支 |
|
|
@@ -42,10 +42,11 @@
|
|
|
42
42
|
| 导入安装前历史 | 首次启动自动 | 安装/升级后首次启动自动回放宿主全部会话日志,把未装插件时期的对话导入账本(缺失日期整日重建,已有日期只补未知会话,幂等不与实时计费重复;金额按事件时刻历史价回推);设置页保留手动重跑入口 |
|
|
43
43
|
| 预算设置 | 设置页顶部 | 额度、周期(今日/本月/累计/自定义日期区间)、已用% |
|
|
44
44
|
| 价格表 | 设置页 | 每模型 谷时/峰时 两档价格(支持 input/output 简写,缓存价自动补齐),增删改自由 |
|
|
45
|
-
| 峰谷计价时段显示 | 设置页 / 预算 / 今日费用 | 显示 UTC 峰时段 01:00–04:00、06:00–10:00
|
|
45
|
+
| 峰谷计价时段显示 | 设置页 / 预算 / 今日费用 | 显示 UTC 峰时段 01:00–04:00、06:00–10:00 与当前档位;2026-08-23 起周末(周六及周日,北京时间)全天按谷价计费并显示「周末时段——全谷价」;展开态显示峰时/平价时段条(当前时段 + 倒计时),收起(rail)态显示竖向峰谷进度条,可单独开关 |
|
|
46
46
|
| 峰/谷切换弹窗提醒 | 全局浮层 | 距进入峰/谷时段不足设定提前量(默认 2 分钟,1-30 可配)时全屏色条徽标弹窗(提醒色区分进入峰/谷);弹窗位置可选**右下角 / 屏幕中心**,提醒类型可选(进入峰 / 进入谷 / 峰和谷),同一切换点只提醒一次;可选**同步发送浏览器(系统)通知**(页面最小化也能收到,需授权通知权限);设置页峰谷计价面板内配置,并可**一键预览弹窗效果**(真实组件渲染,文案/位置/通知与实际触发完全一致) |
|
|
47
|
-
| 官方价格同步 | 设置页 |
|
|
47
|
+
| 官方价格同步 | 设置页 | 抓取解析官方定价页,一键应用;可选**官方价格币种**(美元·英文官方页 / 人民币·中文官方页),人民币价按展示汇率折算入账、展示人民币时与官方账单一致 |
|
|
48
48
|
| 界面语言 | 设置页 → 显示设置 | 简体中文 / English / 跟随浏览器(自动);切换即时生效并自动保存 |
|
|
49
|
+
| 隐藏官方余额 / 隐藏今日消耗 | 设置页 → 显示设置 | 两个独立开关:开启后对应 UI 区块(侧边栏余额行与面板 / 今日费用行、预算明细、概览今日卡片等)**整体不再渲染**,token 与调用次数统计不受影响,共享屏幕/截图防泄露 |
|
|
49
50
|
| AI 价格同步 | [提示词](docs/AI-PRICE-SYNC-PROMPT.md) | DeepSeek 官方同步;其他 provider 使用已核对的官方价格目录与手动配置 |
|
|
50
51
|
| 模型与 Plan 适配说明 | [适配文档](docs/model-and-plan-adaptation.md) | 各厂商模型计费与 8 家 Coding Plan 的适配矩阵、自动匹配机制与价格来源([English](docs/model-and-plan-adaptation.en.md)) |
|
|
51
52
|
| 峰/谷切换提醒图解 | [提醒文档](docs/peak-alert.md) | 峰谷切换前弹窗与系统通知的完整图解:效果截图(中/英)、设置项说明与使用建议([English](docs/peak-alert.en.md)) |
|
|
@@ -230,22 +231,22 @@
|
|
|
230
231
|
dsh plugin --profile web add dsh-cost-meter
|
|
231
232
|
```
|
|
232
233
|
|
|
233
|
-
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.
|
|
234
|
+
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.42`**,建议先下载审阅再运行):
|
|
234
235
|
|
|
235
236
|
```powershell
|
|
236
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
237
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.42/install.ps1 | iex
|
|
237
238
|
```
|
|
238
239
|
|
|
239
240
|
**或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
|
|
240
241
|
|
|
241
242
|
```sh
|
|
242
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
243
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.42
|
|
243
244
|
```
|
|
244
245
|
|
|
245
246
|
没有 git 时可用 GitHub tag 打包直链:
|
|
246
247
|
|
|
247
248
|
```sh
|
|
248
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
249
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.42.tar.gz
|
|
249
250
|
```
|
|
250
251
|
|
|
251
252
|
安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
|
|
@@ -276,6 +277,7 @@ dsh plugin --profile web add link:./dsh-cost-meter # 符号链接,改 lib/clien
|
|
|
276
277
|
- 价格单位与官方文档一致:**美元 / 1M tokens**;
|
|
277
278
|
- 成本 = 未命中输入 × cache-miss + 输出 × output + (缓存读 + 缓存写) × cache-hit(缓存写沿用官方历史规则按命中价计费);
|
|
278
279
|
- **纯峰谷两档计价**(2026-08 起官方方案):峰时段(01:00–04:00、06:00–10:00 UTC)按峰时价,其余按谷时价(谷时价 = 峰时价的一半);基础档与谷时档同价,未启用峰谷时按谷时价计;设置页实时显示当前档位(峰时段/谷时段);预算与今日费用区域显示峰时/平价时段条(当前/下一时段与倒计时),收起态显示竖向峰谷进度条;
|
|
280
|
+
- **周末全谷价新规**(官方通知,2026-08-23 00:00 北京时间起):周末(周六及周日,按北京日历)全天不再区分峰谷,统一按谷价计费;时段条显示「周末时段——全谷价」并倒计时至下周一首个峰时段;该时刻之前的费用仍按原峰谷规则结算(首个受覆盖的周末仅周日全天);
|
|
279
281
|
- **历史计费正确性**:2026-08-16 16:00 UTC(峰谷时代分界)之前的调用按当时的基础价计费,之后的调用按峰谷两档;
|
|
280
282
|
- 账本金额恒以**美元**存储,币种/汇率仅影响显示(默认 1 USD = 7.2 CNY,可改);
|
|
281
283
|
- 会话徽章与当日/月度/累计、预算一样,按每次调用的**实际时刻精确计费**(宿主导出的逐次成本);
|
|
@@ -319,7 +321,7 @@ dsh-cost-meter
|
|
|
319
321
|
|
|
320
322
|
## 官方价格同步原理
|
|
321
323
|
|
|
322
|
-
`fetchPrices` 抓取官方定价页(Docusaurus
|
|
324
|
+
`fetchPrices` 抓取官方定价页(Docusaurus 服务端预渲染;英文页为美元价、中文页为人民币价,由「官方价格币种」设置决定,币种按页面金额符号自动检测,高峰时段中文页按北京时间 −8h 折算为 UTC),解析:
|
|
323
325
|
|
|
324
326
|
1. 基础价格表(转置布局:首行 MODEL + 模型 id,价格行标签后紧跟价格);
|
|
325
327
|
2. 峰谷价格表(每模型两行:OFF-PEAK / PEAK);
|
package/lib/backfill.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
14
14
|
import { join } from 'node:path'
|
|
15
15
|
import * as zlib from 'node:zlib'
|
|
16
|
-
import { costOf, providerPriceEntryFor } from './pricing.js'
|
|
16
|
+
import { costOf, providerPriceEntryFor, usdFromCost } from './pricing.js'
|
|
17
17
|
import { localDayKey, zeroDay } from './store.js'
|
|
18
18
|
|
|
19
19
|
const ZSTD_MAGIC = 4247762216
|
|
@@ -72,35 +72,62 @@ export function scanZstdFrames(buffer) {
|
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* 读取一份会话日志的全部事件行(zstd 逐 frame 解压;明文直接按行)。
|
|
75
|
+
*
|
|
76
|
+
* 内存约束:宿主会话日志可含数万帧(每次追加批次一个独立 zstd frame,
|
|
77
|
+
* 长期会话可达 10 万+ 帧)且解压后数百 MB。旧实现把所有帧一次性解压、
|
|
78
|
+
* Buffer.concat 拼成全文再 split——峰值内存可达数 GB,4GB 堆限制下
|
|
79
|
+
* 会 OOM(实测一份 50MB / 11.5 万帧日志即 ~3GB)。现改为逐帧解压、
|
|
80
|
+
* 逐帧按行切片解析:任一时刻只保留单帧解压结果(通常 ≤ 数 MB),行缓冲
|
|
81
|
+
* 跨帧拼接兜底半行(追加批次以换行结尾,实际不会出现)。
|
|
75
82
|
* @param path - session.jsonl.zstd 或 session.jsonl 路径。
|
|
76
83
|
* @returns 逐行 JSON.parse 后的记录数组(坏行跳过)。
|
|
77
84
|
*/
|
|
78
85
|
export function readSessionRecords(path) {
|
|
79
86
|
const buffer = readFileSync(path)
|
|
80
|
-
|
|
87
|
+
const records = []
|
|
88
|
+
let pending = '' // 跨帧行缓冲:上一帧末尾未换行的残行拼接进下一帧首行。
|
|
89
|
+
const consumeText = (text) => {
|
|
90
|
+
if (text.length === 0) return
|
|
91
|
+
const lines = text.split('\n')
|
|
92
|
+
lines[0] = pending + lines[0]
|
|
93
|
+
pending = lines[lines.length - 1]
|
|
94
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
95
|
+
const line = lines[i]
|
|
96
|
+
if (line.length === 0) continue
|
|
97
|
+
try {
|
|
98
|
+
records.push(JSON.parse(line))
|
|
99
|
+
} catch {
|
|
100
|
+
// 坏行跳过:回放是尽力而为,不让单行损坏阻断整个会话。
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
81
104
|
if (path.endsWith('.zstd')) {
|
|
82
105
|
if (typeof zlib.zstdDecompressSync !== 'function') return []
|
|
83
106
|
const frames = scanZstdFrames(buffer)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
107
|
+
for (const f of frames) {
|
|
108
|
+
consumeText(zlib.zstdDecompressSync(buffer.subarray(f.start, f.end)).toString('utf8'))
|
|
109
|
+
}
|
|
87
110
|
} else {
|
|
88
|
-
|
|
111
|
+
consumeText(buffer.toString('utf8'))
|
|
89
112
|
}
|
|
90
|
-
|
|
91
|
-
for (const line of text.split('\n')) {
|
|
92
|
-
if (line.length === 0) continue
|
|
113
|
+
if (pending.length > 0) {
|
|
93
114
|
try {
|
|
94
|
-
records.push(JSON.parse(
|
|
115
|
+
records.push(JSON.parse(pending))
|
|
95
116
|
} catch {
|
|
96
|
-
//
|
|
117
|
+
// 文件末尾无换行的残行:同上,尽力而为。
|
|
97
118
|
}
|
|
98
119
|
}
|
|
99
120
|
return records
|
|
100
121
|
}
|
|
101
122
|
|
|
102
|
-
/**
|
|
103
|
-
|
|
123
|
+
/**
|
|
124
|
+
* 枚举会话根目录下全部会话日志路径(<root>/<项目>/<会话>/session.jsonl[.zstd])。
|
|
125
|
+
* @param root - 会话根目录。
|
|
126
|
+
* @param onlySessionIds - 可选:仅枚举这些会话目录(Set<会话 id>)。纯标题/时间戳
|
|
127
|
+
* 补齐时按缺失会话定向,避免为补齐一两个标题全量读取全部会话日志(其中可能
|
|
128
|
+
* 含解压后数百 MB 的大文件)。null = 全部。
|
|
129
|
+
*/
|
|
130
|
+
export function listSessionLogs(root, onlySessionIds = null) {
|
|
104
131
|
const paths = []
|
|
105
132
|
let projects
|
|
106
133
|
try {
|
|
@@ -118,6 +145,7 @@ export function listSessionLogs(root) {
|
|
|
118
145
|
}
|
|
119
146
|
for (const session of sessions) {
|
|
120
147
|
if (!session.isDirectory()) continue
|
|
148
|
+
if (onlySessionIds !== null && !onlySessionIds.has(session.name)) continue
|
|
121
149
|
for (const name of ['session.jsonl.zstd', 'session.jsonl']) {
|
|
122
150
|
const path = join(root, project.name, session.name, name)
|
|
123
151
|
try {
|
|
@@ -255,7 +283,12 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
255
283
|
effectiveAtMs: Date.parse(config?.peakEffectiveAt ?? ''),
|
|
256
284
|
windows: config?.peakWindows,
|
|
257
285
|
}
|
|
258
|
-
|
|
286
|
+
// 官方价格币种为人民币(issue #47)时,DeepSeek 主表计出的成本为人民币,
|
|
287
|
+
// 按展示汇率折算为美元入账——与 store.account() 完全同口径。
|
|
288
|
+
const priced = resolved.priced ? costOf(buckets, resolved.entry, atMs, peak) : 0
|
|
289
|
+
const cost = usdFromCost(priced,
|
|
290
|
+
resolved.billingMode === 'deepseek-peak' && config?.prices?.currency === 'CNY' ? 'CNY' : 'USD',
|
|
291
|
+
config?.exchangeRate)
|
|
259
292
|
const providerKey = `${provider}:${model}`
|
|
260
293
|
// 聚合目标按事件分段路由;被替换的旧样本从它当时所在的段扣回(与旧版
|
|
261
294
|
// 单流先减后加的净效果一致,仅种子段的量落到 seedDays 而非 days)。
|
|
@@ -309,7 +342,22 @@ export async function backfillLegacyLedger(ledger, sessionsRoot) {
|
|
|
309
342
|
const titles = new Map()
|
|
310
343
|
const createdAts = new Map()
|
|
311
344
|
let scannedCount = 0
|
|
312
|
-
|
|
345
|
+
// 纯标题/时间戳补齐(日期级不缺):按缺失会话的 id 定向定位日志文件,绝不
|
|
346
|
+
// 全量扫描——活跃会话的标题要到下次启动才写入账本,全量扫描会让每次启动
|
|
347
|
+
// 都重复读全部会话日志(含解压后数百 MB 的大文件,内存峰值几 GB)。
|
|
348
|
+
let onlySessionIds = null
|
|
349
|
+
if (needDates.size === 0) {
|
|
350
|
+
onlySessionIds = new Set()
|
|
351
|
+
for (const day of Object.values(ledger.days ?? {})) {
|
|
352
|
+
for (const session of day?.sessions ?? []) {
|
|
353
|
+
if (session === null || typeof session !== 'object' || typeof session.id !== 'string') continue
|
|
354
|
+
const missingTitle = typeof session.title !== 'string' || session.title.length === 0
|
|
355
|
+
const missingAt = !Number.isFinite(Number(session.at))
|
|
356
|
+
if (missingTitle || missingAt) onlySessionIds.add(session.id)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
for (const path of listSessionLogs(sessionsRoot, onlySessionIds)) {
|
|
313
361
|
// 会话日志多时逐份解压会长时间占住事件循环:每 8 份让出一次,不卡宿主 UI。
|
|
314
362
|
if ((scannedCount += 1) % 8 === 0) await new Promise(resolve => setImmediate(resolve))
|
|
315
363
|
result.scanned += 1
|
|
@@ -682,3 +730,66 @@ export function repairForkSeed(ledger, sessionsRoot) {
|
|
|
682
730
|
}
|
|
683
731
|
return result
|
|
684
732
|
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* 包装路由重复计费一次性清洗(issue #48):modlens / vision-router 这类包装
|
|
736
|
+
* 插件在自身 stream() 体内再发起 ctx.llm.stream(),旧版计费监听器在瀑布
|
|
737
|
+
* 每层都记账,同一次请求在 byProviderModel 里留下 official / modlens /
|
|
738
|
+
* modlens-vision 等多份 token 逐位相同的条目(报告者 08-22 账本:三行
|
|
739
|
+
* 40 次调用 token 逐位相同)。本函数对每个日期与其下每个会话的
|
|
740
|
+
* byProviderModel 按指纹分组——键的模型后缀(首个冒号之后,嵌套包装链
|
|
741
|
+
* 每层透传同一 model)与六值桶(input/output/cacheRead/cacheWrite/
|
|
742
|
+
* reasoning/calls)完全一致才算同组;每组保留字母序第一个,其余条目从
|
|
743
|
+
* 所在容器(day 或 session)的顶层合计(含 cost、calls)中扣除后删除。
|
|
744
|
+
*
|
|
745
|
+
* 幂等由调用方用账本 migrations 标记(provider-dedup-v1)保证只跑一次;
|
|
746
|
+
* 扣除逐字段 clamp ≥ 0,指纹不全同的条目(不同 token 量的真实调用,如
|
|
747
|
+
* 报告者 deepseek-vision 的 24 次独立记录)不碰。
|
|
748
|
+
* @param ledger - 已打开的账本。
|
|
749
|
+
* @returns { groups, removedCost } 合并的重复组数与扣除的金额合计(USD)。
|
|
750
|
+
*/
|
|
751
|
+
export function repairProviderDupes(ledger) {
|
|
752
|
+
const result = { groups: 0, removedCost: 0 }
|
|
753
|
+
// 清洗单个容器(day 或 session):返回是否发生扣除。
|
|
754
|
+
const repairContainer = (container) => {
|
|
755
|
+
const pm = container?.byProviderModel
|
|
756
|
+
if (pm === null || typeof pm !== 'object') return false
|
|
757
|
+
// 指纹 → 键列表。键形如 `${provider}:${model}`,provider 名不含冒号,
|
|
758
|
+
// 首个冒号之后即模型 id;跨 provider 指纹相同且模型相同才是包装链重复。
|
|
759
|
+
const groups = new Map()
|
|
760
|
+
for (const [key, bucket] of Object.entries(pm)) {
|
|
761
|
+
if (bucket === null || typeof bucket !== 'object') continue
|
|
762
|
+
const model = key.slice(key.indexOf(':') + 1)
|
|
763
|
+
const fingerprint = `${model}|${bucket.input ?? 0}|${bucket.output ?? 0}|${bucket.cacheRead ?? 0}|${bucket.cacheWrite ?? 0}|${bucket.reasoning ?? 0}|${bucket.calls ?? 0}`
|
|
764
|
+
const list = groups.get(fingerprint)
|
|
765
|
+
if (list === undefined) groups.set(fingerprint, [key])
|
|
766
|
+
else list.push(key)
|
|
767
|
+
}
|
|
768
|
+
let touched = false
|
|
769
|
+
for (const keys of groups.values()) {
|
|
770
|
+
if (keys.length < 2) continue
|
|
771
|
+
keys.sort()
|
|
772
|
+
for (const key of keys.slice(1)) {
|
|
773
|
+
const bucket = pm[key]
|
|
774
|
+
subtractBucketInto(container, bucket)
|
|
775
|
+
delete pm[key]
|
|
776
|
+
result.removedCost += bucket.cost ?? 0
|
|
777
|
+
touched = true
|
|
778
|
+
}
|
|
779
|
+
result.groups += 1
|
|
780
|
+
}
|
|
781
|
+
return touched
|
|
782
|
+
}
|
|
783
|
+
let scheduled = false
|
|
784
|
+
for (const day of Object.values(ledger.days ?? {})) {
|
|
785
|
+
if (day === null || typeof day !== 'object') continue
|
|
786
|
+
if (repairContainer(day)) scheduled = true
|
|
787
|
+
if (Array.isArray(day.sessions)) {
|
|
788
|
+
for (const session of day.sessions) {
|
|
789
|
+
if (session !== null && typeof session === 'object' && repairContainer(session)) scheduled = true
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (scheduled) ledger.scheduleWrite()
|
|
794
|
+
return result
|
|
795
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* llm/stream 计费包裹(issue #48 包装路由重复计费修复)。
|
|
3
|
+
*
|
|
4
|
+
* 宿主的 `llm/stream` 是一条监听器瀑布:每次 ctx.llm.stream() 都会把全部
|
|
5
|
+
* 监听器按注册序串起来。modlens / vision-router 这类包装路由插件的适配器
|
|
6
|
+
* 在自己 stream() 体内再次调用 ctx.llm.stream({ ..., provider: upstream })
|
|
7
|
+
* ——于是同一次请求沿 `wrapper-vision → wrapper → official` 每层都完整走
|
|
8
|
+
* 一遍瀑布,链尾的计费监听器每层都把 usage 记进账本(同 token 逐位相同 ×3)。
|
|
9
|
+
*
|
|
10
|
+
* 修复:用 AsyncLocalStorage 标记「正在计费消费的流」。外层计费包装在标记
|
|
11
|
+
* 内拉取下游 chunk;下游适配器体内再发起的 ctx.llm.stream() 在同一异步上
|
|
12
|
+
* 下文中同步分发瀑布,内层计费监听器读到标记即判定为嵌套调用,直接透传
|
|
13
|
+
* next() 不再包一层(也就不再记账)。usage 只由最外层记一次。
|
|
14
|
+
*
|
|
15
|
+
* ALS 按异步上下文隔离:两个并发请求各自从无标记的根上下文进入,互不误伤。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
19
|
+
|
|
20
|
+
/** 嵌套深度标记:值恒为 1(存在即嵌套);按异步上下文隔离,并发流互不串扰。 */
|
|
21
|
+
const llmStreamDepth = new AsyncLocalStorage()
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 创建 llm/stream 计费监听器。
|
|
25
|
+
* @param {object} deps
|
|
26
|
+
* @param {(usage: object, model: string, sessionId: string, atMs: number, provider: string) => void} deps.account
|
|
27
|
+
* 计费回调:流结束时以捕获的 usage 块调用(五参签名与 ledger.account 对齐,
|
|
28
|
+
* 由 index.js 负责把 usage 五桶映射过去)。
|
|
29
|
+
* @returns {(options: object, next: () => AsyncIterable) => AsyncIterable} 宿主监听器。
|
|
30
|
+
*/
|
|
31
|
+
export function createLlmStreamBilling({ account }) {
|
|
32
|
+
return (options, next) => {
|
|
33
|
+
const downstream = next()
|
|
34
|
+
// 嵌套内层(包装路由在上层计费流的消费上下文中再发起的 llm/stream):
|
|
35
|
+
// 外层已记账,透传下游流,不再包裹。
|
|
36
|
+
if (llmStreamDepth.getStore() !== undefined) return downstream
|
|
37
|
+
return (async function* costMeterStream() {
|
|
38
|
+
let usage = null
|
|
39
|
+
const iterator = downstream[Symbol.asyncIterator]()
|
|
40
|
+
try {
|
|
41
|
+
for (;;) {
|
|
42
|
+
// 在深度标记内拉取下游:下游适配器(modlens 等)体内再发起的
|
|
43
|
+
// ctx.llm.stream() 同步瀑布分发继承 depth=1,其监听器判定为嵌套。
|
|
44
|
+
const result = await llmStreamDepth.run(1, () => iterator.next())
|
|
45
|
+
if (result.done) break
|
|
46
|
+
const chunk = result.value
|
|
47
|
+
if (chunk !== null && chunk !== undefined && chunk.type === 'usage' && chunk.usage !== undefined) {
|
|
48
|
+
usage = chunk.usage
|
|
49
|
+
}
|
|
50
|
+
yield chunk
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
if (usage !== null) {
|
|
54
|
+
try {
|
|
55
|
+
account(usage, options?.model, options?.sessionId, Date.now(), options?.provider)
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.warn(`[dsh-cost-meter] 计费失败: ${String(error)}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
})()
|
|
62
|
+
}
|
|
63
|
+
}
|