dsh-tokenlens 1.7.5 → 1.7.6

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 CHANGED
@@ -4,6 +4,18 @@
4
4
 
5
5
  **版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
6
6
 
7
+ ## 1.7.6 — 2026-11-06
8
+
9
+ (当日已发布版本:v1.6.5 / v1.6.6 / v1.6.7 / v1.7.0 / v1.7.1 / v1.7.2 / v1.7.3 / v1.7.4 / v1.7.5 / v1.7.6)
10
+
11
+ - **feat(ui): 版本号左侧新增仓库链接**——弹窗底部一行现在是 `[GitHub] 插件 v1.7.6 · 已是最新版本`,链接指向 `github.com/senyingxidi/dsh-tokenlens`,`target=_blank` + `rel=noopener`,带 `title` 说明;图标沿用同一套 stroke 图标(`aria-hidden`,对读屏透明)
12
+ - **fix(ui): TPS 悬停索引不再跨视图残留**——`speedHover` 原先只存在默认状态里,切换「当前供应商 / 全部 Harness」、切换「数据 / 图表」、切换供应商时都不会重置,于是同一个索引会指向新序列里的另一天(或越界)。现在它与 `chartHover` 一起被清理
13
+ - **fix(ui): 单点 TPS 曲线的命中区溢出**——只有一天样本时命中列从负 x 开始,左半区域无法触发悬停;现在把它夹进 viewBox
14
+ - **fix(stats): 启动期统计结果可能带着未锚定的窗口被缓存**——重启后「套餐起点」要等第一次额度读取才知道,而在此之前算出的统计结果会按 TTL 一直复用。现在 Host 在锚点集合变化后强制重算一次
15
+ - **perf: 模型切换不再无谓重取**——只有额度真正随模型变化的供应商(当前仅 `commandcode`)才会在切换模型时丢弃快照;在 DeepSeek / OpenCode 等模型无关的供应商上切模型不再触发上游请求。客户端也只在 `commandcode` 下补发一次快照
16
+ - 文档:`PROTOCOL.md` 补上 `credits.poolUsd / allowanceUsd / allowanceModel / allowanceBasis`、`speed` 序列,以及 v1.7.2 的窗口锚定与 v1.7.3 的分母结论
17
+ - 测试:370 → 373(仓库链接的位置/属性/图标、悬停状态清理、按供应商差异化的缓存失效)
18
+
7
19
  ## 1.7.5 — 2026-11-06
8
20
 
9
21
  (当日已发布版本:v1.6.5 / v1.6.6 / v1.6.7 / v1.7.0 / v1.7.1 / v1.7.2 / v1.7.3 / v1.7.4 / v1.7.5)
package/PROTOCOL.md CHANGED
@@ -84,7 +84,15 @@ from `dsh-app://app/` and forwards non-asset paths to the Host).
84
84
  "monthly": { … } | null
85
85
  } | null, // null for balance-only providers
86
86
  "balance": { "amount": 2.26, "currency": "CNY", "granted": 0, "toppedUp": 2.26 } | null,
87
- "credits": { "remaining": 4.5, "granted": 10, "purchased": 0 } | null,
87
+ "credits": {
88
+ "remaining": 4.5, "granted": 10, "purchased": 0,
89
+ // v1.7.2: the SELECTED model's advertised allowance. Advertising only: the
90
+ // monthly percentage is always measured against the credit pool (v1.7.3),
91
+ // because that is what the provider's own console does.
92
+ "poolUsd": 10, "allowanceUsd": 14,
93
+ "allowanceModel": "deepseek/deepseek-v4.1-flash" | null,
94
+ "allowanceBasis": "model" | "pool" | "unknown"
95
+ } | null,
88
96
  "account": { "name": "…", "userName": "…" } | null, // non-secret display info
89
97
  "links": { "console": "https://opencode.ai/console" } | null, // provider-owned web page, follows the selection
90
98
  "fetchedAt": "2026-10-25T09:03:00.000Z",
