dsh-model-billing 0.1.0 → 0.1.2
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 +31 -11
- package/README.md +227 -173
- package/client.js +310 -21
- package/host.js +37 -0
- package/package.json +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## [0.1.
|
|
4
|
-
|
|
5
|
-
### Added
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.2] - 2026-10-22
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Turn-tail display settings under Settings → Models & Cost: choose models, model provider, cost, tokens, cache-hit rate, and text labels; adjust font size, spacing, and weight.
|
|
8
|
+
- A switch to let another picker take over the composer model selector; turning it off restores the built-in DSH picker.
|
|
9
|
+
- Declared the supported DSH host range in `peerDependencies` so dsh-market can show host compatibility.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Turn-tail values now use the same secondary text color as the surrounding chat metadata instead of bright white.
|
|
14
|
+
- Turn-tail fields are separated by spacing rather than middle dots, and the hover explanation has been removed.
|
|
15
|
+
- The model column in the cost details panel is wide enough to show `DeepSeek-V4.1-Flash` without truncation.
|
|
16
|
+
|
|
17
|
+
## [0.1.1] - 2026-10-21
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Show the model or models used by each turn at the end of that turn, next to the per-turn cost.
|
|
22
|
+
|
|
23
|
+
## [0.1.0] - 2026-10-20
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Custom ordering for configured models in the composer model picker.
|
|
28
|
+
- Three editable per-million-token prices per model: cache-hit input, cache-miss input, and output.
|
|
29
|
+
- Built-in official DeepSeek peak/off-peak prices for `deepseek-flash` and `deepseek-v4-pro`, selected from each usage event's Beijing time.
|
|
30
|
+
- Per-turn and session cost estimates with a composer summary, turn-tail amount, and a detail panel grouped by turn and model.
|
|
31
|
+
- Local persistence at `~/.dsh/model-billing.json` with atomic writes.
|
package/README.md
CHANGED
|
@@ -1,173 +1,227 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="icon.svg" width="72" height="72" alt="dsh-model-billing icon">
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
|
-
<h1 align="center">dsh-model-billing</h1>
|
|
6
|
-
|
|
7
|
-
<p align="center">为 DeepSeek Harness Web GUI 调整模型顺序,并按轮次估算模型费用。</p>
|
|
8
|
-
|
|
9
|
-
<p align="center">
|
|
10
|
-
<a href="
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
DeepSeek
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
```sh
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://unpkg.com/dsh-model-billing@latest/icon.svg" width="72" height="72" alt="dsh-model-billing icon">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">dsh-model-billing</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">为 DeepSeek Harness Web GUI 调整模型顺序,并按轮次估算模型费用。</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/dsh-model-billing"><img alt="npm version" src="https://img.shields.io/npm/v/dsh-model-billing.svg"></a>
|
|
11
|
+
<a href="https://github.com/likaileave/dsh-model-billing/releases"><img alt="GitHub release" src="https://img.shields.io/github/v/release/likaileave/dsh-model-billing"></a>
|
|
12
|
+
<a href="LICENSE"><img alt="License" src="https://img.shields.io/github/license/likaileave/dsh-model-billing"></a>
|
|
13
|
+
<a href="https://github.com/topics/dsh-plugin"><img alt="DeepSeek Harness plugin" src="https://img.shields.io/badge/DeepSeek%20Harness-dsh--plugin-4D6BFE"></a>
|
|
14
|
+
<a href="https://awesome-dsh-plugin.com"><img alt="Awesome DSH Plugin" src="https://awesome-dsh-plugin.com/badge.svg"></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="#中文">中文</a> · <a href="#english">English</a> · <a href="CHANGELOG.md">Changelog</a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
> **AI 生成声明**:本项目的代码、文档和图标由 AI 辅助生成,并经人工审核、修改和测试;发布者对最终发布内容负责。
|
|
22
|
+
>
|
|
23
|
+
> **AI-generated notice**: The code, documentation, and icon in this project were generated with AI assistance, then reviewed, modified, and tested by a human; the publisher is responsible for the released content.
|
|
24
|
+
|
|
25
|
+
## 中文
|
|
26
|
+
|
|
27
|
+
`dsh-model-billing` 是一个 DSH Web 客户端插件。它可以接管输入框右下角的模型选择器,让你按自己的顺序排列模型,并为每个模型维护三档 token 价格;会话过程中按实际 usage 估算每一轮和整个会话的费用。
|
|
28
|
+
|
|
29
|
+
### 功能
|
|
30
|
+
|
|
31
|
+
- **自定义模型顺序**:在“设置 → 模型与费用”中用 ↑ / ↓ 调整已配置模型的顺序。模型选择器按这个顺序展示,跨提供方也保持你排的顺序,并用提供方名称分组。
|
|
32
|
+
- **三档 token 价格**:每个非 DeepSeek 模型可填写每百万 tokens 的缓存命中输入、缓存未命中输入、输出价格。`0` 是有效价格,留空表示未设置。
|
|
33
|
+
- **DeepSeek 官方价内置**:`deepseek-flash` 和 `deepseek-v4-pro` 直接使用 DeepSeek 官网的峰谷价,只读展示,不需要填写。
|
|
34
|
+
- **逐轮费用与使用模型**:每轮结束后在轮尾显示这一轮使用的模型,以及该轮费用和缓存命中率。
|
|
35
|
+
- **可配置轮尾**:在设置中开关使用模型、模型提供商、费用、token、缓存命中率、文字标签,并调整字号、间距和字重;也可以关闭选择器接管,恢复 DSH 内置模型选择器。
|
|
36
|
+
- **本会话费用**:输入框下方显示本会话累计估算、最近一轮费用和缓存命中率;展开后可查看按轮次和按模型的明细。
|
|
37
|
+
- **DeepSeek 也计入金额**:每条 usage 按事件发生时的北京时间自动选择高峰或空闲价,与第三方模型一起进入统计。
|
|
38
|
+
|
|
39
|
+
### 估算口径
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
缓存命中输入 × 命中价
|
|
43
|
+
+(未缓存输入 + 缓存写入)× 未命中价
|
|
44
|
+
+ 输出 × 输出价
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
价格单位是“元 / 百万 tokens”。缓存写入按缓存未命中价计算,这是三档价格模型下的近似处理。金额来自会话日志中的 provider usage,实际账单以 provider 为准。
|
|
48
|
+
|
|
49
|
+
### DeepSeek 官方价
|
|
50
|
+
|
|
51
|
+
以下价格内置在 [`host.js`](host.js) 的 `DEEPSEEK_PRICES` 中,单位“元 / 百万 tokens”。来源:[DeepSeek 模型 & 价格](https://api-docs.deepseek.com/zh-cn/quick_start/pricing/)(查询于 2026-10-19)。
|
|
52
|
+
|
|
53
|
+
| 模型 | 时段 | 缓存命中 | 缓存未命中 | 输出 |
|
|
54
|
+
|---|---|---:|---:|---:|
|
|
55
|
+
| deepseek-flash | 高峰 | 0.04 | 2 | 8 |
|
|
56
|
+
| deepseek-flash | 空闲 | 0.02 | 1 | 4 |
|
|
57
|
+
| deepseek-v4-pro | 高峰 | 0.30 | 9 | 27 |
|
|
58
|
+
| deepseek-v4-pro | 空闲 | 0.15 | 4.5 | 13.5 |
|
|
59
|
+
|
|
60
|
+
高峰时段为北京时间周一至周五 `9:00–12:00`、`14:00–18:00`(中国法定节假日除外);其余时间、周末和法定节假日全天为空闲时段。空闲价是高峰价的一半。
|
|
61
|
+
|
|
62
|
+
DeepSeek 价格只读,不写入本地价格配置。官网调价或节假日安排变化后,需要更新 `host.js` 中的 `DEEPSEEK_PRICES` 和 `DEEPSEEK_HOLIDAYS`;目前内置的是 2026 年节假日。
|
|
63
|
+
|
|
64
|
+
### 其他模型价格
|
|
65
|
+
|
|
66
|
+
插件**不预置**第三方模型价格。首次使用时,在“设置 → 模型与费用”中按 provider 的官方价填写三档价格即可;未填完整价格的模型不会产生金额,界面会标记“未定价”或“部分未定价”。
|
|
67
|
+
|
|
68
|
+
不同平台的缓存折扣、上下文阶梯价和活动价差异较大,建议直接使用你的实际结算价。插件不做自动抓取,也不猜测默认价。
|
|
69
|
+
|
|
70
|
+
### 安装
|
|
71
|
+
|
|
72
|
+
需要 DSH `^0.2.0-rc.2`。
|
|
73
|
+
|
|
74
|
+
**npm**
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
dsh plugin --profile desktop add dsh-model-billing
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**从 dsh-market 安装**
|
|
81
|
+
|
|
82
|
+
在 dsh-market 中搜索 `dsh-model-billing` 安装。
|
|
83
|
+
|
|
84
|
+
**从 GitHub Release 手动安装**
|
|
85
|
+
|
|
86
|
+
1. 从 [Releases](https://github.com/likaileave/dsh-model-billing/releases) 下载 `dsh-model-billing.tgz`。
|
|
87
|
+
2. 执行:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
dsh plugin --profile desktop add ./dsh-model-billing.tgz
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
把 `desktop` 换成你实际使用的 profile。
|
|
94
|
+
|
|
95
|
+
**从源码安装**
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
git clone https://github.com/likaileave/dsh-model-billing.git
|
|
99
|
+
dsh plugin --profile desktop add ./dsh-model-billing
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
安装或更新后刷新 Web 页面。设置页位于“模型与费用”。
|
|
103
|
+
|
|
104
|
+
### 发布
|
|
105
|
+
|
|
106
|
+
npm 包和 GitHub Release 使用相同版本号,两个渠道分别发布:
|
|
107
|
+
|
|
108
|
+
- npm:<https://www.npmjs.com/package/dsh-model-billing>
|
|
109
|
+
- GitHub Releases:<https://github.com/likaileave/dsh-model-billing/releases>
|
|
110
|
+
|
|
111
|
+
两个渠道是并列的发布渠道,不承诺自动同步。
|
|
112
|
+
|
|
113
|
+
### 数据位置
|
|
114
|
+
|
|
115
|
+
模型顺序和价格保存在:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
~/.dsh/model-billing.json
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
若设置了 `DSH_HOME`,则为 `$DSH_HOME/model-billing.json`。保存时先写临时文件再原子替换;文件损坏时回退为空配置,不会在未保存的情况下覆盖原文件。
|
|
122
|
+
|
|
123
|
+
### 已知限制
|
|
124
|
+
|
|
125
|
+
- 模型选择器由本插件接管,依赖 `ctx.modelDirectories`。该服务不可用时,插件不会强行覆盖内置选择器,价格和费用统计不受影响。
|
|
126
|
+
- `/model` 命令仍由 DSH 自带实现,顺序不保证与右下角选择器一致。
|
|
127
|
+
- 一轮里的多次模型尝试(重试、失败等)会分别计入;失败调用如果 provider 没有返回 usage,则无法计入。
|
|
128
|
+
- 只统计当前载入 Host 的会话;未打开的历史会话在进入后按需计算。
|
|
129
|
+
- 第三方价格是静态配置,不会自动跟随官网调价。
|
|
130
|
+
|
|
131
|
+
### 开发
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
node --check host.js
|
|
135
|
+
node --check client.js
|
|
136
|
+
node --test test/host.test.mjs test/routes.test.mjs
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
当前 10 个测试通过。插件没有运行时 npm 依赖。
|
|
140
|
+
|
|
141
|
+
### License
|
|
142
|
+
|
|
143
|
+
[MIT](LICENSE) © 2026 likaileave
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## English
|
|
148
|
+
|
|
149
|
+
`dsh-model-billing` is a DSH Web GUI plugin that can take over the composer's model picker, lets you order configured models yourself, stores three per-million-token prices per model, and estimates per-turn and session cost from provider usage.
|
|
150
|
+
|
|
151
|
+
### Features
|
|
152
|
+
|
|
153
|
+
- **Custom model order** in Settings → Models & Cost; the composer picker follows that order across providers and groups models by provider.
|
|
154
|
+
- **Three prices per model**: cache-hit input, cache-miss input, and output, in CNY per million tokens. `0` is a valid price; blank means unset.
|
|
155
|
+
- **Built-in official DeepSeek pricing** for `deepseek-flash` and `deepseek-v4-pro`, selected per usage event between peak and off-peak Beijing time.
|
|
156
|
+
- **Per-turn model and amount** at the end of each turn, together with the turn's cache-hit rate.
|
|
157
|
+
- **Configurable turn tail**: toggle models, provider names, cost, tokens, cache-hit rate, and text labels; adjust font size, spacing, and weight; or disable the picker takeover to restore the built-in DSH picker.
|
|
158
|
+
- **Session summary** below the composer plus a detail panel grouped by turn and by model.
|
|
159
|
+
- **DeepSeek is billed too**, using the same three-price formula and the event's timestamp.
|
|
160
|
+
|
|
161
|
+
### Cost formula
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
cache-hit input × hit price
|
|
165
|
+
+ (uncached input + cache write) × miss price
|
|
166
|
+
+ output × output price
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Prices are CNY per million tokens. Cache writes are approximated at the cache-miss price. Figures come from provider usage in the session log; the provider's invoice is authoritative.
|
|
170
|
+
|
|
171
|
+
The plugin ships **no third-party price defaults**. Enter your provider's actual rates in Settings → Models & Cost. Models without a complete three-price set are marked as unpriced and contribute no amount. DeepSeek prices are read-only and built into `host.js`.
|
|
172
|
+
|
|
173
|
+
### Install
|
|
174
|
+
|
|
175
|
+
Requires DSH `^0.2.0-rc.2`.
|
|
176
|
+
|
|
177
|
+
**npm**
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
dsh plugin --profile desktop add dsh-model-billing
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**From dsh-market**
|
|
184
|
+
|
|
185
|
+
Search for `dsh-model-billing` in dsh-market.
|
|
186
|
+
|
|
187
|
+
**From a GitHub Release**
|
|
188
|
+
|
|
189
|
+
Download `dsh-model-billing.tgz` from [Releases](https://github.com/likaileave/dsh-model-billing/releases), then:
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
dsh plugin --profile desktop add ./dsh-model-billing.tgz
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**From a checkout**
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
git clone https://github.com/likaileave/dsh-model-billing.git
|
|
199
|
+
dsh plugin --profile desktop add ./dsh-model-billing
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Refresh the Web page after installing or updating.
|
|
203
|
+
|
|
204
|
+
### Release channels
|
|
205
|
+
|
|
206
|
+
The npm package and the GitHub Release use the same version number and are published separately:
|
|
207
|
+
|
|
208
|
+
- npm: <https://www.npmjs.com/package/dsh-model-billing>
|
|
209
|
+
- GitHub Releases: <https://github.com/likaileave/dsh-model-billing/releases>
|
|
210
|
+
|
|
211
|
+
They are parallel distribution channels, not an automatic mirror.
|
|
212
|
+
|
|
213
|
+
### Data
|
|
214
|
+
|
|
215
|
+
Order and prices live in `~/.dsh/model-billing.json` (or `$DSH_HOME/model-billing.json`). Writes use a temporary file plus atomic replace.
|
|
216
|
+
|
|
217
|
+
### Limitations
|
|
218
|
+
|
|
219
|
+
- The custom picker depends on `ctx.modelDirectories`; when that service is unavailable the plugin leaves the built-in picker alone.
|
|
220
|
+
- The `/model` command is still implemented by DSH and may not follow the custom order.
|
|
221
|
+
- Multiple attempts within a turn are counted separately; failed calls without provider usage cannot be counted.
|
|
222
|
+
- Only sessions loaded by the current Host are counted.
|
|
223
|
+
- Third-party prices are static and are not refreshed automatically.
|
|
224
|
+
|
|
225
|
+
### License
|
|
226
|
+
|
|
227
|
+
[MIT](LICENSE) © 2026 likaileave
|
package/client.js
CHANGED
|
@@ -78,8 +78,30 @@ window.__ModuleLoader__.load({
|
|
|
78
78
|
partialShort: '部分未定价',
|
|
79
79
|
partialNote: '有非 DeepSeek 调用尚未配置完整价格,合计只包含已定价部分。',
|
|
80
80
|
estimateNote: '金额由会话日志中的 token 用量和缓存命中估算;DeepSeek 按事件时间使用内置峰谷价,实际账单以提供方为准。',
|
|
81
|
+
listSep: '、',
|
|
81
82
|
turnCost: '费用',
|
|
82
|
-
|
|
83
|
+
turnTokens: '本轮 Token',
|
|
84
|
+
displaySection: '轮尾显示',
|
|
85
|
+
displayHint: '控制每轮结束后在对话尾部显示的内容;修改后点“保存”生效。',
|
|
86
|
+
displayModels: '使用模型',
|
|
87
|
+
displayProvider: '模型提供商',
|
|
88
|
+
displayCost: '本轮费用',
|
|
89
|
+
displayTokens: 'Token 用量',
|
|
90
|
+
displayCacheHit: '缓存命中率',
|
|
91
|
+
displayLabels: '显示文字标签',
|
|
92
|
+
displayFontSize: '字号',
|
|
93
|
+
displayDensity: '间距',
|
|
94
|
+
displayWeight: '字重',
|
|
95
|
+
fontSizeSmall: '小',
|
|
96
|
+
fontSizeNormal: '标准',
|
|
97
|
+
fontSizeLarge: '大',
|
|
98
|
+
densityCompact: '紧凑',
|
|
99
|
+
densityComfortable: '舒适',
|
|
100
|
+
weightNormal: '常规',
|
|
101
|
+
weightBold: '加粗',
|
|
102
|
+
pickerSection: '模型选择器',
|
|
103
|
+
takeoverPicker: '接管模型选择器',
|
|
104
|
+
takeoverHint: '开启后使用本插件按自定义顺序排列的模型选择器;关闭后恢复 DSH 内置选择器。',
|
|
83
105
|
chooseModel: '选择模型',
|
|
84
106
|
loadingModels: '加载模型…',
|
|
85
107
|
searchModel: '搜索模型',
|
|
@@ -150,8 +172,30 @@ window.__ModuleLoader__.load({
|
|
|
150
172
|
partialShort: 'partly unpriced',
|
|
151
173
|
partialNote: 'Some non-DeepSeek calls do not have a complete price set; the total includes only priced calls.',
|
|
152
174
|
estimateNote: 'Amounts are estimated from token usage and cache hits in the session log; DeepSeek uses built-in peak/off-peak prices at the event time, and the provider bill is authoritative.',
|
|
175
|
+
listSep: ', ',
|
|
153
176
|
turnCost: 'Cost',
|
|
154
|
-
|
|
177
|
+
turnTokens: 'Turn tokens',
|
|
178
|
+
displaySection: 'Turn-tail display',
|
|
179
|
+
displayHint: 'Choose what appears below each completed turn; click Save to apply.',
|
|
180
|
+
displayModels: 'Models used',
|
|
181
|
+
displayProvider: 'Model provider',
|
|
182
|
+
displayCost: 'Turn cost',
|
|
183
|
+
displayTokens: 'Token usage',
|
|
184
|
+
displayCacheHit: 'Cache hit rate',
|
|
185
|
+
displayLabels: 'Show text labels',
|
|
186
|
+
displayFontSize: 'Font size',
|
|
187
|
+
displayDensity: 'Spacing',
|
|
188
|
+
displayWeight: 'Font weight',
|
|
189
|
+
fontSizeSmall: 'Small',
|
|
190
|
+
fontSizeNormal: 'Normal',
|
|
191
|
+
fontSizeLarge: 'Large',
|
|
192
|
+
densityCompact: 'Compact',
|
|
193
|
+
densityComfortable: 'Comfortable',
|
|
194
|
+
weightNormal: 'Regular',
|
|
195
|
+
weightBold: 'Bold',
|
|
196
|
+
pickerSection: 'Model picker',
|
|
197
|
+
takeoverPicker: 'Take over the model picker',
|
|
198
|
+
takeoverHint: 'On: use this plugin’s picker with your custom model order. Off: restore the built-in DSH picker.',
|
|
155
199
|
chooseModel: 'Select model',
|
|
156
200
|
loadingModels: 'Loading models…',
|
|
157
201
|
searchModel: 'Search models',
|
|
@@ -213,6 +257,57 @@ window.__ModuleLoader__.load({
|
|
|
213
257
|
return t
|
|
214
258
|
}
|
|
215
259
|
|
|
260
|
+
const DISPLAY_DEFAULTS = Object.freeze({
|
|
261
|
+
showModels: true,
|
|
262
|
+
showProvider: false,
|
|
263
|
+
showCost: true,
|
|
264
|
+
showTokens: false,
|
|
265
|
+
showCacheHit: false,
|
|
266
|
+
showLabels: true,
|
|
267
|
+
takeoverPicker: true,
|
|
268
|
+
fontSize: 'normal',
|
|
269
|
+
density: 'comfortable',
|
|
270
|
+
fontWeight: 'normal',
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const DISPLAY_STORAGE_KEY = 'dsh-model-billing.display'
|
|
274
|
+
|
|
275
|
+
function readStoredDisplay() {
|
|
276
|
+
try {
|
|
277
|
+
const raw = localStorage.getItem(DISPLAY_STORAGE_KEY)
|
|
278
|
+
if (raw === null) return null
|
|
279
|
+
const parsed = JSON.parse(raw)
|
|
280
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null
|
|
281
|
+
} catch {
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function writeStoredDisplay(value) {
|
|
287
|
+
try {
|
|
288
|
+
localStorage.setItem(DISPLAY_STORAGE_KEY, JSON.stringify(value))
|
|
289
|
+
} catch {
|
|
290
|
+
// Storage may be unavailable in restricted browser contexts.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function clearStoredDisplay() {
|
|
295
|
+
try {
|
|
296
|
+
localStorage.removeItem(DISPLAY_STORAGE_KEY)
|
|
297
|
+
} catch {
|
|
298
|
+
// Ignore unavailable storage.
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function readDisplay(data) {
|
|
303
|
+
const source = data?.display
|
|
304
|
+
const server = source !== null && typeof source === 'object' && !Array.isArray(source)
|
|
305
|
+
? { ...DISPLAY_DEFAULTS, ...source }
|
|
306
|
+
: { ...DISPLAY_DEFAULTS }
|
|
307
|
+
const stored = readStoredDisplay()
|
|
308
|
+
return stored === null ? server : { ...server, ...stored }
|
|
309
|
+
}
|
|
310
|
+
|
|
216
311
|
// ---------------------------------------------------------------------
|
|
217
312
|
// Shared catalog state
|
|
218
313
|
// ---------------------------------------------------------------------
|
|
@@ -497,17 +592,37 @@ window.__ModuleLoader__.load({
|
|
|
497
592
|
.mb-turn-table { table-layout: fixed; }
|
|
498
593
|
.mb-turn-table th, .mb-turn-table td, .mb-turn-table th.num, .mb-turn-table td.num { text-align: left; }
|
|
499
594
|
.mb-turn-table th:nth-child(1), .mb-turn-table td:nth-child(1) { width: 5%; }
|
|
500
|
-
.mb-turn-table th:nth-child(2), .mb-turn-table td:nth-child(2) { width:
|
|
595
|
+
.mb-turn-table th:nth-child(2), .mb-turn-table td:nth-child(2) { width: 25.5%; }
|
|
501
596
|
.mb-turn-table th:nth-child(3), .mb-turn-table td:nth-child(3) { width: 12.5%; }
|
|
502
|
-
.mb-turn-table th:nth-child(4), .mb-turn-table td:nth-child(4) { width:
|
|
597
|
+
.mb-turn-table th:nth-child(4), .mb-turn-table td:nth-child(4) { width: 21%; }
|
|
503
598
|
.mb-turn-table th:nth-child(5), .mb-turn-table td:nth-child(5) { width: 10%; }
|
|
504
599
|
.mb-turn-table th:nth-child(6), .mb-turn-table td:nth-child(6) { width: 16%; }
|
|
505
600
|
.mb-turn-table th:nth-child(7), .mb-turn-table td:nth-child(7) { width: 10%; }
|
|
506
601
|
.mb-turn-preview, .mb-turn-model { color: var(--dsw-alias-label-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
507
602
|
.mb-turn-model { color: var(--dsw-alias-label-primary); }
|
|
508
603
|
.mb-panel-note { margin: 9px 0 0; color: var(--dsw-alias-label-secondary); font-size: 11px; }
|
|
509
|
-
.mb-turn { color: var(--dsw-alias-label-secondary); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
510
|
-
.mb-turn
|
|
604
|
+
.mb-turn { color: var(--dsw-alias-label-secondary); font-size: 12px; font-variant-numeric: tabular-nums; display: inline-flex; flex-wrap: wrap; align-items: baseline; column-gap: 10px; row-gap: 2px; max-width: 100%; }
|
|
605
|
+
.mb-turn[data-size="small"] { font-size: 11px; }
|
|
606
|
+
.mb-turn[data-size="large"] { font-size: 13px; }
|
|
607
|
+
.mb-turn[data-density="compact"] { column-gap: 6px; }
|
|
608
|
+
.mb-turn-item { white-space: nowrap; }
|
|
609
|
+
.mb-turn strong { color: inherit; font-weight: 600; }
|
|
610
|
+
.mb-turn[data-weight="normal"] { font-weight: 400; }
|
|
611
|
+
.mb-turn[data-weight="normal"] strong { font-weight: 400; }
|
|
612
|
+
.mb-turn[data-weight="bold"] { font-weight: 600; }
|
|
613
|
+
.mb-turn[data-weight="bold"] strong { font-weight: 600; }
|
|
614
|
+
.mb-turn-partial { color: var(--dsw-alias-state-warn-primary); }
|
|
615
|
+
.mb-display { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 12px 14px; margin: 10px 0 14px; background: var(--dsw-alias-bg-layer-2); }
|
|
616
|
+
.mb-display-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
617
|
+
.mb-display-title { font-weight: 600; }
|
|
618
|
+
.mb-display-hint { color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
619
|
+
.mb-check-grid { display: flex; flex-wrap: wrap; gap: 8px 18px; }
|
|
620
|
+
.mb-check { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; cursor: pointer; }
|
|
621
|
+
.mb-check.is-disabled { opacity: .55; cursor: default; }
|
|
622
|
+
.mb-check input { width: 15px; height: 15px; accent-color: var(--dsw-alias-brand-primary); }
|
|
623
|
+
.mb-display-controls { display: flex; flex-wrap: wrap; gap: 10px 18px; margin-top: 12px; }
|
|
624
|
+
.mb-select { height: 30px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); padding: 0 8px; font: inherit; }
|
|
625
|
+
.mb-select:focus { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -1px; }
|
|
511
626
|
.mb-picker { position: relative; display: flex; min-width: 0; }
|
|
512
627
|
.mb-picker-trigger { border-radius: var(--dsw-radius-sm); min-width: 0; max-width: min(360px, 45cqw); height: 28px; color: var(--dsw-alias-label-secondary); cursor: pointer; background: 0 0; border: none; outline: none; align-items: center; gap: 4px; padding: 0 4px 0 8px; font-size: 13px; font-weight: 400; line-height: 20px; display: flex; }
|
|
513
628
|
.mb-picker-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
@@ -598,6 +713,7 @@ window.__ModuleLoader__.load({
|
|
|
598
713
|
const translate = useT()
|
|
599
714
|
const snap = useCatalog()
|
|
600
715
|
const [draft, setDraft] = useState(null)
|
|
716
|
+
const [display, setDisplay] = useState(null)
|
|
601
717
|
const [dirty, setDirty] = useState(false)
|
|
602
718
|
const [saving, setSaving] = useState(false)
|
|
603
719
|
const [status, setStatus] = useState(null)
|
|
@@ -605,6 +721,7 @@ window.__ModuleLoader__.load({
|
|
|
605
721
|
useEffect(() => {
|
|
606
722
|
if (snap.data === null) return
|
|
607
723
|
setDraft(toDraft(snap.data.models))
|
|
724
|
+
setDisplay(readDisplay(snap.data))
|
|
608
725
|
setDirty(false)
|
|
609
726
|
}, [snap.data])
|
|
610
727
|
|
|
@@ -637,11 +754,89 @@ window.__ModuleLoader__.load({
|
|
|
637
754
|
}
|
|
638
755
|
|
|
639
756
|
const reset = () => {
|
|
640
|
-
|
|
757
|
+
clearStoredDisplay()
|
|
758
|
+
emitCatalog()
|
|
759
|
+
if (snap.data !== null) {
|
|
760
|
+
setDraft(toDraft(snap.data.models))
|
|
761
|
+
setDisplay(readDisplay(snap.data))
|
|
762
|
+
}
|
|
641
763
|
setDirty(false)
|
|
642
764
|
setStatus(null)
|
|
643
765
|
}
|
|
644
766
|
|
|
767
|
+
const displayValue = display ?? readDisplay(snap.data)
|
|
768
|
+
|
|
769
|
+
const updateDisplay = (change) => {
|
|
770
|
+
setDisplay((current) => ({ ...(current ?? readDisplay(snap.data)), ...change }))
|
|
771
|
+
setDirty(true)
|
|
772
|
+
setStatus(null)
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const renderDisplaySettings = () => h(Fragment, null,
|
|
776
|
+
h('div', { className: 'mb-display' },
|
|
777
|
+
h('div', { className: 'mb-display-head' },
|
|
778
|
+
h('div', { className: 'mb-display-title' }, translate('displaySection')),
|
|
779
|
+
h('div', { className: 'mb-display-hint' }, translate('displayHint'))),
|
|
780
|
+
h('div', { className: 'mb-check-grid' },
|
|
781
|
+
[
|
|
782
|
+
['showProvider', 'displayProvider'],
|
|
783
|
+
['showModels', 'displayModels'],
|
|
784
|
+
['showCost', 'displayCost'],
|
|
785
|
+
['showTokens', 'displayTokens'],
|
|
786
|
+
['showCacheHit', 'displayCacheHit'],
|
|
787
|
+
['showLabels', 'displayLabels'],
|
|
788
|
+
].map(([field, label]) => h('label', {
|
|
789
|
+
className: `mb-check${field === 'showProvider' && displayValue.showModels !== true ? ' is-disabled' : ''}`,
|
|
790
|
+
key: field,
|
|
791
|
+
},
|
|
792
|
+
h('input', {
|
|
793
|
+
type: 'checkbox',
|
|
794
|
+
checked: displayValue[field] === true,
|
|
795
|
+
disabled: field === 'showProvider' && displayValue.showModels !== true,
|
|
796
|
+
onChange: (event) => updateDisplay({ [field]: event.target.checked }),
|
|
797
|
+
}),
|
|
798
|
+
h('span', null, translate(label))))),
|
|
799
|
+
h('div', { className: 'mb-display-controls' },
|
|
800
|
+
h('label', { className: 'mb-field' },
|
|
801
|
+
h('span', { className: 'mb-field-label' }, translate('displayFontSize')),
|
|
802
|
+
h('select', {
|
|
803
|
+
className: 'mb-select',
|
|
804
|
+
value: displayValue.fontSize,
|
|
805
|
+
onChange: (event) => updateDisplay({ fontSize: event.target.value }),
|
|
806
|
+
},
|
|
807
|
+
h('option', { value: 'small' }, translate('fontSizeSmall')),
|
|
808
|
+
h('option', { value: 'normal' }, translate('fontSizeNormal')),
|
|
809
|
+
h('option', { value: 'large' }, translate('fontSizeLarge')))),
|
|
810
|
+
h('label', { className: 'mb-field' },
|
|
811
|
+
h('span', { className: 'mb-field-label' }, translate('displayDensity')),
|
|
812
|
+
h('select', {
|
|
813
|
+
className: 'mb-select',
|
|
814
|
+
value: displayValue.density,
|
|
815
|
+
onChange: (event) => updateDisplay({ density: event.target.value }),
|
|
816
|
+
},
|
|
817
|
+
h('option', { value: 'compact' }, translate('densityCompact')),
|
|
818
|
+
h('option', { value: 'comfortable' }, translate('densityComfortable')))),
|
|
819
|
+
h('label', { className: 'mb-field' },
|
|
820
|
+
h('span', { className: 'mb-field-label' }, translate('displayWeight')),
|
|
821
|
+
h('select', {
|
|
822
|
+
className: 'mb-select',
|
|
823
|
+
value: displayValue.fontWeight,
|
|
824
|
+
onChange: (event) => updateDisplay({ fontWeight: event.target.value }),
|
|
825
|
+
},
|
|
826
|
+
h('option', { value: 'normal' }, translate('weightNormal')),
|
|
827
|
+
h('option', { value: 'bold' }, translate('weightBold')))))),
|
|
828
|
+
h('div', { className: 'mb-display' },
|
|
829
|
+
h('div', { className: 'mb-display-head' },
|
|
830
|
+
h('div', { className: 'mb-display-title' }, translate('pickerSection')),
|
|
831
|
+
h('div', { className: 'mb-display-hint' }, translate('takeoverHint'))),
|
|
832
|
+
h('label', { className: 'mb-check' },
|
|
833
|
+
h('input', {
|
|
834
|
+
type: 'checkbox',
|
|
835
|
+
checked: displayValue.takeoverPicker === true,
|
|
836
|
+
onChange: (event) => updateDisplay({ takeoverPicker: event.target.checked }),
|
|
837
|
+
}),
|
|
838
|
+
h('span', null, translate('takeoverPicker')))))
|
|
839
|
+
|
|
645
840
|
const save = async () => {
|
|
646
841
|
if (draft === null) return
|
|
647
842
|
const payload = []
|
|
@@ -667,8 +862,9 @@ window.__ModuleLoader__.load({
|
|
|
667
862
|
await requestJson('/model-billing/save', {
|
|
668
863
|
method: 'POST',
|
|
669
864
|
headers: { 'content-type': 'application/json' },
|
|
670
|
-
body: JSON.stringify({ models: payload }),
|
|
865
|
+
body: JSON.stringify({ models: payload, display: displayValue }),
|
|
671
866
|
})
|
|
867
|
+
writeStoredDisplay(displayValue)
|
|
672
868
|
setDirty(false)
|
|
673
869
|
setStatus({ tone: 'success', text: translate('saved') })
|
|
674
870
|
await ensureCatalog(true)
|
|
@@ -706,6 +902,7 @@ window.__ModuleLoader__.load({
|
|
|
706
902
|
h('section', { className: 'mb-page' },
|
|
707
903
|
h('h2', { className: 'mb-title' }, translate('title')),
|
|
708
904
|
h('p', { className: 'mb-intro' }, translate('intro')),
|
|
905
|
+
renderDisplaySettings(),
|
|
709
906
|
h('div', { className: 'mb-empty' }, translate('noModels'))))
|
|
710
907
|
}
|
|
711
908
|
|
|
@@ -721,6 +918,7 @@ window.__ModuleLoader__.load({
|
|
|
721
918
|
h('button', { type: 'button', className: 'mb-btn', disabled: !dirty || saving, onClick: reset }, translate('undo')),
|
|
722
919
|
h('button', { type: 'button', className: 'mb-btn mb-btn-primary', disabled: !dirty || saving, onClick: () => void save() }, saving ? translate('saving') : translate('save'))),
|
|
723
920
|
status === null ? null : h('div', { className: 'mb-status', 'data-tone': status.tone, role: 'status' }, status.text),
|
|
921
|
+
renderDisplaySettings(),
|
|
724
922
|
h('p', { className: 'mb-note' }, translate('orderHint')),
|
|
725
923
|
h('ul', { className: 'mb-list' },
|
|
726
924
|
models.map((entry, index) => h(SettingsModelCard, {
|
|
@@ -1030,23 +1228,83 @@ window.__ModuleLoader__.load({
|
|
|
1030
1228
|
// Per-turn cost chip
|
|
1031
1229
|
// ---------------------------------------------------------------------
|
|
1032
1230
|
|
|
1033
|
-
function
|
|
1231
|
+
function turnModelText(models, translate, showProvider, catalogModels) {
|
|
1232
|
+
const findModel = (entry) => {
|
|
1233
|
+
if (entry.provider === undefined || entry.model === undefined) return undefined
|
|
1234
|
+
return (catalogModels ?? []).find((candidate) => candidate.provider === entry.provider && candidate.model === entry.model)
|
|
1235
|
+
}
|
|
1236
|
+
const names = (models ?? []).map((entry) => {
|
|
1237
|
+
if (entry === undefined || entry === null) return null
|
|
1238
|
+
const matched = findModel(entry)
|
|
1239
|
+
const modelName = typeof matched?.name === 'string' && matched.name !== ''
|
|
1240
|
+
? matched.name
|
|
1241
|
+
: typeof entry.name === 'string' && entry.name !== ''
|
|
1242
|
+
? entry.name
|
|
1243
|
+
: typeof entry.model === 'string' && entry.model !== '' ? entry.model : null
|
|
1244
|
+
if (modelName === null) return null
|
|
1245
|
+
if (!showProvider) return modelName
|
|
1246
|
+
const providerName = typeof matched?.providerName === 'string' && matched.providerName !== ''
|
|
1247
|
+
? matched.providerName
|
|
1248
|
+
: typeof entry.providerName === 'string' && entry.providerName !== ''
|
|
1249
|
+
? entry.providerName
|
|
1250
|
+
: typeof entry.provider === 'string' && entry.provider !== '' ? entry.provider : null
|
|
1251
|
+
return providerName === null ? modelName : `${providerName} / ${modelName}`
|
|
1252
|
+
}).filter((name) => name !== null)
|
|
1253
|
+
if (names.length === 0) return null
|
|
1254
|
+
return names.join(translate('listSep'))
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function TurnCost(props) {
|
|
1258
|
+
const { turn } = props
|
|
1259
|
+
// The turnTail owner hands over the whole folded turn life cycle, not just its number.
|
|
1260
|
+
const turnNumber = turn === null || turn === undefined
|
|
1261
|
+
? undefined
|
|
1262
|
+
: typeof turn === 'number' ? turn : turn.turn
|
|
1263
|
+
const sessionId = props.sessionId
|
|
1264
|
+
const useProjection = props.useProjection
|
|
1034
1265
|
const translate = useT()
|
|
1035
|
-
const
|
|
1266
|
+
const catalogSnap = useCatalog()
|
|
1267
|
+
const display = readDisplay(catalogSnap.data)
|
|
1268
|
+
const tokenUsage = typeof useProjection === 'function' ? useProjection('tokenUsage') : undefined
|
|
1036
1269
|
const revision = tokenUsageRevision(tokenUsage)
|
|
1037
1270
|
const usage = useSessionUsage(sessionId, revision)
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1271
|
+
const rows = usage.data?.turns
|
|
1272
|
+
const row = rows?.find((entry) => entry.turn === turnNumber)
|
|
1273
|
+
if (row === undefined) return null
|
|
1040
1274
|
const costText = row.costKnown
|
|
1041
1275
|
? `≈ ${formatMoney(row.cost)}`
|
|
1042
1276
|
: row.pricedAttempts > 0 ? `≈ ${formatMoney(row.cost)}*` : translate('pricePlaceholder')
|
|
1277
|
+
const modelText = turnModelText(row.models, translate, display.showProvider === true, catalogSnap.data?.models)
|
|
1278
|
+
const label = (key) => display.showLabels ? `${translate(key)} ` : ''
|
|
1279
|
+
const parts = []
|
|
1280
|
+
if (display.showModels && modelText !== null) {
|
|
1281
|
+
parts.push(h('span', { className: 'mb-turn-item', key: 'model' },
|
|
1282
|
+
label('model'), h('strong', null, modelText)))
|
|
1283
|
+
}
|
|
1284
|
+
if (display.showCost) {
|
|
1285
|
+
parts.push(h('span', { className: 'mb-turn-item', key: 'cost' },
|
|
1286
|
+
label('turnCost'), h('strong', null, costText)))
|
|
1287
|
+
}
|
|
1288
|
+
const promptTokens = Number.isFinite(row.promptTokens) ? row.promptTokens : 0
|
|
1289
|
+
const outputTokens = Number.isFinite(row.outputTokens) ? row.outputTokens : 0
|
|
1290
|
+
if (display.showTokens && (promptTokens > 0 || outputTokens > 0)) {
|
|
1291
|
+
parts.push(h('span', { className: 'mb-turn-item', key: 'tokens' },
|
|
1292
|
+
label('turnTokens'), h('strong', null, `${formatTokens(promptTokens)} / ${formatTokens(outputTokens)}`)))
|
|
1293
|
+
}
|
|
1294
|
+
if (display.showCacheHit && row.cacheHit !== null && row.cacheHit !== undefined) {
|
|
1295
|
+
parts.push(h('span', { className: 'mb-turn-item', key: 'cache' },
|
|
1296
|
+
label('cacheHit'), h('strong', null, formatPercent(row.cacheHit))))
|
|
1297
|
+
}
|
|
1298
|
+
if (row.partial) {
|
|
1299
|
+
parts.push(h('span', { className: 'mb-turn-item mb-turn-partial', key: 'partial' }, translate('partialShort')))
|
|
1300
|
+
}
|
|
1301
|
+
if (parts.length === 0) return null
|
|
1043
1302
|
return h('span', {
|
|
1044
1303
|
className: 'mb-turn',
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
row.partial ? ` · ${translate('partialShort')}` : '')
|
|
1304
|
+
'data-size': display.fontSize,
|
|
1305
|
+
'data-density': display.density,
|
|
1306
|
+
'data-weight': display.fontWeight,
|
|
1307
|
+
}, parts)
|
|
1050
1308
|
}
|
|
1051
1309
|
|
|
1052
1310
|
// ---------------------------------------------------------------------
|
|
@@ -1459,6 +1717,13 @@ window.__ModuleLoader__.load({
|
|
|
1459
1717
|
open ? ReactDOM.createPortal(menu, document.body) : null)
|
|
1460
1718
|
}
|
|
1461
1719
|
|
|
1720
|
+
function ModelPickerSeat(props) {
|
|
1721
|
+
const catalogSnap = useCatalog()
|
|
1722
|
+
const display = readDisplay(catalogSnap.data)
|
|
1723
|
+
if (!display.takeoverPicker) return null
|
|
1724
|
+
return h(OrderedModelPicker, props)
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1462
1727
|
return {
|
|
1463
1728
|
inject: ['slots', 'locale'],
|
|
1464
1729
|
apply(ctx) {
|
|
@@ -1466,10 +1731,34 @@ window.__ModuleLoader__.load({
|
|
|
1466
1731
|
publishModelDirectories(ctx.get('modelDirectories'))
|
|
1467
1732
|
ctx.inject(['modelDirectories'], (scope) => {
|
|
1468
1733
|
publishModelDirectories(scope.modelDirectories)
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1734
|
+
let seat = null
|
|
1735
|
+
const takeoverEnabled = () => readDisplay(catalog.snap.data).takeoverPicker !== false
|
|
1736
|
+
const syncSeat = () => {
|
|
1737
|
+
if (takeoverEnabled()) {
|
|
1738
|
+
if (seat === null) {
|
|
1739
|
+
seat = scope.slots.register({
|
|
1740
|
+
name: 'conversation.input.model',
|
|
1741
|
+
priority: -1,
|
|
1742
|
+
}, ModelPickerSeat)
|
|
1743
|
+
}
|
|
1744
|
+
return
|
|
1745
|
+
}
|
|
1746
|
+
if (seat !== null) {
|
|
1747
|
+
seat()
|
|
1748
|
+
seat = null
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
scope.slots.inject('conversation.input.model', () => {
|
|
1752
|
+
const unsubscribe = subscribeCatalog(syncSeat)
|
|
1753
|
+
syncSeat()
|
|
1754
|
+
return () => {
|
|
1755
|
+
unsubscribe()
|
|
1756
|
+
if (seat !== null) {
|
|
1757
|
+
seat()
|
|
1758
|
+
seat = null
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
})
|
|
1473
1762
|
})
|
|
1474
1763
|
|
|
1475
1764
|
void ensureCatalog()
|
package/host.js
CHANGED
|
@@ -25,6 +25,22 @@ const MAX_STORED_PRICES = 2000
|
|
|
25
25
|
const MAX_BODY_BYTES = 256 * 1024
|
|
26
26
|
const PRICE_FIELDS = ['cacheHit', 'cacheMiss', 'output']
|
|
27
27
|
const KEY_SEPARATOR = '\u0000'
|
|
28
|
+
const DISPLAY_DEFAULTS = Object.freeze({
|
|
29
|
+
showModels: true,
|
|
30
|
+
showProvider: false,
|
|
31
|
+
showCost: true,
|
|
32
|
+
showTokens: false,
|
|
33
|
+
showCacheHit: false,
|
|
34
|
+
showLabels: true,
|
|
35
|
+
takeoverPicker: true,
|
|
36
|
+
fontSize: 'normal',
|
|
37
|
+
density: 'comfortable',
|
|
38
|
+
fontWeight: 'normal',
|
|
39
|
+
})
|
|
40
|
+
const DISPLAY_FLAGS = ['showModels', 'showProvider', 'showCost', 'showTokens', 'showCacheHit', 'showLabels', 'takeoverPicker']
|
|
41
|
+
const DISPLAY_FONT_SIZES = new Set(['small', 'normal', 'large'])
|
|
42
|
+
const DISPLAY_DENSITIES = new Set(['compact', 'comfortable'])
|
|
43
|
+
const DISPLAY_FONT_WEIGHTS = new Set(['normal', 'bold'])
|
|
28
44
|
const DEEPSEEK_PRICING_SCHEDULE = '北京时间周一至周五 9:00–12:00、14:00–18:00(法定节假日除外)'
|
|
29
45
|
const DEEPSEEK_PRICES = {
|
|
30
46
|
'deepseek-flash': {
|
|
@@ -174,6 +190,7 @@ function stateEnvelope(state) {
|
|
|
174
190
|
stateVersion: STATE_VERSION,
|
|
175
191
|
savedAt: state.savedAt,
|
|
176
192
|
order: state.order.map((entry) => ({ provider: entry.provider, model: entry.model })),
|
|
193
|
+
display: { ...state.display },
|
|
177
194
|
}
|
|
178
195
|
}
|
|
179
196
|
|
|
@@ -614,12 +631,26 @@ async function loadState(ctx) {
|
|
|
614
631
|
}
|
|
615
632
|
}
|
|
616
633
|
|
|
634
|
+
/** Validate persisted turn-tail display settings, keeping unknown keys out. */
|
|
635
|
+
function normalizeDisplay(raw) {
|
|
636
|
+
const source = raw !== null && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}
|
|
637
|
+
const display = { ...DISPLAY_DEFAULTS }
|
|
638
|
+
for (const key of DISPLAY_FLAGS) {
|
|
639
|
+
if (typeof source[key] === 'boolean') display[key] = source[key]
|
|
640
|
+
}
|
|
641
|
+
if (DISPLAY_FONT_SIZES.has(source.fontSize)) display.fontSize = source.fontSize
|
|
642
|
+
if (DISPLAY_DENSITIES.has(source.density)) display.density = source.density
|
|
643
|
+
if (DISPLAY_FONT_WEIGHTS.has(source.fontWeight)) display.fontWeight = source.fontWeight
|
|
644
|
+
return display
|
|
645
|
+
}
|
|
646
|
+
|
|
617
647
|
/** Validate a persisted state document into the current shape. */
|
|
618
648
|
function normalizeState(raw) {
|
|
619
649
|
const state = {
|
|
620
650
|
version: STATE_VERSION,
|
|
621
651
|
order: [],
|
|
622
652
|
pricing: {},
|
|
653
|
+
display: normalizeDisplay(raw?.display),
|
|
623
654
|
savedAt: typeof raw?.savedAt === 'number' && Number.isFinite(raw.savedAt) ? raw.savedAt : 0,
|
|
624
655
|
}
|
|
625
656
|
const seen = new Set()
|
|
@@ -674,6 +705,10 @@ async function saveOrderAndPrices(ctx, body) {
|
|
|
674
705
|
throw new BillingRefusal(400, 'bad-request', `models must be an array of at most ${MAX_MODELS} entries`)
|
|
675
706
|
}
|
|
676
707
|
const state = await loadState(ctx)
|
|
708
|
+
if (body?.display !== undefined && (body.display === null || typeof body.display !== 'object' || Array.isArray(body.display))) {
|
|
709
|
+
throw new BillingRefusal(400, 'bad-request', 'display must be an object')
|
|
710
|
+
}
|
|
711
|
+
const display = body?.display === undefined ? state.display : normalizeDisplay(body.display)
|
|
677
712
|
const order = []
|
|
678
713
|
const seen = new Set()
|
|
679
714
|
const pricing = { ...state.pricing }
|
|
@@ -693,6 +728,7 @@ async function saveOrderAndPrices(ctx, body) {
|
|
|
693
728
|
version: STATE_VERSION,
|
|
694
729
|
order,
|
|
695
730
|
pricing,
|
|
731
|
+
display,
|
|
696
732
|
savedAt: Date.now(),
|
|
697
733
|
}
|
|
698
734
|
await enqueueWrite(ctx, next)
|
|
@@ -888,6 +924,7 @@ export const __test = {
|
|
|
888
924
|
isDeepSeekRoute,
|
|
889
925
|
loadState,
|
|
890
926
|
modelKey,
|
|
927
|
+
normalizeDisplay,
|
|
891
928
|
normalizeState,
|
|
892
929
|
normalizeUsage,
|
|
893
930
|
priceSetComplete,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model-billing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Reorder models in the DSH composer picker, store three per-million-token prices, and estimate per-turn and session cost with built-in DeepSeek peak/off-peak rates.",
|
|
6
6
|
"keywords": [
|
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
"bugs": {
|
|
23
23
|
"url": "https://github.com/likaileave/dsh-model-billing/issues"
|
|
24
24
|
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@deepseek-ai/dsh": "^0.2.0-rc.2"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"@deepseek-ai/dsh": {
|
|
30
|
+
"optional": true
|
|
31
|
+
}
|
|
32
|
+
},
|
|
25
33
|
"icon": "./icon.svg",
|
|
26
34
|
"meta": {
|
|
27
35
|
"title": "模型与费用 / Models & Cost",
|