dsh-cost-meter 1.7.49 → 1.8.0
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 +7 -6
- package/README.zh-CN.md +6 -6
- package/docs/billing-statistics.md +49 -0
- package/lib/billing-statistics.js +70 -0
- package/lib/client.js +5 -5
- package/lib/client.statistics.js +19 -0
- package/lib/index.js +4 -1
- package/lib/turn-cost.js +38 -6
- package/lib/typert.host.js +21 -0
- package/package.json +2 -1
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 90+ model price 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
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
12
12
|
|
|
13
|
-
**v1.
|
|
13
|
+
**v1.8.0** adds a dedicated cost statistics screen: Today / Last 7 days / Last 30 days / All retained / custom dates, provider and model filters, spending trends and conversation drill-down with token × rate details. Open **Settings → Cost → Cost statistics**, or **Cost details** in a conversation header. See the [statistics guide](docs/billing-statistics.md#english) and [release notes](docs/release-notes/v1.8.0.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
|
|
|
@@ -30,6 +30,7 @@ Desktop users: follow the [Desktop installation instructions](docs/install-troub
|
|
|
30
30
|
|
|
31
31
|
| Feature | Location | Description |
|
|
32
32
|
|---|---|---|
|
|
33
|
+
| Cost statistics | Settings → Cost → Cost statistics / conversation header | Day, week, month, all retained and custom periods; API/Plan split, trends, model/conversation rankings and per-call costs. [Guide](docs/billing-statistics.md#english); statistics requires the DSH 0.2.0-rc.2 module loader |
|
|
33
34
|
| Per-model cost card | Sidebar / composer dock (optional) | Disabled by default; inline Top-N, Other totals, shares and optional tokens, Today / Last 90 days, remembered expansion and a Top-1 chip. See the [guide](docs/model-cost-card.md#english) |
|
|
34
35
|
| Per-conversation cost | Below the composer / session title bar | Live accumulated cost + input/cache/output tokens; the composer footer shows cache hit rate before Input (cache reads / all input, including cache writes); position configurable |
|
|
35
36
|
| 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) |
|
|
@@ -319,22 +320,22 @@ On Node.js 20, use `npm install -g pnpm@10` instead. See [pnpm installation and
|
|
|
319
320
|
dsh plugin --profile web add dsh-cost-meter
|
|
320
321
|
```
|
|
321
322
|
|
|
322
|
-
**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.
|
|
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.0`** — review the script before running):
|
|
323
324
|
|
|
324
325
|
```powershell
|
|
325
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.
|
|
326
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.0/install.ps1 | iex
|
|
326
327
|
```
|
|
327
328
|
|
|
328
329
|
**Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
|
|
329
330
|
|
|
330
331
|
```sh
|
|
331
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.
|
|
332
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.0
|
|
332
333
|
```
|
|
333
334
|
|
|
334
335
|
Without git, use the GitHub tag archive:
|
|
335
336
|
|
|
336
337
|
```sh
|
|
337
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.
|
|
338
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.0.tar.gz
|
|
338
339
|
```
|
|
339
340
|
|
|
340
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 用量热图 · 多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配) · 主流 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
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
12
12
|
|
|
13
|
-
**v1.
|
|
13
|
+
**v1.8.0**:新增独立计费统计界面,支持今天、近 7 天、近 30 天、全部保留记录和自定义日期,提供模型与提供商筛选、费用趋势、对话排行和 Token × 单价的逐次明细。入口为 **设置 → 费用 → 计费统计**,也可点击对话标题栏的 **费用明细**。详见[统计说明](docs/billing-statistics.md#简体中文)及[更新说明](docs/release-notes/v1.8.0.md)。
|
|
14
14
|
|
|
15
15
|
桌面端用户请按 [Desktop 安装说明](docs/install-troubleshooting.md#desktop-安装与更新),使用应用自带的 CLI 和 `desktop` Profile。
|
|
16
16
|
|
|
@@ -319,22 +319,22 @@ Node.js 20 请改用 `npm install -g pnpm@10`。版本要求见 [pnpm 官方安
|
|
|
319
319
|
dsh plugin --profile web add dsh-cost-meter
|
|
320
320
|
```
|
|
321
321
|
|
|
322
|
-
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.
|
|
322
|
+
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.8.0`**,建议先下载审阅再运行):
|
|
323
323
|
|
|
324
324
|
```powershell
|
|
325
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.
|
|
325
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.0/install.ps1 | iex
|
|
326
326
|
```
|
|
327
327
|
|
|
328
328
|
**或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
|
|
329
329
|
|
|
330
330
|
```sh
|
|
331
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.
|
|
331
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.0
|
|
332
332
|
```
|
|
333
333
|
|
|
334
334
|
没有 git 时可用 GitHub tag 打包直链:
|
|
335
335
|
|
|
336
336
|
```sh
|
|
337
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.
|
|
337
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.0.tar.gz
|
|
338
338
|
```
|
|
339
339
|
|
|
340
340
|
安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Cost statistics / 计费统计
|
|
2
|
+
|
|
3
|
+
## English
|
|
4
|
+
|
|
5
|
+
Open **Settings → Cost → Cost statistics**, or choose **Cost details** in a conversation's header. The conversation entry opens its entire retained history. The settings entry starts with the last seven calendar days, including today.
|
|
6
|
+
|
|
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
|
+
- **Filters:** Provider and model. Choose API cost, Plan equivalent, or their combined equivalent for chart amounts and rankings.
|
|
9
|
+
- **Overview:** API cost, Plan equivalent, call count, average cost per call, cache hit rate, spending trend, model ranking, token composition, and conversation ranking. Select a chart bar to narrow its date range; select a conversation to inspect it.
|
|
10
|
+
- **Conversation detail:** Input, output, cache read, cache write, and reasoning costs; individual model, compaction and native-search calls; tokens × price per million; long-context rates and unavailable prices. Small amounts use at least eight decimal places. Fifty calls per page remain part of the complete detail total.
|
|
11
|
+
|
|
12
|
+
### What the numbers mean
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
The statistics screen uses DSH's package-local asynchronous module loader, verified on **0.2.0-rc.2**. Older hosts without that loader show an upgrade message; existing metering and settings remain available. Both shipped client files stay below the 256 KiB per-file limit.
|
|
23
|
+
|
|
24
|
+
## 简体中文
|
|
25
|
+
|
|
26
|
+
入口为 **设置 → 费用 → 计费统计**,也可以点击对话标题栏中的 **费用明细**。对话入口默认显示该对话保留的全部历史,设置入口默认显示包含今天的近 7 个自然日。
|
|
27
|
+
|
|
28
|
+
- **时间范围:**今天、近 7 天、近 30 天、全部保留记录,以及包含起止日期的自定义区间。日期按页面标明的宿主时区划分。
|
|
29
|
+
- **筛选和金额口径:**提供商、模型;可选择 API 费用、Plan 等值费用或两者合计,趋势和排行同步切换。
|
|
30
|
+
- **汇总:**API 费用、Plan 等值费用、调用次数、平均单次费用、缓存命中率、费用趋势、模型排行、Token 构成和对话排行。点击柱形缩小日期范围,点击对话查看明细。
|
|
31
|
+
- **单对话:**输入、输出、缓存读取、缓存写入、推理费用,以及模型调用、上下文压缩、原生搜索的逐次明细。每次调用列出 Token × 每百万 Token 单价、金额、长上下文档位及缺价提示。小额费用至少保留八位小数。每页显示 50 次调用,分页不影响完整明细合计。
|
|
32
|
+
|
|
33
|
+
### 统计口径
|
|
34
|
+
|
|
35
|
+
汇总直接读取 DSH 账本,打开页面不会重新定价或修改历史金额。跨天对话合并成一行,子代理作为独立对话分别统计,不把父级合计再次累加。历史中未归属模型或对话的金额会单独提示。
|
|
36
|
+
|
|
37
|
+
API 费用是按上报用量和配置单价计算的估算,不是厂商账单;Plan 费用是订阅用量的 API 等值,不代表额外扣款。外部用量快照仍在原概览中单列,这个页面统计 DSH 自身账本。
|
|
38
|
+
|
|
39
|
+
逐次明细仅在进入某个对话时读取其用量日志和搜索记录,按调用时间套用当前配置中的历史价格规则,包括峰谷价和长上下文价。修改过价格或日志不完整时,页面同时显示账本金额与可用明细金额。缺少日志、缺少价格都不会被解释为免费。
|
|
40
|
+
|
|
41
|
+
缓存命中率为 `缓存读取 / (未缓存输入 + 缓存读取 + 缓存写入)`。Token 构成展示数量占比,不是费用占比。推理 Token 可能包含在输出中,因此单独列示,不再次加入总 Token;是否另收推理费取决于配置的单价。
|
|
42
|
+
|
|
43
|
+
统计页使用 DSH 的异步模块加载器,已在 **0.2.0-rc.2** 验证。没有该能力的旧宿主会显示升级提示,原有计费和设置继续可用。两个客户端文件分别遵守 256 KiB 大小限制。
|
|
44
|
+
|
|
45
|
+
## Design references
|
|
46
|
+
|
|
47
|
+
The organization of summary statistics, time ranges, conversation drill-down and on-demand details was informed by [dsh-context v0.62.0](https://github.com/bowenliang123/dsh-context/tree/v0.62.0), especially its [overview](https://github.com/bowenliang123/dsh-context/blob/v0.62.0/src/client/overview.ts) and [detail loading](https://github.com/bowenliang123/dsh-context/blob/v0.62.0/src/client/timelineSource.ts). This implementation uses dsh-cost-meter's existing ledger and pricing engine with its own layout.
|
|
48
|
+
|
|
49
|
+
Its [context category allocation](https://github.com/bowenliang123/dsh-context/blob/v0.62.0/src/client/categories.ts) is useful for explaining token composition. An estimated allocation of system/tool/history tokens cannot establish their individual cache prices, so this screen does not present those estimated shares as exact component costs.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Read-only analytics over the canonical ledger. No repricing, log scans or writes. */
|
|
2
|
+
import { localDayKey } from './store.js'
|
|
3
|
+
import { providerPriceEntryFor } from './pricing.js'
|
|
4
|
+
|
|
5
|
+
export const STAT_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite', 'reasoning', 'calls', 'cost', 'apiCost']
|
|
6
|
+
const num = n => typeof n === 'number' && Number.isFinite(n) && n > 0 ? n : 0
|
|
7
|
+
export const emptyStats = () => Object.fromEntries(STAT_FIELDS.map(key => [key, 0]))
|
|
8
|
+
const add = (to, from) => { for (const key of STAT_FIELDS) to[key] += num(key === 'apiCost' ? from?.apiCost ?? from?.cost : from?.[key]); return to }
|
|
9
|
+
const valueOf = (row, basis) => basis === 'total' ? row.cost : basis === 'plan' ? Math.max(0, row.cost - row.apiCost) : row.apiCost
|
|
10
|
+
const identity = key => { const i = key.indexOf(':'); return i < 0 ? { provider: '', model: key } : { provider: key.slice(0, i), model: key.slice(i + 1) } }
|
|
11
|
+
export const matchesStats = (key, query) => { const id = identity(key); return (!query.provider || query.provider === id.provider) && (!query.model || query.model === id.model) }
|
|
12
|
+
const validDate = value => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value
|
|
13
|
+
|
|
14
|
+
export function statisticsQuery(raw = {}, now = Date.now()) {
|
|
15
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('invalid statistics query')
|
|
16
|
+
const to = raw.to || localDayKey(now)
|
|
17
|
+
const from = raw.from || to
|
|
18
|
+
if (!validDate(from) || !validDate(to) || from > to || (Date.parse(to) - Date.parse(from)) / 86400000 >= 3660) throw new Error('invalid statistics date range (maximum 3660 days)')
|
|
19
|
+
for (const key of ['provider', 'model', 'sessionId']) if (raw[key] != null && (typeof raw[key] !== 'string' || raw[key].length > 512)) throw new Error('invalid statistics filter')
|
|
20
|
+
const offset = raw.offset ?? 0
|
|
21
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > 1e7) throw new Error('invalid statistics offset')
|
|
22
|
+
if (raw.basis != null && !['api', 'total', 'plan'].includes(raw.basis)) throw new Error('invalid statistics basis')
|
|
23
|
+
return { from, to, provider: raw.provider || '', model: raw.model || '', sessionId: raw.sessionId || '', basis: raw.basis || 'api', offset }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ledgerStatisticsQuery(ledger, raw) {
|
|
27
|
+
return statisticsQuery({ ...raw, from: raw?.from || Object.keys(ledger.days ?? {}).sort()[0] || localDayKey(Date.now()) })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function selectedStats(container, query) {
|
|
31
|
+
if (!query.provider && !query.model) return add(emptyStats(), container)
|
|
32
|
+
const total = emptyStats()
|
|
33
|
+
for (const [key, bucket] of Object.entries(container?.byProviderModel ?? {})) if (matchesStats(key, query)) add(total, bucket)
|
|
34
|
+
return total
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function billingStatistics(ledger, raw) {
|
|
38
|
+
const query = ledgerStatisticsQuery(ledger, raw), totals = emptyStats(), days = [], models = new Map(), sessions = new Map()
|
|
39
|
+
const providers = new Set(), modelOptions = new Set(), retained = Object.keys(ledger.days ?? {}).sort()
|
|
40
|
+
const assigned = emptyStats(), modeled = emptyStats()
|
|
41
|
+
for (let at = Date.parse(query.from); at <= Date.parse(query.to); at += 86400000) {
|
|
42
|
+
const date = new Date(at).toISOString().slice(0, 10), stored = ledger.days?.[date]
|
|
43
|
+
const day = query.sessionId ? stored?.sessions?.find(s => s.id === query.sessionId) : stored
|
|
44
|
+
const selected = selectedStats(day, query)
|
|
45
|
+
days.push({ date, ...selected }); add(totals, selected)
|
|
46
|
+
for (const [key, bucket] of Object.entries(day?.byProviderModel ?? {})) {
|
|
47
|
+
const id = identity(key)
|
|
48
|
+
if (id.provider) providers.add(id.provider)
|
|
49
|
+
if (!query.provider || query.provider === id.provider) modelOptions.add(id.model)
|
|
50
|
+
if (!matchesStats(key, query)) continue
|
|
51
|
+
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 }
|
|
52
|
+
add(row, bucket); add(modeled, bucket); models.set(key, row)
|
|
53
|
+
}
|
|
54
|
+
for (const session of query.sessionId ? (day ? [day] : []) : day?.sessions ?? []) {
|
|
55
|
+
if (!session?.id) continue
|
|
56
|
+
const selected = selectedStats(session, query)
|
|
57
|
+
if (!selected.calls && !selected.cost && !STAT_FIELDS.slice(0, 5).some(key => selected[key])) continue
|
|
58
|
+
const row = sessions.get(session.id) ?? { id: session.id, title: session.id, ...emptyStats() }
|
|
59
|
+
if (session.title) row.title = session.title
|
|
60
|
+
add(row, selected); add(assigned, selected); sessions.set(session.id, row)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
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))
|
|
64
|
+
return { from: query.from, to: query.to, retainedFrom: retained[0] ?? '', retainedTo: retained.at(-1) ?? '',
|
|
65
|
+
totals, days, models: [...models.values()].sort(sort), providers: [...providers].sort(), modelOptions: [...modelOptions].sort(),
|
|
66
|
+
sessions: [...sessions.values()].sort(sort).slice(query.offset, query.offset + 25), sessionCount: sessions.size, offset: query.offset,
|
|
67
|
+
unassignedCost: Math.max(0, valueOf(totals, query.basis) - valueOf(assigned, query.basis)),
|
|
68
|
+
unmodeledCost: Math.max(0, valueOf(totals, query.basis) - valueOf(modeled, query.basis)),
|
|
69
|
+
}
|
|
70
|
+
}
|