dsh-tokenlens 1.4.0 → 1.5.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 +13 -0
- package/PROTOCOL.md +42 -0
- package/README.md +3 -1
- package/client.js +105 -1
- package/index.js +64 -1
- package/lib/design-plan-ledger.mjs +266 -0
- package/lib/go-usage.mjs +4 -1
- package/lib/providers.mjs +28 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
**版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
|
|
6
6
|
|
|
7
|
+
## 1.5.0 — 2026-11-02
|
|
8
|
+
|
|
9
|
+
(当日已发布版本:v1.3.5 / v1.3.6 / v1.3.7 / v1.3.8 / v1.3.9 / v1.4.0 / v1.5.0)
|
|
10
|
+
|
|
11
|
+
- **feat(open-design): 本机累加 Design Plan 周期用量估算**——Open Design 只提供滚动的 5 小时 / 7 天窗口,**没有「本账期已用」接口**(控制台的 `/billing/usage`、`/billing/coding-plan-usage` 都是 cookie 会话鉴权,Control Key 一律 401)。插件改为累加它观测到的 7 天窗口:每个窗口实例记录历史最大 `usedCredits`,把同一账期内已完成 + 当前的窗口相加,再按 `creditsPerUsd` 折算成美元
|
|
12
|
+
- feat(open-design): **只统计 7 天窗口**——5 小时窗口测量的是同一笔消耗(实测两者 `usedCredits` 完全相同),相加会双重计数
|
|
13
|
+
- feat(open-design): 估算带**覆盖率标注**——`complete: true` 仅当最早观测到的窗口起点等于账期起点;否则显示「仅统计已观测的 N 个窗口」,绝不把下限当完整值
|
|
14
|
+
- feat(wire): snapshot 升到 v4,新增可选 `designPlan: {periodStart, periodEnd, usedCredits, estimatedUsd, windows, complete, since, limitPerWindow}`(仅 open-design);原始上游窗口行不下发
|
|
15
|
+
- feat(storage): 新增 `$DSH_HOME/dsh-tokenlens/design-plan-ledger.json`(原子写、0600、仅数字与时间戳、保留 6 个账期);写入失败只降级为内存折叠,绝不影响用量读取
|
|
16
|
+
- feat(ui): 容量卡片新增一行「本期 Design Plan 累计 $X(≈ N 个周额度 · 已覆盖本账期全部窗口 / 仅统计已观测的 N 个窗口)」,悬浮说明「Open Design 没有月度用量接口,此值由本机累加观测得出,仅供参考」
|
|
17
|
+
- feat(config): `designPlanLedgerFile` 可配置(空 = 默认路径)
|
|
18
|
+
- 测试:292 → 307(ledger 单测 10 个:只记周窗口不双计、同窗口取最大值、跨窗口累加、完整性判定、账期切换清零、磁盘往返与历史上限;Host 端到端:三次读累积与瞬时回落不降值、原始行不下发、写入失败仍出估算;客户端:完整/部分两种覆盖率渲染、其它供应商不渲染该行)
|
|
19
|
+
|
|
7
20
|
## 1.4.0 — 2026-11-02
|
|
8
21
|
|
|
9
22
|
(当日已发布版本:v1.3.5 / v1.3.6 / v1.3.7 / v1.3.8 / v1.3.9 / v1.4.0)
|
package/PROTOCOL.md
CHANGED
|
@@ -138,6 +138,48 @@ HTTP 401 there (the console UI itself authenticates with a session cookie), so i
|
|
|
138
138
|
is only a fallback. The wallet is therefore always read first; the plan endpoint
|
|
139
139
|
must not turn a readable balance into a hard error.
|
|
140
140
|
|
|
141
|
+
### Design Plan period estimate (v1.5.0, open-design only)
|
|
142
|
+
|
|
143
|
+
Open Design publishes a **monthly** cap (Go: $120/month = 4 × the $30 weekly
|
|
144
|
+
window) but serves no "used this period" endpoint — not even cookie-free reads
|
|
145
|
+
(`/billing/usage` and `/billing/coding-plan-usage` both answer 401 to a Control
|
|
146
|
+
Key). So `snapshot.designPlan` (v4 wire) is a **local estimate** the Host derives
|
|
147
|
+
by observing the weekly windows it sees:
|
|
148
|
+
|
|
149
|
+
```jsonc
|
|
150
|
+
"designPlan": {
|
|
151
|
+
"periodStart": "2026-11-02T09:53:18.000Z", // billing period (billing/summary)
|
|
152
|
+
"periodEnd": "2026-12-02T09:53:18.000Z",
|
|
153
|
+
"usedCredits": 50025, // Σ final+current weekly windows
|
|
154
|
+
"estimatedUsd": 5.0025, // usedCredits / creditsPerUsd
|
|
155
|
+
"windows": 1, // weekly windows observed
|
|
156
|
+
"complete": true, // earliest window starts at periodStart
|
|
157
|
+
"since": "2026-11-02T09:53:18.000Z", // first window actually observed
|
|
158
|
+
"limitPerWindow": 150000 // the account's real weekly cap
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Rules the Host guarantees:
|
|
163
|
+
|
|
164
|
+
- **Only the 7-day window is counted.** The 5-hour window measures the same spend
|
|
165
|
+
(live: both report identical `usedCredits`), so summing them double-counts.
|
|
166
|
+
- Within one window instance (`windowStart`) the stored `usedCredits` only ever
|
|
167
|
+
grows to the **maximum** observed; a transient smaller read never lowers it.
|
|
168
|
+
- A window that disappears from the upstream payload keeps its last (final) value.
|
|
169
|
+
- `complete` is true only when the earliest observed window starts at
|
|
170
|
+
`periodStart`. Otherwise earlier windows of the period were never seen and the
|
|
171
|
+
number is a **lower bound** — the UI must say so.
|
|
172
|
+
- Persisted in `$DSH_HOME/dsh-tokenlens/design-plan-ledger.json` (atomic, 0600,
|
|
173
|
+
numbers/timestamps only, 6 periods retained). A write failure degrades to an
|
|
174
|
+
in-memory fold and never fails the usage read.
|
|
175
|
+
- The raw upstream window rows never cross the wire; only the summary above does.
|
|
176
|
+
- `designPlan` is absent on every other provider and on an older Host.
|
|
177
|
+
|
|
178
|
+
Note the account's real caps may differ from the public pricing table (this
|
|
179
|
+
machine reports 50,000 credits per 5h / 150,000 per 7d = $5 / $15 at
|
|
180
|
+
`creditsPerUsd: 10000`, half of the advertised $10 / $30), so the summary reports
|
|
181
|
+
`limitPerWindow` as returned rather than assuming a published number.
|
|
182
|
+
|
|
141
183
|
Rules:
|
|
142
184
|
|
|
143
185
|
- `percent` is clamped to `0..100`; an unknown percentage is `null` (render "—"), never a fake 0.
|
package/README.md
CHANGED
|
@@ -258,6 +258,7 @@ Control Key 是 **Open Design 桌面端/CLI 登录时写入本机的会话凭据
|
|
|
258
258
|
pollIntervalMs: 120000 # 后台轮询间隔(毫秒);0 = 关闭轮询
|
|
259
259
|
statusFile: '' # 诊断状态 JSON 路径;空 = 不写文件
|
|
260
260
|
providerStoreFile: '' # 空 = $DSH_HOME/dsh-tokenlens/providers.json
|
|
261
|
+
designPlanLedgerFile: '' # 空 = $DSH_HOME/dsh-tokenlens/design-plan-ledger.json
|
|
261
262
|
stats:
|
|
262
263
|
enabled: true
|
|
263
264
|
ttlMs: 30000 # 统计结果复用时长;fresh:true 强制重扫
|
|
@@ -326,6 +327,7 @@ Control Key 是 **Open Design 桌面端/CLI 登录时写入本机的会话凭据
|
|
|
326
327
|
- OpenCode 的额度定义见[官方文档](https://opencode.ai/docs/go/#usage-limits)(5h = 月度额度的 20%、周 = 50%、月 = 100%);接口只返回百分比,插件也只画百分比。
|
|
327
328
|
- Command Code 的 `/alpha/*` 是官方 CLI 自用的私有计费路由;免费 / 无方案账户返回空窗口(显示 `—`),插件不会用方案目录猜额度。
|
|
328
329
|
- Open Design 的窗口按账户实际返回的时长渲染:Go 同时有 5 小时和 7 天窗口,Plus / Pro / Max 只有 7 天窗口;钱包余额是独立于窗口的 USD 额度,用尽后继续扣钱包。查询要求工作区权限,鉴权失败会明确显示错误而不是 0;工作区 ID 与 Control Key 缺任一项时,弹窗会指出缺哪一项并给出「去配置」入口(v1.3.5)。窗口、余额、套餐档位是**三层独立读取**(v1.4.0):任一接口不可用时只少那一块数据,不会整套报错。
|
|
330
|
+
- **「本期 Design Plan 累计」是插件本机算的估算值**(v1.5.0)。Open Design 有月度额度(Go 为每月 $120 = 4 × 每周 $30),但**没有「本账期已用」接口**——控制台的 `/billing/usage`、`/billing/coding-plan-usage` 都靠浏览器 Cookie 鉴权,Control Key 一律 401。于是插件累加它观测到的 **7 天窗口**(只记 7 天,因为 5 小时窗口测量的是同一笔消耗,相加会重复计数),按 `creditsPerUsd` 折算成美元;弹窗会标注覆盖情况(「已覆盖本账期全部窗口」或「仅统计已观测的 N 个窗口」),插件没运行期间错过的窗口无法计入。
|
|
329
331
|
- 未配置凭据的供应商 chip 静默隐藏;`zai`、`minimax`、`deepinfra` 属 best-effort 适配(未公开接口,失败时按可重试错误优雅降级)。
|
|
330
332
|
- 各供应商的元数据(`kind` / `label` / `shortLabel` / `baseURL` / `apiKeyRef` / `consoleUrl`)集中在 `lib/providers.mjs` 的 `PROVIDER_DEFAULTS` 与 `ADAPTERS` 中,新增适配器只需在该文件登记并补测试。
|
|
331
333
|
|
|
@@ -437,7 +439,7 @@ Host 半注册 `tokenlens_usage`,参数 `action`(默认 `snapshot`)与可
|
|
|
437
439
|
|
|
438
440
|
## 验证
|
|
439
441
|
|
|
440
|
-
- `node --test`:**
|
|
442
|
+
- `node --test`:**307 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、Design Plan 周期账本、插件集成与浏览器端渲染 / 交互。
|
|
441
443
|
- 在真实环境中验证过 **5 个 harness** 的数据源;本机统计与独立的 SQLite 查询、独立的日志解析逐项对账(差额为 0),跨 harness 合并与供应商映射算术自洽。
|
|
442
444
|
- 供应商接口按各官方文档与参考客户端行为验证;未配置凭据时 chip 静默隐藏,接口异常时按可重试错误降级。
|
|
443
445
|
|
package/client.js
CHANGED
|
@@ -339,6 +339,12 @@ window.__ModuleLoader__.load({
|
|
|
339
339
|
providerControlKeySaved: '已保存;要替换请重新粘贴',
|
|
340
340
|
configureProvider: '去配置',
|
|
341
341
|
errorOpenDesignConfig: 'Open Design 额度查询需要工作区 ID 与 Control Key(不是推理 Key)',
|
|
342
|
+
designPlanLabel: '本期 Design Plan 累计',
|
|
343
|
+
designPlanHint:
|
|
344
|
+
'Open Design 没有月度用量接口,此值由插件在本机累加观测到的 7 天窗口得出,仅供参考;未观测到的窗口不会计入',
|
|
345
|
+
designPlanWindowsOf: '≈ {count} 个周额度',
|
|
346
|
+
designPlanComplete: '已覆盖本账期全部窗口',
|
|
347
|
+
designPlanPartial: '仅统计已观测的 {count} 个窗口',
|
|
342
348
|
providerSave: '保存',
|
|
343
349
|
providerSaving: '保存中…',
|
|
344
350
|
providerTest: '测试连接',
|
|
@@ -594,6 +600,12 @@ window.__ModuleLoader__.load({
|
|
|
594
600
|
providerControlKeySaved: 'Saved; paste again to replace it',
|
|
595
601
|
configureProvider: 'Configure',
|
|
596
602
|
errorOpenDesignConfig: 'Open Design quota needs a workspace ID and a Control Key (not an inference key)',
|
|
603
|
+
designPlanLabel: 'Period Design Plan (estimate)',
|
|
604
|
+
designPlanHint:
|
|
605
|
+
'Open Design serves no period-usage endpoint: this value accumulates the 7-day windows observed on this machine. Windows observed while the plugin was not running are not counted.',
|
|
606
|
+
designPlanWindowsOf: '≈ {count} weekly allowances',
|
|
607
|
+
designPlanComplete: 'covers every window of this period',
|
|
608
|
+
designPlanPartial: 'only {count} observed window(s)',
|
|
597
609
|
providerConsoleURLPlaceholder: 'Leave empty for the built-in default',
|
|
598
610
|
providerSave: 'Save',
|
|
599
611
|
providerSaving: 'Saving…',
|
|
@@ -1059,6 +1071,32 @@ window.__ModuleLoader__.load({
|
|
|
1059
1071
|
return { console: url }
|
|
1060
1072
|
}
|
|
1061
1073
|
|
|
1074
|
+
/**
|
|
1075
|
+
* The open-design period estimate (v1.5.0), or null.
|
|
1076
|
+
*
|
|
1077
|
+
* Open Design serves no period-usage endpoint, so the Host accumulates the
|
|
1078
|
+
* weekly windows it observes. Everything here is optional: an older Host
|
|
1079
|
+
* omits the field entirely and renders no line.
|
|
1080
|
+
*/
|
|
1081
|
+
function normalizeDesignPlan(raw) {
|
|
1082
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return null
|
|
1083
|
+
const usedCredits = numberOrNull(raw.usedCredits)
|
|
1084
|
+
if (usedCredits === null || usedCredits <= 0) return null
|
|
1085
|
+
const windows = Number.isFinite(raw.windows) && raw.windows > 0 ? Math.round(raw.windows) : 0
|
|
1086
|
+
const estimatedUsd = numberOrNull(raw.estimatedUsd)
|
|
1087
|
+
const limitPerWindow = numberOrNull(raw.limitPerWindow)
|
|
1088
|
+
return {
|
|
1089
|
+
periodStart: stringOrEmpty(raw.periodStart),
|
|
1090
|
+
periodEnd: stringOrEmpty(raw.periodEnd),
|
|
1091
|
+
usedCredits,
|
|
1092
|
+
estimatedUsd,
|
|
1093
|
+
windows,
|
|
1094
|
+
complete: raw.complete === true,
|
|
1095
|
+
since: stringOrEmpty(raw.since),
|
|
1096
|
+
limitPerWindow: limitPerWindow !== null && limitPerWindow > 0 ? limitPerWindow : null,
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1062
1100
|
/** The whole wire snapshot, or null. */
|
|
1063
1101
|
function normalizeSnapshot(raw) {
|
|
1064
1102
|
if (raw === null || typeof raw !== 'object') return null
|
|
@@ -1080,6 +1118,8 @@ window.__ModuleLoader__.load({
|
|
|
1080
1118
|
credits: normalizeCredits(raw.credits),
|
|
1081
1119
|
account: normalizeAccount(raw.account),
|
|
1082
1120
|
links: normalizeLinks(raw.links),
|
|
1121
|
+
// v1.5.0: open-design only; absent everywhere else.
|
|
1122
|
+
designPlan: normalizeDesignPlan(raw.designPlan),
|
|
1083
1123
|
fetchedAt: stringOrEmpty(raw.fetchedAt),
|
|
1084
1124
|
ageMs: Number.isFinite(raw.ageMs) ? raw.ageMs : null,
|
|
1085
1125
|
stale: raw.stale === true,
|
|
@@ -3911,6 +3951,66 @@ window.__ModuleLoader__.load({
|
|
|
3911
3951
|
)
|
|
3912
3952
|
}
|
|
3913
3953
|
|
|
3954
|
+
/**
|
|
3955
|
+
* The open-design period estimate row (v1.5.0).
|
|
3956
|
+
*
|
|
3957
|
+
* Open Design has no period-usage endpoint, so this is the Host's local
|
|
3958
|
+
* accumulation of the weekly windows it observed. It is labelled as an
|
|
3959
|
+
* estimate and carries a coverage note: when the first observed window does
|
|
3960
|
+
* not start at the billing period start, earlier windows were never seen and
|
|
3961
|
+
* the value is a lower bound.
|
|
3962
|
+
*/
|
|
3963
|
+
function buildDesignPlanRow(designPlan, t) {
|
|
3964
|
+
if (designPlan === null) return null
|
|
3965
|
+
const parts = []
|
|
3966
|
+
if (designPlan.estimatedUsd !== null) {
|
|
3967
|
+
parts.push(
|
|
3968
|
+
h(
|
|
3969
|
+
'span',
|
|
3970
|
+
{ key: 'usd', className: 'dsh-tokenlens-percent dsh-tokenlens-capacity-money' },
|
|
3971
|
+
formatMoney(designPlan.estimatedUsd, 'USD'),
|
|
3972
|
+
),
|
|
3973
|
+
)
|
|
3974
|
+
} else {
|
|
3975
|
+
parts.push(
|
|
3976
|
+
h(
|
|
3977
|
+
'span',
|
|
3978
|
+
{ key: 'credits', className: 'dsh-tokenlens-percent dsh-tokenlens-capacity-money' },
|
|
3979
|
+
formatTokensPrecise(designPlan.usedCredits),
|
|
3980
|
+
),
|
|
3981
|
+
)
|
|
3982
|
+
}
|
|
3983
|
+
const notes = []
|
|
3984
|
+
if (designPlan.limitPerWindow !== null && designPlan.usedCredits > 0) {
|
|
3985
|
+
const ratio = designPlan.usedCredits / designPlan.limitPerWindow
|
|
3986
|
+
notes.push(t('designPlanWindowsOf', { count: ratio.toFixed(1) }))
|
|
3987
|
+
}
|
|
3988
|
+
if (designPlan.complete === true) notes.push(t('designPlanComplete'))
|
|
3989
|
+
else if (designPlan.windows > 0) notes.push(t('designPlanPartial', { count: designPlan.windows }))
|
|
3990
|
+
return h(
|
|
3991
|
+
'div',
|
|
3992
|
+
{
|
|
3993
|
+
key: 'design-plan',
|
|
3994
|
+
className: 'dsh-tokenlens-row',
|
|
3995
|
+
'data-capacity-designplan': 'true',
|
|
3996
|
+
title: t('designPlanHint'),
|
|
3997
|
+
},
|
|
3998
|
+
h(
|
|
3999
|
+
'div',
|
|
4000
|
+
{ className: 'dsh-tokenlens-row-head' },
|
|
4001
|
+
h('span', { className: 'dsh-tokenlens-row-label' }, t('designPlanLabel')),
|
|
4002
|
+
parts,
|
|
4003
|
+
),
|
|
4004
|
+
notes.length > 0
|
|
4005
|
+
? h(
|
|
4006
|
+
'div',
|
|
4007
|
+
{ className: 'dsh-tokenlens-capacity-tokens', 'data-kind': designPlan.complete === true ? 'complete' : 'partial' },
|
|
4008
|
+
notes.join(' · '),
|
|
4009
|
+
)
|
|
4010
|
+
: null,
|
|
4011
|
+
)
|
|
4012
|
+
}
|
|
4013
|
+
|
|
3914
4014
|
/** The provider/plan/account header line, or null when there is nothing to say. */
|
|
3915
4015
|
function buildProviderHeader(snapshot, t) {
|
|
3916
4016
|
if (snapshot === null) return null
|
|
@@ -5267,15 +5367,19 @@ window.__ModuleLoader__.load({
|
|
|
5267
5367
|
} else if (snapshot.provider.kind === 'windows') {
|
|
5268
5368
|
// A windows provider may ALSO report an independent wallet balance
|
|
5269
5369
|
// (OpenDesign): show it as one extra row of the same capacity card,
|
|
5270
|
-
// after the allowance windows (or after the "no usage" hint).
|
|
5370
|
+
// after the allowance windows (or after the "no usage" hint). The same
|
|
5371
|
+
// applies to the local period estimate (v1.5.0).
|
|
5271
5372
|
const balanceRow = buildCapacityBalanceRow(snapshot.balance, t)
|
|
5373
|
+
const designPlanRow = buildDesignPlanRow(snapshot.designPlan, t)
|
|
5272
5374
|
if (snapshot.windows !== null) {
|
|
5273
5375
|
const rows = WINDOW_ORDER.map((id) => buildWindowRow(id, snapshot.windows[id], state, now, t, statsRow))
|
|
5274
5376
|
if (balanceRow !== null) rows.push(balanceRow)
|
|
5377
|
+
if (designPlanRow !== null) rows.push(designPlanRow)
|
|
5275
5378
|
section.push(h('div', { key: 'rows', className: 'dsh-tokenlens-rows' }, rows))
|
|
5276
5379
|
} else {
|
|
5277
5380
|
section.push(h('div', { key: 'no-windows', className: 'dsh-tokenlens-hint' }, t('chipNoUsage')))
|
|
5278
5381
|
if (balanceRow !== null) section.push(balanceRow)
|
|
5382
|
+
if (designPlanRow !== null) section.push(designPlanRow)
|
|
5279
5383
|
}
|
|
5280
5384
|
} else {
|
|
5281
5385
|
section.push(h('div', { key: 'no-kind', className: 'dsh-tokenlens-hint' }, t('chipNoUsage')))
|
package/index.js
CHANGED
|
@@ -38,6 +38,13 @@ import {
|
|
|
38
38
|
saveProviderStore,
|
|
39
39
|
} from './lib/provider-store.mjs'
|
|
40
40
|
import { makeCustomAdapter, sanitizeUsageBaseURL, validateCustomProvider } from './lib/custom-provider.mjs'
|
|
41
|
+
import {
|
|
42
|
+
defaultDesignPlanLedgerFile,
|
|
43
|
+
loadLedger,
|
|
44
|
+
observeDesignPlan,
|
|
45
|
+
saveLedger,
|
|
46
|
+
summarizeDesignPlan,
|
|
47
|
+
} from './lib/design-plan-ledger.mjs'
|
|
41
48
|
import { redactEndpoint, redactPaths, redactTextPaths } from './lib/redact.mjs'
|
|
42
49
|
import { UPDATE_CHECK_URL, UPDATE_PACKAGE_URL, buildUpdateInfo, parseLatestVersion } from './lib/update-check.mjs'
|
|
43
50
|
|
|
@@ -76,7 +83,7 @@ export const inject = []
|
|
|
76
83
|
export const PLUGIN_NAME = 'dsh-tokenlens'
|
|
77
84
|
|
|
78
85
|
/** Plugin version; mirrors package.json (kept here so diagnostics need no fs read). */
|
|
79
|
-
export const PLUGIN_VERSION = '1.
|
|
86
|
+
export const PLUGIN_VERSION = '1.5.0'
|
|
80
87
|
|
|
81
88
|
/** Documented defaults; every field of the raw row config is optional. */
|
|
82
89
|
const DEFAULTS = {
|
|
@@ -88,6 +95,8 @@ const DEFAULTS = {
|
|
|
88
95
|
statusFile: '',
|
|
89
96
|
// Empty selects $DSH_HOME/dsh-tokenlens/providers.json.
|
|
90
97
|
providerStoreFile: '',
|
|
98
|
+
// Empty selects $DSH_HOME/dsh-tokenlens/design-plan-ledger.json.
|
|
99
|
+
designPlanLedgerFile: '',
|
|
91
100
|
stats: {
|
|
92
101
|
enabled: true,
|
|
93
102
|
ttlMs: 30_000,
|
|
@@ -175,6 +184,10 @@ export function normalize(raw) {
|
|
|
175
184
|
statusFile: typeof value.statusFile === 'string' ? value.statusFile : DEFAULTS.statusFile,
|
|
176
185
|
providerStoreFile:
|
|
177
186
|
typeof value.providerStoreFile === 'string' ? value.providerStoreFile : DEFAULTS.providerStoreFile,
|
|
187
|
+
designPlanLedgerFile:
|
|
188
|
+
typeof value.designPlanLedgerFile === 'string'
|
|
189
|
+
? value.designPlanLedgerFile
|
|
190
|
+
: DEFAULTS.designPlanLedgerFile,
|
|
178
191
|
stats: {
|
|
179
192
|
enabled: statsRaw.enabled !== false,
|
|
180
193
|
ttlMs: nonNegative(statsRaw.ttlMs, DEFAULTS.stats.ttlMs),
|
|
@@ -383,6 +396,49 @@ export function apply(ctx, rawConfig) {
|
|
|
383
396
|
config.providerStoreFile !== '' ? config.providerStoreFile : defaultProviderStoreFile(dshHomeOf(process.env))
|
|
384
397
|
let providerStore = loadProviderStore(providerStoreFile)
|
|
385
398
|
|
|
399
|
+
// Design Plan period ledger (v1.5.0): OpenDesign serves rolling 5h/7d windows
|
|
400
|
+
// but no period-usage endpoint, so the Host accumulates what it observes.
|
|
401
|
+
const designPlanLedgerFile =
|
|
402
|
+
config.designPlanLedgerFile !== ''
|
|
403
|
+
? config.designPlanLedgerFile
|
|
404
|
+
: defaultDesignPlanLedgerFile(dshHomeOf(process.env))
|
|
405
|
+
let designPlanLedger = loadLedger(designPlanLedgerFile)
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Fold one OpenDesign observation into the period ledger.
|
|
409
|
+
*
|
|
410
|
+
* `designPlan` is the provider read's extra context (period bounds + credits
|
|
411
|
+
* rate + the upstream window rows); it is absent for every other provider and
|
|
412
|
+
* whenever the quota route was unreadable, in which case nothing is recorded.
|
|
413
|
+
*/
|
|
414
|
+
function recordDesignPlan(designPlan, now) {
|
|
415
|
+
if (designPlan === null || typeof designPlan !== 'object') return
|
|
416
|
+
const folded = observeDesignPlan(designPlanLedger, {
|
|
417
|
+
periodStart: designPlan.periodStart,
|
|
418
|
+
periodEnd: designPlan.periodEnd,
|
|
419
|
+
creditsPerUsd: designPlan.creditsPerUsd,
|
|
420
|
+
windows: designPlan.windows,
|
|
421
|
+
now,
|
|
422
|
+
})
|
|
423
|
+
if (!folded.changed) return
|
|
424
|
+
try {
|
|
425
|
+
designPlanLedger = saveLedger(designPlanLedgerFile, folded.ledger)
|
|
426
|
+
noteStatus('design-plan-ledger', { designPlanLedgerFile })
|
|
427
|
+
} catch (error) {
|
|
428
|
+
// A ledger write must never break a usage read: keep the in-memory fold.
|
|
429
|
+
designPlanLedger = folded.ledger
|
|
430
|
+
warn(`design plan ledger write failed: ${String(error?.message ?? error)}`)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** The period estimate for the open-design snapshot, or null. */
|
|
435
|
+
function designPlanSummaryFor(providerId, designPlan) {
|
|
436
|
+
if (canonicalProviderId(providerId) !== 'open-design') return null
|
|
437
|
+
const periodStart = designPlan?.periodStart
|
|
438
|
+
if (typeof periodStart !== 'string' || periodStart === '') return null
|
|
439
|
+
return summarizeDesignPlan(designPlanLedger, periodStart)
|
|
440
|
+
}
|
|
441
|
+
|
|
386
442
|
/** Built-in defaults + profile config + management-page overrides. */
|
|
387
443
|
function effectiveProviderConfig(providerId) {
|
|
388
444
|
const id = canonicalProviderId(providerId)
|
|
@@ -1110,6 +1166,11 @@ export function apply(ctx, rawConfig) {
|
|
|
1110
1166
|
}
|
|
1111
1167
|
|
|
1112
1168
|
function snapshotFromResult(providerId, read, now) {
|
|
1169
|
+
// v1.5.0: the Design Plan period estimate. The ledger observes every read
|
|
1170
|
+
// (refreshes and background polls), then the current period is summarized
|
|
1171
|
+
// into the snapshot so the dialog can show it without a second round-trip.
|
|
1172
|
+
recordDesignPlan(read.result.designPlan ?? null, now)
|
|
1173
|
+
const designPlan = designPlanSummaryFor(providerId, read.result.designPlan)
|
|
1113
1174
|
return buildSnapshot({
|
|
1114
1175
|
provider: providerInfo(providerId),
|
|
1115
1176
|
plan: read.result.plan ?? null,
|
|
@@ -1118,6 +1179,8 @@ export function apply(ctx, rawConfig) {
|
|
|
1118
1179
|
credits: read.result.credits ?? null,
|
|
1119
1180
|
account: read.result.account ?? null,
|
|
1120
1181
|
links: consoleLinks(providerId, read.result),
|
|
1182
|
+
// Only open-design reports a period estimate (see design-plan-ledger.mjs).
|
|
1183
|
+
...(designPlan === null ? {} : { designPlan }),
|
|
1121
1184
|
fetchedAtMs: read.fetchedAtMs,
|
|
1122
1185
|
now,
|
|
1123
1186
|
latencyMs: read.latencyMs,
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Open Design "Design Plan" ledger: a local, honest reverse-derivation of
|
|
3
|
+
* how much of a billing period's allowance has been used.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS
|
|
6
|
+
*
|
|
7
|
+
* Open Design exposes the Design Plan allowance only as *rolling windows*
|
|
8
|
+
* (5 hours and 7 days). There is no "current period usage" endpoint: the
|
|
9
|
+
* console's own `/billing/usage` and `/billing/coding-plan-usage` routes are
|
|
10
|
+
* cookie-authenticated, and a Control Key gets HTTP 401. The monthly cap itself
|
|
11
|
+
* is real (`Go` is advertised as $120/month, 4 × the $30 weekly window) but the
|
|
12
|
+
* "used so far this period" number is never served.
|
|
13
|
+
*
|
|
14
|
+
* So the Host remembers what it has *observed*. Every successful open-design
|
|
15
|
+
* read reports the current window instances; this ledger keeps, per instance,
|
|
16
|
+
* the HIGHEST `usedCredits` ever seen (a window's usage only grows until it
|
|
17
|
+
* resets), and sums the finished + current weekly windows that belong to one
|
|
18
|
+
* billing period.
|
|
19
|
+
*
|
|
20
|
+
* WHY THE 7-DAY WINDOW IS THE UNIT
|
|
21
|
+
*
|
|
22
|
+
* The 5-hour and 7-day windows are simultaneous, overlapping limits over the
|
|
23
|
+
* same spend — a moment can show `usedCredits` identical in both (verified
|
|
24
|
+
* live: 48,195 in the 5h window and 48,195 in the 7d window). Summing both
|
|
25
|
+
* would double-count every credit. The 7-day window is the longest Design Plan
|
|
26
|
+
* window, so it alone tiles the period without overlap.
|
|
27
|
+
*
|
|
28
|
+
* WHAT IT CANNOT DO
|
|
29
|
+
*
|
|
30
|
+
* Observation starts when the plugin first runs. Earlier windows of the same
|
|
31
|
+
* period are unknowable, so the summary reports `complete` (true only when the
|
|
32
|
+
* earliest observed window starts at the period start) and `since`, and the UI
|
|
33
|
+
* must present the number as an estimate rather than an official figure.
|
|
34
|
+
*
|
|
35
|
+
* Storage: `$DSH_HOME/dsh-tokenlens/design-plan-ledger.json`, atomic, owner-only,
|
|
36
|
+
* numbers and timestamps only — no credentials, no prompts.
|
|
37
|
+
*/
|
|
38
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
39
|
+
import { dirname, join } from 'node:path'
|
|
40
|
+
|
|
41
|
+
/** Ledger schema version; a mismatch discards the file (regenerate by observing). */
|
|
42
|
+
export const LEDGER_VERSION = 1
|
|
43
|
+
|
|
44
|
+
/** The weekly window length; it is the accounting unit (see the module doc). */
|
|
45
|
+
export const WEEKLY_SECONDS = 604800
|
|
46
|
+
|
|
47
|
+
/** How many periods to retain (older ones only cost a little disk). */
|
|
48
|
+
const MAX_PERIODS = 6
|
|
49
|
+
|
|
50
|
+
/** How many window observations one period may hold (guards a runaway file). */
|
|
51
|
+
const MAX_WINDOWS = 64
|
|
52
|
+
|
|
53
|
+
/** Credits are decimal strings in the upstream document; accept a safe integer. */
|
|
54
|
+
function creditsOf(value) {
|
|
55
|
+
if (typeof value === 'string' && /^\d{1,40}$/.test(value)) return Number(value)
|
|
56
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) return value
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** An ISO string, or '' when the value is not one. */
|
|
61
|
+
function isoOf(value) {
|
|
62
|
+
if (typeof value !== 'string' || value.trim() === '') return ''
|
|
63
|
+
const ms = Date.parse(value)
|
|
64
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : ''
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The empty ledger document. */
|
|
68
|
+
export function emptyLedger() {
|
|
69
|
+
return { version: LEDGER_VERSION, periods: {} }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Read the ledger; a missing or unusable file is an empty ledger (never throws). */
|
|
73
|
+
export function loadLedger(file) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'))
|
|
76
|
+
if (parsed === null || typeof parsed !== 'object' || parsed.version !== LEDGER_VERSION) return emptyLedger()
|
|
77
|
+
const periods = {}
|
|
78
|
+
for (const [start, period] of Object.entries(parsed.periods ?? {})) {
|
|
79
|
+
if (!isoOf(start)) continue
|
|
80
|
+
if (period === null || typeof period !== 'object') continue
|
|
81
|
+
const windows = {}
|
|
82
|
+
for (const [key, window] of Object.entries(period.windows ?? {})) {
|
|
83
|
+
if (!isoOf(key) || window === null || typeof window !== 'object') continue
|
|
84
|
+
const used = creditsOf(window.usedCredits)
|
|
85
|
+
if (used === null) continue
|
|
86
|
+
windows[key] = {
|
|
87
|
+
usedCredits: used,
|
|
88
|
+
limitCredits: creditsOf(window.limitCredits) ?? 0,
|
|
89
|
+
durationSeconds: Number.isSafeInteger(window.durationSeconds) ? window.durationSeconds : WEEKLY_SECONDS,
|
|
90
|
+
resetsAt: isoOf(window.resetsAt),
|
|
91
|
+
lastSeenAt: isoOf(window.lastSeenAt),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
periods[start] = {
|
|
95
|
+
periodEnd: isoOf(period.periodEnd),
|
|
96
|
+
creditsPerUsd: Number.isSafeInteger(period.creditsPerUsd) && period.creditsPerUsd > 0
|
|
97
|
+
? period.creditsPerUsd
|
|
98
|
+
: null,
|
|
99
|
+
windows,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { version: LEDGER_VERSION, periods }
|
|
103
|
+
} catch {
|
|
104
|
+
return emptyLedger()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Persist the ledger atomically (tmp + rename, mode 0600). Returns the document written. */
|
|
109
|
+
export function saveLedger(file, ledger) {
|
|
110
|
+
const periods = {}
|
|
111
|
+
// Newest periods first, then keep a bounded history.
|
|
112
|
+
for (const [start, period] of Object.entries(ledger.periods ?? {})
|
|
113
|
+
.sort(([left], [right]) => right.localeCompare(left))
|
|
114
|
+
.slice(0, MAX_PERIODS)) {
|
|
115
|
+
periods[start] = {
|
|
116
|
+
periodEnd: period.periodEnd ?? '',
|
|
117
|
+
creditsPerUsd: period.creditsPerUsd ?? null,
|
|
118
|
+
windows: Object.fromEntries(Object.entries(period.windows ?? {}).slice(0, MAX_WINDOWS)),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const document = { version: LEDGER_VERSION, periods }
|
|
122
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
123
|
+
const temp = `${file}.${process.pid}.${Date.now().toString(36)}.tmp`
|
|
124
|
+
writeFileSync(temp, `${JSON.stringify(document, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 })
|
|
125
|
+
try {
|
|
126
|
+
chmodSync(temp, 0o600)
|
|
127
|
+
} catch {
|
|
128
|
+
/* POSIX-only nicety; Windows ACLs are inherited */
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
renameSync(temp, file)
|
|
132
|
+
} catch (error) {
|
|
133
|
+
try {
|
|
134
|
+
unlinkSync(temp)
|
|
135
|
+
} catch {
|
|
136
|
+
/* best-effort cleanup */
|
|
137
|
+
}
|
|
138
|
+
throw error
|
|
139
|
+
}
|
|
140
|
+
return document
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Fold one observation into the ledger (pure: returns a new document).
|
|
145
|
+
*
|
|
146
|
+
* @param ledger - the current document (`loadLedger` or `emptyLedger`).
|
|
147
|
+
* @param input.periodStart - the billing period start (ISO); the period key.
|
|
148
|
+
* @param input.periodEnd - the billing period end (ISO), optional.
|
|
149
|
+
* @param input.creditsPerUsd - credits per US dollar, optional.
|
|
150
|
+
* @param input.windows - the upstream window rows (`durationSeconds`, `usedCredits`,
|
|
151
|
+
* `limitCredits`, `windowStart`/`resetsAt`).
|
|
152
|
+
* @param input.now - epoch ms of the observation.
|
|
153
|
+
* @returns `{ ledger, changed }` where `changed` is false when nothing grew.
|
|
154
|
+
*
|
|
155
|
+
* Only the weekly window is recorded: the 5-hour window measures the same
|
|
156
|
+
* spending over a shorter span, so keeping both would double-count the period.
|
|
157
|
+
* Within one window instance the stored value only ever grows, and an instance
|
|
158
|
+
* that disappears from the upstream payload keeps its last (final) value.
|
|
159
|
+
*/
|
|
160
|
+
export function observeDesignPlan(ledger, input) {
|
|
161
|
+
const periodStart = isoOf(input?.periodStart)
|
|
162
|
+
if (periodStart === '') return { ledger, changed: false }
|
|
163
|
+
const now = Number.isFinite(input?.now) ? input.now : Date.now()
|
|
164
|
+
const period = ledger?.periods?.[periodStart] ?? { periodEnd: '', creditsPerUsd: null, windows: {} }
|
|
165
|
+
const windows = { ...(period.windows ?? {}) }
|
|
166
|
+
let changed = false
|
|
167
|
+
for (const row of Array.isArray(input?.windows) ? input.windows : []) {
|
|
168
|
+
if (row === null || typeof row !== 'object') continue
|
|
169
|
+
if (row.durationSeconds !== WEEKLY_SECONDS) continue
|
|
170
|
+
const used = creditsOf(row.usedCredits)
|
|
171
|
+
if (used === null) continue
|
|
172
|
+
const start = isoOf(row.windowStart) || isoOf(row.resetsAt)
|
|
173
|
+
if (start === '') continue
|
|
174
|
+
const previous = windows[start]
|
|
175
|
+
const nextLimit = creditsOf(row.limitCredits) ?? 0
|
|
176
|
+
const resetsAt = isoOf(row.resetsAt)
|
|
177
|
+
if (previous === undefined) {
|
|
178
|
+
windows[start] = {
|
|
179
|
+
usedCredits: used,
|
|
180
|
+
limitCredits: nextLimit,
|
|
181
|
+
durationSeconds: WEEKLY_SECONDS,
|
|
182
|
+
resetsAt,
|
|
183
|
+
lastSeenAt: new Date(now).toISOString(),
|
|
184
|
+
}
|
|
185
|
+
changed = true
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
// A window's usage only grows until it resets; keep the maximum so a
|
|
189
|
+
// transient smaller read (or a retried request) never lowers the record.
|
|
190
|
+
const grew = used > previous.usedCredits
|
|
191
|
+
const limitGrew = nextLimit > previous.limitCredits
|
|
192
|
+
if (!grew && !limitGrew && resetsAt === previous.resetsAt) continue
|
|
193
|
+
windows[start] = {
|
|
194
|
+
...previous,
|
|
195
|
+
usedCredits: grew ? used : previous.usedCredits,
|
|
196
|
+
limitCredits: limitGrew ? nextLimit : previous.limitCredits,
|
|
197
|
+
resetsAt: resetsAt || previous.resetsAt,
|
|
198
|
+
lastSeenAt: new Date(now).toISOString(),
|
|
199
|
+
}
|
|
200
|
+
changed = true
|
|
201
|
+
}
|
|
202
|
+
if (!changed && period.periodEnd === isoOf(input?.periodEnd) && period.creditsPerUsd === (input?.creditsPerUsd ?? null)) {
|
|
203
|
+
return { ledger, changed: false }
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
ledger: {
|
|
207
|
+
version: LEDGER_VERSION,
|
|
208
|
+
periods: {
|
|
209
|
+
...(ledger?.periods ?? {}),
|
|
210
|
+
[periodStart]: {
|
|
211
|
+
periodEnd: isoOf(input?.periodEnd) || period.periodEnd,
|
|
212
|
+
creditsPerUsd:
|
|
213
|
+
Number.isSafeInteger(input?.creditsPerUsd) && input.creditsPerUsd > 0
|
|
214
|
+
? input.creditsPerUsd
|
|
215
|
+
: period.creditsPerUsd,
|
|
216
|
+
windows,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
changed,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Summarize one period: the estimated Design Plan usage the Host has observed.
|
|
226
|
+
*
|
|
227
|
+
* @returns `null` when the period has no observations at all, else
|
|
228
|
+
* `{ usedCredits, estimatedUsd, windows, complete, since, periodStart, periodEnd, limitPerWindow }`.
|
|
229
|
+
* `complete` is true only when the earliest observed window starts at the
|
|
230
|
+
* period start — otherwise earlier windows of this period were never seen and
|
|
231
|
+
* the number is a lower bound.
|
|
232
|
+
*/
|
|
233
|
+
export function summarizeDesignPlan(ledger, periodStart) {
|
|
234
|
+
const key = isoOf(periodStart)
|
|
235
|
+
if (key === '') return null
|
|
236
|
+
const period = ledger?.periods?.[key]
|
|
237
|
+
if (period === undefined) return null
|
|
238
|
+
const entries = Object.entries(period.windows ?? {}).sort(([left], [right]) => left.localeCompare(right))
|
|
239
|
+
if (entries.length === 0) return null
|
|
240
|
+
let usedCredits = 0
|
|
241
|
+
let windows = 0
|
|
242
|
+
for (const [, window] of entries) {
|
|
243
|
+
usedCredits += window.usedCredits
|
|
244
|
+
windows += 1
|
|
245
|
+
}
|
|
246
|
+
const creditsPerUsd = period.creditsPerUsd
|
|
247
|
+
const estimatedUsd =
|
|
248
|
+
Number.isSafeInteger(creditsPerUsd) && creditsPerUsd > 0 ? usedCredits / creditsPerUsd : null
|
|
249
|
+
const since = entries[0][0]
|
|
250
|
+
const limitPerWindow = entries[entries.length - 1][1].limitCredits
|
|
251
|
+
return {
|
|
252
|
+
periodStart: key,
|
|
253
|
+
periodEnd: period.periodEnd || '',
|
|
254
|
+
usedCredits,
|
|
255
|
+
estimatedUsd,
|
|
256
|
+
windows,
|
|
257
|
+
complete: since === key,
|
|
258
|
+
since,
|
|
259
|
+
limitPerWindow: Number.isSafeInteger(limitPerWindow) && limitPerWindow > 0 ? limitPerWindow : null,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** The default ledger file: `$DSH_HOME/dsh-tokenlens/design-plan-ledger.json`. */
|
|
264
|
+
export function defaultDesignPlanLedgerFile(dshHome) {
|
|
265
|
+
return join(dshHome, 'dsh-tokenlens', 'design-plan-ledger.json')
|
|
266
|
+
}
|
package/lib/go-usage.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
export const WINDOW_IDS = ['rolling', 'weekly', 'monthly']
|
|
20
20
|
|
|
21
21
|
/** Version of the wire snapshot shape (see PROTOCOL.md). */
|
|
22
|
-
export const SNAPSHOT_VERSION =
|
|
22
|
+
export const SNAPSHOT_VERSION = 4
|
|
23
23
|
|
|
24
24
|
/** A typed failure the plugin can classify, serialize and test. */
|
|
25
25
|
export class UsageError extends Error {
|
|
@@ -141,6 +141,9 @@ export function buildSnapshot(input) {
|
|
|
141
141
|
balance: input.balance ?? null,
|
|
142
142
|
credits: input.credits ?? null,
|
|
143
143
|
account: input.account ?? null,
|
|
144
|
+
// v1.5.0 (open-design only): the locally accumulated Design Plan period
|
|
145
|
+
// estimate. `null` on every other provider and until the first observation.
|
|
146
|
+
...(input.designPlan === undefined || input.designPlan === null ? {} : { designPlan: input.designPlan }),
|
|
144
147
|
// Provider-owned web pages the dialog links to (they follow the selection).
|
|
145
148
|
links: input.links ?? null,
|
|
146
149
|
fetchedAt: new Date(input.fetchedAtMs).toISOString(),
|
package/lib/providers.mjs
CHANGED
|
@@ -897,10 +897,22 @@ export function parseOpenDesignWallet(payload, workspaceId) {
|
|
|
897
897
|
*/
|
|
898
898
|
export function parseOpenDesignSummary(payload) {
|
|
899
899
|
const document = payload !== null && typeof payload === 'object' && !Array.isArray(payload) ? payload : null
|
|
900
|
-
if (document === null)
|
|
900
|
+
if (document === null) {
|
|
901
|
+
return { plan: null, periodStart: '', periodEnd: '', creditsPerUsd: null }
|
|
902
|
+
}
|
|
901
903
|
const tier = typeof document.membershipTier === 'string' ? document.membershipTier.trim() : ''
|
|
902
904
|
const plan = tier === '' ? null : { id: tier, label: tier.toUpperCase() }
|
|
903
|
-
|
|
905
|
+
// v1.5.0: the billing period bounds and the credits→USD rate, which the Host
|
|
906
|
+
// needs to turn observed window usage into a period estimate.
|
|
907
|
+
const iso = (value) =>
|
|
908
|
+
typeof value === 'string' && Number.isFinite(Date.parse(value)) ? new Date(value).toISOString() : ''
|
|
909
|
+
const rate = document.creditsPerUsd
|
|
910
|
+
return {
|
|
911
|
+
plan,
|
|
912
|
+
periodStart: iso(document.subscriptionCurrentPeriodStart),
|
|
913
|
+
periodEnd: iso(document.subscriptionCurrentPeriodEnd),
|
|
914
|
+
creditsPerUsd: Number.isSafeInteger(rate) && rate > 0 ? rate : null,
|
|
915
|
+
}
|
|
904
916
|
}
|
|
905
917
|
|
|
906
918
|
/**
|
|
@@ -1008,11 +1020,25 @@ export const ADAPTERS = {
|
|
|
1008
1020
|
() => undefined,
|
|
1009
1021
|
)
|
|
1010
1022
|
const parsedSummary = parseOpenDesignSummary(summary)
|
|
1023
|
+
// v1.5.0: hand the period context + the raw weekly window rows to the Host
|
|
1024
|
+
// so it can accumulate a period estimate (OpenDesign serves no period-usage
|
|
1025
|
+
// endpoint). `designPlan` is not a wire field: the Host converts it into a
|
|
1026
|
+
// summary and never forwards the raw document.
|
|
1027
|
+
const designPlan =
|
|
1028
|
+
funding !== undefined && Array.isArray(funding?.codingPlan?.windows)
|
|
1029
|
+
? {
|
|
1030
|
+
periodStart: parsedSummary.periodStart,
|
|
1031
|
+
periodEnd: parsedSummary.periodEnd,
|
|
1032
|
+
creditsPerUsd: parsedSummary.creditsPerUsd,
|
|
1033
|
+
windows: funding.codingPlan.windows,
|
|
1034
|
+
}
|
|
1035
|
+
: null
|
|
1011
1036
|
return {
|
|
1012
1037
|
kind: 'windows',
|
|
1013
1038
|
windows: parsed.windows,
|
|
1014
1039
|
plan: parsed.plan ?? parsedSummary.plan,
|
|
1015
1040
|
balance: walletParsed.balance,
|
|
1041
|
+
designPlan,
|
|
1016
1042
|
}
|
|
1017
1043
|
},
|
|
1018
1044
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tokenlens",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|