@@ -129,9 +137,13 @@ plugin never sizes a month from a plan catalog.
129
137
 
130
138
  **The denominator is deliberately model-independent.** GOAT advertises "up to $100" and lists
131
139
  per-model allowances ($100 on DeepSeek V4.1 Flash, $60 on several others) because the same
132
- credits stretch further on a discounted model; the pool itself is one number. Letting the
133
- picker change the denominator would move the meter while the account did not change, so
134
- `parseCommandCode` takes no model input.
140
+ credits stretch further on a discounted model; the pool itself is one number. Measured against
141
+ the provider's own console: with $2.94 spent it prints 每月限额 **4%**, i.e. `2.94 / 70 = 4.2%`;
142
+ `2.94 / 100` would print 2.9%. So the percentage is pool-based, and the per-model allowance is
143
+ reported as `credits.allowanceUsd` for context only (v1.7.3). The model *is* forwarded with the
144
+ request (`session.modelSelection.next.model`) so that the allowance can be resolved, and the Host
145
+ drops the cached snapshot on a model switch **only** for providers whose allowance actually
146
+ follows the model (`MODEL_SENSITIVE_ALLOWANCE`, currently `commandcode`).
135
147
 
136
148
  `windowLimits.limited` is a **capability flag** ("this plan paces you with rolling windows"),
137
149
  not a state: the live payload carries `limited: true` together with `fiveHour.exceeded: false`.
@@ -311,7 +323,11 @@ provider effective-date fallback (`effectiveDay`, `effectiveDays`,
311
323
  folder basename, used by the client to match a sidebar hover card; the full
312
324
  `cwd` is Host-only and never sent on the wire), the rolling 5-hour local window
313
325
  (`fiveHours`, DSH-only because external harnesses are day-granular), and the
314
- bounded `daily[]` series used by the chart view. Absolute local paths are
326
+ bounded `daily[]` series used by the chart view, the `speed` series behind the
327
+ TPS trend (v1.7.0), and the subscription window (`effectiveDay` is the plan's
328
+ period start when the Host learned it, and `sinceEffective` is summed from the
329
+ per-day buckets inside `[window start, today]` rather than borrowed from the
330
+ 30-day or all-time bucket — v1.7.2). Absolute local paths are
315
331
  stripped from the wire; free-form error messages have path substrings replaced
316
332
  with `[path]`.
317
333
 
@@ -357,7 +373,7 @@ with `[path]`.
357
373
  "last30Days": { "total": …, "messages": … },
358
374
  "allTime": { "total": …, "messages": … } }
359
375
  ],
360
- "daily": [ // <=120 days, ascending; chart series
376
+ "daily": [ // <=30 days (v1.7.0), ascending; chart series
361
377
  { "day": "2026-10-25",
362
378
  "total": { "total": 123456, "messages": 42 },
363
379
  "hours": [0, 0, 0, 1234, 5678, …], // 24 integers: local-hour token totals
@@ -366,6 +382,20 @@ with `[path]`.
366
382
  "hours": [0, 0, 0, 500, 734, …] }
367
383
  } }
368
384
  ],
