dsh-cost-meter 1.8.8 → 1.8.10

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.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  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; weekends and Chinese public holidays are off-peak all day, with separate labels) · 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 170+ model-ID catalog with auto-matching) · mainstream Coding Plan quota queries & display (Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow / CommandCode / SCNet / Volcano Ark / Qwen / Xiaomi MiMo) 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)
10
10
 
11
- [![version](https://img.shields.io/badge/version-1.8.8-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
11
+ [![version](https://img.shields.io/badge/version-1.8.10-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
12
12
 
13
- **v1.8.8** includes Desktop account usage in official-balance reconciliation and the daily-spend bar, fixing understated totals for signed-in accounts. See the [release notes](docs/release-notes/v1.8.8.md).
13
+ **v1.8.10** aligns cumulative totals, conversation badges and rankings, including subagent spending. See the [release notes](docs/release-notes/v1.8.10.md).
14
14
 
15
15
  Desktop users: follow the [Desktop installation instructions](docs/install-troubleshooting.md#desktop-安装与更新) for the application's own CLI and `desktop` Profile.
16
16
 
@@ -320,22 +320,22 @@ On Node.js 20, use `npm install -g pnpm@10` instead. See [pnpm installation and
320
320
  dsh plugin --profile web add dsh-cost-meter
321
321
  ```
322
322
 
323
- **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.8.8`** — review the script before running):
323
+ **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.8.10`** — review the script before running):
324
324
 
325
325
  ```powershell
326
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.8/install.ps1 | iex
326
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.10/install.ps1 | iex
327
327
  ```
328
328
 
329
329
  **Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
330
330
 
331
331
  ```sh
332
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.8
332
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.10
333
333
  ```
334
334
 
335
335
  Without git, use the GitHub tag archive:
336
336
 
337
337
  ```sh
338
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.8.tar.gz
338
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.10.tar.gz
339
339
  ```
340
340
 
341
341
  After installing, **restart** `dsh web` (plugin rows, the Typert manifest and the client bundle are all scanned at startup):
package/README.zh-CN.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  本会话费用 · 当日费用 · OpenCode Go 订阅额度显示 · 预算与已用百分比 · 官方账户余额 · 自定义 Provider 余额查询(可配任意 HTTP 端点) · 余额三段进度条 · 历史记录 · 峰谷计价时段显示(UTC 01:00–04:00、06:00–10:00 为峰时段;周末与中国法定假日全天按谷价,分别标注) · 峰/谷切换前弹窗与系统通知提醒(位置/提前量/提醒类型可配) · 官方价格一键同步 · 类 Codex Token 用量热图 · 多厂商多模型价格计费(内置 170+ 模型 ID 价格目录与自动匹配) · 主流 Coding Plan 额度查询与显示(Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow / CommandCode / SCNet / 火山方舟 / 千问 / 小米 MiMo 十一家,含 Volcano Ark AK/SK 签名与 MiMo 控制台 Cookie 查询) · Plan/API 双轨计费(订阅额度与按量金额分离统计,每 1% 额度与满窗的 token/等值金额估算及日/周/月曲线) · 输入框上方额度横条(预算/Go/Coding Plan 用量一条横排显示,可开关)
10
10
 
11
- [![version](https://img.shields.io/badge/version-1.8.8-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
11
+ [![version](https://img.shields.io/badge/version-1.8.10-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
12
12
 
13
- **v1.8.8** 修复桌面端登录账号用量未计入官方余额对账和当日已用进度条的问题。详见[更新说明](docs/release-notes/v1.8.8.md)。
13
+ **v1.8.10** 统一累计费用、会话徽章和对话排行,修复历史日总额与各对话费用相加不一致的问题,并统一子代理费用统计。详见[更新说明](docs/release-notes/v1.8.10.md)。
14
14
 
15
15
  桌面端用户请按 [Desktop 安装说明](docs/install-troubleshooting.md#desktop-安装与更新),使用应用自带的 CLI 和 `desktop` Profile。
16
16
 
@@ -320,22 +320,22 @@ Node.js 20 请改用 `npm install -g pnpm@10`。版本要求见 [pnpm 官方安
320
320
  dsh plugin --profile web add dsh-cost-meter
321
321
  ```
322
322
 
323
- **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.8.8`**,建议先下载审阅再运行):
323
+ **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.8.10`**,建议先下载审阅再运行):
324
324
 
325
325
  ```powershell
326
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.8/install.ps1 | iex
326
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.10/install.ps1 | iex
327
327
  ```
328
328
 
329
329
  **或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
330
330
 
331
331
  ```sh
332
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.8
332
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.10
333
333
  ```
334
334
 
335
335
  没有 git 时可用 GitHub tag 打包直链:
336
336
 
337
337
  ```sh
338
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.8.tar.gz
338
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.10.tar.gz
339
339
  ```
340
340
 
341
341
  安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## English
4
4
 
5
- Open a conversation and click **Cost details** in the same row as turns, speed, tokens and cache-hit statistics below its input box. **Settings → Cost → Display** has separate switches to hide the header entry, composer entry and turn cost summaries; all remain visible by default. The header entry automatically hides at viewport widths of 640px or less. The composer entry follows its own switch. Both buttons use compact 12px text. Budget chips take only their content width and no longer create an extra row. Cost badge visibility remains independent. The conversation entry opens its entire retained history, shows API and Plan equivalents separately, and puts component details before overview charts. Switching conversations closes the previous dialog. For all conversations, open **Settings → Cost → Cost statistics**; this entry starts with the last seven calendar days, including today.
5
+ Open a conversation and click **Cost details** in the same row as turns, speed, tokens and cache-hit statistics below its input box. **Settings → Cost → Display** has separate switches to hide the header entry, composer entry and turn cost summaries; all remain visible by default. The header entry automatically hides at viewport widths of 640px or less. The composer entry follows its own switch. Both buttons use compact 12px text. Budget chips take only their content width and no longer create an extra row. Cost badge visibility remains independent. The conversation entry opens its entire retained history, shows API and Plan equivalents separately, uses the sidebar cost basis by default, and puts component details before overview charts. Switching conversations closes the previous dialog. For all conversations, open **Settings → Cost → Cost statistics**; this entry starts with the last seven calendar days, including today.
6
6
 
7
7
  - **Periods:** Today, Last 7 days, Last 30 days, All retained, and custom inclusive dates. Dates follow the host timezone shown on the page.
8
8
  - **Filters:** Provider and model. Choose API cost, Plan equivalent, or their combined equivalent for chart amounts and rankings.
@@ -16,11 +16,15 @@ Open a conversation and click **Cost details** in the same row as turns, speed,
16
16
 
17
17
  ### What the numbers mean
18
18
 
19
- Overview amounts come directly from the retained DSH ledger. Opening statistics does not reprice or modify it. A conversation spanning several days has one ranking row. Subagents remain separate conversations, so their amounts are counted once. Historical amounts without a known model or conversation are explicitly listed as unassigned costs.
19
+ Overview amounts come from the retained DSH ledger and refresh records written by other host processes. Opening statistics does not reprice history. Rankings combine days into one row per conversation. With subagent costs enabled, each agent is grouped under its owning conversation so the ranking matches its detail. Otherwise sessions stay separate. Each cost is counted once. Historical amounts without a known model or conversation are explicitly listed as unassigned costs.
20
+
21
+ From 1.8.10, the ledger retains every conversation instead of pruning details after 200 sessions per day. Legacy daily totals are restored from recorded conversation amounts only when all token buckets and call counts match exactly. Anonymous usage and old pruned residues retain their original totals. This repair does not reprice requests.
20
22
 
21
23
  API costs are **estimates from reported usage and configured rates**, not provider invoices. Plan costs are the API-equivalent value of subscription usage, not an extra charge. External usage snapshots remain separately visible in Overview; this page analyzes DSH's own ledger.
22
24
 
23
- Call details are reconstructed on demand from one conversation's available usage logs and native-search journal. They use call timestamps and the currently configured historical price rules, including peak/off-peak and long-context tiers. If prices changed or logs are incomplete, the page shows the stored amount and reconstructed amount separately. Missing logs and missing prices do not mean zero cost.
25
+ With **Include subagents in session cost** enabled, the conversation badge, selected-conversation statistics and details include all continuous subagent descendants. The detail view lists each agent's recorded costs and identifies the owning conversation for every call, turn and step. Expanding a child turn reads that child's input and tools. Ordinary forks are excluded and inherited history is not billed twice. Disabling the setting limits the selection to the main conversation.
26
+
27
+ Call details are reconstructed on demand from the selected conversations' available usage logs and native-search journals. They use call timestamps and the currently configured historical price rules, including peak/off-peak and long-context tiers. If prices changed or logs are incomplete, the page shows the stored amount and reconstructed amount separately. Missing logs and missing prices do not mean zero cost.
24
28
 
25
29
  Cache hit rate is `cacheRead / (input + cacheRead + cacheWrite)`. Token composition is a token-count share, not a cost share. Reported reasoning tokens can overlap output and are shown separately; the configured reasoning rate determines any additional charge.
26
30
 
@@ -28,7 +32,7 @@ The statistics screen uses DSH's package-local asynchronous module loader. Since
28
32
 
29
33
  ## 简体中文
30
34
 
31
- 打开一个会话,点击**输入框下方与轮数、速度、Token 和缓存命中率同一行的「费用明细」**。在 **设置 → 费用 → 显示设置** 中,可分别隐藏标题栏入口、输入框下方入口和每轮回复后的费用行;默认均保留。视口不超过 640px 时,标题栏入口自动隐藏,输入框下方入口继续遵守自己的开关。两个明细按钮使用 12px 字号和紧凑间距;预算标签仅占内容所需宽度,不再独占整行。关闭费用徽章不影响明细入口。对话入口默认显示该对话保留的全部历史,API 与 Plan 等值分别列出,费用构成放在趋势图之前。切换会话会关闭旧明细。全部会话统计的入口为 **设置 → 费用 → 计费统计**,默认显示包含今天的近 7 个自然日。
35
+ 打开一个会话,点击**输入框下方与轮数、速度、Token 和缓存命中率同一行的「费用明细」**。在 **设置 → 费用 → 显示设置** 中,可分别隐藏标题栏入口、输入框下方入口和每轮回复后的费用行;默认均保留。视口不超过 640px 时,标题栏入口自动隐藏,输入框下方入口继续遵守自己的开关。两个明细按钮使用 12px 字号和紧凑间距;预算标签仅占内容所需宽度,不再独占整行。关闭费用徽章不影响明细入口。对话入口默认显示该对话保留的全部历史,API 与 Plan 等值分别列出,默认金额口径跟随侧栏的“含 Plan 总额”设置,费用构成放在趋势图之前。切换会话会关闭旧明细。全部会话统计的入口为 **设置 → 费用 → 计费统计**,默认显示包含今天的近 7 个自然日。
32
36
 
33
37
  - **时间范围:**今天、近 7 天、近 30 天、全部保留记录,以及包含起止日期的自定义区间。日期按页面标明的宿主时区划分。
34
38
  - **筛选和金额口径:**提供商、模型;可选择 API 费用、Plan 等值费用或两者合计,趋势和排行同步切换。
@@ -42,7 +46,9 @@ The statistics screen uses DSH's package-local asynchronous module loader. Since
42
46
 
43
47
  ### 统计口径
44
48
 
45
- 汇总直接读取 DSH 账本,打开页面不会重新定价或修改历史金额。跨天对话合并成一行,子代理作为独立对话分别统计,不把父级合计再次累加。历史中未归属模型或对话的金额会单独提示。
49
+ 汇总读取 DSH 账本,打开页面会同步其他进程已写入的记录,不重新定价或修改历史金额。全局日、周及全部统计中,跨天对话合并成一行,开启子代理费用合计时,排行把子代理及其后代归入所属主会话,点击后的明细与排行金额一致;关闭时各会话分别列出。每笔费用只计一次。历史中未归属模型或对话的金额会单独提示。从 1.8.10 起,账本保留全部对话记录,不再按每日 200 个裁剪。若已记录的全部 Token 与调用次数完整覆盖一个日期,旧日总额会按这些对话已记录的金额修复;匿名调用和旧版裁剪留下的金额保留。此修复不重新定价。
50
+
51
+ 开启设置中的“本会话费用包含子代理”后,会话费用徽章、单对话汇总和费用明细均包含该会话的子代理及全部子代理后代。明细单列主会话与各子代理的账本费用,逐次调用、轮次和步骤标注所属会话,展开子代理轮次可查看它自己的输入与工具调用。普通分叉会话不归入父会话,子代理继承的历史消息不重复收费。关闭开关时只显示主会话自身费用。
46
52
 
47
53
  API 费用是按上报用量和配置单价计算的估算,不是厂商账单;Plan 费用是订阅用量的 API 等值,不代表额外扣款。外部用量快照仍在原概览中单列,这个页面统计 DSH 自身账本。
48
54
 
package/lib/backfill.js CHANGED
@@ -362,7 +362,7 @@ export function replaySessionRecords(records, config, wantDates = null, collectS
362
362
  const fp = usageFingerprint(sampleModel, {
363
363
  input: num(u.inputTokens), output: num(u.outputTokens), cacheRead: num(u.cacheReadTokens),
364
364
  cacheWrite: num(u.cacheWriteTokens), reasoning: num(u.reasoningTokens),
365
- })
365
+ }, sampleProvider)
366
366
  const times = plainFingerprints.get(fp)
367
367
  if (times === undefined) plainFingerprints.set(fp, [at])
368
368
  else times.push(at)
@@ -464,7 +464,7 @@ export function replaySessionRecords(records, config, wantDates = null, collectS
464
464
  if (isWrapperProviderId(sampleProvider)) {
465
465
  effectiveProvider = wrapperUpstreamProvider(sampleProvider) ?? sampleProvider
466
466
  if (!isCompaction && !isNativeSearch) {
467
- const fp = usageFingerprint(sampleModel, buckets)
467
+ const fp = usageFingerprint(sampleModel, buckets, effectiveProvider)
468
468
  remappedAdmitted = remappedAdmitted.filter(entry => atMs - entry.at <= USAGE_DEDUP_WINDOW_MS)
469
469
  const plainTimes = plainFingerprints.get(fp)
470
470
  const plainHit = plainTimes !== undefined && plainTimes.some(t => Math.abs(atMs - t) <= USAGE_DEDUP_WINDOW_MS)
@@ -1,6 +1,7 @@
1
1
  /** Read-only analytics over the canonical ledger. No repricing, log scans or writes. */
2
2
  import { localDayKey } from './store.js'
3
3
  import { providerPriceEntryFor } from './pricing.js'
4
+ import { conversationOwners } from './session-tree.js'
4
5
 
5
6
  export const STAT_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite', 'reasoning', 'calls', 'cost', 'apiCost']
6
7
  const num = n => typeof n === 'number' && Number.isFinite(n) && n > 0 ? n : 0
@@ -35,13 +36,28 @@ export function selectedStats(container, query) {
35
36
  return total
36
37
  }
37
38
 
38
- export function billingStatistics(ledger, raw) {
39
+ export function billingStatistics(ledger, raw, sessionIds = null, headers = []) {
39
40
  const query = ledgerStatisticsQuery(ledger, raw), totals = emptyStats(), days = [], models = new Map(), sessions = new Map()
40
41
  const providers = new Set(), modelOptions = new Set(), retained = Object.keys(ledger.days ?? {}).sort()
41
42
  const assigned = emptyStats(), modeled = emptyStats()
43
+ const owners = !query.sessionId && ledger.config?.includeSubagentCost === true
44
+ ? conversationOwners(headers, Object.values(ledger.days ?? {}).flatMap(day => (day.sessions ?? []).map(row => row.id))) : new Map()
45
+ const titles = new Map(Object.values(ledger.days ?? {}).flatMap(day => (day.sessions ?? []).filter(row => row.title).map(row => [row.id, row.title])))
46
+ const scope = sessionIds ?? new Set([query.sessionId])
42
47
  for (let at = Date.parse(query.from); at <= Date.parse(query.to); at += 86400000) {
43
48
  const date = new Date(at).toISOString().slice(0, 10), stored = ledger.days?.[date]
44
- const day = query.sessionId ? stored?.sessions?.find(s => s.id === query.sessionId) : stored
49
+ let day = stored
50
+ if (query.sessionId) {
51
+ const selected = (stored?.sessions ?? []).filter(row => scope.has(row.id))
52
+ day = { ...emptyStats(), sessions: selected, byProviderModel: {} }
53
+ for (const row of selected) {
54
+ add(day, row)
55
+ for (const [key, bucket] of Object.entries(row.byProviderModel ?? {})) {
56
+ if (!Object.hasOwn(day.byProviderModel, key)) Object.defineProperty(day.byProviderModel, key, { value: emptyStats(), enumerable: true })
57
+ add(day.byProviderModel[key], bucket)
58
+ }
59
+ }
60
+ }
45
61
  const selected = selectedStats(day, query)
46
62
  days.push({ date, ...selected }); add(totals, selected)
47
63
  for (const [key, bucket] of Object.entries(day?.byProviderModel ?? {})) {
@@ -52,13 +68,14 @@ export function billingStatistics(ledger, raw) {
52
68
  const row = models.get(key) ?? { key, ...id, ...emptyStats(), priced: providerPriceEntryFor(id.provider, id.model, ledger.config.prices, { mode: ledger.config.priceMatch, overrides: ledger.config.priceOverrides }).priced }
53
69
  add(row, bucket); add(modeled, bucket); models.set(key, row)
54
70
  }
55
- for (const session of query.sessionId ? (day ? [day] : []) : day?.sessions ?? []) {
71
+ for (const session of day?.sessions ?? []) {
56
72
  if (!session?.id) continue
57
73
  const selected = selectedStats(session, query)
58
74
  if (!selected.calls && !selected.cost && !STAT_FIELDS.slice(0, 5).some(key => selected[key])) continue
59
- const row = sessions.get(session.id) ?? { id: session.id, title: session.id, ...emptyStats() }
60
- if (session.title) row.title = session.title
61
- add(row, selected); add(assigned, selected); sessions.set(session.id, row)
75
+ const id = owners.get(session.id) ?? session.id
76
+ const row = sessions.get(id) ?? { id, title: titles.get(id) || id, ...emptyStats() }
77
+ if (session.id === id && session.title) row.title = session.title
78
+ add(row, selected); add(assigned, selected); sessions.set(id, row)
62
79
  }
63
80
  }
64
81
  const sort = (a, b) => valueOf(b, query.basis) - valueOf(a, query.basis) || b.calls - a.calls || String(a.id ?? a.key).localeCompare(String(b.id ?? b.key))