dsh-tokenlens 1.7.0 → 1.7.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 +21 -1
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/client.js +270 -61
- package/index.js +66 -2
- package/lib/commandcode-plans.mjs +124 -0
- package/lib/providers.mjs +46 -5
- package/lib/usage-stats.mjs +48 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,12 +4,32 @@
|
|
|
4
4
|
|
|
5
5
|
**版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
|
|
6
6
|
|
|
7
|
+
## 1.7.2 — 2026-11-06
|
|
8
|
+
|
|
9
|
+
(当日已发布版本:v1.6.5 / v1.6.6 / v1.6.7 / v1.7.0 / v1.7.1 / v1.7.2)
|
|
10
|
+
|
|
11
|
+
- **fix(stats): 「最近X天」改为按套餐购买日起算**——此前窗口起点是「本机第一次记录到该供应商的那一天」,与你实际买的套餐周期无关。现在 Host 在每次读取额度时记下当前计费周期的起点(Command Code 取 `subscription.data.currentPeriodStart`,Open Design 取 Design Plan 的 `periodStart`),统计数据以它为窗口起点,`subscribed` 也随之正确。实机验证:commandcode `effectiveDay 2026-11-05`、`effectiveDays 2`(购买日为 11-05)
|
|
12
|
+
- **fix(stats): 窗口区间与标签严格一致**——`sinceEffective` 不再借用「最近30天」或「全时段」桶,而是按 `[窗口起点, 今天]` 逐日求和;超过 30 天时收缩**窗口起点**而非仅改天数标签
|
|
13
|
+
- **feat(ui): TPS 曲线加纵坐标与悬停读数**——左侧三档刻度(峰值 / 半值 / 0),鼠标划过时显示十字准线、绿点标记与读数条「日期 · 速率 · (输出 ÷ 耗时)」,对齐 DSH 原生图表的交互;曲线与 token 统计共用同一窗口
|
|
14
|
+
- **fix(ui): 本月额度按所选模型计算**——Command Code 的额度池与模型额度是两个数:GOAT 的池是 $70,但控制台对 **DeepSeek V4.1 Flash 标注 $100**(该模型相对同一池子有折扣)。此前一律用池,导致额度与所选模型无关,与你的预期不符。现在客户端把当前模型随请求上报,Host 用控制台同款按模型额度表定分母,`credits.granted` 与本月百分比随之变化;未上报模型或模型不在表内时回退额度池,绝不臆造
|
|
15
|
+
- 表来源:控制台 bundle `assets/plan-tiers-*.js`,2026-11-06 转录(见 `lib/commandcode-plans.mjs`)。已覆盖 `individual-goat` 与 `individual-go`
|
|
16
|
+
- 测试:364 → 369(按模型额度/回退/裸 id 匹配、周期起点上报、订阅窗口锚定与逐日求和、TPS 纵坐标与悬停接线、请求携带模型 id)
|
|
17
|
+
|
|
18
|
+
## 1.7.1 — 2026-11-06
|
|
19
|
+
|
|
20
|
+
(当日已发布版本:v1.6.5 / v1.6.6 / v1.6.7 / v1.7.0 / v1.7.1)
|
|
21
|
+
|
|
22
|
+
- **fix(ui): 未最大化窗口时芯片悬停导致整个对话区闪烁**——`Tooltip` 的气泡锚在芯片右缘,窗口变窄时会溢出 dock,撑出**对话区底部的横向滚动条**;滚动条又挤压布局、导致 hover 被反复触发,于是整片区域持续重绘。芯片改用**原生 `title`**:不产生任何 DOM,不可能影响布局。v1.7.0 只去掉原生 `title` 保留 Tooltip,方向反了——真正的成因是 Tooltip 本身
|
|
23
|
+
- **feat(ui): TPS 趋势图由柱状改为曲线**——速率是连续量,折线比逐日立柱更易读。改为单条 `polyline` 曲线 + 淡色面积填充;每天一个透明命中列承载 `title`,鼠标移上去显示「日期 · 速率(输出 ÷ 耗时)」
|
|
24
|
+
- fix(ui): **TPS 卡片宽度与「明细」卡片一致**(`flex: 1 1 100%`)——此前它没有 flex 规则,宽度收缩到内容宽
|
|
25
|
+
- 测试:363 → 364(芯片不再包 Tooltip、标题为原生字符串、速度卡片满宽、曲线为 polyline+area 且不填充线体、透明命中列携带日期与速率)
|
|
26
|
+
|
|
7
27
|
## 1.7.0 — 2026-11-06
|
|
8
28
|
|
|
9
29
|
(当日已发布版本:v1.6.5 / v1.6.6 / v1.6.7 / v1.7.0)
|
|
10
30
|
|
|
11
31
|
- **fix(ui): 弹窗改为 portal 到 `<body>`,恢复视口居中**——DSH 0.2.2-rc.1 里 `conversation.input.overlay` 槽位的祖先建立了包含块(transform / filter / contain),`position: fixed` 因此相对「对话区」解析,弹窗只能贴在对话区底部。改用 `react-dom` 的 `createPortal` 挂到 `document.body`,彻底脱离该祖先链。`react-dom` 缺失时(含 Node 测试环境)自动回退为原地渲染,绝不抛错
|
|
12
|
-
- **fix(ui):
|
|
32
|
+
- **fix(ui): 芯片悬停不再让整个对话区闪烁**(v1.7.1 进一步修正为改用原生 `title`)——v1.7.0 曾移除冗余的原生 `title` 并保留平台 `Tooltip`,但窄窗口下成因其实是 Tooltip 气泡溢出
|
|
13
33
|
- **feat(ui): 新增「输出速度(TPS)」趋势图**,位于明细下方、独占整行——输出 Token ÷ 模型自身耗时(`step/start` → `assistant/message`)。工具调用发生在消息之后,因此等待时间不被计入;输出 Token 含思考 Token,弹窗提示已说明
|
|
14
34
|
- feat(stats): Host 侧新增 `speed` 序列(顶层为全 Harness 合计,每个 provider 行另有自己的序列,跟随作用域切换);按天聚合 `{day, outputTokens, seconds, tps, steps}`,无正耗时的日期直接跳过而不是给出无限速率。缓存 schema 升至 6,首次会重扫
|
|
15
35
|
- **fix(stats): 「最近X天」封顶 30 天**——此前 provider 首次出现超过 30 天时会报「最近90天」,而同期所有其他数字与图表都只覆盖 30 天。现在 `effectiveDays` 封顶 30,且 `sinceEffective` 在超窗时改用 30 天桶,保证**标签与数字描述同一区间**;`daily` 序列尾部由 120 天收敛为 30 天
|
package/README.en.md
CHANGED
|
@@ -144,7 +144,7 @@ Host bridge route: `/api/tokenlens`; agent tool: `tokenlens_usage`.
|
|
|
144
144
|
|
|
145
145
|
## Development
|
|
146
146
|
|
|
147
|
-
Node.js ≥ 24, zero runtime dependencies: `node --test` runs the
|
|
147
|
+
Node.js ≥ 24, zero runtime dependencies: `node --test` runs the 369 tests.
|
|
148
148
|
|
|
149
149
|
Release: `node scripts/bump-version.mjs <version> "<note>"` updates `package.json`, `PLUGIN_VERSION` and `CHANGELOG.md`; see [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md).
|
|
150
150
|
|
package/README.md
CHANGED
|
@@ -445,7 +445,7 @@ Host 半注册 `tokenlens_usage`,参数 `action`(默认 `snapshot`)与可
|
|
|
445
445
|
|
|
446
446
|
## 验证
|
|
447
447
|
|
|
448
|
-
- `node --test`:**
|
|
448
|
+
- `node --test`:**369 个自动化用例**,覆盖 Host 数据层、供应商适配器(含小米 MiMo 控制台 Cookie 适配器、Command Code 月度额度反推)、统计折叠、各 harness 读取、Design Plan 周期账本与额度计价、TPS 速度序列、窗口锚点切片、工作区自动识别、插件集成与浏览器端渲染 / 交互。
|
|
449
449
|
- 在真实环境中验证过 **5 个 harness** 的数据源;本机统计与独立的 SQLite 查询、独立的日志解析逐项对账(差额为 0),跨 harness 合并与供应商映射算术自洽。
|
|
450
450
|
- 供应商接口按各官方文档与参考客户端行为验证;未配置凭据时 chip 静默隐藏,接口异常时按可重试错误降级。
|
|
451
451
|
|
package/client.js
CHANGED
|
@@ -231,8 +231,10 @@ window.__ModuleLoader__.load({
|
|
|
231
231
|
statsSpeedTitle: '输出速度(TPS)',
|
|
232
232
|
statsSpeedHint: '输出 Token ÷ 模型自身耗时(step 开始到消息返回);不含工具调用等待,但输出含思考 Token',
|
|
233
233
|
statsSpeedTotal: '均值 {tps} tok/s',
|
|
234
|
+
statsSpeedPeak: '峰值 {tps} tok/s',
|
|
234
235
|
statsSpeedEmpty: '暂无可计算的速度样本(需要 step 计时数据)',
|
|
235
236
|
statsSpeedBarTitle: '{date} · {tps} tok/s({tokens} 输出 ÷ {seconds}s)',
|
|
237
|
+
statsSpeedDetail: '{tokens} 输出 ÷ {seconds}s',
|
|
236
238
|
statsLast7Days: '最近7天',
|
|
237
239
|
stats7DaysShort: '7天',
|
|
238
240
|
statsDaysShort: '{n}天',
|
|
@@ -506,8 +508,10 @@ window.__ModuleLoader__.load({
|
|
|
506
508
|
statsSpeedHint:
|
|
507
509
|
'Output tokens ÷ the model’s own request time (step start to message); tool waits excluded, reasoning tokens included',
|
|
508
510
|
statsSpeedTotal: 'avg {tps} tok/s',
|
|
511
|
+
statsSpeedPeak: 'peak {tps} tok/s',
|
|
509
512
|
statsSpeedEmpty: 'No speed samples yet (needs step timing data)',
|
|
510
513
|
statsSpeedBarTitle: '{date} · {tps} tok/s ({tokens} output ÷ {seconds}s)',
|
|
514
|
+
statsSpeedDetail: '{tokens} output ÷ {seconds}s',
|
|
511
515
|
statsLast7Days: 'Last 7 days',
|
|
512
516
|
stats7DaysShort: '7d',
|
|
513
517
|
statsDaysShort: '{n}d',
|
|
@@ -2058,6 +2062,10 @@ window.__ModuleLoader__.load({
|
|
|
2058
2062
|
let state = {
|
|
2059
2063
|
// Provider reported by the session projection; null = "Host default".
|
|
2060
2064
|
providerId: null,
|
|
2065
|
+
// v1.7.2: model reported by the same projection. Command Code's monthly
|
|
2066
|
+
// allowance depends on it ($100 on GOAT for DeepSeek V4.1 Flash), so the
|
|
2067
|
+
// Host needs it with every usage request.
|
|
2068
|
+
modelId: null,
|
|
2061
2069
|
// Usage for the current provider.
|
|
2062
2070
|
phase: 'idle', // idle | loading | ready | error
|
|
2063
2071
|
snapshot: null,
|
|
@@ -2084,6 +2092,8 @@ window.__ModuleLoader__.load({
|
|
|
2084
2092
|
// v0.25.0 chart hover: the nearest bar index under the pointer, or null.
|
|
2085
2093
|
// Reset on mouse leave and whenever the scope/view/provider changes.
|
|
2086
2094
|
chartHover: null,
|
|
2095
|
+
// v1.7.2: the hovered point of the output-speed (TPS) curve.
|
|
2096
|
+
speedHover: null,
|
|
2087
2097
|
// v1.2.0 chart drill-down: the clicked day whose 0..23 hour histogram
|
|
2088
2098
|
// is charted instead of the daily trend, or null (daily trend).
|
|
2089
2099
|
chartDay: null,
|
|
@@ -2216,7 +2226,11 @@ window.__ModuleLoader__.load({
|
|
|
2216
2226
|
|
|
2217
2227
|
/** Build a bridge payload, omitting the provider when the Host should pick. */
|
|
2218
2228
|
function usagePayload(action, providerId) {
|
|
2219
|
-
|
|
2229
|
+
const base = providerId === null || providerId === undefined ? { action } : { action, providerId }
|
|
2230
|
+
// v1.7.2: the selected model rides along — Command Code's monthly allowance
|
|
2231
|
+
// depends on it (GOAT gives DeepSeek V4.1 Flash $100, its pool is $70).
|
|
2232
|
+
const modelId = store.get().modelId
|
|
2233
|
+
return typeof modelId === 'string' && modelId !== '' ? { ...base, modelId } : base
|
|
2220
2234
|
}
|
|
2221
2235
|
|
|
2222
2236
|
/** Build a stats payload (stats are global but the call reports the provider too). */
|
|
@@ -3280,6 +3294,26 @@ window.__ModuleLoader__.load({
|
|
|
3280
3294
|
void requestUsage('snapshot')
|
|
3281
3295
|
}
|
|
3282
3296
|
|
|
3297
|
+
/**
|
|
3298
|
+
* Sync BOTH halves of the session selection (v1.7.2).
|
|
3299
|
+
*
|
|
3300
|
+
* The provider decides which adapter runs; the model decides Command Code's
|
|
3301
|
+
* per-model monthly allowance. A model switch must therefore refetch even
|
|
3302
|
+
* when the provider did not move, or the chip would keep the old allowance
|
|
3303
|
+
* until the next poll.
|
|
3304
|
+
*/
|
|
3305
|
+
function syncSelection(providerId, modelId) {
|
|
3306
|
+
const nextModel = typeof modelId === 'string' && modelId !== '' ? modelId : null
|
|
3307
|
+
const before = store.get()
|
|
3308
|
+
const modelChanged = before.modelId !== nextModel
|
|
3309
|
+
if (modelChanged) store.set({ modelId: nextModel })
|
|
3310
|
+
syncProvider(providerId)
|
|
3311
|
+
const after = store.get()
|
|
3312
|
+
if (modelChanged && after.providerId !== null && after.inFlight !== true) {
|
|
3313
|
+
void requestUsage('snapshot')
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3283
3317
|
/** The refresh button: usage first, then a fresh stats scan, independently. */
|
|
3284
3318
|
async function refreshAll() {
|
|
3285
3319
|
await requestUsage('refresh')
|
|
@@ -3440,6 +3474,16 @@ window.__ModuleLoader__.load({
|
|
|
3440
3474
|
.dsh-tokenlens-chart-layout-row>.dsh-tokenlens-capacity{flex:0.85 1 260px}
|
|
3441
3475
|
.dsh-tokenlens-chart-layout-row>.dsh-tokenlens-stats{flex:1.25 1 330px;min-width:0}
|
|
3442
3476
|
.dsh-tokenlens-chart-layout-row>.dsh-tokenlens-details{flex:1 1 100%;min-width:0}
|
|
3477
|
+
/* v1.7.1: the TPS trend is a full-width row, exactly like the details card. */
|
|
3478
|
+
.dsh-tokenlens-chart-layout-row>.dsh-tokenlens-section[data-speed-section]{flex:1 1 100%;min-width:0}
|
|
3479
|
+
.dsh-tokenlens-speed-svg{display:block;width:100%;height:96px;overflow:visible}
|
|
3480
|
+
/* v1.7.2: Y axis + hover readout for the TPS curve. */
|
|
3481
|
+
.dsh-tokenlens-speed-body{display:flex;gap:6px;align-items:stretch;min-width:0}
|
|
3482
|
+
.dsh-tokenlens-speed-yaxis{display:flex;flex-direction:column;justify-content:space-between;align-items:flex-end;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;font-variant-numeric:tabular-nums;flex:0 0 auto;padding:1px 0}
|
|
3483
|
+
.dsh-tokenlens-speed-plot{position:relative;flex:1 1 auto;min-width:0}
|
|
3484
|
+
.dsh-tokenlens-speed-dot{position:absolute;width:6px;height:6px;margin:-3px 0 0 -3px;border-radius:50%;background:#2e9e5b;box-shadow:0 0 0 2px var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.35));pointer-events:none}
|
|
3485
|
+
.dsh-tokenlens-speed-axis{display:flex;justify-content:space-between;gap:6px;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1.2;font-variant-numeric:tabular-nums;min-width:0}
|
|
3486
|
+
.dsh-tokenlens-speed-axis>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3443
3487
|
@media (max-width:680px){.dsh-tokenlens-chart-layout-row>*{flex-basis:100%!important}}
|
|
3444
3488
|
.dsh-tokenlens-details-charts{min-width:0}
|
|
3445
3489
|
/* Data-mode 明细: three FULL-WIDTH blocks stacked vertically (v0.30.0 layout). */
|
|
@@ -3796,12 +3840,33 @@ window.__ModuleLoader__.load({
|
|
|
3796
3840
|
}
|
|
3797
3841
|
}
|
|
3798
3842
|
|
|
3843
|
+
/**
|
|
3844
|
+
* The MODEL selected in this session (v1.7.2).
|
|
3845
|
+
*
|
|
3846
|
+
* The projection's selection is `{provider, model, reasoningEffort?}`, so the
|
|
3847
|
+
* same read that yields the provider also yields the model id — which is what
|
|
3848
|
+
* decides Command Code's per-model monthly allowance.
|
|
3849
|
+
*/
|
|
3850
|
+
function useSelectedModel(props) {
|
|
3851
|
+
const useProjection = props?.useProjection
|
|
3852
|
+
if (typeof useProjection !== 'function') return null
|
|
3853
|
+
const model = useProjection('modelSelection')
|
|
3854
|
+
try {
|
|
3855
|
+
const selection = model?.next ?? model?.lastUsed
|
|
3856
|
+
const id = selection?.model
|
|
3857
|
+
return typeof id === 'string' && id !== '' ? id : null
|
|
3858
|
+
} catch {
|
|
3859
|
+
return null
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3799
3863
|
/** Keep the shared store's provider in sync with the session selection. */
|
|
3800
3864
|
function useProviderSync(props) {
|
|
3801
3865
|
const providerId = useSelectedProvider(props)
|
|
3866
|
+
const modelId = useSelectedModel(props)
|
|
3802
3867
|
React.useEffect(() => {
|
|
3803
|
-
|
|
3804
|
-
}, [providerId])
|
|
3868
|
+
syncSelection(providerId, modelId)
|
|
3869
|
+
}, [providerId, modelId])
|
|
3805
3870
|
return providerId
|
|
3806
3871
|
}
|
|
3807
3872
|
|
|
@@ -3923,29 +3988,28 @@ window.__ModuleLoader__.load({
|
|
|
3923
3988
|
const snapshot = normalizeSnapshot(state.snapshot)
|
|
3924
3989
|
const brand = providerBrand(state, snapshot)
|
|
3925
3990
|
const tooltip = chipTooltip(snapshot, state.providerId, t)
|
|
3991
|
+
// v1.7.1: a NATIVE title, not the platform Tooltip. On an unmaximized
|
|
3992
|
+
// window the Tooltip's bubble rendered at the chip's right edge, overflowed
|
|
3993
|
+
// the dock and produced a horizontal scrollbar on the conversation area;
|
|
3994
|
+
// that scrollbar shifted the layout, which re-triggered the hover and
|
|
3995
|
+
// flickered the whole area. A native title draws nothing in the DOM, so it
|
|
3996
|
+
// cannot affect layout at all. The text is unchanged.
|
|
3926
3997
|
return h(
|
|
3927
3998
|
'span',
|
|
3928
3999
|
{ className: 'dsh-tokenlens-anchor', 'data-composer-stat': 'tokenlens' },
|
|
3929
4000
|
h(
|
|
3930
|
-
|
|
3931
|
-
{
|
|
3932
|
-
|
|
3933
|
-
'
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
'aria-haspopup': 'dialog',
|
|
3943
|
-
onClick: () => openDialog(sessionId),
|
|
3944
|
-
},
|
|
3945
|
-
h('span', { className: 'dsh-tokenlens-dot', 'data-level': chipLevel(snapshot, state), 'aria-hidden': true }),
|
|
3946
|
-
h('span', { className: 'dsh-tokenlens-brand' }, brand),
|
|
3947
|
-
chipPieces(snapshot, state.providerId, t),
|
|
3948
|
-
),
|
|
4001
|
+
'button',
|
|
4002
|
+
{
|
|
4003
|
+
type: 'button',
|
|
4004
|
+
className: 'dsh-tokenlens-chip',
|
|
4005
|
+
title: tooltip,
|
|
4006
|
+
'aria-label': t('chipHint'),
|
|
4007
|
+
'aria-haspopup': 'dialog',
|
|
4008
|
+
onClick: () => openDialog(sessionId),
|
|
4009
|
+
},
|
|
4010
|
+
h('span', { className: 'dsh-tokenlens-dot', 'data-level': chipLevel(snapshot, state), 'aria-hidden': true }),
|
|
4011
|
+
h('span', { className: 'dsh-tokenlens-brand' }, brand),
|
|
4012
|
+
chipPieces(snapshot, state.providerId, t),
|
|
3949
4013
|
),
|
|
3950
4014
|
)
|
|
3951
4015
|
}
|
|
@@ -3965,25 +4029,23 @@ window.__ModuleLoader__.load({
|
|
|
3965
4029
|
'span',
|
|
3966
4030
|
{ className: 'dsh-tokenlens-anchor', 'data-composer-stat': 'tokenlens' },
|
|
3967
4031
|
h(
|
|
3968
|
-
|
|
3969
|
-
{
|
|
3970
|
-
|
|
3971
|
-
'
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
h('span', { className: 'dsh-tokenlens-metric' }, label),
|
|
3986
|
-
),
|
|
4032
|
+
'button',
|
|
4033
|
+
{
|
|
4034
|
+
type: 'button',
|
|
4035
|
+
className: 'dsh-tokenlens-chip',
|
|
4036
|
+
// v1.7.1: native title, for the same reason as the normal chip — a
|
|
4037
|
+
// Tooltip bubble here could overflow a narrow dock and add a
|
|
4038
|
+
// horizontal scrollbar that made the conversation area flicker.
|
|
4039
|
+
title: label,
|
|
4040
|
+
'aria-label': label,
|
|
4041
|
+
'aria-haspopup': 'dialog',
|
|
4042
|
+
...(needsConfig ? { 'data-needs-config': 'true' } : {}),
|
|
4043
|
+
...(statsOnly ? { 'data-stats-only': 'true' } : {}),
|
|
4044
|
+
onClick: () => openDialog(sessionId),
|
|
4045
|
+
},
|
|
4046
|
+
h('span', { className: 'dsh-tokenlens-dot', 'data-level': statsOnly ? 'normal' : 'warning', 'aria-hidden': true }),
|
|
4047
|
+
h('span', { className: 'dsh-tokenlens-brand' }, brand),
|
|
4048
|
+
h('span', { className: 'dsh-tokenlens-metric' }, label),
|
|
3987
4049
|
),
|
|
3988
4050
|
)
|
|
3989
4051
|
}
|
|
@@ -4645,6 +4707,12 @@ window.__ModuleLoader__.load({
|
|
|
4645
4707
|
if (store.get().chartHover !== next) store.set({ chartHover: next })
|
|
4646
4708
|
}
|
|
4647
4709
|
|
|
4710
|
+
/** The hovered point of the output-speed curve (v1.7.2). */
|
|
4711
|
+
function setSpeedHover(index) {
|
|
4712
|
+
const next = Number.isInteger(index) && index >= 0 ? index : null
|
|
4713
|
+
if (store.get().speedHover !== next) store.set({ speedHover: next })
|
|
4714
|
+
}
|
|
4715
|
+
|
|
4648
4716
|
/** The active today bucket of the current stats scope (v0.27.2). */
|
|
4649
4717
|
function todayBucketOf(ctx) {
|
|
4650
4718
|
return ctx.providerScoped ? ctx.currentRow?.today ?? null : ctx.stats?.today ?? null
|
|
@@ -6088,35 +6156,176 @@ window.__ModuleLoader__.load({
|
|
|
6088
6156
|
h('span', { key: 'hint', className: 'dsh-tokenlens-hint' }, t('statsSpeedEmpty')),
|
|
6089
6157
|
)
|
|
6090
6158
|
: (() => {
|
|
6159
|
+
// v1.7.1: a CURVE, not bars — the trend is a rate, and a line reads
|
|
6160
|
+
// it far better than per-day columns. The SVG scales to the card
|
|
6161
|
+
// width; `preserveAspectRatio="none"` lets it fill without keeping
|
|
6162
|
+
// the viewBox ratio, and the per-day hover targets are transparent
|
|
6163
|
+
// columns so nothing is distorted.
|
|
6164
|
+
const width = 320
|
|
6165
|
+
const height = 90
|
|
6166
|
+
const pad = 4
|
|
6091
6167
|
const max = Math.max(...byDay.map((entry) => entry.tps ?? 0), 1)
|
|
6092
|
-
const
|
|
6093
|
-
|
|
6094
|
-
const
|
|
6095
|
-
|
|
6096
|
-
|
|
6168
|
+
const stepX = byDay.length > 1 ? (width - pad * 2) / (byDay.length - 1) : 0
|
|
6169
|
+
const points = byDay.map((entry, index) => {
|
|
6170
|
+
const x = byDay.length > 1 ? pad + stepX * index : width / 2
|
|
6171
|
+
const y = height - pad - ((entry.tps ?? 0) / max) * (height - pad * 2)
|
|
6172
|
+
return { x, y }
|
|
6173
|
+
})
|
|
6174
|
+
const line = points.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(' ')
|
|
6175
|
+
const area = `${pad.toFixed(1)},${(height - pad).toFixed(1)} ${line} ${(width - pad).toFixed(1)},${(height - pad).toFixed(1)}`
|
|
6176
|
+
const columnWidth = byDay.length > 1 ? stepX : width - pad * 2
|
|
6177
|
+
const columns = byDay.map((entry, index) =>
|
|
6178
|
+
h(
|
|
6179
|
+
'rect',
|
|
6097
6180
|
{
|
|
6098
|
-
key: `
|
|
6099
|
-
|
|
6181
|
+
key: `hit-${entry.day}-${index}`,
|
|
6182
|
+
x: (points[index].x - columnWidth / 2).toFixed(1),
|
|
6183
|
+
y: 0,
|
|
6184
|
+
width: Math.max(1, columnWidth).toFixed(1),
|
|
6185
|
+
height: String(height),
|
|
6186
|
+
fill: 'transparent',
|
|
6100
6187
|
'data-speed-day': entry.day,
|
|
6101
|
-
'data-speed-tps': String(tps),
|
|
6102
|
-
|
|
6188
|
+
'data-speed-tps': String(entry.tps ?? 0),
|
|
6189
|
+
},
|
|
6190
|
+
h(
|
|
6191
|
+
'title',
|
|
6192
|
+
{ key: 'tip' },
|
|
6193
|
+
t('statsSpeedBarTitle', {
|
|
6103
6194
|
date: entry.day,
|
|
6104
|
-
tps: tps.toFixed(1),
|
|
6195
|
+
tps: (entry.tps ?? 0).toFixed(1),
|
|
6105
6196
|
tokens: formatTokensPrecise(entry.outputTokens),
|
|
6106
6197
|
seconds: entry.seconds.toFixed(1),
|
|
6107
6198
|
}),
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6199
|
+
),
|
|
6200
|
+
),
|
|
6201
|
+
)
|
|
6202
|
+
const svg = h(
|
|
6203
|
+
'svg',
|
|
6204
|
+
{
|
|
6205
|
+
key: 'svg',
|
|
6206
|
+
className: 'dsh-tokenlens-speed-svg',
|
|
6207
|
+
'data-speed-plot': 'true',
|
|
6208
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
6209
|
+
preserveAspectRatio: 'none',
|
|
6210
|
+
role: 'img',
|
|
6211
|
+
'aria-label': t('statsSpeedTitle'),
|
|
6212
|
+
},
|
|
6213
|
+
h('polygon', {
|
|
6214
|
+
key: 'area',
|
|
6215
|
+
className: 'dsh-tokenlens-speed-area',
|
|
6216
|
+
points: area,
|
|
6217
|
+
fill: 'rgba(46,158,91,.18)',
|
|
6218
|
+
'aria-hidden': 'true',
|
|
6219
|
+
}),
|
|
6220
|
+
h('polyline', {
|
|
6221
|
+
key: 'line',
|
|
6222
|
+
className: 'dsh-tokenlens-speed-line',
|
|
6223
|
+
points: line,
|
|
6224
|
+
fill: 'none',
|
|
6225
|
+
stroke: '#2e9e5b',
|
|
6226
|
+
'stroke-width': '1.6',
|
|
6227
|
+
'stroke-linecap': 'round',
|
|
6228
|
+
'stroke-linejoin': 'round',
|
|
6229
|
+
'vector-effect': 'non-scaling-stroke',
|
|
6230
|
+
'aria-hidden': 'true',
|
|
6231
|
+
}),
|
|
6232
|
+
columns,
|
|
6233
|
+
)
|
|
6234
|
+
const hoverIndex =
|
|
6235
|
+
Number.isInteger(state.speedHover) && state.speedHover >= 0 && state.speedHover < byDay.length
|
|
6236
|
+
? state.speedHover
|
|
6237
|
+
: null
|
|
6238
|
+
const hovered = hoverIndex === null ? null : byDay[hoverIndex]
|
|
6239
|
+
const handleMove = (event) => {
|
|
6240
|
+
const index = chartIndexFromEvent(event, byDay.length)
|
|
6241
|
+
if (index !== null) setSpeedHover(index)
|
|
6242
|
+
}
|
|
6243
|
+
const handleLeave = () => setSpeedHover(null)
|
|
6244
|
+
// v1.7.2: a Y axis, like the native charts — the curve alone gives no
|
|
6245
|
+
// sense of magnitude.
|
|
6246
|
+
const ticks = [max, max / 2, 0]
|
|
6247
|
+
const yAxis = h(
|
|
6248
|
+
'div',
|
|
6249
|
+
{ key: 'y', className: 'dsh-tokenlens-speed-yaxis', 'aria-hidden': 'true' },
|
|
6250
|
+
ticks.map((value, index) =>
|
|
6251
|
+
h('span', { key: `tick-${index}`, className: 'dsh-tokenlens-chart-tick' }, `${Math.round(value)}`),
|
|
6252
|
+
),
|
|
6253
|
+
)
|
|
6254
|
+
const crosshair =
|
|
6255
|
+
hoverIndex === null
|
|
6256
|
+
? null
|
|
6257
|
+
: h('div', {
|
|
6258
|
+
key: 'cross',
|
|
6259
|
+
className: 'dsh-tokenlens-chart-crosshair',
|
|
6260
|
+
'data-speed-crosshair': String(hoverIndex),
|
|
6261
|
+
'aria-hidden': 'true',
|
|
6262
|
+
style: { left: `${(points[hoverIndex].x / width) * 100}%` },
|
|
6263
|
+
})
|
|
6264
|
+
const marker =
|
|
6265
|
+
hoverIndex === null
|
|
6266
|
+
? null
|
|
6267
|
+
: h('div', {
|
|
6268
|
+
key: 'dot',
|
|
6269
|
+
className: 'dsh-tokenlens-speed-dot',
|
|
6270
|
+
'aria-hidden': 'true',
|
|
6271
|
+
style: {
|
|
6272
|
+
left: `${(points[hoverIndex].x / width) * 100}%`,
|
|
6273
|
+
top: `${(points[hoverIndex].y / height) * 100}%`,
|
|
6274
|
+
},
|
|
6275
|
+
})
|
|
6276
|
+
const readout =
|
|
6277
|
+
hovered === null
|
|
6278
|
+
? null
|
|
6279
|
+
: h(
|
|
6280
|
+
'div',
|
|
6281
|
+
{
|
|
6282
|
+
key: 'readout',
|
|
6283
|
+
className: 'dsh-tokenlens-chart-readout',
|
|
6284
|
+
'data-speed-readout': 'true',
|
|
6285
|
+
role: 'status',
|
|
6286
|
+
style:
|
|
6287
|
+
hoverIndex === 0
|
|
6288
|
+
? { left: '0%' }
|
|
6289
|
+
: hoverIndex === byDay.length - 1
|
|
6290
|
+
? { right: '0%' }
|
|
6291
|
+
: {
|
|
6292
|
+
left: `${(points[hoverIndex].x / width) * 100}%`,
|
|
6293
|
+
transform: 'translateX(-50%)',
|
|
6294
|
+
},
|
|
6295
|
+
},
|
|
6296
|
+
`${hovered.day} · ${(hovered.tps ?? 0).toFixed(1)} tok/s`,
|
|
6297
|
+
` · ${t('statsSpeedDetail', {
|
|
6298
|
+
tokens: formatTokensPrecise(hovered.outputTokens),
|
|
6299
|
+
seconds: hovered.seconds.toFixed(1),
|
|
6300
|
+
})}`,
|
|
6301
|
+
)
|
|
6302
|
+
const axis = h(
|
|
6303
|
+
'div',
|
|
6304
|
+
{ key: 'axis', className: 'dsh-tokenlens-speed-axis', 'aria-hidden': 'true' },
|
|
6305
|
+
h('span', { key: 'first' }, byDay[0].day),
|
|
6306
|
+
byDay.length > 1 ? h('span', { key: 'last' }, byDay[byDay.length - 1].day) : null,
|
|
6307
|
+
)
|
|
6116
6308
|
return h(
|
|
6117
6309
|
'div',
|
|
6118
|
-
{ key: '
|
|
6119
|
-
|
|
6310
|
+
{ key: 'plot', className: 'dsh-tokenlens-speed-body' },
|
|
6311
|
+
yAxis,
|
|
6312
|
+
h(
|
|
6313
|
+
'div',
|
|
6314
|
+
{
|
|
6315
|
+
key: 'plotarea',
|
|
6316
|
+
className: 'dsh-tokenlens-speed-plot',
|
|
6317
|
+
'data-speed-plot': 'true',
|
|
6318
|
+
onMouseMove: handleMove,
|
|
6319
|
+
onMouseLeave: handleLeave,
|
|
6320
|
+
onPointerMove: handleMove,
|
|
6321
|
+
onPointerLeave: handleLeave,
|
|
6322
|
+
},
|
|
6323
|
+
svg,
|
|
6324
|
+
crosshair,
|
|
6325
|
+
marker,
|
|
6326
|
+
readout,
|
|
6327
|
+
axis,
|
|
6328
|
+
),
|
|
6120
6329
|
)
|
|
6121
6330
|
})()
|
|
6122
6331
|
return h(
|
package/index.js
CHANGED
|
@@ -84,7 +84,7 @@ export const inject = []
|
|
|
84
84
|
export const PLUGIN_NAME = 'dsh-tokenlens'
|
|
85
85
|
|
|
86
86
|
/** Plugin version; mirrors package.json (kept here so diagnostics need no fs read). */
|
|
87
|
-
export const PLUGIN_VERSION = '1.7.
|
|
87
|
+
export const PLUGIN_VERSION = '1.7.2'
|
|
88
88
|
|
|
89
89
|
/** Documented defaults; every field of the raw row config is optional. */
|
|
90
90
|
const DEFAULTS = {
|
|
@@ -299,6 +299,12 @@ export function apply(ctx, rawConfig) {
|
|
|
299
299
|
keys: new Map(), // providerId 鈫?{ ref, source, detail } (never the secret)
|
|
300
300
|
requestCounts: new Map(), // providerId 鈫?successful reads
|
|
301
301
|
subscribers: new Set(),
|
|
302
|
+
// v1.7.2: the model the browser last reported per provider, so per-model
|
|
303
|
+
// monthly allowances resolve without a round-trip of their own.
|
|
304
|
+
selectedModels: new Map(),
|
|
305
|
+
// v1.7.2: providerId → local day ('YYYY-MM-DD') the current plan period
|
|
306
|
+
// started, used to anchor the 「最近X天(套餐购买日起算)」 statistics window.
|
|
307
|
+
subscriptionStarts: new Map(),
|
|
302
308
|
client: null, // last browser `hello`
|
|
303
309
|
// Optional startup update check (settings.updateCheck): the last result
|
|
304
310
|
// `{current, latest, available, checkedAt, error}` or null before the first
|
|
@@ -1121,6 +1127,11 @@ export function apply(ctx, rawConfig) {
|
|
|
1121
1127
|
const custom = effectiveCustomProvider(id)
|
|
1122
1128
|
const mapping = dshRouteMapping(String(providerId))
|
|
1123
1129
|
let provider = custom ?? dshRouteProviderConfig(String(providerId)) ?? effectiveProviderConfig(id)
|
|
1130
|
+
// v1.7.2: the model the browser reported decides per-model allowances.
|
|
1131
|
+
const selectedModel = state.selectedModels?.get(id) ?? ''
|
|
1132
|
+
if (selectedModel !== '' && provider !== undefined) {
|
|
1133
|
+
provider = { ...provider, selectedModel }
|
|
1134
|
+
}
|
|
1124
1135
|
if (provider === undefined || provider.enabled === false) {
|
|
1125
1136
|
throw new UsageError('disabled', `provider ${JSON.stringify(providerId)} is disabled`, {
|
|
1126
1137
|
retryable: false,
|
|
@@ -1297,6 +1308,10 @@ export function apply(ctx, rawConfig) {
|
|
|
1297
1308
|
// into the snapshot so the dialog can show it without a second round-trip.
|
|
1298
1309
|
recordDesignPlan(read.result.designPlan ?? null, now)
|
|
1299
1310
|
const designPlan = await designPlanSummaryFor(providerId, read.result.designPlan)
|
|
1311
|
+
// v1.7.2: remember when the current plan period began, so the local token
|
|
1312
|
+
// statistics can be anchored to the purchase date ("套餐购买日起算") instead of
|
|
1313
|
+
// to the first day this machine happened to record that provider.
|
|
1314
|
+
rememberPeriodStart(providerId, read.result)
|
|
1300
1315
|
return buildSnapshot({
|
|
1301
1316
|
provider: providerInfo(providerId),
|
|
1302
1317
|
plan: read.result.plan ?? null,
|
|
@@ -1455,6 +1470,30 @@ export function apply(ctx, rawConfig) {
|
|
|
1455
1470
|
: null
|
|
1456
1471
|
|
|
1457
1472
|
/** Token statistics for the dialog: today + all-time totals. */
|
|
1473
|
+
/**
|
|
1474
|
+
* Remember the local day the current plan period started (v1.7.2).
|
|
1475
|
+
*
|
|
1476
|
+
* The token statistics window is 「最近X天(套餐购买日起算)」, so it must start at
|
|
1477
|
+
* the plan's period start — not at the first day this machine recorded the
|
|
1478
|
+
* provider. Both providers that report a plan also report a period bound:
|
|
1479
|
+
* Command Code in `subscription.data.currentPeriodStart`, Open Design in its
|
|
1480
|
+
* Design Plan `periodStart`.
|
|
1481
|
+
*/
|
|
1482
|
+
function rememberPeriodStart(providerId, result) {
|
|
1483
|
+
const raw =
|
|
1484
|
+
(typeof result?.period?.start === 'string' ? result.period.start : '') ||
|
|
1485
|
+
(typeof result?.designPlan?.periodStart === 'string' ? result.designPlan.periodStart : '')
|
|
1486
|
+
if (raw === '') return
|
|
1487
|
+
const at = Date.parse(raw)
|
|
1488
|
+
if (!Number.isFinite(at)) return
|
|
1489
|
+
const date = new Date(at)
|
|
1490
|
+
const day = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
|
1491
|
+
const id = canonicalProviderId(String(providerId))
|
|
1492
|
+
if (state.subscriptionStarts.get(id) === day) return
|
|
1493
|
+
state.subscriptionStarts.set(id, day)
|
|
1494
|
+
noteStatus('subscription-period', { providerId: id, periodStartDay: day })
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1458
1497
|
/** Whether the last snapshot of this provider represents a subscription/plan. */
|
|
1459
1498
|
function providerHasSubscription(providerId) {
|
|
1460
1499
|
const cached = state.caches.get(canonicalProviderId(String(providerId)))
|
|
@@ -1583,7 +1622,11 @@ export function apply(ctx, rawConfig) {
|
|
|
1583
1622
|
}
|
|
1584
1623
|
}
|
|
1585
1624
|
try {
|
|
1586
|
-
const stats = await statsService.get({
|
|
1625
|
+
const stats = await statsService.get({
|
|
1626
|
+
fresh: options.fresh === true,
|
|
1627
|
+
// v1.7.2: anchor each subscription provider's window to its plan start.
|
|
1628
|
+
subscriptionStarts: Object.fromEntries(state.subscriptionStarts),
|
|
1629
|
+
})
|
|
1587
1630
|
// v0.22.1: only subscription-backed providers use the 最近X天 window;
|
|
1588
1631
|
// pay-as-you-go/balance providers keep the fixed 最近30天 column.
|
|
1589
1632
|
const decorated =
|
|
@@ -1785,6 +1828,25 @@ export function apply(ctx, rawConfig) {
|
|
|
1785
1828
|
ensurePoll()
|
|
1786
1829
|
}
|
|
1787
1830
|
|
|
1831
|
+
/**
|
|
1832
|
+
* Remember the model the user picked, per provider (v1.7.2).
|
|
1833
|
+
*
|
|
1834
|
+
* The monthly allowance depends on it (Command Code's GOAT gives DeepSeek
|
|
1835
|
+
* V4.1 Flash $100 while its credit pool is $70), so a model switch must also
|
|
1836
|
+
* drop the cached snapshot — otherwise the chip would keep showing the old
|
|
1837
|
+
* allowance until the next poll.
|
|
1838
|
+
*/
|
|
1839
|
+
function rememberRequestedModel(body, providerId) {
|
|
1840
|
+
const modelId = typeof body?.modelId === 'string' ? body.modelId.trim() : ''
|
|
1841
|
+
if (modelId === '') return
|
|
1842
|
+
const id = canonicalProviderId(providerId)
|
|
1843
|
+
if (state.selectedModels.get(id) === modelId) return
|
|
1844
|
+
state.selectedModels.set(id, modelId)
|
|
1845
|
+
state.caches.delete(id)
|
|
1846
|
+
state.flights.delete(id)
|
|
1847
|
+
noteStatus('client-model', { providerId: id, modelId })
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1788
1850
|
/** One JSON request/response exchange with the browser half. */
|
|
1789
1851
|
async function handleBridge(request) {
|
|
1790
1852
|
let body
|
|
@@ -1870,10 +1932,12 @@ export function apply(ctx, rawConfig) {
|
|
|
1870
1932
|
}
|
|
1871
1933
|
if (action === 'snapshot' || action === 'status') {
|
|
1872
1934
|
rememberRequestedProvider(body)
|
|
1935
|
+
rememberRequestedModel(body, requestProvider(body))
|
|
1873
1936
|
return json(await get(requestProvider(body)))
|
|
1874
1937
|
}
|
|
1875
1938
|
if (action === 'refresh') {
|
|
1876
1939
|
rememberRequestedProvider(body)
|
|
1940
|
+
rememberRequestedModel(body, requestProvider(body))
|
|
1877
1941
|
return json(await get(requestProvider(body), { force: true }))
|
|
1878
1942
|
}
|
|
1879
1943
|
if (action === 'stats') return json(await getStats({ fresh: body?.fresh === true }))
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Code per-model monthly allowances (v1.7.2).
|
|
3
|
+
*
|
|
4
|
+
* `mimo`-style plan catalogues are not served by any public API — the numbers
|
|
5
|
+
* live in the provider's own console bundle (`assets/plan-tiers-*.js`), which is
|
|
6
|
+
* where the console gets the allowance it prints per model. The plugin needs the
|
|
7
|
+
* same table to answer "how much does THIS model get?", because a plan's credit
|
|
8
|
+
* pool and a model's allowance are two different numbers:
|
|
9
|
+
*
|
|
10
|
+
* - the POOL is what the API meters (`credits.monthlyCredits`, and
|
|
11
|
+
* `windowLimits.*.used` are charged against it): `individual-goat` = $70;
|
|
12
|
+
* - the ALLOWANCE is what the console advertises for a given model, because a
|
|
13
|
+
* discounted model stretches the same pool further: DeepSeek V4.1 Flash on
|
|
14
|
+
* GOAT = $100, even though the pool is $70.
|
|
15
|
+
*
|
|
16
|
+
* Transcribed 2026-11-06 from the console bundle:
|
|
17
|
+
* ```js
|
|
18
|
+
* {"zai-org/GLM-5.2":70,"tencent/hy3-paid":70,"Qwen/Qwen3.8-27B":70,
|
|
19
|
+
* "MiniMaxAI/MiniMax-M3":47,"deepseek/deepseek-v4-flash":60,
|
|
20
|
+
* "deepseek/deepseek-v4.1-flash":100,"deepseek/deepseek-v4.1-flash-fast":60,
|
|
21
|
+
* "moonshotai/Kimi-K2.7-Code":60,"xiaomi/mimo-v2.6-pro-ultraspeed":10,
|
|
22
|
+
* "claude-sonnet-5-5":10,"Qwen/Qwen3.7-Max":33,"Qwen/Qwen3.7-Plus":33,
|
|
23
|
+
* "Qwen/Qwen3.6-Plus":33,"google/gemini-3.7-flash":40,
|
|
24
|
+
* "google/gemini-3.8-flash":40,"z-ai/glm-5.3-flash":100,
|
|
25
|
+
* "meituan/LongCat-2.0":50,"inclusionai/ling-3.1-flash":100,
|
|
26
|
+
* "inclusionai/ling-3.1-flash-1019":100}
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Only the two plans whose tables were captured carry per-model numbers; every
|
|
30
|
+
* other plan falls back to its pool, and a model missing from a table falls back
|
|
31
|
+
* to that plan's pool too — the plugin never invents an allowance.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** Plan id -> `{ poolUsd, byModel }`, with model ids as the console spells them. */
|
|
35
|
+
export const COMMANDCODE_PLAN_ALLOWANCES = {
|
|
36
|
+
// GOAT: $70 of credits, but discounted models advertise up to $100 of usage.
|
|
37
|
+
'individual-goat': {
|
|
38
|
+
poolUsd: 70,
|
|
39
|
+
byModel: {
|
|
40
|
+
'zai-org/GLM-5.2': 70,
|
|
41
|
+
'tencent/hy3-paid': 70,
|
|
42
|
+
'Qwen/Qwen3.8-27B': 70,
|
|
43
|
+
'MiniMaxAI/MiniMax-M3': 47,
|
|
44
|
+
'deepseek/deepseek-v4-flash': 60,
|
|
45
|
+
'deepseek/deepseek-v4.1-flash': 100,
|
|
46
|
+
'deepseek/deepseek-v4.1-flash-fast': 60,
|
|
47
|
+
'moonshotai/Kimi-K2.7-Code': 60,
|
|
48
|
+
'xiaomi/mimo-v2.6-pro-ultraspeed': 10,
|
|
49
|
+
'claude-sonnet-5-5': 10,
|
|
50
|
+
'Qwen/Qwen3.7-Max': 33,
|
|
51
|
+
'Qwen/Qwen3.7-Plus': 33,
|
|
52
|
+
'Qwen/Qwen3.6-Plus': 33,
|
|
53
|
+
'google/gemini-3.7-flash': 40,
|
|
54
|
+
'google/gemini-3.8-flash': 40,
|
|
55
|
+
'z-ai/glm-5.3-flash': 100,
|
|
56
|
+
'meituan/LongCat-2.0': 50,
|
|
57
|
+
'inclusionai/ling-3.1-flash': 100,
|
|
58
|
+
'inclusionai/ling-3.1-flash-1019': 100,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
// Go ($10 of credits).
|
|
62
|
+
'individual-go': {
|
|
63
|
+
poolUsd: 10,
|
|
64
|
+
byModel: {
|
|
65
|
+
'zai-org/GLM-5.2': 10,
|
|
66
|
+
'tencent/hy3-paid': 10,
|
|
67
|
+
'Qwen/Qwen3.8-27B': 10,
|
|
68
|
+
'Qwen/Qwen3.7-Max': 10,
|
|
69
|
+
'Qwen/Qwen3.7-Plus': 10,
|
|
70
|
+
'Qwen/Qwen3.6-Plus': 10,
|
|
71
|
+
'meituan/LongCat-2.0': 10,
|
|
72
|
+
'zai-org/GLM-5.3': 10,
|
|
73
|
+
'z-ai/glm-5.3-flash': 10,
|
|
74
|
+
'deepseek/deepseek-v4.1-flash': 10,
|
|
75
|
+
'moonshotai/Kimi-K2.7-Code': 9,
|
|
76
|
+
'zai-org/GLM-5.2-Fast': 9,
|
|
77
|
+
'MiniMaxAI/MiniMax-M3': 8,
|
|
78
|
+
'moonshotai/Kimi-K3': 8,
|
|
79
|
+
'deepseek/deepseek-v4-flash-fast': 8,
|
|
80
|
+
'Qwen/Qwen3.8-Max': 7,
|
|
81
|
+
'Qwen/Qwen3.8-Max-0902': 7,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
// Plans whose pools are documented but whose per-model tables were not captured.
|
|
85
|
+
'individual-go-v1': { poolUsd: 10, byModel: {} },
|
|
86
|
+
'individual-pro': { poolUsd: 30, byModel: {} },
|
|
87
|
+
'individual-pro-v1': { poolUsd: 80, byModel: {} },
|
|
88
|
+
'individual-provider': { poolUsd: 15, byModel: {} },
|
|
89
|
+
'individual-max': { poolUsd: 150, byModel: {} },
|
|
90
|
+
'individual-ultra': { poolUsd: 300, byModel: {} },
|
|
91
|
+
'teams-pro': { poolUsd: 40, byModel: {} },
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A model id without its vendor prefix (`z-ai/glm-5.3-flash` -> `glm-5.3-flash`). */
|
|
95
|
+
function bareModel(modelId) {
|
|
96
|
+
return typeof modelId === 'string' ? modelId.trim().toLowerCase().replace(/^[^/]+\//, '') : ''
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The monthly allowance advertised for one model on one plan.
|
|
101
|
+
*
|
|
102
|
+
* Matching is tolerant about the vendor prefix and casing, because the same model
|
|
103
|
+
* reaches the plugin as `deepseek/deepseek-v4.1-flash` from DSH and as
|
|
104
|
+
* `deepseek-v4.1-flash` in some payloads.
|
|
105
|
+
*
|
|
106
|
+
* @returns `{ allowanceUsd, poolUsd, matched, basis }` where `basis` is
|
|
107
|
+
* `'model'` when a per-model number was found and `'pool'` otherwise.
|
|
108
|
+
* `allowanceUsd` is `null` when the plan itself is unknown.
|
|
109
|
+
*/
|
|
110
|
+
export function commandcodeAllowance(planId, modelId) {
|
|
111
|
+
const plan = typeof planId === 'string' ? COMMANDCODE_PLAN_ALLOWANCES[planId.trim()] : undefined
|
|
112
|
+
if (plan === undefined) return { allowanceUsd: null, poolUsd: null, matched: '', basis: 'unknown' }
|
|
113
|
+
const wanted = bareModel(modelId)
|
|
114
|
+
if (wanted !== '') {
|
|
115
|
+
// Exact key first, then a bare-id match so a missing vendor prefix still hits.
|
|
116
|
+
for (const [key, usd] of Object.entries(plan.byModel)) {
|
|
117
|
+
if (key.toLowerCase() === wanted) return { allowanceUsd: usd, poolUsd: plan.poolUsd, matched: key, basis: 'model' }
|
|
118
|
+
}
|
|
119
|
+
for (const [key, usd] of Object.entries(plan.byModel)) {
|
|
120
|
+
if (bareModel(key) === wanted) return { allowanceUsd: usd, poolUsd: plan.poolUsd, matched: key, basis: 'model' }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { allowanceUsd: plan.poolUsd, poolUsd: plan.poolUsd, matched: '', basis: 'pool' }
|
|
124
|
+
}
|
package/lib/providers.mjs
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* fake zero.
|
|
19
19
|
*/
|
|
20
20
|
import { createHash, createHmac } from 'node:crypto'
|
|
21
|
+
import { commandcodeAllowance } from './commandcode-plans.mjs'
|
|
21
22
|
import { normalizePercent, parseTimestamp, UsageError } from './go-usage.mjs'
|
|
22
23
|
import {
|
|
23
24
|
XIAOMI_CONSOLE_URL,
|
|
@@ -486,7 +487,10 @@ export function parseOpencodeGo(payload, now = Date.now()) {
|
|
|
486
487
|
* why `usageSummary` is consulted); the windows' `used`/`cap` are USD, `resetAt`
|
|
487
488
|
* is epoch seconds or milliseconds.
|
|
488
489
|
*/
|
|
489
|
-
export function parseCommandCode(
|
|
490
|
+
export function parseCommandCode(
|
|
491
|
+
{ credits, subscription, whoami, usageSummary, selectedModel } = {},
|
|
492
|
+
now = Date.now(),
|
|
493
|
+
) {
|
|
490
494
|
const document = credits !== null && typeof credits === 'object' ? credits : {}
|
|
491
495
|
const credit = document.credits !== null && typeof document.credits === 'object' ? document.credits : {}
|
|
492
496
|
const limits = document.windowLimits !== null && typeof document.windowLimits === 'object'
|
|
@@ -563,13 +567,19 @@ export function parseCommandCode({ credits, subscription, whoami, usageSummary }
|
|
|
563
567
|
: [toNumber(limits.fiveHour?.used), toNumber(limits.weekly?.used)].filter((value) => value !== null)
|
|
564
568
|
const spend = periodSpend === null ? null : Math.max(periodSpend, ...liveWindowSpend)
|
|
565
569
|
const reportedGrant = toNumber(credit.monthlyCreditsGranted)
|
|
566
|
-
const
|
|
570
|
+
const poolGrant =
|
|
567
571
|
reportedGrant !== null && reportedGrant > 0
|
|
568
572
|
? reportedGrant
|
|
569
573
|
: remaining !== null && spend !== null
|
|
570
574
|
? remaining + spend
|
|
571
575
|
: null
|
|
572
|
-
|
|
576
|
+
// v1.7.2: the SELECTED MODEL's allowance sizes the month when the plan has a
|
|
577
|
+
// per-model table (GOAT + DeepSeek V4.1 Flash = $100 even though the credit
|
|
578
|
+
// pool is $70 — a discounted model stretches the same pool further). Without a
|
|
579
|
+
// known model or plan the pool is used, so nothing is invented.
|
|
580
|
+
const allowance = commandcodeAllowance(planId, selectedModel)
|
|
581
|
+
const granted = allowance.basis === 'model' && allowance.allowanceUsd !== null ? allowance.allowanceUsd : poolGrant
|
|
582
|
+
const monthlyUsed = poolGrant !== null && remaining !== null ? Math.max(0, poolGrant - remaining) : spend
|
|
573
583
|
const monthlyPercent =
|
|
574
584
|
granted !== null && granted > 0 && monthlyUsed !== null
|
|
575
585
|
? normalizePercent((monthlyUsed / granted) * 100)
|
|
@@ -596,10 +606,27 @@ export function parseCommandCode({ credits, subscription, whoami, usageSummary }
|
|
|
596
606
|
return {
|
|
597
607
|
windows,
|
|
598
608
|
plan,
|
|
609
|
+
// v1.7.2: the billing period the plan runs on. The statistics window is
|
|
610
|
+
// anchored to `start`, so 最近X天 counts from the day the plan was bought
|
|
611
|
+
// rather than from whenever this machine first saw the provider.
|
|
612
|
+
period: {
|
|
613
|
+
start: typeof data?.currentPeriodStart === 'string' ? data.currentPeriodStart : null,
|
|
614
|
+
end: typeof data?.currentPeriodEnd === 'string' ? data.currentPeriodEnd : null,
|
|
615
|
+
},
|
|
599
616
|
credits:
|
|
600
617
|
remaining === null && granted === null && purchased === null
|
|
601
618
|
? null
|
|
602
|
-
: {
|
|
619
|
+
: {
|
|
620
|
+
remaining,
|
|
621
|
+
// `granted` is the allowance the month is measured against, so the
|
|
622
|
+
// credits card and the monthly row agree. `poolUsd` keeps the plan's
|
|
623
|
+
// real credit pool visible when a model allowance is larger.
|
|
624
|
+
granted,
|
|
625
|
+
purchased,
|
|
626
|
+
poolUsd: poolGrant,
|
|
627
|
+
allowanceModel: allowance.basis === 'model' ? allowance.matched : null,
|
|
628
|
+
allowanceBasis: allowance.basis,
|
|
629
|
+
},
|
|
603
630
|
account:
|
|
604
631
|
profile === null
|
|
605
632
|
? null
|
|
@@ -1395,7 +1422,21 @@ export const ADAPTERS = {
|
|
|
1395
1422
|
? undefined
|
|
1396
1423
|
: fetchJson(joinUrl(config.baseURL, config.usageSummaryPath)).catch(() => undefined),
|
|
1397
1424
|
])
|
|
1398
|
-
|
|
1425
|
+
// v1.7.2: the model the user picked decides the monthly allowance (GOAT's
|
|
1426
|
+
// DeepSeek V4.1 Flash gets $100 while the credit pool stays $70).
|
|
1427
|
+
return {
|
|
1428
|
+
kind: 'windows',
|
|
1429
|
+
...parseCommandCode(
|
|
1430
|
+
{
|
|
1431
|
+
credits,
|
|
1432
|
+
subscription,
|
|
1433
|
+
whoami,
|
|
1434
|
+
usageSummary,
|
|
1435
|
+
selectedModel: typeof provider?.selectedModel === 'string' ? provider.selectedModel : '',
|
|
1436
|
+
},
|
|
1437
|
+
now,
|
|
1438
|
+
),
|
|
1439
|
+
}
|
|
1399
1440
|
},
|
|
1400
1441
|
},
|
|
1401
1442
|
deepseek: {
|
package/lib/usage-stats.mjs
CHANGED
|
@@ -1147,7 +1147,7 @@ export function createUsageStats(input) {
|
|
|
1147
1147
|
}
|
|
1148
1148
|
|
|
1149
1149
|
/** Compose DSH + external harnesses into the wire stats (v3). */
|
|
1150
|
-
async function compose(dsh, catalog, now) {
|
|
1150
|
+
async function compose(dsh, catalog, now, subscriptionStarts = {}) {
|
|
1151
1151
|
const thirtyStart = dsh.thirtyStart
|
|
1152
1152
|
const today = emptyBucket()
|
|
1153
1153
|
const last7Days = emptyBucket()
|
|
@@ -1372,14 +1372,14 @@ export function createUsageStats(input) {
|
|
|
1372
1372
|
* therefore excluded. A day with no positive duration is dropped instead of
|
|
1373
1373
|
* being given an infinite rate.
|
|
1374
1374
|
*/
|
|
1375
|
-
const speedSeries = (providerId) => {
|
|
1375
|
+
const speedSeries = (providerId, fromDay = thirtyStart) => {
|
|
1376
1376
|
const source = dsh.speed ?? {}
|
|
1377
1377
|
const byDay = []
|
|
1378
1378
|
let output = 0
|
|
1379
1379
|
let seconds = 0
|
|
1380
1380
|
let steps = 0
|
|
1381
1381
|
for (const day of Object.keys(source).sort()) {
|
|
1382
|
-
if (day <
|
|
1382
|
+
if (day < fromDay) continue
|
|
1383
1383
|
let dayOutput = 0
|
|
1384
1384
|
let daySeconds = 0
|
|
1385
1385
|
let daySteps = 0
|
|
@@ -1411,25 +1411,60 @@ export function createUsageStats(input) {
|
|
|
1411
1411
|
},
|
|
1412
1412
|
}
|
|
1413
1413
|
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Sum one provider's per-day buckets inside `[fromDay, today]` (v1.7.2).
|
|
1416
|
+
*
|
|
1417
|
+
* Used for the 最近X天 window so the label and the number describe the same
|
|
1418
|
+
* range even when that range is neither 30 days nor all time (a plan bought
|
|
1419
|
+
* three days ago). Returns null when no day in the range has a record, so the
|
|
1420
|
+
* caller can fall back instead of showing a fabricated zero.
|
|
1421
|
+
*/
|
|
1422
|
+
const sumRange = (providerId, fromDay) => {
|
|
1423
|
+
let found = false
|
|
1424
|
+
const bucket = emptyBucket()
|
|
1425
|
+
for (const entry of daily) {
|
|
1426
|
+
if (entry.day < fromDay) continue
|
|
1427
|
+
const row = entry.providers === undefined ? undefined : entry.providers[providerId]
|
|
1428
|
+
if (row === undefined) continue
|
|
1429
|
+
bucket.total += typeof row.total === 'number' ? row.total : 0
|
|
1430
|
+
bucket.messages += typeof row.messages === 'number' ? row.messages : 0
|
|
1431
|
+
found = true
|
|
1432
|
+
}
|
|
1433
|
+
return found ? bucket : null
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1414
1436
|
for (const provider of providers) {
|
|
1415
|
-
|
|
1437
|
+
// v1.7.2: a plan-backed provider's window starts on the day the plan was
|
|
1438
|
+
// bought (the Host reports it), not on the day this machine first recorded
|
|
1439
|
+
// it. Falling back to the first-record day keeps every other provider on
|
|
1440
|
+
// the old behaviour.
|
|
1441
|
+
const planStart = subscriptionStarts[provider.providerId]
|
|
1442
|
+
const effectiveDay =
|
|
1443
|
+
typeof planStart === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(planStart) && planStart <= todayDay
|
|
1444
|
+
? planStart
|
|
1445
|
+
: (firstDays.get(provider.providerId) ?? todayDay)
|
|
1416
1446
|
provider.effectiveDay = effectiveDay
|
|
1417
1447
|
// v1.7.0: the 最近X天 window is capped at the same 30 days the charts and
|
|
1418
1448
|
// the 最近30天 bucket use. A provider first seen 90 days ago used to report
|
|
1419
1449
|
// 最近90天 while every other figure on the page covered 30.
|
|
1420
|
-
|
|
1450
|
+
// v1.7.2: the cap applies to the WINDOW START, so the label and the summed
|
|
1451
|
+
// bucket always describe the same range (`effectiveDay` stays informative).
|
|
1452
|
+
const windowStart = effectiveDay < thirtyStart ? thirtyStart : effectiveDay
|
|
1453
|
+
const fullDays = dayCount(windowStart, todayDay)
|
|
1421
1454
|
const cappedDays = Math.min(DAILY_SERIES_DAYS, fullDays)
|
|
1422
1455
|
provider.effectiveDays = cappedDays
|
|
1423
|
-
// The label and the numbers must describe the SAME span.
|
|
1424
|
-
//
|
|
1425
|
-
//
|
|
1426
|
-
|
|
1427
|
-
provider.sinceEffective =
|
|
1428
|
-
cappedDays < fullDays ? cloneBucket(provider.last30Days) : cloneBucket(provider.allTime)
|
|
1456
|
+
// The label and the numbers must describe the SAME span (v1.7.2): sum the
|
|
1457
|
+
// per-day buckets inside [windowStart, today] rather than borrowing the
|
|
1458
|
+
// 30-day or all-time bucket, which described a different range.
|
|
1459
|
+
const windowBucket = sumRange(provider.providerId, windowStart)
|
|
1460
|
+
provider.sinceEffective = windowBucket === null ? cloneBucket(provider.last30Days) : windowBucket
|
|
1429
1461
|
// v1.7.0: each provider row carries its own output-speed series so the
|
|
1430
1462
|
// chart can follow the same scope switch as the token sections.
|
|
1431
|
-
|
|
1463
|
+
// v1.7.2: it uses the SAME window as the tokens above.
|
|
1464
|
+
provider.speed = speedSeries(provider.providerId, windowStart)
|
|
1432
1465
|
for (const harness of Object.values(provider.harnesses ?? {})) {
|
|
1466
|
+
// The daily series carries provider totals only, so the harness split
|
|
1467
|
+
// keeps the 30-day / all-time fallback rather than a wrong range sum.
|
|
1433
1468
|
harness.sinceEffective =
|
|
1434
1469
|
cappedDays < fullDays && harness.last30Days !== undefined
|
|
1435
1470
|
? cloneBucket(harness.last30Days)
|
|
@@ -1470,7 +1505,7 @@ export function createUsageStats(input) {
|
|
|
1470
1505
|
const work = (async () => {
|
|
1471
1506
|
const dsh = dshEnabled ? aggregate(scan()) : emptyDshAggregate(Date.now())
|
|
1472
1507
|
const catalog = await resolveProviderCatalog(Date.now())
|
|
1473
|
-
const result = await compose(dsh, catalog, Date.now())
|
|
1508
|
+
const result = await compose(dsh, catalog, Date.now(), options.subscriptionStarts ?? {})
|
|
1474
1509
|
lastResult = result
|
|
1475
1510
|
lastResultAt = Date.now()
|
|
1476
1511
|
return result
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tokenlens",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
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",
|