dsh-tokenlens 1.5.3 → 1.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 +10 -0
- package/PROTOCOL.md +47 -8
- package/README.md +3 -2
- package/client.js +41 -19
- package/index.js +60 -7
- package/lib/design-plan-cost.mjs +168 -0
- package/lib/design-plan-ledger.mjs +32 -10
- package/lib/usage-stats.mjs +10 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
**版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
|
|
6
6
|
|
|
7
|
+
## 1.6.0 — 2026-11-03
|
|
8
|
+
|
|
9
|
+
- **feat(open-design): 「本期 Design Plan 累计」改按官方定价表的模型单价计算**——原来是 `usedCredits ÷ creditsPerUsd`,这个算法隐含「按 1× 倍率消耗」的假设,而官方倍率表跨度 0.25×–4×:对 DeepSeek V4.1 Flash(0.5×)会**少算一半**,对 GPT-6 Luna(4×)会**多算 4 倍**。现在按每个模型的实际 Token × 官方单价(输入/输出/缓存读分别计价)累加,结果可直接与定价表比对
|
|
10
|
+
- **feat(open-design): 给出与倍率匹配的月额度**——定价表里 $120 是**列表价**口径且按倍率消耗:只用 0.5× 模型(DeepSeek V4.1 Flash)可动用**完整 $120**,1× 用 $60,2× 用 $30,4× 用 $15。弹窗改为显示「**已用 / 月额度 + 百分比**」(如 `$27.31 / $120.00 22.8%`),并注明「按 0.5× 倍率模型计价」
|
|
11
|
+
- feat(open-design): 混合模型时按**最高**倍率取额度(不是平均)——4× 模型的每一单位消耗其份额的速度是 4 倍,约束由它决定
|
|
12
|
+
- feat(wire): `designPlan` 新增 `monthlyCapUsd` / `capPercent` / `multiplier` / `creditsUsd`;`estimatedUsd` 语义改为「官方单价口径的消费额」,旧算法结果保留在 `creditsUsd` 便于对照
|
|
13
|
+
- feat(stats): `models[].last30Days` 增加 `input` / `output` / `cacheRead` / `cacheWrite` 拆分,供按单价计价使用(此前只有 total)
|
|
14
|
+
- fix(open-design): 单价表按官方文档转录 13 个 Design Plan 模型;`hunyuan-h4-preview` / `glm-5.3-flash-x` 不在公开价目接口里,价格缺失时按「未定价」处理并如实上报,绝不臆造
|
|
15
|
+
- 测试:318 → 328(倍率阶梯 0.5×→$120 / 1×→$60 / 2×→$30 / 4×→$15、混合取最高倍率、未知模型不臆造倍率与额度、按 token 种类计价、**用控制台真实账单核对:$0.0048 列表价 × 0.5 = $0.0024 实扣**、未定价模型上报)
|
|
16
|
+
|
|
7
17
|
## 1.5.3 — 2026-11-03
|
|
8
18
|
|
|
9
19
|
- **feat(open-design): 初次配置只需一个 Control Key**——工作区 ID 改为**可选**。Control Key 本身就能确定它属于哪个工作区,插件改用 `GET /api/v1/workspaces/current` 自动识别(回退 `GET /api/v1/workspaces`,仅当可见工作区恰好一个时采纳)
|
package/PROTOCOL.md
CHANGED
|
@@ -161,24 +161,63 @@ by observing the weekly windows it sees:
|
|
|
161
161
|
"designPlan": {
|
|
162
162
|
"periodStart": "2026-11-02T09:53:18.000Z", // billing period (billing/summary)
|
|
163
163
|
"periodEnd": "2026-12-02T09:53:18.000Z",
|
|
164
|
-
"usedCredits":
|
|
165
|
-
"estimatedUsd":
|
|
164
|
+
"usedCredits": 142090, // Σ final+current weekly windows
|
|
165
|
+
"estimatedUsd": 27.31, // v1.6.0: documented list-price cost
|
|
166
|
+
"creditsUsd": 14.209, // the OLD (1x-assuming) conversion
|
|
167
|
+
"monthlyCapUsd": 120, // documented cap for this model mix
|
|
168
|
+
"multiplier": 0.5, // worst documented multiplier seen
|
|
169
|
+
"capPercent": 22.76, // estimatedUsd / monthlyCapUsd
|
|
166
170
|
"windows": 1, // weekly windows observed
|
|
167
171
|
"complete": true, // earliest window starts at periodStart
|
|
168
172
|
"since": "2026-11-02T09:53:18.000Z", // first window actually observed
|
|
169
173
|
"limitPerWindow": 150000, // the account's real weekly cap
|
|
170
174
|
"periodLimitCredits": 600000, // limitPerWindow * 4
|
|
171
|
-
"percent":
|
|
175
|
+
"percent": 23.68 // usedCredits / periodLimitCredits
|
|
172
176
|
}
|
|
173
177
|
```
|
|
174
178
|
|
|
179
|
+
### Why `estimatedUsd` is not `usedCredits / creditsPerUsd` (v1.6.0)
|
|
180
|
+
|
|
181
|
+
Open Design bills **credits**, but the pricing page states every cap in
|
|
182
|
+
**list-price dollars** and assigns each model a credit-burn **multiplier**:
|
|
183
|
+
|
|
184
|
+
| model | multiplier | monthly cap it unlocks |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| DeepSeek V4.1 Flash | 0.5× | $120 (the whole Go cap) |
|
|
187
|
+
| MiMo V2.6 Flash / DeepSeek V4 Pro / GLM-5.3 Flash … | 1× | $60 |
|
|
188
|
+
| Claude Haiku 5.5 / GLM-5.3 Flash-X | 2× | $30 |
|
|
189
|
+
| GPT-6 Luna | 4× | $15 |
|
|
190
|
+
|
|
191
|
+
So `usedCredits / creditsPerUsd` silently assumes a 1× burn: it reports **half** the
|
|
192
|
+
true list-price value for a 0.5× model and **4× too much** for a 4× model. The
|
|
193
|
+
account's own windows confirm the unit (`50,000` credits per 5h = the documented
|
|
194
|
+
$10, `150,000` per 7d = $30, i.e. 5,000 credits per list-price dollar), which is
|
|
195
|
+
why `creditsPerUsd` is 10,000 — two credits per list-price dollar at 0.5×.
|
|
196
|
+
|
|
197
|
+
`estimatedUsd` is therefore computed from the tokens actually consumed:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
cost = Σ per model ( input×p_in + output×p_out + cacheRead×p_read ) / 1e6
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
with the prices transcribed from the same page. Verified against one real console
|
|
204
|
+
receipt: input 311 + output 2,610 + cacheRead 258,560 on DeepSeek V4.1 Flash gives
|
|
205
|
+
`$0.0048` list price, ×0.5 multiplier = `$0.0024` charged — exactly what the usage
|
|
206
|
+
table shows. Note the table's "input" column is non-cached + cache, so `input` in
|
|
207
|
+
this formula **excludes** `cacheRead` or the cache bytes would be billed twice.
|
|
208
|
+
|
|
209
|
+
The cap uses the **highest** multiplier in the mix, not an average: a 4× model
|
|
210
|
+
consumes its share four times faster, so it binds. `creditsUsd` keeps the old
|
|
211
|
+
number on the wire for comparison; a client showing the old figure can migrate
|
|
212
|
+
without a wire break.
|
|
213
|
+
|
|
175
214
|
`windows.monthly` is then filled from that estimate (v1.5.1), with `resetsAt` taken
|
|
176
215
|
from `periodEnd`, so the existing monthly row and the chip's `月 N%` piece carry it
|
|
177
|
-
without a second UI. The ×4 monthly-to-weekly ratio is the
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
216
|
+
without a second UI. The ×4 monthly-to-weekly ratio is documented too: the pricing
|
|
217
|
+
table states the monthly cap as exactly four weekly windows for every tier (Go
|
|
218
|
+
$120/$30, Plus $160/$40, Pro $360/$90, Max $600/$150). A partial estimate still
|
|
219
|
+
reports a percentage but sets the window `status` to `unknown`, so it can never
|
|
220
|
+
paint a warning/critical dot.
|
|
182
221
|
|
|
183
222
|
Rules the Host guarantees:
|
|
184
223
|
|
package/README.md
CHANGED
|
@@ -327,7 +327,8 @@ Control Key 是 **Open Design 桌面端/CLI 登录时写入本机的会话凭据
|
|
|
327
327
|
- OpenCode 的额度定义见[官方文档](https://opencode.ai/docs/go/#usage-limits)(5h = 月度额度的 20%、周 = 50%、月 = 100%);接口只返回百分比,插件也只画百分比。
|
|
328
328
|
- Command Code 的 `/alpha/*` 是官方 CLI 自用的私有计费路由;免费 / 无方案账户返回空窗口(显示 `—`),插件不会用方案目录猜额度。
|
|
329
329
|
- Open Design 的窗口按账户实际返回的时长渲染:Go 同时有 5 小时和 7 天窗口,Plus / Pro / Max 只有 7 天窗口;钱包余额是独立于窗口的 USD 额度,用尽后继续扣钱包。查询要求工作区权限,鉴权失败会明确显示错误而不是 0。**只需填一个 Control Key**:工作区 ID 是可选项(v1.5.3),插件会用 Key 调 `/api/v1/workspaces/current` 自动识别你的工作区;只有账号有多个工作区、需要指定其中一个时才在管理页填「工作区 ID」。真正缺 Key 时弹窗会指出并给出「去配置」入口。窗口、余额、套餐档位是**三层独立读取**(v1.4.0):任一接口不可用时只少那一块数据,不会整套报错。
|
|
330
|
-
- **chip 的「月 N%」与「本期 Design Plan 累计」是插件本机算的估算值**(v1.5.0 / v1.
|
|
330
|
+
- **chip 的「月 N%」与「本期 Design Plan 累计」是插件本机算的估算值**(v1.5.0 / v1.6.0)。Open Design 有月度额度(Go 为每月 $120 = 4 × 每周 $30),但**没有「本账期已用」接口**——控制台的 `/billing/usage`、`/billing/coding-plan-usage` 都靠浏览器 Cookie 鉴权,Control Key 一律 401。于是插件累加它观测到的 **7 天窗口**(只记 7 天,因为 5 小时窗口测量的是同一笔消耗,相加会重复计数)。
|
|
331
|
+
- **金额按官方定价表的模型单价计算**(v1.6.0)。原来用 `usedCredits ÷ creditsPerUsd`,隐含「按 1× 倍率消耗」,而官方倍率跨度 0.25×–4×,对 DeepSeek V4.1 Flash(0.5×)会少算一半、对 GPT-6 Luna(4×)会多算 4 倍。现在按每个模型的实际 Token × 官方单价(输入/输出/缓存读分别计价)累加,弹窗显示「已用 / 月额度 + 百分比」(如 `$27.31 / $120.00 22.8%`)并注明「按 0.5× 倍率模型计价」——**只用 0.5× 模型可用满完整 $120**,1× 只能到 $60,2× 到 $30,4× 到 $15。弹窗会标注覆盖情况(「已覆盖本账期全部窗口」或「仅统计已观测的 N 个窗口」);覆盖不完整时百分比仍显示但**不会触发告警配色**,插件没运行期间错过的窗口无法计入。
|
|
331
332
|
- 未配置凭据的供应商 chip 静默隐藏;`zai`、`minimax`、`deepinfra` 属 best-effort 适配(未公开接口,失败时按可重试错误优雅降级)。
|
|
332
333
|
- 各供应商的元数据(`kind` / `label` / `shortLabel` / `baseURL` / `apiKeyRef` / `consoleUrl`)集中在 `lib/providers.mjs` 的 `PROVIDER_DEFAULTS` 与 `ADAPTERS` 中,新增适配器只需在该文件登记并补测试。
|
|
333
334
|
|
|
@@ -439,7 +440,7 @@ Host 半注册 `tokenlens_usage`,参数 `action`(默认 `snapshot`)与可
|
|
|
439
440
|
|
|
440
441
|
## 验证
|
|
441
442
|
|
|
442
|
-
- `node --test`:**
|
|
443
|
+
- `node --test`:**328 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、Design Plan 周期账本与官方单价计价、窗口锚点切片、工作区自动识别、插件集成与浏览器端渲染 / 交互。
|
|
443
444
|
- 在真实环境中验证过 **5 个 harness** 的数据源;本机统计与独立的 SQLite 查询、独立的日志解析逐项对账(差额为 0),跨 harness 合并与供应商映射算术自洽。
|
|
444
445
|
- 供应商接口按各官方文档与参考客户端行为验证;未配置凭据时 chip 静默隐藏,接口异常时按可重试错误降级。
|
|
445
446
|
|
package/client.js
CHANGED
|
@@ -341,8 +341,8 @@ window.__ModuleLoader__.load({
|
|
|
341
341
|
errorOpenDesignConfig: 'Open Design 额度查询需要工作区 ID 与 Control Key(不是推理 Key)',
|
|
342
342
|
designPlanLabel: '本期 Design Plan 累计',
|
|
343
343
|
designPlanHint:
|
|
344
|
-
'Open Design
|
|
345
|
-
|
|
344
|
+
'按官方定价表的模型单价换算本机实际消耗的 Token(不是 usedCredits ÷ 汇率)。Open Design 没有月度用量接口,此值由插件在本机累计观测到的 7 天窗口得出,仅供参考',
|
|
345
|
+
designPlanMultiplier: '按 {multiplier}× 倍率模型计价',
|
|
346
346
|
designPlanComplete: '已覆盖本账期全部窗口',
|
|
347
347
|
designPlanPartial: '仅统计已观测的 {count} 个窗口',
|
|
348
348
|
providerSave: '保存',
|
|
@@ -601,10 +601,10 @@ window.__ModuleLoader__.load({
|
|
|
601
601
|
providerControlKeySaved: 'Saved; paste again to replace it',
|
|
602
602
|
configureProvider: 'Configure',
|
|
603
603
|
errorOpenDesignConfig: 'Open Design quota needs a workspace ID and a Control Key (not an inference key)',
|
|
604
|
-
designPlanLabel: 'Period Design Plan
|
|
604
|
+
designPlanLabel: 'Period Design Plan',
|
|
605
605
|
designPlanHint:
|
|
606
|
-
'
|
|
607
|
-
|
|
606
|
+
'Priced from the official per-model rates over the tokens this machine actually consumed (not usedCredits ÷ rate). Open Design serves no period-usage endpoint, so the window total is accumulated locally; treat it as an estimate.',
|
|
607
|
+
designPlanMultiplier: 'priced at the {multiplier}× model rate',
|
|
608
608
|
designPlanComplete: 'covers every window of this period',
|
|
609
609
|
designPlanPartial: 'only {count} observed window(s)',
|
|
610
610
|
providerConsoleURLPlaceholder: 'Leave empty for the built-in default',
|
|
@@ -1090,6 +1090,11 @@ window.__ModuleLoader__.load({
|
|
|
1090
1090
|
const windows = Number.isFinite(raw.windows) && raw.windows > 0 ? Math.round(raw.windows) : 0
|
|
1091
1091
|
const estimatedUsd = numberOrNull(raw.estimatedUsd)
|
|
1092
1092
|
const limitPerWindow = numberOrNull(raw.limitPerWindow)
|
|
1093
|
+
// v1.6.0 documented-price context: the cap the model mix entitles and the
|
|
1094
|
+
// share of it consumed. Both are optional (an older Host omits them).
|
|
1095
|
+
const monthlyCapUsd = numberOrNull(raw.monthlyCapUsd)
|
|
1096
|
+
const capPercent = numberOrNull(raw.capPercent)
|
|
1097
|
+
const multiplier = numberOrNull(raw.multiplier)
|
|
1093
1098
|
return {
|
|
1094
1099
|
periodStart: stringOrEmpty(raw.periodStart),
|
|
1095
1100
|
periodEnd: stringOrEmpty(raw.periodEnd),
|
|
@@ -1099,6 +1104,9 @@ window.__ModuleLoader__.load({
|
|
|
1099
1104
|
complete: raw.complete === true,
|
|
1100
1105
|
since: stringOrEmpty(raw.since),
|
|
1101
1106
|
limitPerWindow: limitPerWindow !== null && limitPerWindow > 0 ? limitPerWindow : null,
|
|
1107
|
+
monthlyCapUsd: monthlyCapUsd !== null && monthlyCapUsd > 0 ? monthlyCapUsd : null,
|
|
1108
|
+
capPercent,
|
|
1109
|
+
multiplier: multiplier !== null && multiplier > 0 ? multiplier : null,
|
|
1102
1110
|
}
|
|
1103
1111
|
}
|
|
1104
1112
|
|
|
@@ -4018,25 +4026,38 @@ window.__ModuleLoader__.load({
|
|
|
4018
4026
|
}
|
|
4019
4027
|
|
|
4020
4028
|
/**
|
|
4021
|
-
* The open-design period estimate row (v1.5.0).
|
|
4029
|
+
* The open-design period estimate row (v1.5.0, priced in v1.6.0).
|
|
4022
4030
|
*
|
|
4023
4031
|
* Open Design has no period-usage endpoint, so this is the Host's local
|
|
4024
|
-
* accumulation of the
|
|
4025
|
-
*
|
|
4026
|
-
*
|
|
4027
|
-
*
|
|
4032
|
+
* accumulation of the observed windows. The dollar figure is the DOCUMENTED
|
|
4033
|
+
* LIST-PRICE value of the tokens consumed this period, which makes it
|
|
4034
|
+
* comparable with the published caps — that is what "can I use $120?" turns
|
|
4035
|
+
* on. The denominator is the cap those model multipliers entitle the account
|
|
4036
|
+
* to (0.5x models unlock the full $120; 1x unlocks $60).
|
|
4028
4037
|
*/
|
|
4029
4038
|
function buildDesignPlanRow(designPlan, t) {
|
|
4030
4039
|
if (designPlan === null) return null
|
|
4031
4040
|
const parts = []
|
|
4032
|
-
|
|
4041
|
+
const hasUsd = designPlan.estimatedUsd !== null
|
|
4042
|
+
if (hasUsd) {
|
|
4043
|
+
// "$27.31 / $120" — the numerator is documented list-price usage, the
|
|
4044
|
+
// denominator the documented cap for this model mix.
|
|
4045
|
+
const value =
|
|
4046
|
+
designPlan.monthlyCapUsd !== null
|
|
4047
|
+
? `${formatMoney(designPlan.estimatedUsd, 'USD')} / ${formatMoney(designPlan.monthlyCapUsd, 'USD')}`
|
|
4048
|
+
: formatMoney(designPlan.estimatedUsd, 'USD')
|
|
4033
4049
|
parts.push(
|
|
4034
|
-
h(
|
|
4035
|
-
'span',
|
|
4036
|
-
{ key: 'usd', className: 'dsh-tokenlens-percent dsh-tokenlens-capacity-money' },
|
|
4037
|
-
formatMoney(designPlan.estimatedUsd, 'USD'),
|
|
4038
|
-
),
|
|
4050
|
+
h('span', { key: 'usd', className: 'dsh-tokenlens-percent dsh-tokenlens-capacity-money' }, value),
|
|
4039
4051
|
)
|
|
4052
|
+
if (designPlan.capPercent !== null) {
|
|
4053
|
+
parts.push(
|
|
4054
|
+
h(
|
|
4055
|
+
'span',
|
|
4056
|
+
{ key: 'cap', className: 'dsh-tokenlens-percent', 'data-level': levelOf(designPlan.capPercent, 'ok') },
|
|
4057
|
+
formatPercent(designPlan.capPercent, t),
|
|
4058
|
+
),
|
|
4059
|
+
)
|
|
4060
|
+
}
|
|
4040
4061
|
} else {
|
|
4041
4062
|
parts.push(
|
|
4042
4063
|
h(
|
|
@@ -4047,9 +4068,10 @@ window.__ModuleLoader__.load({
|
|
|
4047
4068
|
)
|
|
4048
4069
|
}
|
|
4049
4070
|
const notes = []
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4071
|
+
// Say WHICH multiplier set the cap, since that is why the ceiling is not
|
|
4072
|
+
// always $120 (the pricing table: 0.5x→$120, 1x→$60, 2x→$30, 4x→$15).
|
|
4073
|
+
if (designPlan.multiplier !== null) {
|
|
4074
|
+
notes.push(t('designPlanMultiplier', { multiplier: designPlan.multiplier }))
|
|
4053
4075
|
}
|
|
4054
4076
|
if (designPlan.complete === true) notes.push(t('designPlanComplete'))
|
|
4055
4077
|
else if (designPlan.windows > 0) notes.push(t('designPlanPartial', { count: designPlan.windows }))
|
package/index.js
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
saveProviderStore,
|
|
39
39
|
} from './lib/provider-store.mjs'
|
|
40
40
|
import { makeCustomAdapter, sanitizeUsageBaseURL, validateCustomProvider } from './lib/custom-provider.mjs'
|
|
41
|
+
import { capsForModels, designPlanCostUsd } from './lib/design-plan-cost.mjs'
|
|
41
42
|
import {
|
|
42
43
|
defaultDesignPlanLedgerFile,
|
|
43
44
|
loadLedger,
|
|
@@ -83,7 +84,7 @@ export const inject = []
|
|
|
83
84
|
export const PLUGIN_NAME = 'dsh-tokenlens'
|
|
84
85
|
|
|
85
86
|
/** Plugin version; mirrors package.json (kept here so diagnostics need no fs read). */
|
|
86
|
-
export const PLUGIN_VERSION = '1.
|
|
87
|
+
export const PLUGIN_VERSION = '1.6.0'
|
|
87
88
|
|
|
88
89
|
/** Documented defaults; every field of the raw row config is optional. */
|
|
89
90
|
const DEFAULTS = {
|
|
@@ -431,12 +432,64 @@ export function apply(ctx, rawConfig) {
|
|
|
431
432
|
}
|
|
432
433
|
}
|
|
433
434
|
|
|
434
|
-
/**
|
|
435
|
-
|
|
435
|
+
/**
|
|
436
|
+
* The period estimate for the open-design snapshot, or null.
|
|
437
|
+
*
|
|
438
|
+
* v1.6.0: the dollar figure is the documented LIST-PRICE value of the tokens
|
|
439
|
+
* this period consumed, not `usedCredits / creditsPerUsd`. The latter assumes
|
|
440
|
+
* a 1x burn rate while the documented multipliers range 0.25x–4x, which made
|
|
441
|
+
* the number incomparable with the published caps (2x too low for DeepSeek
|
|
442
|
+
* V4.1 Flash, 4x too high for GPT-6 Luna). The token usage comes from the
|
|
443
|
+
* same local session-log stats the rest of the dialog uses.
|
|
444
|
+
*/
|
|
445
|
+
async function designPlanSummaryFor(providerId, designPlan) {
|
|
436
446
|
if (canonicalProviderId(providerId) !== 'open-design') return null
|
|
437
447
|
const periodStart = designPlan?.periodStart
|
|
438
448
|
if (typeof periodStart !== 'string' || periodStart === '') return null
|
|
439
|
-
|
|
449
|
+
const pricing = await designPlanPricing()
|
|
450
|
+
return summarizeDesignPlan(designPlanLedger, periodStart, pricing)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The documented-price context of the current billing period.
|
|
455
|
+
*
|
|
456
|
+
* Reads the per-model token buckets for `open-design` from the local stats
|
|
457
|
+
* (scoped to this period by the day histograms' own totals) and converts them
|
|
458
|
+
* with the documented list prices, then derives the caps those multipliers
|
|
459
|
+
* entitle the account to. Every failure degrades to `null`s, never to a throw:
|
|
460
|
+
* a pricing problem must not hide the credits/balance the Host already has.
|
|
461
|
+
*/
|
|
462
|
+
async function designPlanPricing() {
|
|
463
|
+
if (statsService === null) return null
|
|
464
|
+
let stats = null
|
|
465
|
+
try {
|
|
466
|
+
stats = await statsService.get({ fresh: false })
|
|
467
|
+
} catch {
|
|
468
|
+
return null
|
|
469
|
+
}
|
|
470
|
+
// `last30Days` is the closest wire window to a billing period (this account's
|
|
471
|
+
// is 2026-11-02 → 12-02) and it carries the per-token-kind split the
|
|
472
|
+
// documented prices need.
|
|
473
|
+
const models = Object.create(null)
|
|
474
|
+
for (const entry of Array.isArray(stats?.models) ? stats.models : []) {
|
|
475
|
+
if (canonicalProviderId(entry?.providerId) !== 'open-design') continue
|
|
476
|
+
const modelId = typeof entry.modelId === 'string' ? entry.modelId : ''
|
|
477
|
+
if (modelId === '') continue
|
|
478
|
+
const bucket = models[modelId] ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
479
|
+
bucket.input += typeof entry.last30Days?.input === 'number' ? entry.last30Days.input : 0
|
|
480
|
+
bucket.output += typeof entry.last30Days?.output === 'number' ? entry.last30Days.output : 0
|
|
481
|
+
bucket.cacheRead += typeof entry.last30Days?.cacheRead === 'number' ? entry.last30Days.cacheRead : 0
|
|
482
|
+
bucket.cacheWrite += typeof entry.last30Days?.cacheWrite === 'number' ? entry.last30Days.cacheWrite : 0
|
|
483
|
+
models[modelId] = bucket
|
|
484
|
+
}
|
|
485
|
+
const cost = designPlanCostUsd(models)
|
|
486
|
+
const caps = capsForModels(Object.keys(models))
|
|
487
|
+
return {
|
|
488
|
+
listPriceUsd: cost.priced === true ? cost.usd : null,
|
|
489
|
+
monthlyCapUsd: caps.monthlyCapUsd,
|
|
490
|
+
multiplier: caps.multiplier,
|
|
491
|
+
unpricedModels: cost.unpricedModels,
|
|
492
|
+
}
|
|
440
493
|
}
|
|
441
494
|
|
|
442
495
|
/**
|
|
@@ -1196,12 +1249,12 @@ export function apply(ctx, rawConfig) {
|
|
|
1196
1249
|
return consoleUrl === null ? null : { console: consoleUrl }
|
|
1197
1250
|
}
|
|
1198
1251
|
|
|
1199
|
-
function snapshotFromResult(providerId, read, now) {
|
|
1252
|
+
async function snapshotFromResult(providerId, read, now) {
|
|
1200
1253
|
// v1.5.0: the Design Plan period estimate. The ledger observes every read
|
|
1201
1254
|
// (refreshes and background polls), then the current period is summarized
|
|
1202
1255
|
// into the snapshot so the dialog can show it without a second round-trip.
|
|
1203
1256
|
recordDesignPlan(read.result.designPlan ?? null, now)
|
|
1204
|
-
const designPlan = designPlanSummaryFor(providerId, read.result.designPlan)
|
|
1257
|
+
const designPlan = await designPlanSummaryFor(providerId, read.result.designPlan)
|
|
1205
1258
|
return buildSnapshot({
|
|
1206
1259
|
provider: providerInfo(providerId),
|
|
1207
1260
|
plan: read.result.plan ?? null,
|
|
@@ -1240,7 +1293,7 @@ export function apply(ctx, rawConfig) {
|
|
|
1240
1293
|
state.lastAttempts.set(canonicalId, new Date().toISOString())
|
|
1241
1294
|
try {
|
|
1242
1295
|
const read = await readProvider(canonicalId, Date.now())
|
|
1243
|
-
const snapshot = snapshotFromResult(canonicalId, read, Date.now())
|
|
1296
|
+
const snapshot = await snapshotFromResult(canonicalId, read, Date.now())
|
|
1244
1297
|
state.caches.set(canonicalId, { snapshot, fetchedAtMs: read.fetchedAtMs })
|
|
1245
1298
|
state.lastErrors.delete(canonicalId)
|
|
1246
1299
|
state.requestCounts.set(canonicalId, (state.requestCounts.get(canonicalId) ?? 0) + 1)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design Plan cost accounting (v1.6.0).
|
|
3
|
+
*
|
|
4
|
+
* WHY
|
|
5
|
+
*
|
|
6
|
+
* Open Design bills the Design Plan in "credits", never in dollars. The
|
|
7
|
+
* published pricing page (`open-design.ai/zh/model-credits/`) states the caps in
|
|
8
|
+
* LIST-PRICE dollars ($120/month, $30/7d, $10/5h for Go) and, crucially, that
|
|
9
|
+
* the credits a model BURNS are `list-price value × that model's multiplier`:
|
|
10
|
+
*
|
|
11
|
+
* DeepSeek V4.1 Flash 0.5x -> the whole $120 monthly cap is available
|
|
12
|
+
* MiMo V2.6 Flash 1x -> half of it ($60)
|
|
13
|
+
* Claude Haiku 5.5 2x -> $30
|
|
14
|
+
* GPT-6 Luna 4x -> $15
|
|
15
|
+
*
|
|
16
|
+
* So a dollar amount computed from credits alone (usedCredits / creditsPerUsd)
|
|
17
|
+
* is NOT comparable with the pricing table: it silently assumes the account
|
|
18
|
+
* burns credits at 1x, understating a 0.5x model by 2x and overstating a 4x
|
|
19
|
+
* model by 4x. The honest figure is the LIST-PRICE value of the tokens actually
|
|
20
|
+
* consumed, which is what this module computes:
|
|
21
|
+
*
|
|
22
|
+
* cost = Σ over models ( input×p_in + output×p_out + cacheRead×p_read ) / 1e6
|
|
23
|
+
*
|
|
24
|
+
* That number is directly comparable with the documented caps, and the cap that
|
|
25
|
+
* applies depends on the model mix — hence `monthlyCapFor` and
|
|
26
|
+
* `effectiveMonthlyCap`.
|
|
27
|
+
*
|
|
28
|
+
* Credits remain the authoritative *consumption* signal (the server owns the
|
|
29
|
+
* counters); prices only turn observed tokens into a dollar figure the user can
|
|
30
|
+
* check against the pricing page.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Documented per-million-token list prices (USD) and Design Plan multipliers.
|
|
35
|
+
*
|
|
36
|
+
* Transcribed from `open-design.ai/zh/model-credits/` (pricing table + the
|
|
37
|
+
* "Design Plan 模型与额度扣除倍率" table). The public
|
|
38
|
+
* `GET /api/v1/public/model-prices` endpoint serves the same input/output/
|
|
39
|
+
* cacheRead numbers; only `hunyuan-h4-preview` and `glm-5.3-flash-x` are absent
|
|
40
|
+
* from it, so their prices fall back to the endpoint at runtime when available.
|
|
41
|
+
*
|
|
42
|
+
* `multiplier` is what the DOCUMENTED table says; an unknown model gets `null`
|
|
43
|
+
* and is reported as unpriceable rather than assumed to be 1x.
|
|
44
|
+
*/
|
|
45
|
+
export const DESIGN_PLAN_MODELS = {
|
|
46
|
+
'deepseek-v4.1-flash': { multiplier: 0.5, input: 0.3, output: 1.2, cacheRead: 0.006, label: 'DeepSeek V4.1 Flash' },
|
|
47
|
+
'deepseek-v4-flash': { multiplier: 1, input: 0.44, output: 1.32, cacheRead: 0.014, label: 'DeepSeek V4 Flash' },
|
|
48
|
+
'deepseek-v4-flash-vision-exp': {
|
|
49
|
+
multiplier: 1,
|
|
50
|
+
input: 0.44,
|
|
51
|
+
output: 1.32,
|
|
52
|
+
cacheRead: 0.014,
|
|
53
|
+
label: 'DeepSeek V4 Flash Vision Exp',
|
|
54
|
+
},
|
|
55
|
+
'deepseek-v4-pro': { multiplier: 1, input: 1.32, output: 3.96, cacheRead: 0.044, label: 'DeepSeek V4 Pro' },
|
|
56
|
+
'glm-5.3-flash': { multiplier: 1, input: 0.075, output: 0.25, cacheRead: 0.015, label: 'GLM-5.3 Flash' },
|
|
57
|
+
'glm-5.3-flash-x': { multiplier: 2, input: 0.37, output: 1.25, cacheRead: 0.075, label: 'GLM-5.3 Flash-X' },
|
|
58
|
+
'kimi-k2.7-code': { multiplier: 1, input: 0.95, output: 4, cacheRead: 0.19, label: 'Kimi K2.7 Code' },
|
|
59
|
+
'mimo-v2.6-flash': { multiplier: 1, input: 0.14, output: 0.28, cacheRead: 0.0028, label: 'MiMo V2.6 Flash' },
|
|
60
|
+
'mimo-v2.6-pro': { multiplier: 1, input: 0.435, output: 0.87, cacheRead: 0.0036, label: 'MiMo V2.6 Pro' },
|
|
61
|
+
'minimax-m3.1-flash': { multiplier: 1, input: 0.15, output: 0.6, cacheRead: 0.015, label: 'MiniMax M3.1 Flash' },
|
|
62
|
+
'claude-haiku-5.5': { multiplier: 2, input: 0.1, output: 0.5, cacheRead: 0.01, label: 'Claude Haiku 5.5' },
|
|
63
|
+
'gpt-6-luna': { multiplier: 4, input: 0.1, output: 0.5, cacheRead: 0.01, label: 'GPT-6 Luna' },
|
|
64
|
+
'hunyuan-h4-preview': { multiplier: 1, input: null, output: null, cacheRead: null, label: 'Hunyuan H4 Preview' },
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The documented list-price dollar caps of the Go tier (used only as a default). */
|
|
68
|
+
export const DESIGN_PLAN_CAPS_USD = { rolling: 10, weekly: 30, monthly: 120 }
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The monthly list-price cap available to a model mix.
|
|
72
|
+
*
|
|
73
|
+
* The $120 monthly cap is entirely Design Plan value and is consumed at the
|
|
74
|
+
* model's multiplier, so using ONLY a 0.5x model (DeepSeek V4.1 Flash) unlocks
|
|
75
|
+
* the full $120 of list-price usage; a 1x model reaches $60; 2x reaches $30.
|
|
76
|
+
* With a mixed history the binding constraint is the WORST (highest) multiplier
|
|
77
|
+
* the account actually used, because every unit of that model spends its share
|
|
78
|
+
* faster. That is why this returns the max multiplier seen, not an average.
|
|
79
|
+
*
|
|
80
|
+
* @returns `{ monthlyCapUsd, weeklyCapUsd, fiveHourCapUsd, multiplier }` where
|
|
81
|
+
* `multiplier` is the max documented multiplier among `modelIds` (0 when none),
|
|
82
|
+
* or all-null caps when no model is known.
|
|
83
|
+
*/
|
|
84
|
+
export function capsForModels(modelIds) {
|
|
85
|
+
let worst = 0
|
|
86
|
+
for (const id of Array.isArray(modelIds) ? modelIds : []) {
|
|
87
|
+
const model = DESIGN_PLAN_MODELS[normalizeModelId(id)]
|
|
88
|
+
if (model === undefined || !Number.isFinite(model.multiplier) || model.multiplier <= 0) continue
|
|
89
|
+
if (model.multiplier > worst) worst = model.multiplier
|
|
90
|
+
}
|
|
91
|
+
if (worst === 0) {
|
|
92
|
+
return { monthlyCapUsd: null, weeklyCapUsd: null, fiveHourCapUsd: null, multiplier: null }
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
// 600,000 credits / mult at 10,000 credits per dollar == $60 / mult.
|
|
96
|
+
monthlyCapUsd: 60 / worst,
|
|
97
|
+
weeklyCapUsd: 15 / worst,
|
|
98
|
+
fiveHourCapUsd: 5 / worst,
|
|
99
|
+
multiplier: worst,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A model id as the documented table spells it (lower-case, trimmed). */
|
|
104
|
+
export function normalizeModelId(value) {
|
|
105
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The documented entry for a model id, or null. */
|
|
109
|
+
export function designPlanModel(modelId) {
|
|
110
|
+
return DESIGN_PLAN_MODELS[normalizeModelId(modelId)] ?? null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A token count as a finite non-negative number. */
|
|
114
|
+
function tokens(value) {
|
|
115
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The documented list-price cost of one bucket of tokens for one model.
|
|
120
|
+
*
|
|
121
|
+
* @returns `{ usd, priced }`; `priced` is false when the model has no documented
|
|
122
|
+
* price (the cost is then 0 for that model, and callers must say so rather
|
|
123
|
+
* than pretend the total is complete).
|
|
124
|
+
*/
|
|
125
|
+
export function modelCostUsd(modelId, bucket) {
|
|
126
|
+
const model = designPlanModel(modelId)
|
|
127
|
+
if (model === null) return { usd: 0, priced: false }
|
|
128
|
+
const input = tokens(bucket?.input)
|
|
129
|
+
const output = tokens(bucket?.output)
|
|
130
|
+
const cacheRead = tokens(bucket?.cacheRead)
|
|
131
|
+
const cacheWrite = tokens(bucket?.cacheWrite)
|
|
132
|
+
// cacheWrite has no documented Design Plan price; bill it as fresh input so a
|
|
133
|
+
// value is never silently dropped, and flag it so the caller can explain.
|
|
134
|
+
const priced =
|
|
135
|
+
Number.isFinite(model.input) && Number.isFinite(model.output) && Number.isFinite(model.cacheRead)
|
|
136
|
+
if (!priced) return { usd: 0, priced: true, unavailability: 'the model has no documented Design Plan price' }
|
|
137
|
+
const usd =
|
|
138
|
+
(input / 1e6) * model.input +
|
|
139
|
+
(output / 1e6) * model.output +
|
|
140
|
+
(cacheRead / 1e6) * model.cacheRead +
|
|
141
|
+
(cacheWrite / 1e6) * model.input
|
|
142
|
+
return { usd, priced: true }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Total documented list-price cost of a per-model token map.
|
|
147
|
+
*
|
|
148
|
+
* @param models - `{ [modelId]: { input, output, cacheRead, cacheWrite } }`
|
|
149
|
+
* @returns `{ usd, priced, unpricedModels }` where `unpricedModels` lists the
|
|
150
|
+
* ids that had tokens but no documented price (their cost is not included).
|
|
151
|
+
*/
|
|
152
|
+
export function designPlanCostUsd(models) {
|
|
153
|
+
let usd = 0
|
|
154
|
+
let priced = false
|
|
155
|
+
const unpricedModels = []
|
|
156
|
+
for (const [modelId, bucket] of Object.entries(models ?? {})) {
|
|
157
|
+
const total = tokens(bucket?.input) + tokens(bucket?.output) + tokens(bucket?.cacheRead) + tokens(bucket?.cacheWrite)
|
|
158
|
+
if (total === 0) continue
|
|
159
|
+
const cost = modelCostUsd(modelId, bucket)
|
|
160
|
+
if (cost.priced !== true) {
|
|
161
|
+
unpricedModels.push(modelId)
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
usd += cost.usd
|
|
165
|
+
priced = true
|
|
166
|
+
}
|
|
167
|
+
return { usd, priced, unpricedModels }
|
|
168
|
+
}
|
|
@@ -234,17 +234,25 @@ export const WEEKS_PER_PERIOD = 4
|
|
|
234
234
|
/**
|
|
235
235
|
* Summarize one period: the estimated Design Plan usage the Host has observed.
|
|
236
236
|
*
|
|
237
|
+
* @param ledger - the ledger document.
|
|
238
|
+
* @param periodStart - the billing period key.
|
|
239
|
+
* @param input - optional documented-price context (v1.6.0):
|
|
240
|
+
* `{ listPriceUsd, monthlyCapUsd, multiplier }`. `listPriceUsd` replaces the
|
|
241
|
+
* credits÷rate conversion as `estimatedUsd`; it is the documented list-price
|
|
242
|
+
* value of the tokens actually consumed, which is the figure comparable with
|
|
243
|
+
* the published caps.
|
|
237
244
|
* @returns `null` when the period has no observations at all, else
|
|
238
|
-
* `{ usedCredits, estimatedUsd,
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* `
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
245
|
+
* `{ usedCredits, estimatedUsd, creditsUsd, monthlyCapUsd, multiplier,
|
|
246
|
+
* capPercent, windows, complete, since, periodStart, periodEnd,
|
|
247
|
+
* limitPerWindow, periodLimitCredits, percent }`.
|
|
248
|
+
* `percent` is `usedCredits / periodLimitCredits` (observed credits against
|
|
249
|
+
* the account's own windows); `capPercent` is `estimatedUsd / monthlyCapUsd`
|
|
250
|
+
* (documented dollars against the documented cap). The two differ per model
|
|
251
|
+
* mix and are both reported. `complete` is true only when the earliest
|
|
252
|
+
* observed window starts at the period start — otherwise earlier windows of
|
|
253
|
+
* this period were never seen and the numbers are lower bounds.
|
|
246
254
|
*/
|
|
247
|
-
export function summarizeDesignPlan(ledger, periodStart) {
|
|
255
|
+
export function summarizeDesignPlan(ledger, periodStart, input = null) {
|
|
248
256
|
const key = isoOf(periodStart)
|
|
249
257
|
if (key === '') return null
|
|
250
258
|
const period = ledger?.periods?.[key]
|
|
@@ -258,18 +266,32 @@ export function summarizeDesignPlan(ledger, periodStart) {
|
|
|
258
266
|
windows += 1
|
|
259
267
|
}
|
|
260
268
|
const creditsPerUsd = period.creditsPerUsd
|
|
261
|
-
|
|
269
|
+
// v1.6.0: `creditsPerUsd` conversion is kept ONLY as a fallback. It assumes the
|
|
270
|
+
// account burns credits at 1x, but the documented multipliers range 0.25x–4x,
|
|
271
|
+
// so it is wrong for any non-1x model (2x too low for DeepSeek V4.1 Flash).
|
|
272
|
+
// The caller passes the documented list-price cost instead.
|
|
273
|
+
const creditsUsd =
|
|
262
274
|
Number.isSafeInteger(creditsPerUsd) && creditsPerUsd > 0 ? usedCredits / creditsPerUsd : null
|
|
275
|
+
const estimatedUsd = Number.isFinite(input?.listPriceUsd) ? input.listPriceUsd : creditsUsd
|
|
263
276
|
const since = entries[0][0]
|
|
264
277
|
const limitPerWindow = entries[entries.length - 1][1].limitCredits
|
|
265
278
|
const usableWindowLimit = Number.isSafeInteger(limitPerWindow) && limitPerWindow > 0 ? limitPerWindow : null
|
|
266
279
|
const periodLimitCredits = usableWindowLimit === null ? null : usableWindowLimit * WEEKS_PER_PERIOD
|
|
267
280
|
const percent = periodLimitCredits === null ? null : (usedCredits / periodLimitCredits) * 100
|
|
281
|
+
const monthlyCapUsd = Number.isFinite(input?.monthlyCapUsd) ? input.monthlyCapUsd : null
|
|
282
|
+
const capPercent =
|
|
283
|
+
estimatedUsd !== null && monthlyCapUsd !== null && monthlyCapUsd > 0
|
|
284
|
+
? Math.min(100, Math.max(0, (estimatedUsd / monthlyCapUsd) * 100))
|
|
285
|
+
: null
|
|
268
286
|
return {
|
|
269
287
|
periodStart: key,
|
|
270
288
|
periodEnd: period.periodEnd || '',
|
|
271
289
|
usedCredits,
|
|
272
290
|
estimatedUsd,
|
|
291
|
+
creditsUsd,
|
|
292
|
+
monthlyCapUsd,
|
|
293
|
+
multiplier: Number.isFinite(input?.multiplier) ? input.multiplier : null,
|
|
294
|
+
capPercent,
|
|
273
295
|
windows,
|
|
274
296
|
complete: since === key,
|
|
275
297
|
since,
|
package/lib/usage-stats.mjs
CHANGED
|
@@ -665,7 +665,16 @@ export function createUsageStats(input) {
|
|
|
665
665
|
modelId: model.model,
|
|
666
666
|
today: { total: todayModel.total, messages: todayModel.messages },
|
|
667
667
|
last7Days: { total: model.last7Days.total, messages: model.last7Days.messages },
|
|
668
|
-
|
|
668
|
+
// v1.6.0: the input/output/cache split is included so a caller can price
|
|
669
|
+
// the tokens per model (the documented prices differ per token kind).
|
|
670
|
+
last30Days: {
|
|
671
|
+
total: model.last30Days.total,
|
|
672
|
+
messages: model.last30Days.messages,
|
|
673
|
+
input: model.last30Days.input,
|
|
674
|
+
output: model.last30Days.output,
|
|
675
|
+
cacheRead: model.last30Days.cacheRead,
|
|
676
|
+
cacheWrite: model.last30Days.cacheWrite,
|
|
677
|
+
},
|
|
669
678
|
allTime: { total: model.total.total, messages: model.total.messages },
|
|
670
679
|
sinceEffective: { total: model.total.total, messages: model.total.messages },
|
|
671
680
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tokenlens",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "dsh-TokenLens (词元透镜) — provider-aware plan capacity plus local token statistics (today/7d/30d/all-time, by harness/provider/model/workspace) for DSH.",
|
|
6
6
|
"license": "MIT",
|