385
+ // v1.7.0: output-speed (TPS) series. `tps` is output tokens over the summed
386
+ // model time of the steps that produced them (`step/start` ->
387
+ // `assistant/message`), so a step's tool calls — which are emitted after the
388
+ // message — do not dilute it. A day with no positive duration is omitted
389
+ // instead of being given an infinite rate. `total.tps` is null when the
390
+ // window holds no samples. Each provider row carries its own `speed` object
391
+ // with the same shape, following the same window as its tokens (v1.7.2).
392
+ "speed": {
393
+ "byDay": [
394
+ { "day": "2026-11-05", "outputTokens": 516951, "seconds": 8493.1,
395
+ "tps": 60.9, "steps": 859 }
396
+ ],
397
+ "total": { "outputTokens": 939643, "seconds": 14123.9, "tps": 66.5, "steps": 1424 }
398
+ },
369
399
  "zcode": { // extra-harness detail
370
400
  "dbPath": "C:\\Users\\…\\.zcode\\cli\\db\\db.sqlite",
371
401
  "available": true, "error": null, "rows": 65, "cached": false,
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 370 tests.
147
+ Node.js ≥ 24, zero runtime dependencies: `node --test` runs the 373 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`:**370 个自动化用例**,覆盖 Host 数据层、供应商适配器(含小米 MiMo 控制台 Cookie 适配器、Command Code 月度额度反推)、统计折叠、各 harness 读取、Design Plan 周期账本与额度计价、TPS 速度序列、窗口锚点切片、工作区自动识别、插件集成与浏览器端渲染 / 交互。
448
+ - `node --test`:**373 个自动化用例**,覆盖 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
@@ -165,6 +165,8 @@ window.__ModuleLoader__.load({
165
165
  }
166
166
  /** The npm package page the 「打开 npm」 update links point at. */
167
167
  const NPM_PACKAGE_URL = 'https://www.npmjs.com/package/dsh-tokenlens'
168
+ /** The source repository, linked to the left of the version (v1.7.6). */
169
+ const REPO_URL = 'https://github.com/senyingxidi/dsh-tokenlens'
168
170
 
169
171
  const zh = {
170
172
  title: '词元透镜(dsh-TokenLens)',
@@ -272,6 +274,8 @@ window.__ModuleLoader__.load({
272
274
  capacityTokenNoEstimate: '预计可消耗 —',
273
275
  capacityTokenEstimateHint: '本机 Token 记录 ÷ 当前套餐百分比估算,仅供参考',
274
276
  pluginVersion: '插件 v{version}',
277
+ repoLink: 'GitHub',
278
+ repoLinkHint: '在 GitHub 上打开 dsh-TokenLens 仓库(源码、更新日志、问题反馈)',
275
279
  statsToday: '今日',
276
280
  statsAllTime: '总计',
277
281
  statsInput: '输入',
@@ -547,6 +551,8 @@ window.__ModuleLoader__.load({
547
551
  capacityTokenNoEstimate: 'Est. quota —',
548
552
  capacityTokenEstimateHint: 'Local token records ÷ current plan percentage; estimate only',
549
553
  pluginVersion: 'Plugin v{version}',
554
+ repoLink: 'GitHub',
555
+ repoLinkHint: 'Open the dsh-TokenLens repository on GitHub (source, changelog, issues)',
550
556
  statsToday: 'Today',
551
557
  statsAllTime: 'All time',
552
558
  statsInput: 'Input',
@@ -774,6 +780,10 @@ window.__ModuleLoader__.load({
774
780
  windows: ['M2.6 3.4h10.8v9.2H2.6z', 'M8 3.4v9.2', 'M2.6 7.9h10.8'],
775
781
  coin: ['M8 2.4a5.6 5.6 0 1 0 0 11.2A5.6 5.6 0 0 0 8 2.4Z', 'M8 5.2v5.6', 'M6.3 6.6h3.4', 'M6.3 9.4h3.4'],
776
782
  check: ['M3 8.4 6.2 11.4 13 4.4'],
783
+ // v1.7.6: the repository mark, drawn stroke-only like every other icon.
784
+ github: [
785
+ 'M8 2.4a5.6 5.6 0 0 0-1.7 10.9c.3.1.4-.1.4-.3v-1c-1.5.3-1.9-.7-1.9-.7-.2-.6-.6-.7-.6-.7-.5-.4 0-.4 0-.4.5 0 .8.5.8.5.5.9 1.3.6 1.7.5 0-.4.2-.7.3-.8-1.3-.1-2.6-.6-2.6-2.7 0-.6.2-1.1.6-1.5-.1-.1-.2-.7 0-1.4 0 0 .5-.2 1.6.6a5.4 5.4 0 0 1 2.8 0c1.1-.8 1.6-.6 1.6-.6.3.7.1 1.3.1 1.4.3.4.5.9.5 1.5 0 2.1-1.3 2.6-2.6 2.7.2.2.4.5.4 1v1.5c0 .2.1.4.4.3A5.6 5.6 0 0 0 8 2.4Z',
786
+ ],
777
787
  alert: ['M8 2.5 14 13.2H2z', 'M8 6.2v3.2', 'M8 11.3h.01'],
778
788
  spinner: ['M8 2.5a5.5 5.5 0 1 1-5.5 5.5', 'M8 5.5v2.8'],
779
789
  ban: ['M3.2 3.2 12.8 12.8', 'M8 2.4a5.6 5.6 0 1 0 0 11.2A5.6 5.6 0 0 0 8 2.4Z'],
@@ -2206,6 +2216,9 @@ window.__ModuleLoader__.load({
2206
2216
  detailsWindow: settings.detailsWindow,
2207
2217
  chartHover: null,
2208
2218
  chartDay: null,
2219
+ // v1.7.6: the TPS hover index is view state like the chart's, so it must
2220
+ // not survive a provider switch (the series behind it changed).
2221
+ speedHover: null,
2209
2222
  }
2210
2223
  }
2211
2224
 
@@ -3312,7 +3325,12 @@ window.__ModuleLoader__.load({
3312
3325
  if (modelChanged) store.set({ modelId: nextModel })
3313
3326
  syncProvider(providerId)
3314
3327
  const after = store.get()
3315
- if (modelChanged && after.providerId !== null && after.inFlight !== true) {
3328
+ // v1.7.6: only Command Code's allowance follows the model, so only its
3329
+ // snapshots are worth refetching on a switch (see index.js
3330
+ // MODEL_SENSITIVE_ALLOWANCE). Everyone else keeps the cached snapshot.
3331
+ const modelSensitive =
3332
+ after.providerId !== null && canonicalProviderId(after.providerId) === 'commandcode'
3333
+ if (modelChanged && modelSensitive && after.inFlight !== true) {
3316
3334
  void requestUsage('snapshot')
3317
3335
  }
3318
3336
  }
@@ -3614,7 +3632,11 @@ window.__ModuleLoader__.load({
3614
3632
  .dsh-tokenlens-chart-drill-title{color:var(--dsw-alias-label-secondary);font-size:var(--dsw-alias-font-size-sm,12px);line-height:1.4;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums}
3615
3633
  /* 明细 donut window switch (v1.2.0): one pill row under the details title. */
3616
3634
  .dsh-tokenlens-details-window{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}
3617
- .dsh-tokenlens-version{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;text-align:right;opacity:.75;overflow-wrap:anywhere}
3635
+ .dsh-tokenlens-version{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;text-align:right;opacity:.75;overflow-wrap:anywhere;display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:wrap}
3636
+ /* v1.7.6: the repository link sits left of the version text. */
3637
+ .dsh-tokenlens-repo{display:inline-flex;align-items:center;gap:4px;color:inherit;text-decoration:none;flex:0 0 auto;border-radius:var(--dsw-radius-sm);transition:color .12s ease,opacity .12s ease}
3638
+ .dsh-tokenlens-repo:hover,.dsh-tokenlens-repo:focus-visible{color:var(--dsw-alias-label-primary,currentColor);opacity:1;text-decoration:underline}
3639
+ .dsh-tokenlens-version-text{flex:0 0 auto}
3618
3640
  /* Update status on the plugin-version line (dialog body): wraps on narrow screens. */
3619
3641
  .dsh-tokenlens-version-update{white-space:normal;overflow-wrap:anywhere}
3620
3642
  .dsh-tokenlens-version .dsh-tokenlens-update-link{color:inherit;font-size:inherit;vertical-align:baseline}
@@ -4495,7 +4517,7 @@ window.__ModuleLoader__.load({
4495
4517
  'data-scope': scope,
4496
4518
  'data-active': effective === scope ? 'true' : 'false',
4497
4519
  disabled: stats === null,
4498
- onClick: () => store.set({ statsScope: scope, chartHover: null, chartDay: null }),
4520
+ onClick: () => store.set({ statsScope: scope, chartHover: null, chartDay: null, speedHover: null }),
4499
4521
  },
4500
4522
  label,
4501
4523
  )
@@ -4521,7 +4543,7 @@ window.__ModuleLoader__.load({
4521
4543
  'data-active': active === view ? 'true' : 'false',
4522
4544
  'aria-pressed': active === view ? 'true' : 'false',
4523
4545
  onClick: () => {
4524
- if (active !== view) store.set({ statsView: view, chartHover: null, chartDay: null })
4546
+ if (active !== view) store.set({ statsView: view, chartHover: null, chartDay: null, speedHover: null })
4525
4547
  },
4526
4548
  },
4527
4549
  Icon(iconName, 12),
@@ -6173,9 +6195,11 @@ window.__ModuleLoader__.load({
6173
6195
  'rect',
6174
6196
  {
6175
6197
  key: `hit-${entry.day}-${index}`,
6176
- x: (points[index].x - columnWidth / 2).toFixed(1),
6198
+ // v1.7.6: clamp the hit column into the viewBox — a single-day
6199
+ // series used to start at a negative x and lose half its area.
6200
+ x: Math.max(0, Math.min(width - 1, points[index].x - columnWidth / 2)).toFixed(1),
6177
6201
  y: 0,
6178
- width: Math.max(1, columnWidth).toFixed(1),
6202
+ width: Math.max(1, Math.min(columnWidth, width)).toFixed(1),
6179
6203
  height: String(height),
6180
6204
  fill: 'transparent',
6181
6205
  'data-speed-day': entry.day,
@@ -7804,11 +7828,29 @@ window.__ModuleLoader__.load({
7804
7828
  const body = [buildManageButton(state, t), statsArea]
7805
7829
  const displayVersion = currentPluginVersionOf(state)
7806
7830
  if (displayVersion !== '') {
7831
+ // v1.7.6: the repository link sits to the LEFT of the version, so the
7832
+ // version stays the right-most element of the row.
7833
+ const repo = h(
7834
+ 'a',
7835
+ {
7836
+ key: 'repo',
7837
+ className: 'dsh-tokenlens-repo',
7838
+ href: REPO_URL,
7839
+ target: '_blank',
7840
+ rel: 'noreferrer noopener',
7841
+ 'data-repo-link': 'true',
7842
+ title: t('repoLinkHint'),
7843
+ 'aria-label': t('repoLinkHint'),
7844
+ },
7845
+ Icon('github', 13),
7846
+ t('repoLink'),
7847
+ )
7807
7848
  body.push(
7808
7849
  h(
7809
7850
  'div',
7810
7851
  { key: 'version', className: 'dsh-tokenlens-version' },
7811
- t('pluginVersion', { version: displayVersion }),
7852
+ repo,
7853
+ h('span', { key: 'text', className: 'dsh-tokenlens-version-text' }, t('pluginVersion', { version: displayVersion })),
7812
7854
  // Update status sits right next to the plugin version: 有新版本 vX
7813
7855
  // (link to the npm page) or 已是最新版本, only for a successful check.
7814
7856
  buildVersionUpdateStatus(state, t),
package/index.js CHANGED
@@ -84,7 +84,16 @@ 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.5'
87
+ export const PLUGIN_VERSION = '1.7.6'
88
+
89
+ /**
90
+ * Providers whose monthly allowance depends on the SELECTED model (v1.7.6).
91
+ *
92
+ * Only these need their cached snapshot dropped when the picker changes: for
93
+ * everyone else a model switch does not alter a single number, so refetching
94
+ * would be pure waste.
95
+ */
96
+ const MODEL_SENSITIVE_ALLOWANCE = new Set(['commandcode'])
88
97
 
89
98
  /** Documented defaults; every field of the raw row config is optional. */
90
99
  const DEFAULTS = {
@@ -1460,8 +1469,7 @@ export function apply(ctx, rawConfig) {
1460
1469
  return catalog
1461
1470
  }
1462
1471
 
1463
- const statsService = config.stats.enabled
1464
- ? createUsageStats({
1472
+ const statsService = config.stats.enabled ? createUsageStats({
1465
1473
  dshHome: config.stats.dshHome !== '' ? config.stats.dshHome : dshHomeOf(process.env),
1466
1474
  config,
1467
1475
  providerCatalog: providerModelCatalog,
@@ -1614,6 +1622,14 @@ export function apply(ctx, rawConfig) {
1614
1622
  return scrubMessagePaths(out)
1615
1623
  }
1616
1624
 
1625
+ /**
1626
+ * The subscription-anchor set the last stats result was computed with (v1.7.6).
1627
+ *
1628
+ * Empty until a plan period start is known; a change invalidates the cached
1629
+ * stats result so the 最近X天 window is anchored immediately.
1630
+ */
1631
+ let statsAnchorSignature = '[]'
1632
+
1617
1633
  async function getStats(options = {}) {
1618
1634
  if (statsService === null) {
1619
1635
  return {
@@ -1622,11 +1638,18 @@ export function apply(ctx, rawConfig) {
1622
1638
  }
1623
1639
  }
1624
1640
  try {
1641
+ const starts = Object.fromEntries(state.subscriptionStarts)
1642
+ // v1.7.6: a cached stats result computed BEFORE a plan period start was
1643
+ // known would keep the un-anchored window until the TTL expired. Recompute
1644
+ // once whenever the anchor set changes, so the window is right immediately.
1645
+ const signature = JSON.stringify(Object.entries(starts).sort())
1646
+ const anchorChanged = signature !== statsAnchorSignature
1625
1647
  const stats = await statsService.get({
1626
- fresh: options.fresh === true,
1648
+ fresh: options.fresh === true || (signature !== '[]' && anchorChanged),
1627
1649
  // v1.7.2: anchor each subscription provider's window to its plan start.
1628
- subscriptionStarts: Object.fromEntries(state.subscriptionStarts),
1650
+ subscriptionStarts: starts,
1629
1651
  })
1652
+ if (signature !== '[]') statsAnchorSignature = signature
1630
1653
  // v0.22.1: only subscription-backed providers use the 最近X天 window;
1631
1654
  // pay-as-you-go/balance providers keep the fixed 最近30天 column.
1632
1655
  const decorated =
@@ -1835,6 +1858,10 @@ export function apply(ctx, rawConfig) {
1835
1858
  * V4.1 Flash $100 while its credit pool is $70), so a model switch must also
1836
1859
  * drop the cached snapshot — otherwise the chip would keep showing the old
1837
1860
  * allowance until the next poll.
1861
+ *
1862
+ * v1.7.6: only for providers whose allowance is actually model-sensitive.
1863
+ * Invalidating on every provider made switching models on DeepSeek/OpenCode
1864
+ * throw away a perfectly good snapshot and refetch it.
1838
1865
  */
1839
1866
  function rememberRequestedModel(body, providerId) {
1840
1867
  const modelId = typeof body?.modelId === 'string' ? body.modelId.trim() : ''
@@ -1842,8 +1869,10 @@ export function apply(ctx, rawConfig) {
1842
1869
  const id = canonicalProviderId(providerId)
1843
1870
  if (state.selectedModels.get(id) === modelId) return
1844
1871
  state.selectedModels.set(id, modelId)
1845
- state.caches.delete(id)
1846
- state.flights.delete(id)
1872
+ if (MODEL_SENSITIVE_ALLOWANCE.has(id)) {
1873
+ state.caches.delete(id)
1874
+ state.flights.delete(id)
1875
+ }
1847
1876
  noteStatus('client-model', { providerId: id, modelId })
1848
1877
  }
1849
1878
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tokenlens",
3
- "version": "1.7.5",
3
+ "version": "1.7.6",
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",