dsh-tokenlens 1.0.8 → 1.1.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 CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  本项目遵循 [Semantic Versioning](https://semver.org/):修复 = PATCH,新功能 = MINOR,不兼容变更 = MAJOR。发布时同步更新 `package.json` 的 `version`、`index.js` 的 `PLUGIN_VERSION` 与本文件(`node scripts/bump-version.mjs <version> "<note>"`);已发布的条目保持原样。
4
4
 
5
+ ## 1.1.0 — 2026-10-30
6
+
7
+ - feat(providers): 接入 Open Design(OpenDesign)只读额度查询——Design Plan 的 5 小时 / 7 天窗口已用百分比与重置时间,以及独立的钱包 USD 余额
8
+ - feat(providers): Open Design 使用工作区 `workspaceId` + Control Key(`OPENDESIGN_CONTROL_KEY` / `VELA_CONTROL_KEY`)鉴权;推理 API Key 不能查询计费接口
9
+ - feat(ui): `windows` 快照现在可以附带钱包余额——详情弹窗在额度窗口下方多一行「账户余额」,chip 悬浮提示追加金额
10
+ - docs: 中英 README 与 PROTOCOL 补充 Open Design;说明其余仅元数据供应商没有可用 API Key 查询的公开用量接口(千问/百炼额度仅官方 CLI 可查,腾讯混元/百度千帆只有云账户余额 API)
11
+
12
+ ## 1.0.9 — 2026-10-29
13
+
14
+ - ci: GitHub Actions 升级到 v7(actions/checkout、actions/setup-node),发布链路仅依赖 OIDC
15
+
5
16
  ## 1.0.8 — 2026-10-29
6
17
 
7
18
  - fix(release): npm registry 发布延迟时索引更新回退到 PUBLISH_TIMESTAMP;补录 1.0.7 到市场索引
package/PROTOCOL.md CHANGED
@@ -95,10 +95,15 @@ Provider semantics:
95
95
  | `commandcode` | `windows` | rolling = 5-hour `used/cap`, weekly = `used/cap`, monthly = `(granted - remaining)/granted` (only when the API reports the grant) | `plan`, `credits` (USD), `account` |
96
96
  | `deepseek` (picker id `deepseek-official`) | `balance` | `null` | `balance` (API-key account balance, CNY/USD) |
97
97
  | `deepseek-account` | `balance` | `null` | `balance` from the composed Platform account service (signed-in wallets) |
98
+ | `open-design` | `windows` | rolling = 5-hour Design Plan window, weekly = 7-day window, monthly = 30-day window when reported (used / limit credits, capped at 100%) | `balance` (independent wallet balance, USD) |
98
99
 
99
100
  `balance` is `{ amount, currency }` plus, for the account provider,
100
101
  `wallets: [{currency, amount}]` and `bonus: [{currency, amount}]`.
101
102
 
103
+ A `windows` snapshot may also carry a non-null `balance` (Open Design reports
104
+ allowance windows and a wallet independently); the Client renders it as one extra
105
+ row at the end of the capacity card and appends the amount to the chip tooltip.
106
+
102
107
  Rules:
103
108
 
104
109
  - `percent` is clamped to `0..100`; an unknown percentage is `null` (render "—"), never a fake 0.
package/README.en.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  > One glance at your usage: plan capacity for the selected provider plus local token statistics across harnesses, right below the DSH composer.
11
11
 
12
- **dsh-TokenLens** is a usage plugin for DSH (DeepSeek Harness), version v1.0.8. It keeps two kinds of numbers separate:
12
+ **dsh-TokenLens** is a usage plugin for DSH (DeepSeek Harness), version v1.1.0. It keeps two kinds of numbers separate:
13
13
 
14
14
  - **Plan capacity** — the provider's authoritative server-side values (percentage windows, used / cap, balance or remaining credit);
15
15
  - **Local token stats** — tokens consumed on this machine across every connected harness, for the selected provider or for all providers combined.
@@ -23,7 +23,7 @@ Everything is computed locally: zero runtime dependencies, no external database,
23
23
  - **Data and chart modes** — tables, a daily trend chart and ring charts; breakdowns by harness, provider, top model and unmapped providers.
24
24
  - **Provider management page** — enabled / built-in / all filters, enable switches, credential status, console links, label editing, connection tests, subscription badges and plugin settings.
25
25
  - **Auto-discovery and agent tool** — providers configured in DSH Settings → Model Services are detected by route id or base-URL host and reuse their DSH credentials; `tokenlens_usage` lets the model query snapshots, local stats and diagnostics.
26
- - **Local-only** — 5 harnesses (DSH, ZCode, OpenCode CLI, WorkBuddy / CodeBuddy CLI, Qoder CN CLI), 25 built-in providers (15 with plan / balance adapters), read-only local data, no telemetry.
26
+ - **Local-only** — 5 harnesses (DSH, ZCode, OpenCode CLI, WorkBuddy / CodeBuddy CLI, Qoder CN CLI), 25 built-in providers (16 with plan / balance adapters), read-only local data, no telemetry.
27
27
 
28
28
  ## Screenshots
29
29
 
@@ -66,7 +66,7 @@ dsh plugin --profile <profile> add dsh-tokenlens
66
66
  Then make sure `dsh.profile.bundles` in `$DSH_HOME/profiles/<profile>/package.json` contains `"dsh-tokenlens"`:
67
67
 
68
68
  ```jsonc
69
- { "dependencies": { "dsh-tokenlens": "^1.0.8" },
69
+ { "dependencies": { "dsh-tokenlens": "^1.1.0" },
70
70
  "dsh": { "profile": { "bundles": ["…", "dsh-tokenlens"] } } }
71
71
  ```
72
72
 
@@ -105,11 +105,13 @@ Every option is optional. Override the `dsh-tokenlens` row in `$DSH_HOME/profile
105
105
 
106
106
  Credentials come from DSH itself: providers configured in DSH inherit their `apiKeyEnv`, or you can set environment variables such as `OPENCODE_GO_API_KEY`, `COMMANDCODE_API_KEY` and `DEEPSEEK_API_KEY`.
107
107
 
108
+ Open Design (OpenDesign) is the exception: its read-only billing API needs a **workspace ID** plus a **Control Key** (`OPENDESIGN_CONTROL_KEY` or `VELA_CONTROL_KEY`), which is *not* the inference API key from the Cloud API Keys page. Set `providers.open-design.workspaceId` (or `OPENDESIGN_WORKSPACE_ID`) and provide the Control Key through the environment, DSH credentials, or the management page's key field.
109
+
108
110
  See the [Chinese README](README.md) for the full configuration reference, preview and provider tables.
109
111
 
110
112
  ## Supported providers and harnesses
111
113
 
112
- **Plan capacity / balance adapters:** OpenCode (`opencode-go`, `opencode`), Command Code, DeepSeek, DeepSeek Account, OpenRouter, Moonshot / Kimi, SiliconFlow, StepFun, Novita, Hyperbolic, DeepInfra, Z.ai GLM Coding Plan, MiniMax Coding Plan, Zhipu AI (GLM Coding Plan, CN), Volcengine (Coding / Agent Plan).
114
+ **Plan capacity / balance adapters:** OpenCode (`opencode-go`, `opencode`), Command Code, DeepSeek, DeepSeek Account, OpenRouter, Moonshot / Kimi, SiliconFlow, StepFun, Novita, Hyperbolic, DeepInfra, Z.ai GLM Coding Plan, MiniMax Coding Plan, Zhipu AI (GLM Coding Plan, CN), Volcengine (Coding / Agent Plan), Open Design (Design Plan windows + wallet balance).
113
115
 
114
116
  **Harnesses:** DSH, ZCode, OpenCode CLI, WorkBuddy / CodeBuddy CLI, Qoder CN CLI.
115
117
 
@@ -132,7 +134,7 @@ Host bridge route: `/api/tokenlens`; agent tool: `tokenlens_usage`.
132
134
 
133
135
  ## Development
134
136
 
135
- Node.js ≥ 24, zero runtime dependencies: `node --test` runs the 231 tests.
137
+ Node.js ≥ 24, zero runtime dependencies: `node --test` runs the 237 tests.
136
138
 
137
139
  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).
138
140
 
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  > **一眼看清你的用量**:在 DSH 输入框下方显示当前供应商的套餐容量,并汇总本机跨 Harness 的 Token 消耗。
11
11
 
12
- **dsh-TokenLens(词元透镜)** 是 DSH(DeepSeek Harness)的用量插件(v1.0.8,公开版本)。它把两类数字**按不同口径**分开呈现:
12
+ **dsh-TokenLens(词元透镜)** 是 DSH(DeepSeek Harness)的用量插件(v1.1.0,公开版本)。它把两类数字**按不同口径**分开呈现:
13
13
 
14
14
  - **套餐容量**(供应商服务端权威值):百分比窗口、`已用 / 上限`、账户余额或剩余额度;
15
15
  - **本机 Token 统计**(本机记录):当前供应商在本机所有已接入 Harness 上的合计用量,也可切到「全部 Harness」看总账。
@@ -45,7 +45,7 @@
45
45
  - **自动识别**:在 DSH「设置 → 模型服务」中配置过的供应商会自动出现在管理页,并按路由 id 或 baseURL 主机匹配内置用量适配器,自动继承 DSH 中的凭据,无需重复填写。
46
46
  - **Agent 工具**:注册 `tokenlens_usage`,模型可以自行查询套餐容量与本机用量。
47
47
 
48
- 支持 5 个 Harness:**DSH、ZCode、OpenCode CLI、WorkBuddy / CodeBuddy CLI、Qoder CN CLI**;内置 25 家供应商元数据,其中 15 家带套餐 / 余额适配器。
48
+ 支持 5 个 Harness:**DSH、ZCode、OpenCode CLI、WorkBuddy / CodeBuddy CLI、Qoder CN CLI**;内置 25 家供应商元数据,其中 16 家带套餐 / 余额适配器。
49
49
 
50
50
  ## 效果截图
51
51
 
@@ -106,7 +106,7 @@ dsh plugin --profile <profile> add dsh-tokenlens
106
106
  ```jsonc
107
107
  {
108
108
  "dependencies": {
109
- "dsh-tokenlens": "^1.0.8"
109
+ "dsh-tokenlens": "^1.1.0"
110
110
  },
111
111
  "dsh": {
112
112
  "profile": {
@@ -190,6 +190,7 @@ dsh plugin --profile <profile> remove dsh-tokenlens
190
190
  - 在 DSH「设置 → 模型服务」中配置过的供应商会被自动识别,插件按路由 id 或 baseURL 主机匹配内置适配器,并继承该路由的 `apiKeyEnv` 凭据。
191
191
  - 也可以直接用环境变量提供,例如 `OPENCODE_GO_API_KEY`、`COMMANDCODE_API_KEY`、`DEEPSEEK_API_KEY`、`MOONSHOT_API_KEY` 等(每家供应商的引用见 `lib/providers.mjs`)。
192
192
  - 或写入 `$DSH_HOME/.credentials.yaml` 的 `refs:` 映射(与 DSH 凭据服务同一份文档)。
193
+ - **Open Design(OpenDesign)** 的额度查询需要 OpenDesign Cloud 的 **workspace ID + Control Key**(`OPENDESIGN_CONTROL_KEY` 或 `VELA_CONTROL_KEY`),**不是** Cloud API Keys 页的推理 Key(推理 Key 只能调用模型)。workspace ID 写在 provider 配置里,或用 `OPENDESIGN_WORKSPACE_ID` 环境变量;Control Key 也可以在供应商管理页的 Key 输入框填写。
193
194
 
194
195
  解析顺序(先命中先用):DSH 凭据服务 → 环境变量 → `$DSH_HOME/.credentials.yaml` → OpenCode CLI 的 `auth.json`(仅默认的 `OPENCODE_GO_API_KEY` 引用)。
195
196
 
@@ -239,6 +240,10 @@ dsh plugin --profile <profile> remove dsh-tokenlens
239
240
  opencode-go: { enabled: true, apiKeyRef: OPENCODE_GO_API_KEY }
240
241
  commandcode: { enabled: true, apiKeyRef: COMMANDCODE_API_KEY }
241
242
  deepseek: { enabled: true, apiKeyRef: DEEPSEEK_API_KEY }
243
+ open-design: # 额度查询需要工作区 + Control Key
244
+ enabled: true
245
+ workspaceId: '<open-design-workspace-id>' # 空 = 环境变量 OPENDESIGN_WORKSPACE_ID
246
+ controlKeyRef: OPENDESIGN_CONTROL_KEY # 也可用 VELA_CONTROL_KEY
242
247
  ```
243
248
 
244
249
  覆盖规则:
@@ -269,13 +274,15 @@ dsh plugin --profile <profile> remove dsh-tokenlens
269
274
  | `minimax` | `GET https://api.minimax.io/v1/api/openplatform/coding_plan/remains`(订阅 Key;国内站 `api.minimaxi.com`) | 5 小时 / 每周 已用百分比(best-effort) |
270
275
  | `zhipu-cn` | `GET https://open.bigmodel.cn/api/monitor/usage/quota/limit`(`ZHIPU_API_KEY`) | GLM Coding Plan:5 小时 / 本周 / 每月 百分比 + 方案 |
271
276
  | `volcengine` | Coding Plan / AgentPlan:`POST https://open.volcengineapi.com/?Action=GetCodingPlanUsage&Version=2024-01-01`(Signature V4,`VOLCENGINE_ACCESS_KEY_ID` + `VOLCENGINE_SECRET_ACCESS_KEY`) | 5 小时 / 本周 / 每月 百分比 + 档位 |
277
+ | `open-design` | 只读计费接口:`GET https://amr-api.open-design.ai/api/v1/workspaces/{workspaceId}/billing/coding-plan-usage` + `…/wallet/balance`(workspace ID + Control Key:`OPENDESIGN_CONTROL_KEY` / `VELA_CONTROL_KEY`,非推理 Key) | Design Plan 的 5 小时 / 7 天 已用百分比 + 重置时间;钱包余额(USD) |
272
278
 
273
279
  - OpenCode 的额度定义见[官方文档](https://opencode.ai/docs/go/#usage-limits)(5h = 月度额度的 20%、周 = 50%、月 = 100%);接口只返回百分比,插件也只画百分比。
274
280
  - Command Code 的 `/alpha/*` 是官方 CLI 自用的私有计费路由;免费 / 无方案账户返回空窗口(显示 `—`),插件不会用方案目录猜额度。
281
+ - Open Design 的窗口按账户实际返回的时长渲染:Go 同时有 5 小时和 7 天窗口,Plus / Pro / Max 只有 7 天窗口;钱包余额是独立于窗口的 USD 额度,用尽后继续扣钱包。查询要求工作区权限,鉴权失败会明确显示错误而不是 0。
275
282
  - 未配置凭据的供应商 chip 静默隐藏;`zai`、`minimax`、`deepinfra` 属 best-effort 适配(未公开接口,失败时按可重试错误优雅降级)。
276
283
  - 各供应商的元数据(`kind` / `label` / `shortLabel` / `baseURL` / `apiKeyRef` / `consoleUrl`)集中在 `lib/providers.mjs` 的 `PROVIDER_DEFAULTS` 与 `ADAPTERS` 中,新增适配器只需在该文件登记并补测试。
277
284
 
278
- 另有 10 家**仅元数据**的内置供应商(管理页「内置」中可见):`sensenova`(商汤 SenseNova)、`qwen`(通义千问)、`qianfan`(百度千帆)、`intern-ai`、`agnes-ai`、`open-design`、`hunyuan`(腾讯混元)、`spark`(讯飞星火)、`yi`(零一万物)、`baichuan`(百川智能)。它们暂缺公开的套餐 / 余额接口适配器,chip 静默隐藏;社区补齐适配器后即可自动生效。
285
+ 另有 9 家**仅元数据**的内置供应商(管理页「内置」中可见):`sensenova`(商汤 SenseNova)、`qwen`(通义千问)、`qianfan`(百度千帆)、`intern-ai`、`agnes-ai`、`hunyuan`(腾讯混元)、`spark`(讯飞星火)、`yi`(零一万物)、`baichuan`(百川智能)。它们没有可用 API Key 查询的公开套餐 / 余额接口:千问 / 百炼的订阅额度只能通过官方 CLI 的控制台登录查询,腾讯混元 / 百度千帆只有云账户余额 API(需云账号 AK/SK),其余供应商仅控制台可见。chip 对它们静默隐藏,后续有稳定接口时再补适配器。
279
286
 
280
287
  ### 自动识别 DSH 模型设置
281
288
 
@@ -382,7 +389,7 @@ Host 半注册 `tokenlens_usage`,参数 `action`(默认 `snapshot`)与可
382
389
 
383
390
  ## 验证
384
391
 
385
- - `node --test`:**231 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、插件集成与浏览器端渲染 / 交互。
392
+ - `node --test`:**237 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、插件集成与浏览器端渲染 / 交互。
386
393
  - 在真实环境中验证过 **5 个 harness** 的数据源;本机统计与独立的 SQLite 查询、独立的日志解析逐项对账(差额为 0),跨 harness 合并与供应商映射算术自洽。
387
394
  - 供应商接口按各官方文档与参考客户端行为验证;未配置凭据时 chip 静默隐藏,接口异常时按可重试错误降级。
388
395
 
@@ -393,7 +400,7 @@ Host 半注册 `tokenlens_usage`,参数 `action`(默认 `snapshot`)与可
393
400
  ```powershell
394
401
  git clone https://github.com/daigahou/dsh-tokenlens.git
395
402
  cd dsh-tokenlens
396
- node --test # 231 个用例
403
+ node --test # 237 个用例
397
404
  ```
398
405
 
399
406
  本地联调安装方式见[方式 C](#方式-c本地开发从源码-link)。
package/client.js CHANGED
@@ -3145,7 +3145,13 @@ window.__ModuleLoader__.load({
3145
3145
  return `${providerLabel} · ${parts.join(' · ')}`
3146
3146
  }
3147
3147
  if (snapshot !== null && snapshot.provider.kind === 'windows') {
3148
- return `${providerLabel} · ${windowsSummaryText(snapshot.windows, t)}`
3148
+ const summary = `${providerLabel} · ${windowsSummaryText(snapshot.windows, t)}`
3149
+ // A windows provider may also carry a wallet balance (OpenDesign): keep the
3150
+ // chip body unchanged and append the money to the hover detail only.
3151
+ if (snapshot.balance !== null) {
3152
+ return `${summary} · ${formatMoney(snapshot.balance.amount, snapshot.balance.currency)}`
3153
+ }
3154
+ return summary
3149
3155
  }
3150
3156
  return `${providerLabel} · ${t('chipNoUsage')}`
3151
3157
  }
@@ -3347,6 +3353,32 @@ window.__ModuleLoader__.load({
3347
3353
  )
3348
3354
  }
3349
3355
 
3356
+ /**
3357
+ * The wallet-balance row of a windows provider (OpenDesign).
3358
+ *
3359
+ * Some providers report allowance windows AND an independent account balance:
3360
+ * a `windows` snapshot may therefore carry a non-null `balance`. The row reuses
3361
+ * the window row's label/value layout so it reads as part of the same card, and
3362
+ * the shared narrow-screen wrapping of `.dsh-tokenlens-row-head` keeps it in view.
3363
+ */
3364
+ function buildCapacityBalanceRow(balance, t) {
3365
+ if (balance === null) return null
3366
+ return h(
3367
+ 'div',
3368
+ { key: 'balance', className: 'dsh-tokenlens-row', 'data-capacity-balance': 'true' },
3369
+ h(
3370
+ 'div',
3371
+ { className: 'dsh-tokenlens-row-head' },
3372
+ h('span', { className: 'dsh-tokenlens-row-label' }, t('balanceLabel')),
3373
+ h(
3374
+ 'span',
3375
+ { className: 'dsh-tokenlens-percent dsh-tokenlens-capacity-money' },
3376
+ formatMoney(balance.amount, balance.currency),
3377
+ ),
3378
+ ),
3379
+ )
3380
+ }
3381
+
3350
3382
  /** The provider/plan/account header line, or null when there is nothing to say. */
3351
3383
  function buildProviderHeader(snapshot, t) {
3352
3384
  if (snapshot === null) return null
@@ -4365,16 +4397,17 @@ window.__ModuleLoader__.load({
4365
4397
  if (card !== null) section.push(card)
4366
4398
  else section.push(h('div', { key: 'no-balance', className: 'dsh-tokenlens-hint' }, t('chipNoUsage')))
4367
4399
  } else if (snapshot.provider.kind === 'windows') {
4400
+ // A windows provider may ALSO report an independent wallet balance
4401
+ // (OpenDesign): show it as one extra row of the same capacity card,
4402
+ // after the allowance windows (or after the "no usage" hint).
4403
+ const balanceRow = buildCapacityBalanceRow(snapshot.balance, t)
4368
4404
  if (snapshot.windows !== null) {
4369
- section.push(
4370
- h(
4371
- 'div',
4372
- { key: 'rows', className: 'dsh-tokenlens-rows' },
4373
- WINDOW_ORDER.map((id) => buildWindowRow(id, snapshot.windows[id], state, now, t, statsRow)),
4374
- ),
4375
- )
4405
+ const rows = WINDOW_ORDER.map((id) => buildWindowRow(id, snapshot.windows[id], state, now, t, statsRow))
4406
+ if (balanceRow !== null) rows.push(balanceRow)
4407
+ section.push(h('div', { key: 'rows', className: 'dsh-tokenlens-rows' }, rows))
4376
4408
  } else {
4377
4409
  section.push(h('div', { key: 'no-windows', className: 'dsh-tokenlens-hint' }, t('chipNoUsage')))
4410
+ if (balanceRow !== null) section.push(balanceRow)
4378
4411
  }
4379
4412
  } else {
4380
4413
  section.push(h('div', { key: 'no-kind', className: 'dsh-tokenlens-hint' }, t('chipNoUsage')))
package/index.js CHANGED
@@ -71,7 +71,7 @@ export const inject = []
71
71
  export const PLUGIN_NAME = 'dsh-tokenlens'
72
72
 
73
73
  /** Plugin version; mirrors package.json (kept here so diagnostics need no fs read). */
74
- export const PLUGIN_VERSION = '1.0.8'
74
+ export const PLUGIN_VERSION = '1.1.0'
75
75
 
76
76
  /** Documented defaults; every field of the raw row config is optional. */
77
77
  const DEFAULTS = {
@@ -926,6 +926,49 @@ export function apply(ctx, rawConfig) {
926
926
  }
927
927
  provider = { ...provider, accessKeyId: accessValue, secretAccessKey: secretValue }
928
928
  keyInfo = { ref: accessRef, source: access.source, detail: access.detail }
929
+ } else if (adapterId === 'open-design') {
930
+ const workspaceId =
931
+ String(provider.workspaceId ?? '').trim() || String(process.env.OPENDESIGN_WORKSPACE_ID ?? '').trim()
932
+ const refs = []
933
+ for (const ref of [provider.controlKeyRef, 'OPENDESIGN_CONTROL_KEY', 'VELA_CONTROL_KEY']) {
934
+ if (typeof ref === 'string' && ref.trim() !== '' && !refs.includes(ref.trim())) refs.push(ref.trim())
935
+ }
936
+ // The management page's per-provider key field holds the Control Key here
937
+ // (the inference API key cannot query billing).
938
+ let controlKey = typeof provider.apiKey === 'string' ? provider.apiKey.trim() : ''
939
+ let controlRef = refs[0] ?? 'OPENDESIGN_CONTROL_KEY'
940
+ let controlSource = controlKey === '' ? 'none' : 'config'
941
+ let controlDetail = controlKey === '' ? 'no OpenDesign control key found' : 'plugin config'
942
+ if (controlKey === '') {
943
+ for (const ref of refs) {
944
+ const resolved = await resolveApiKey({
945
+ config: { apiKeyRef: ref },
946
+ credentials: ctx.get('credentials'),
947
+ env: process.env,
948
+ })
949
+ const value = typeof resolved.value === 'string' ? resolved.value.trim() : ''
950
+ if (value === '') continue
951
+ controlKey = value
952
+ controlRef = resolved.ref ?? ref
953
+ controlSource = resolved.source
954
+ controlDetail = resolved.detail
955
+ break
956
+ }
957
+ }
958
+ if (workspaceId === '') {
959
+ throw new UsageError(
960
+ 'missing-key',
961
+ 'OpenDesign quota needs a workspace id (providers.open-design.workspaceId or OPENDESIGN_WORKSPACE_ID)',
962
+ { retryable: false },
963
+ )
964
+ }
965
+ if (controlKey === '') {
966
+ throw new UsageError('missing-key', `OpenDesign quota needs a Control Key (${refs.join(' / ')})`, {
967
+ retryable: false,
968
+ })
969
+ }
970
+ provider = { ...provider, workspaceId, controlKey }
971
+ keyInfo = { ref: controlRef, source: controlSource, detail: controlDetail }
929
972
  } else if (typeof provider.apiKeyRef === 'string' && provider.apiKeyRef.trim() !== '') {
930
973
  const key = await resolveApiKey({
931
974
  config: { apiKeyRef: provider.apiKeyRef, apiKey: provider.apiKey ?? '' },
package/lib/providers.mjs CHANGED
@@ -236,12 +236,16 @@ export const PROVIDER_DEFAULTS = {
236
236
  consoleUrl: 'https://agnes.ai',
237
237
  },
238
238
  'open-design': {
239
- kind: 'balance',
239
+ kind: 'windows',
240
240
  label: 'Open Design',
241
241
  shortLabel: 'OD',
242
- baseURL: 'https://api.open-design.ai',
243
- apiKeyRef: 'OPEN_DESIGN_API_KEY',
244
- consoleUrl: 'https://open-design.ai',
242
+ baseURL: 'https://amr-api.open-design.ai',
243
+ // The read-only billing API is scoped to one workspace and authenticated
244
+ // with a Control Key (not the inference API key used for model calls).
245
+ workspaceId: '',
246
+ controlKeyRef: 'OPENDESIGN_CONTROL_KEY',
247
+ apiKeyRef: 'OPENDESIGN_CONTROL_KEY',
248
+ consoleUrl: 'https://open-design.ai/cloud/dashboard',
245
249
  },
246
250
  hunyuan: {
247
251
  kind: 'balance',
@@ -753,6 +757,119 @@ export function parseVolcenginePlan(payload, now = Date.now()) {
753
757
  return { windows, plan: tier === null ? null : { id: tier, label: tier } }
754
758
  }
755
759
 
760
+ /**
761
+ * OpenDesign Design Plan window seconds → the canonical window id.
762
+ *
763
+ * Design Plan allowances are independent simultaneous limits; Go has both a
764
+ * 5-hour and a 7-day window, while Plus/Pro/Max only have the 7-day one. Any
765
+ * other duration is not one of the plugin's canonical windows.
766
+ */
767
+ const OPEN_DESIGN_WINDOW_SECONDS = {
768
+ 18000: 'rolling',
769
+ 604800: 'weekly',
770
+ 2592000: 'monthly',
771
+ }
772
+
773
+ /** A non-negative integer credit amount serialized as a decimal string. */
774
+ function openDesignCredits(value) {
775
+ return typeof value === 'string' && /^\d{1,40}$/.test(value) ? BigInt(value) : null
776
+ }
777
+
778
+ /**
779
+ * Parse the OpenDesign `billing/coding-plan-usage` document.
780
+ *
781
+ * Shape (v1, verified against the official Vela billing adapter):
782
+ * `{ workspaceId, workspaceMemberId?, eligible, windows: [{ policyId,
783
+ * durationSeconds, usedCredits, remainingCredits, limitCredits, resetsAt }] }`
784
+ * where the credits are decimal strings and `resetsAt` is ISO (or null).
785
+ * A window's percentage is `used / limit`, capped at 100%; unknown durations
786
+ * and repeated durations keep only the first canonical window.
787
+ */
788
+ export function parseOpenDesignUsage(payload, workspaceId, now = Date.now()) {
789
+ const document = payload !== null && typeof payload === 'object' && !Array.isArray(payload) ? payload : null
790
+ if (document === null) throw new UsageError('parse', 'the OpenDesign quota response is not an object')
791
+ const expected = String(workspaceId ?? '').trim()
792
+ if (typeof document.workspaceId !== 'string' || document.workspaceId !== expected) {
793
+ throw new UsageError('parse', 'the OpenDesign quota response belongs to a different workspace')
794
+ }
795
+ if (typeof document.eligible !== 'boolean') {
796
+ throw new UsageError('parse', 'the OpenDesign quota response has no eligibility flag')
797
+ }
798
+ if (!Array.isArray(document.windows) || document.windows.length > 32) {
799
+ throw new UsageError('parse', 'the OpenDesign quota response has an invalid windows array')
800
+ }
801
+ const inputs = {}
802
+ const seenPolicies = new Set()
803
+ for (const row of document.windows) {
804
+ if (row === null || typeof row !== 'object') {
805
+ throw new UsageError('parse', 'the OpenDesign quota response has a malformed window')
806
+ }
807
+ if (typeof row.policyId !== 'string' || row.policyId === '' || seenPolicies.has(row.policyId)) {
808
+ throw new UsageError('parse', 'the OpenDesign quota response has an empty or duplicate policy id')
809
+ }
810
+ seenPolicies.add(row.policyId)
811
+ if (!Number.isSafeInteger(row.durationSeconds) || row.durationSeconds <= 0) {
812
+ throw new UsageError('parse', 'the OpenDesign quota window has no duration')
813
+ }
814
+ const used = openDesignCredits(row.usedCredits)
815
+ const limit = openDesignCredits(row.limitCredits)
816
+ const remaining = openDesignCredits(row.remainingCredits)
817
+ if (used === null || limit === null || remaining === null) {
818
+ throw new UsageError('parse', 'the OpenDesign quota window has invalid credit amounts')
819
+ }
820
+ const id = OPEN_DESIGN_WINDOW_SECONDS[row.durationSeconds]
821
+ if (id === undefined || inputs[id] !== undefined) continue
822
+ if (limit <= 0n) continue
823
+ const cappedUsed = used > limit ? limit : used
824
+ const percent = (Number(cappedUsed) / Number(limit)) * 100
825
+ inputs[id] = {
826
+ percent,
827
+ status: 'ok',
828
+ resetsAt:
829
+ typeof row.resetsAt === 'string' || typeof row.resetsAt === 'number' ? row.resetsAt : undefined,
830
+ }
831
+ }
832
+ const windows = windowSet(inputs, now)
833
+ const usable = WINDOW_IDS.some((id) => windows[id] !== null)
834
+ if (document.eligible === false) {
835
+ if (usable) throw new UsageError('parse', 'the OpenDesign quota response reports an ineligible plan with windows')
836
+ return { windows: windowSet({}, now), plan: null }
837
+ }
838
+ if (!usable) throw new UsageError('parse', 'the OpenDesign quota response has no usable allowance windows')
839
+ return { windows, plan: null }
840
+ }
841
+
842
+ /**
843
+ * Parse the OpenDesign `wallet/balance` document.
844
+ *
845
+ * Shape (v2): `{ billingScopeVersion: 2, workspaceId, workspaceMemberId,
846
+ * balanceUsd }` where `balanceUsd` is a decimal string (negative = debt).
847
+ */
848
+ export function parseOpenDesignWallet(payload, workspaceId) {
849
+ const document = payload !== null && typeof payload === 'object' && !Array.isArray(payload) ? payload : null
850
+ if (document === null) throw new UsageError('parse', 'the OpenDesign wallet response is not an object')
851
+ if (document.billingScopeVersion !== 2) {
852
+ throw new UsageError('parse', 'the OpenDesign wallet response has an unsupported scope version')
853
+ }
854
+ const expected = String(workspaceId ?? '').trim()
855
+ if (typeof document.workspaceId !== 'string' || document.workspaceId !== expected) {
856
+ throw new UsageError('parse', 'the OpenDesign wallet response belongs to a different workspace')
857
+ }
858
+ if (typeof document.workspaceMemberId !== 'string' || document.workspaceMemberId === '') {
859
+ throw new UsageError('parse', 'the OpenDesign wallet response has no member identity')
860
+ }
861
+ const raw = typeof document.balanceUsd === 'string' && /^-?\d+(?:\.\d+)?$/.test(document.balanceUsd)
862
+ ? Number(document.balanceUsd)
863
+ : null
864
+ if (raw === null || !Number.isFinite(raw)) {
865
+ throw new UsageError('parse', 'the OpenDesign wallet response has no usable balance')
866
+ }
867
+ return {
868
+ balance: { amount: raw, currency: 'USD' },
869
+ workspaceMemberId: document.workspaceMemberId,
870
+ }
871
+ }
872
+
756
873
  /**
757
874
  * The provider adapter table. `read()` performs the required network reads and
758
875
  * returns the normalized result; optional reads (subscription/whoami) are
@@ -791,6 +908,49 @@ export const ADAPTERS = {
791
908
  return { kind: 'windows', ...parseVolcenginePlan(payload, now) }
792
909
  },
793
910
  },
911
+ 'open-design': {
912
+ kind: 'windows',
913
+ async read({ providerId = 'open-design', provider, httpJson, now }) {
914
+ const config = providerConfig(providerId, provider)
915
+ const workspaceId = String(config.workspaceId ?? '').trim()
916
+ if (workspaceId === '') {
917
+ throw new UsageError(
918
+ 'missing-key',
919
+ 'OpenDesign quota needs a workspace id (providers.open-design.workspaceId or OPENDESIGN_WORKSPACE_ID)',
920
+ { retryable: false },
921
+ )
922
+ }
923
+ const controlKey = typeof provider.controlKey === 'string' ? provider.controlKey : ''
924
+ if (controlKey === '') {
925
+ throw new UsageError('missing-key', 'OpenDesign quota needs a Control Key (OPENDESIGN_CONTROL_KEY or VELA_CONTROL_KEY)', {
926
+ retryable: false,
927
+ })
928
+ }
929
+ const workspaceBase = joinUrl(config.baseURL, `/api/v1/workspaces/${encodeURIComponent(workspaceId)}`)
930
+ const headers = {
931
+ accept: 'application/json',
932
+ authorization: `Bearer ${controlKey}`,
933
+ 'x-vela-workspace-id': workspaceId,
934
+ }
935
+ const usage = await httpJson(`${workspaceBase}/billing/coding-plan-usage`, { method: 'GET', headers })
936
+ const parsed = parseOpenDesignUsage(usage, workspaceId, now)
937
+ const wallet = await httpJson(`${workspaceBase}/wallet/balance`, { method: 'GET', headers })
938
+ const usageMember =
939
+ usage !== null && typeof usage === 'object' && typeof usage.workspaceMemberId === 'string'
940
+ ? usage.workspaceMemberId
941
+ : undefined
942
+ const walletParsed = parseOpenDesignWallet(wallet, workspaceId)
943
+ if (usageMember !== undefined && usageMember !== walletParsed.workspaceMemberId) {
944
+ throw new UsageError('parse', 'the OpenDesign wallet and quota responses name different workspace members')
945
+ }
946
+ return {
947
+ kind: 'windows',
948
+ windows: parsed.windows,
949
+ plan: parsed.plan,
950
+ balance: walletParsed.balance,
951
+ }
952
+ },
953
+ },
794
954
  'zhipu-cn': {
795
955
  kind: 'windows',
796
956
  async read({ providerId = 'zhipu-cn', provider, fetchJson, now }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tokenlens",
3
- "version": "1.0.8",
3
+ "version": "1.1.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",