@sixzjd/dsh-usage-dashboard 0.0.0-stage → 0.6.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/CHANGELOG.md +44 -0
- package/LICENSE +21 -0
- package/README.en.md +212 -0
- package/README.md +194 -2
- package/client.js +352 -0
- package/cordis.patch.yml +11 -0
- package/lib/aggregate.js +422 -0
- package/lib/backfill.js +116 -0
- package/lib/catalog.js +230 -0
- package/lib/history.js +150 -0
- package/lib/index.js +2 -0
- package/lib/plugin.js +939 -0
- package/lib/pricing.js +232 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +81 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Fixed — deleted sessions no longer erase their usage
|
|
6
|
+
|
|
7
|
+
The panel folded only what `sessionQuery.listSessions()` returned, which lists
|
|
8
|
+
the session logs that still exist on disk. Deleting a conversation therefore
|
|
9
|
+
subtracted every token it had ever spent from every total, silently: nothing
|
|
10
|
+
errored, the numbers just shrank.
|
|
11
|
+
|
|
12
|
+
On the machine that motivated this release, 362 deleted sessions were worth
|
|
13
|
+
~3.1B tokens against a 6.67B lifetime figure — the panel showed 3.5B and looked
|
|
14
|
+
like it had lost half the history.
|
|
15
|
+
|
|
16
|
+
- Per-call usage is now recorded durably (`lib/history.js`) and replayed into
|
|
17
|
+
every later scan. Retained rows rank BELOW the live log — history loads first
|
|
18
|
+
and disk logs fold afterwards — so a session that still exists is always
|
|
19
|
+
measured from its own log.
|
|
20
|
+
- Sessions deleted before this release are recovered once from the host's
|
|
21
|
+
session projection cache `tokenUsage` totals (`lib/backfill.js`).
|
|
22
|
+
- Known limitation: those checkpoints carry one whole-session total with no
|
|
23
|
+
per-day or per-model split, so a backfilled row is anchored at the session's
|
|
24
|
+
creation time. The total is accurate; its position on the daily chart is
|
|
25
|
+
approximate.
|
|
26
|
+
|
|
27
|
+
### Changed — renamed to `@sixzjd/dsh-usage-dashboard`
|
|
28
|
+
|
|
29
|
+
The unscoped name `dsh-usage-dashboard` was already taken on npm, so the
|
|
30
|
+
package publishes under the `@sixzjd` scope. The GitHub repository moves to
|
|
31
|
+
`sixzjd/dsh-usage-dashboard` (GitHub keeps redirects from the old path).
|
|
32
|
+
|
|
33
|
+
Because the rename touches the module id, the route prefix
|
|
34
|
+
(`/api/dsh-usage-dashboard/*`), the settings-section id, the locale namespace,
|
|
35
|
+
and the storage unit names, **0.6.0 does not upgrade in place over 0.5.x**:
|
|
36
|
+
remove the old bundle before adding this one.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
dsh plugin --profile web remove dsh-usage-dash-board
|
|
40
|
+
dsh plugin --profile web add @sixzjd/dsh-usage-dashboard
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The rewrite of the storage units only renames paths for a store that never
|
|
44
|
+
materialized, so no price or history data is migrated or lost.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sixzjd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# dsh-usage-dashboard
|
|
2
|
+
|
|
3
|
+
A local usage-analytics plugin for DeepSeek Harness: **Settings → Usage Statistics** shows token consumption, request count, cache hit rate and a daily trend.
|
|
4
|
+
|
|
5
|
+
All data comes from your local session logs. No network, no upload, no API key.
|
|
6
|
+
|
|
7
|
+
> The panel body is currently Chinese-only; the settings nav label follows your browser language. The full design notes live in [README.md](README.md).
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
DSH already reports per-turn token usage, but nothing aggregates across sessions. External tools like cc-switch do, at the cost of a separate desktop app that parses log files and guesses their format.
|
|
12
|
+
|
|
13
|
+
This plugin runs entirely inside DSH. The host exposes a public `sessionQuery` service that owns decompression, ordering and interrupted-turn balancing, so the plugin consumes structured events instead of parsing files — and it does not break when the log layout changes.
|
|
14
|
+
|
|
15
|
+
## Flow
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
durable session log
|
|
19
|
+
│ ctx.sessionQuery.listSessions() / readSession()
|
|
20
|
+
▼
|
|
21
|
+
event stream (decoded, ordered)
|
|
22
|
+
│ assistant/message.usage ▸ supersedes assistant/chunk for the same turn
|
|
23
|
+
│ request/header / request/context ▸ model and provider identity
|
|
24
|
+
▼
|
|
25
|
+
fold into one row per (session, turn, step)
|
|
26
|
+
│ bucket by local calendar day
|
|
27
|
+
▼
|
|
28
|
+
daily / per-model aggregates + price-table costing
|
|
29
|
+
│ GET /api/dsh-usage-dashboard/summary
|
|
30
|
+
▼
|
|
31
|
+
native settings section (settings.section slot)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
One call counts once: the final `assistant/message` usage **replaces** the streaming `assistant/chunk` for the same `turn:step` rather than adding to it, and an all-zero replay never overwrites a recorded real number.
|
|
35
|
+
|
|
36
|
+
## Deleted sessions still count
|
|
37
|
+
|
|
38
|
+
The data source, `sessionQuery`, can only list session logs that **still exist on disk**. Counting straight from it means deleting a conversation silently subtracts every token it ever spent — nothing errors, the totals just quietly shrink.
|
|
39
|
+
|
|
40
|
+
So the plugin also stores per-call usage from each scan (`storages/dsh_usage_dashboard_history.json`). Retained rows rank BELOW the live log: history loads first and the disk logs fold afterwards, so a session that still exists is always measured from its own log, while a deleted one keeps counting from the archive.
|
|
41
|
+
|
|
42
|
+
For sessions deleted **before** this feature shipped, the plugin runs a one-time backfill from the host's session projection cache (`session_projcache`) `tokenUsage` totals.
|
|
43
|
+
|
|
44
|
+
> **Known limitation**: `tokenUsage` carries one whole-session total with no per-day or per-model split. A backfilled row is anchored at the session's creation time, so the **total is accurate** but its position on the daily chart is approximate.
|
|
45
|
+
|
|
46
|
+
## Metric definitions
|
|
47
|
+
|
|
48
|
+
Token buckets follow the host's documented **DISJOINT** semantics (`@deepseek-ai/dsh-llm`): `inputTokens` is uncached input only; cached input is reported separately.
|
|
49
|
+
|
|
50
|
+
| Metric | Formula |
|
|
51
|
+
|---|---|
|
|
52
|
+
| Tokens consumed | `input + output + cacheWrite + cacheRead` |
|
|
53
|
+
| Requests | calls carrying usage, deduplicated by `session:turn:step` |
|
|
54
|
+
| Cache hit rate | `cacheRead ÷ (input + cacheWrite + cacheRead)` |
|
|
55
|
+
|
|
56
|
+
> **Note**: OpenAI-style protocols do not report `cacheWriteTokens`, so that term is always 0 and the hit rate stays correct (the denominator omits it too). This is a missing upstream field, not a counting gap.
|
|
57
|
+
|
|
58
|
+
## Features
|
|
59
|
+
|
|
60
|
+
| Area | Content |
|
|
61
|
+
|---|---|
|
|
62
|
+
| Three summary cards | Tokens consumed / Requests / Cache hit rate; click to expand that metric's breakdown |
|
|
63
|
+
| Trend | Bar chart, switchable between Tokens and Requests; values appear on hover or keyboard focus only. Daily by default; **Today** (or a custom range whose start and end are the same day) switches it to **hourly** |
|
|
64
|
+
| Filters | Provider, model, range (Today / 7 / 30 / 90 days / All time / custom start–end), opening on the last 30 days |
|
|
65
|
+
| Request log | Per-call time, provider, model, tokens, cost |
|
|
66
|
+
| Providers / Models | Grouped totals and shares |
|
|
67
|
+
| Pricing | Editable price table, USD per million tokens across four buckets |
|
|
68
|
+
|
|
69
|
+
The X axis is filled with **every calendar day** in range, so idle days render as zero rather than being skipped — otherwise "two idle weeks then one burst" would read as "steady low usage with a spike". The hourly view works the same way: all 24 slots are present, so a quiet 3 a.m. keeps its place.
|
|
70
|
+
|
|
71
|
+
**All time** starts at the oldest record actually on disk, not at a fixed day count; with no records at all it degrades to Today but stays daily, so an empty history is never drawn as 24 hourly bars.
|
|
72
|
+
|
|
73
|
+
The header stamps **when the numbers were captured** (`· 数据更新于 15:27:45`). A single response is always internally consistent — a provider's request count equals the sum of its models' — so two tables disagreeing can only mean usage grew between two fetches. The panel does not auto-refresh: numbers change when you change a filter or press refresh, and the stamp says which fetch you are looking at.
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
dsh plugin --profile web add @sixzjd/dsh-usage-dashboard
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Once installed, **Settings → Plugins** checks for and installs updates directly — no CLI needed. For the development build from GitHub:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
dsh plugin --profile web add github:sixzjd/dsh-usage-dashboard
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Refresh the page after installing. The entry mounts into the settings page's `settings.section` slot.
|
|
88
|
+
|
|
89
|
+
## Built-in prices
|
|
90
|
+
|
|
91
|
+
DeepSeek's official peak/off-peak table ships as in-package constants, sourced from the official announcement:
|
|
92
|
+
|
|
93
|
+
| Model | Off-peak in / out / cacheRead | Peak in / out / cacheRead |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `deepseek-v4-flash` | 0.15 / 0.6 / 0.003 | 0.44 / 1.32 / 0.014 |
|
|
96
|
+
| `deepseek-v4-pro` | 0.66 / 1.98 / 0.022 | 1.32 / 3.96 / 0.044 |
|
|
97
|
+
|
|
98
|
+
USD per million tokens. Peak hours are UTC `01:00–04:00` and `06:00–10:00` on Monday–Friday, excluding Chinese statutory holidays.
|
|
99
|
+
|
|
100
|
+
**Models with no public price are reported as unpriced and never receive a guessed rate.** You can add prices manually under Pricing.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
client.js browser half (the settings panel)
|
|
106
|
+
lib/index.js host half entry
|
|
107
|
+
lib/plugin.js scan, aggregation scheduling, persistence, HTTP routes
|
|
108
|
+
lib/aggregate.js event fold and daily / per-model bucketing
|
|
109
|
+
lib/catalog.js route-key -> display name, read from the upstream cache (fails closed)
|
|
110
|
+
lib/pricing.js price table and peak-band resolution
|
|
111
|
+
locale/{en,zh}.json plugin page display name (en.json must exist)
|
|
112
|
+
test/ node:test suite, no third-party dependencies
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm test # runs offline, no dependencies
|
|
117
|
+
npm run check
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Model display names
|
|
121
|
+
|
|
122
|
+
Some providers expose models under opaque **route keys** rather than real model
|
|
123
|
+
names. Qoder does exactly this: the durable log records
|
|
124
|
+
`{provider:"qoder", model:"qfmodel"}`, while the model actually behind that
|
|
125
|
+
route is `Qwen3.8-Flash`. The route key is handed back to the upstream verbatim
|
|
126
|
+
and **cannot** be replaced at the source, so the real name only exists in that
|
|
127
|
+
plugin's own model-discovery cache.
|
|
128
|
+
|
|
129
|
+
The panel reads that cache and shows the real model name.
|
|
130
|
+
|
|
131
|
+
Only models whose logged modelId **is itself an alias** need this step. Most providers
|
|
132
|
+
return real model names (`openai/gpt-oss-120b`, `minimaxai/minimax-m3`,
|
|
133
|
+
`deepseek-v4-flash-free`, …); the alias table has no entry for them, the panel shows
|
|
134
|
+
those verbatim, and no other plugin is required. Someone without the qoder plugin will
|
|
135
|
+
never see `qfmodel`, because that route key cannot appear in their logs — resolving
|
|
136
|
+
fewer aliases changes nothing on screen.
|
|
137
|
+
|
|
138
|
+
Matching is **exact on the whole id** (case-insensitive); it does not match the trailing
|
|
139
|
+
segment after a namespace. The alias cache carries no provider field, so letting
|
|
140
|
+
`vendor/dmodel` hit Qoder's `dmodel` would label another vendor's model with a Qoder
|
|
141
|
+
name — a wrong label on screen is worse than no label.
|
|
142
|
+
|
|
143
|
+
**Display only.** Grouping, counting, filtering and pricing all keep using the
|
|
144
|
+
route id, so:
|
|
145
|
+
|
|
146
|
+
- labelling never moves a request between rows or changes any figure
|
|
147
|
+
- when two accounts' caches disagree about one route key, that key is **dropped**
|
|
148
|
+
(a route id on screen beats a wrong model name)
|
|
149
|
+
- a missing, unreadable or reshaped cache falls back **silently** to the route
|
|
150
|
+
id: the UI says nothing, and you simply see keys like `qfmodel`. To check
|
|
151
|
+
whether resolution is working, read `catalog.aliasCount` in the
|
|
152
|
+
`GET /api/dsh-usage-dashboard/summary` response — 0 means nothing resolved
|
|
153
|
+
|
|
154
|
+
## Seams used
|
|
155
|
+
|
|
156
|
+
| Seam | Purpose |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `ctx.sessionQuery` | list sessions, read decoded event streams |
|
|
159
|
+
| `ctx.storage` (json backend, kv) | persist the price table under `$DSH_HOME/storages` |
|
|
160
|
+
| `ctx.webServer` | register `/api/dsh-usage-dashboard/*` |
|
|
161
|
+
| `ctx.timer` | periodic reconcile and live debouncing |
|
|
162
|
+
| `settings.section` slot | mount the settings section |
|
|
163
|
+
| `Modal` from `@deepseek-ai/dsh-client-ui-primitives` | metric detail dialog (falls back to a self-drawn overlay when absent) |
|
|
164
|
+
|
|
165
|
+
### One read outside those seams
|
|
166
|
+
|
|
167
|
+
This plugin does **not** read `$DSH_HOME/sessions` (every session arrives
|
|
168
|
+
through `ctx.sessionQuery`), but to resolve the model names above it does read
|
|
169
|
+
`dsh-qoder-connect`'s own discovery cache:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
$DSH_HOME/profiles/*/.dsh-qoder-connect/state/*-catalog.json
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
That is **another plugin's private file, not a published contract** — the path,
|
|
176
|
+
the JSON shape and the per-account keying can all change with its releases. I
|
|
177
|
+
know this contradicts the earlier "public seams only" claim in this repository,
|
|
178
|
+
and this section is the corrected statement rather than a silent edit. The
|
|
179
|
+
tradeoff: without it the panel can only ever show meaningless route keys.
|
|
180
|
+
|
|
181
|
+
The read is therefore written to fail closed:
|
|
182
|
+
|
|
183
|
+
- only files named `*-catalog.json` matching `{version:1, entries:{…models:[…]}}`
|
|
184
|
+
are accepted; `.qoder-probe.json` and `.qoder-host-heartbeat.json` (which
|
|
185
|
+
carries a `pid`) in the same directory are excluded
|
|
186
|
+
- the cache is keyed by `pat:<hash>` and **only `{id, name}` is extracted**, so no
|
|
187
|
+
account identifier can reach a response (a test pins this down)
|
|
188
|
+
- any failure falls back to the route key: no throw, no guessing, no effect on
|
|
189
|
+
the statistics
|
|
190
|
+
- if upstream reshapes the file, the worst case is names reverting to route keys;
|
|
191
|
+
the numbers stay correct
|
|
192
|
+
|
|
193
|
+
## Security boundaries
|
|
194
|
+
|
|
195
|
+
- Both routes require **loopback**: the socket peer and the `Host` header must both be loopback, and `Sec-Fetch-Site: cross-site` is rejected, which blocks DNS rebinding.
|
|
196
|
+
- The write route (saving prices) additionally requires a per-process token handed out by `GET /pricing`, which a cross-origin page cannot read.
|
|
197
|
+
- Responses contain usage numbers, model identifiers and session IDs (UUIDs). They do **not** contain prompts, reply bodies, session titles, workspace paths, or the account identifiers mentioned above.
|
|
198
|
+
- The only file read is the catalog noted above; it is never written, and no other file in that directory is touched.
|
|
199
|
+
|
|
200
|
+
## Known limitations
|
|
201
|
+
|
|
202
|
+
- **Cost is an estimate** from the built-in or manual price table, not a provider invoice. Cache read/write and reasoning follow whichever buckets the upstream reports.
|
|
203
|
+
- **Qoder routes bill in credits, not tokens** — its cache carries a per-model `billing.credits` multiplier such as `x0.5`. Applying a models.dev USD rate to such a route yields an "equivalent API price", **not what you actually paid**, so this plugin resolves names only and never auto-prices a route key.
|
|
204
|
+
- An interrupted upstream request may carry only `assistant/chunk` usage with no final `assistant/message`; the chunk usage is kept. If the upstream reports no usage event at all, tokens cannot be recovered precisely from reply content.
|
|
205
|
+
- Sessions lacking `request/header` / `request/context` show their model as `unknown`; the plugin never infers a model from a provider name.
|
|
206
|
+
- Model display names depend on `dsh-qoder-connect`'s private cache format (see above) and need following if it changes.
|
|
207
|
+
- The panel body is Chinese-only; only the settings nav label is localized.
|
|
208
|
+
- Every session the host lists is counted, not just registered workspaces.
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,3 +1,195 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-usage-dashboard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DeepSeek Harness 的本地用量统计插件:在**设置 → 用量统计**里查看 token 消耗、请求数、缓存命中率与按天趋势。
|
|
4
|
+
|
|
5
|
+
数据全部来自本机会话日志,不联网、不上传、不需要任何 API Key。
|
|
6
|
+
|
|
7
|
+
## 背景
|
|
8
|
+
|
|
9
|
+
DSH 自带每轮对话的 token 明细,但没有跨会话的汇总视图。社区里 cc-switch 这类外部工具能做,代价是要额外装一个桌面应用、并且靠解析日志文件来猜格式。
|
|
10
|
+
|
|
11
|
+
这个插件解决同一件事,但完全跑在 DSH 内部:宿主已经提供了公开的 `sessionQuery` 服务,会话日志的解压、排序与中断轮次平衡都由它负责,插件只消费结构化的事件。因此没有文件解析、没有格式猜测,也不会因为 DSH 升级改了日志布局而失效。
|
|
12
|
+
|
|
13
|
+
## 原理
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
durable session log
|
|
17
|
+
│ ctx.sessionQuery.listSessions() / readSession()
|
|
18
|
+
▼
|
|
19
|
+
事件流(已解压、已排序)
|
|
20
|
+
│ assistant/message.usage ▸ 覆盖同轮的 assistant/chunk
|
|
21
|
+
│ request/header / request/context ▸ 模型与供应商标识
|
|
22
|
+
▼
|
|
23
|
+
按 (session, turn, step) 折叠成"一次调用"
|
|
24
|
+
│ 本地日历日分桶
|
|
25
|
+
▼
|
|
26
|
+
日聚合 / 模型聚合 / 区间汇总 + 价格表计价
|
|
27
|
+
│ GET /api/dsh-usage-dashboard/summary
|
|
28
|
+
▼
|
|
29
|
+
设置页原生分区(settings.section slot)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
一次调用只计一份用量:`assistant/message` 上的最终 usage 会**替换**同一 `turn:step` 的流式 `assistant/chunk`,而不是相加;全零的重放事件也不会覆盖已记录的真实数字。
|
|
33
|
+
|
|
34
|
+
## 删除的会话仍计入历史
|
|
35
|
+
|
|
36
|
+
数据源 `sessionQuery` 只能列出**磁盘上还存在的**会话日志。如果直接用它做统计,删掉一个对话,它花过的 token 就会从合计里凭空消失 —— 数字不会报错,只是安静地变小。
|
|
37
|
+
|
|
38
|
+
所以插件把每次扫描到的调用用量另存一份(`storages/dsh_usage_dashboard_history.json`)。历史行的优先级低于活日志:**先载入历史、再折叠磁盘日志**,所以还存在的会话永远以日志为准,删掉的会话继续按存档计入。
|
|
39
|
+
|
|
40
|
+
对在本功能上线**之前**就已删除的会话,插件会从宿主的会话投影缓存(`session_projcache`)里一次性回填 `tokenUsage` 总量。
|
|
41
|
+
|
|
42
|
+
> **口径限制**:`tokenUsage` 每个会话只有一个总量,没有按天、按模型拆分。回填行只能锚定在会话创建时间,所以历史部分的**总量准确**,但它在按天图上的位置是近似的。
|
|
43
|
+
|
|
44
|
+
## 指标口径
|
|
45
|
+
|
|
46
|
+
token 桶遵循宿主 `@deepseek-ai/dsh-llm` 声明的 **DISJOINT** 语义:`inputTokens` 只含未命中缓存的输入,缓存部分单列在 `cacheReadTokens` / `cacheWriteTokens`。
|
|
47
|
+
|
|
48
|
+
| 指标 | 计算方式 |
|
|
49
|
+
|---|---|
|
|
50
|
+
| 消耗 token | `输入 + 输出 + 缓存写 + 缓存读` |
|
|
51
|
+
| 总请求数 | 携带 usage 的调用次数(按 `session:turn:step` 去重) |
|
|
52
|
+
| 缓存命中率 | `缓存读 ÷ (输入 + 缓存写 + 缓存读)` |
|
|
53
|
+
|
|
54
|
+
> **注意**:OpenAI 式协议不上报 `cacheWriteTokens`,此时该项恒为 0,命中率仍然正确(分母同样不含它)。这不是统计缺失,是上游没有这个字段。
|
|
55
|
+
|
|
56
|
+
## 功能
|
|
57
|
+
|
|
58
|
+
| 位置 | 内容 |
|
|
59
|
+
|---|---|
|
|
60
|
+
| 顶部三张卡 | 消耗 token / 总请求数 / 缓存命中率,点击展开该指标的构成明细 |
|
|
61
|
+
| 使用趋势 | 柱状图,可切 Tokens / 请求;数值悬浮或键盘聚焦时显示,不常驻。默认按天,选**今天**(或起止同一天的自定义区间)自动切成**按小时** |
|
|
62
|
+
| 筛选 | 供应商、模型、时间范围(今天 / 近 7 / 30 / 90 天 / 全部时间 / 自定义起止),打开时默认近 30 天 |
|
|
63
|
+
| 请求明细 | 逐次调用的时间、供应商、模型、token、费用 |
|
|
64
|
+
| 供应商 / 模型 | 分组聚合与占比 |
|
|
65
|
+
| 定价 | 可编辑价格表,按 USD / 百万 token 计四桶价格 |
|
|
66
|
+
|
|
67
|
+
趋势图的 X 轴按**日历日补齐**,没有数据的日期显示为 0 而不是跳过 —— 否则"闲置两周后爆发一天"会被画成"长期低用量带个尖峰"。按小时视图同理:24 个时段一个不少,安静的凌晨 3 点也占一格。
|
|
68
|
+
|
|
69
|
+
「全部时间」的起点是**实际最早一条记录**,不是某个固定天数;没有任何记录时退化为「今天」,但视图仍按天,不会把空历史画成 24 个小时柱。
|
|
70
|
+
|
|
71
|
+
标题下会标出这组数字的**抓取时刻**(`· 数据更新于 15:27:45`)。一次响应里所有表恒定自洽(供应商的请求数 === 它各模型请求数之和),所以两张表数字不同只可能是**两次抓取之间用量又涨了** —— 时间戳就是用来分辨这一点的。面板不自动刷新,数字只在你改筛选或点「刷新」时更新。
|
|
72
|
+
|
|
73
|
+
## 安装
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
dsh plugin --profile web add @sixzjd/dsh-usage-dashboard
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
装好后可在 **设置 → 插件** 页直接检查并更新版本,无需再手敲命令。若想装 GitHub 上的开发版:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
dsh plugin --profile web add github:sixzjd/dsh-usage-dashboard
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
安装后刷新浏览器页面即可。插件挂载在设置页的 `settings.section` 插槽,入口为「用量统计」(英文界面下为 Usage Statistics)。
|
|
86
|
+
|
|
87
|
+
## 卸载
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
dsh plugin --profile web remove @sixzjd/dsh-usage-dashboard
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 内置价格
|
|
94
|
+
|
|
95
|
+
预置 DeepSeek 官方峰谷价(含中国法定节假日全天按谷价),数据以插件内常量形式随版本发布,来源为官方公告:
|
|
96
|
+
|
|
97
|
+
| 模型 | 谷时 in / out / 缓存读 | 峰时 in / out / 缓存读 |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `deepseek-v4-flash` | 0.15 / 0.6 / 0.003 | 0.44 / 1.32 / 0.014 |
|
|
100
|
+
| `deepseek-v4-pro` | 0.66 / 1.98 / 0.022 | 1.32 / 3.96 / 0.044 |
|
|
101
|
+
|
|
102
|
+
单位均为 USD / 百万 token。峰时段为 UTC `01:00–04:00` 与 `06:00–10:00`(北京时间 09:00–12:00、14:00–18:00)的周一至周五,法定节假日除外。
|
|
103
|
+
|
|
104
|
+
**没有公开单价的模型一律标为「未计价」,不会套用任何猜测价格。** 你可以在「定价」里手工补价。
|
|
105
|
+
|
|
106
|
+
## 开发
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
client.js 浏览器半区(设置页面板)
|
|
110
|
+
lib/index.js 宿主半区入口
|
|
111
|
+
lib/plugin.js 扫描、聚合调度、持久化与 HTTP 路由
|
|
112
|
+
lib/aggregate.js 事件折叠与日/模型分桶
|
|
113
|
+
lib/catalog.js 读取路由别名缓存,解析真实模型名(失败即关闭)
|
|
114
|
+
lib/pricing.js 价格表与峰谷判定
|
|
115
|
+
locale/{en,zh}.json 插件页显示名(en.json 必须存在,否则 zh.json 不被读取)
|
|
116
|
+
test/ node:test 套件,无第三方依赖
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm test # 全部离线,无需依赖
|
|
121
|
+
npm run check
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 模型显示名
|
|
125
|
+
|
|
126
|
+
部分供应商用**路由别名**暴露模型,而不是真实模型名。Qoder 就是这样:日志里记的是
|
|
127
|
+
`{provider:"qoder", model:"qfmodel"}`,而该路由背后的实际模型是 `Qwen3.8-Flash`。
|
|
128
|
+
这个别名会原样回传给上游,**不能**在源头替换掉,真实名字只存在于该插件自己的模型发现缓存里。
|
|
129
|
+
|
|
130
|
+
面板会读取该缓存,把路由别名显示为真实模型名。
|
|
131
|
+
|
|
132
|
+
只有**日志里的 modelId 本身就是别名**的模型需要这一步。绝大多数供应商返回的就是真实模型名
|
|
133
|
+
(`openai/gpt-oss-120b`、`minimaxai/minimax-m3`、`deepseek-v4-flash-free`……),别名表里查不到,
|
|
134
|
+
面板原样显示,也不需要装任何别的插件。没装 qoder 的人不会看到 `qfmodel`,因为他本来就不会有这个
|
|
135
|
+
路由键 —— 少解析几个别名,界面上没有任何变化。
|
|
136
|
+
|
|
137
|
+
匹配是**整串精确匹配**(忽略大小写),不匹配命名空间后的尾段。因为别名缓存里没有 provider 字段,
|
|
138
|
+
`vendor/dmodel` 若按尾段命中 Qoder 的 `dmodel`,就会把别家的模型标成 Qoder 的名字 —— 界面上标错
|
|
139
|
+
比不标更糟。
|
|
140
|
+
|
|
141
|
+
**这仅影响显示。** 分组、计数、筛选与计价一律仍按路由 id 进行,所以:
|
|
142
|
+
|
|
143
|
+
- 贴标签不会把某次调用挪到另一行,也不会改变任何统计数字
|
|
144
|
+
- 两个账号的缓存对同一别名给出不同名字时,该别名被**丢弃**(宁可显示路由 id,也不显示错的模型名)
|
|
145
|
+
- 缓存缺失、格式变更或不可读时**静默**退回路由 id:界面上不提示,看到的就只是 `qfmodel` 这类路由键。要确认是否生效,查 `GET /api/dsh-usage-dashboard/summary` 响应里的 `catalog.aliasCount`,为 0 即表示没解析到任何别名
|
|
146
|
+
|
|
147
|
+
## 依赖的接缝
|
|
148
|
+
|
|
149
|
+
| 接缝 | 用途 |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `ctx.sessionQuery` | 列出会话、读取已解压的事件流 |
|
|
152
|
+
| `ctx.storage` (json backend, kv) | 持久化价格表到 `$DSH_HOME/storages` |
|
|
153
|
+
| `ctx.webServer` | 注册 `/api/dsh-usage-dashboard/*` |
|
|
154
|
+
| `ctx.timer` | 定时对账与实时去抖 |
|
|
155
|
+
| `settings.section` slot | 挂载设置页分区 |
|
|
156
|
+
| `@deepseek-ai/dsh-client-ui-primitives` 的 `Modal` | 指标详情浮层(缺失时降级为自绘浮层) |
|
|
157
|
+
|
|
158
|
+
### 一处公开接缝之外的读取
|
|
159
|
+
|
|
160
|
+
本插件**不读** `$DSH_HOME/sessions`(会话日志全部经 `ctx.sessionQuery`),
|
|
161
|
+
但为了上面的模型显示名,它会读取 `dsh-qoder-connect` 自己的模型发现缓存:
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
$DSH_HOME/profiles/*/.dsh-qoder-connect/state/*-catalog.json
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
这是**另一个插件的私有文件,不是公开契约** —— 路径、JSON 结构与按账号分键的格式都可能随对方版本改变。我明确知道这违背了本仓库先前"只用公开接缝"的表述,此处为修订后的准确说明。取舍是:不读它就只能在界面上永远显示 `qfmodel` 这类无语义的路由键。
|
|
168
|
+
|
|
169
|
+
因此该读取被写成**失败即关闭**:
|
|
170
|
+
|
|
171
|
+
- 只接受文件名以 `-catalog.json` 结尾、且结构为 `{version:1, entries:{…models:[…]}}` 的文件;同目录下的 `.qoder-probe.json`、`.qoder-host-heartbeat.json`(含 `pid`)被排除
|
|
172
|
+
- 缓存按 `pat:<hash>` 分账号,**只提取 `{id, name}` 两个字段**,账号标识符不进入任何响应(有测试钉住这一点)
|
|
173
|
+
- 任何一步失败都退回原路由键,不抛错、不猜测、不影响其余统计
|
|
174
|
+
- 若上游改了格式,最坏结果是名字回到路由键,统计数字不受牵连
|
|
175
|
+
|
|
176
|
+
## 安全边界
|
|
177
|
+
|
|
178
|
+
- 两个 HTTP 路由都要求**回环地址**:socket 对端与 `Host` 头必须都是 loopback,`Sec-Fetch-Site: cross-site` 直接拒绝,防止 DNS rebinding。
|
|
179
|
+
- 写接口(保存价格)额外要求进程令牌,由 `GET /pricing` 下发,跨源页面读不到。
|
|
180
|
+
- 响应只含用量数字、模型标识与会话 ID(UUID)。**不包含**提示词、回复正文、会话标题、工作区路径,也不包含上文的账号标识符。
|
|
181
|
+
- 唯一的文件读取是上面那处 catalog 缓存;不写入该文件,也不读取同目录其它文件。
|
|
182
|
+
|
|
183
|
+
## 已知限制
|
|
184
|
+
|
|
185
|
+
- **成本是估算值**,按内置/手工价格表计算,不等于供应商账单。缓存读写与推理的口径取决于上游是否上报。
|
|
186
|
+
- **Qoder 路由按 credits 配额计费,不按 token 计费**(其缓存里每个模型带 `billing.credits` 倍率,例如 `x0.5`)。对这类路由套用 models.dev 的 USD 单价得到的是"等价 API 价",**不是你实际支付的钱**;本插件因此只做名称解析,不自动为路由别名填价。
|
|
187
|
+
- 上游请求被中断时可能只有 `assistant/chunk` 的 usage 而没有最终 `assistant/message`;此时保留 chunk 用量。若上游完全没有任何 usage 事件,则无法从回复内容精确还原 token 数。
|
|
188
|
+
- 缺少 `request/header` / `request/context` 的会话,模型标识显示为 `unknown`,不会从供应商标名猜测模型。
|
|
189
|
+
- 模型显示名依赖 `dsh-qoder-connect` 的私有缓存格式(见上文),对方变更需跟进适配。
|
|
190
|
+
- 面板正文目前为中文,仅设置页侧栏标签跟随浏览器语言。
|
|
191
|
+
- 统计覆盖宿主列出的**全部**会话,不限于已注册工作区。
|
|
192
|
+
|
|
193
|
+
## 许可
|
|
194
|
+
|
|
195
|
+
MIT
|