@mars-sea/dsh-commandcode-provider 0.15.4 → 0.15.5

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,39 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.15.5] - 2026-10-27
8
+
9
+
10
+ ### Changed
11
+
12
+ - **请求路径与账户读取的一批性能优化(线上字节不变)。**
13
+ - `streamAttempt()` 的凭据解析与正文装配改为并发:两者互不依赖(正文从不携带密钥,健康账号池的解析只读内存),从前却是严格串行,带图请求因此把图片读取与 JSON 装配整个排在 `resolveApiKey()` 之后。与密钥无关的通道判断先算成 `provisionalProtocol`,凭据返回后目标一致即复用,不一致才重建一次(与既有协议回退、账号轮换同价)。代价是 Go 计划/已学 CLI 通道的账号每次请求会多装配一次正文(线上字节不变,纯 CPU 开销)。图片预算的**会计**仍先于任何 I/O 执行,但**抛错**推迟到凭据与真实通道落定之后:预算门与缓存门一次算全、一次抛出。
14
+ - `listModels()` 在**本进程联网成功过**该网关目录时不再等一次联网刷新,改用内存中的行立即返回、后台刷新留给下次打开;进程首次加载仍照旧等待。新判据 `GatewayFacts.loaded()` 以「来源」为准而不是「内存里有行」:`snapshot()` 从磁盘灌入的行、以及刷新失败后的空目录都不算热数据(磁盘副本没有年龄校验)。`resolveModel()` 与 `stream()` 不变,仍等真实刷新。
15
+ - `gradeHeaderTimeout()` 的指纹改用**已经序列化好的实发字符串**求摘要(`sha256Hex()`),不再对 `body` 重新 `JSON.stringify`:超时是最常重复的路径,而带图历史可达数 MB。这也更准确——实发字符串就是网关看到的字节,重新 stringify 会在 surrogate 清洗真的删掉过字符时描述到不同的字节,把一次连续超时拆成两条 streak。
16
+ - `toolParametersSchema()` 的根节点结果按 `parameters` 对象身份记忆化(`WeakMap`)。每个通道在每次尝试时都重建工具列表,而账号轮换、协议回退、降档重试都会让同一批 schema 再走一遍。同时纠正原注释「每条路径都返回副本」——`type: 'object'` 快路径一直返回调用方自己的对象,调用方靠它拿到同一个已冻结的 schema。
17
+ - 用量卡片与选择器的套餐过滤共享账户端点响应:两处读的是同一批端点、同一个账号,成功响应按 `[apiBase, key, 完整 URL]` 记入短命表(`ACCOUNT_FACTS_TTL_MS` 30 秒)。实际共享的是 `whoami` 与 `subscriptions`——`credits` 的 URL 形状带 `orgId`,两处不同,不会互相命中。只缓存成功响应(失败必须重试,否则报告的 `failures` 与 `classifyTotalFailure` 结论会描述一次没真正发出的请求);`/alpha/usage/summary` 明确不共享——那是用户正盯着、旁边写着读取时间的数字。
18
+ - 用量刷新区分自动与显式路径:`refresh({ auto: true })` 用于账户卡片首屏、面板首屏与面板 tick(injected 面因此分成 `refreshUsageAuto()` 与 `refreshUsage()` 两个入口),可在 `USAGE_AUTO_REFRESH_COALESCE_MS`(5 秒)内复用刚刚读到的结果;按钮、保存、换凭据等显式调用不查合并窗口(请求在途时仍按既有 `inFlight` 规则折叠),`fetchedAt` 因此仍如实反映读取时刻。
19
+ - 面板自动刷新在文档隐藏时跳过读取但照旧重排定时器,重新可见立即补读一次(该补读走 auto,5 秒内仍可能被合并);首屏仍无条件读取。可见性做成可注入接缝(`AutoRefreshVisibility`),非浏览器环境视为始终可见;监听句柄随循环状态放在模块层,任意卸载顺序都能解绑。
20
+ - **设置页账户卡片:账期与更新时间并入余额标签行。** 「月额度剩余」标签右接账期区间、同行右侧显示「更新于」;原单独一行的「官方账期用量 …」与底部重复的「账期截止」删除。账期未对齐时标签改接订阅自身的「账期截止」日期,不再显示「统计周期未确认」(额度面板不变);余额没取到时该行仍保留,区间前补「官方账期用量」。底部只在部分接口失败时显示「部分数据未取到」。
21
+
22
+ - **同步上游两处:Command Code 目录新增 `gpt-6.1-terra`,DSH `0.2.1-rc.4` 补入验证矩阵。**
23
+ - **Command Code(CLI 版本不变,仍 `1.88.0`)**:2026-10-27 复核 npm `latest` 与官网 changelog 均无新 CLI 版本(最新条目仍是 `1.87.3`);服务端目录 `?view=client` 的 `catalogVersion` 由 `f41a48ed…` 变为 `7f405e24…`,默认视图 93 → 94、client 视图 105 → 106,唯一新增行是 `gpt-6.1-terra`(GPT-6.1 Terra):1,050,000 上下文、文本 + 图像、推理档位 `low`/`medium`/`high`/`xhigh`/`max`、最低计划 Pro、`$1/$5`(≤272K 上下文)与 `$2/$7.5`(>272K)、缓存读取 `$0.05`/`$0.1`、缓存写入 `$1.25`/`$2.5`、月度额度 Go `$6`/GOAT `$20`/Pro `$20`,ZDR 默认覆盖,无促销、无峰值时段、无输出上限公布值。官方 models 页与 pricing 页均已上架,`1.88.0` 的 CLI 注册表尚未收录(审计 `incomplete`,能力字段以 `?view=client` 目录为准)。快照 +1:`KNOWN_PLANS` 93 → 94、`KNOWN_EFFORTS` 66 → 67、`KNOWN_IMAGE_MODELS` 72 → 73、内置价格行 93 → 94;ZDR 例外集(18)、自动思考模型表(16)、订阅表(9)、促销(4)、输出上限表(79)不变。`scripts/sync-model-prices.mjs` 重跑(页面 94 行,diff 仅新增该行)。
24
+ - **DSH `0.2.1-rc.4`**:npm `next` 于北京时间 2026-10-26 23:28(`2026-10-26T15:28:13Z`)从 `0.2.1-rc.3` 前进到 `0.2.1-rc.4`,`latest` 仍是 `0.2.1-rc.2`;GitHub 无对应 tag/release(均 404),`master` 停在 10-23,产物内构建哈希 `ec17a2c` 在公开仓库不可见——官方没有发布说明。产物级全量比对 235 个双版本包,实质变化仅 4 处:`dsh-mcp-client` 的资源能力探测(服务器未声明 `resources` 时直接返回空列表)、`dsh-client-ui-conversation` 的 View 标签提示框(按字符宽度估算、仅溢出时显示)与「更多会话视图」文案精简、`dsh-llm-deepseek-account` 的一条日志级别(`console.info` → `console.error`)、`dsh-mcp-resources` 的文档;另有 3 个包仅构建元数据(commit 短哈希、版本串)变化。插件导入的宿主包(`llm`/`settings`/`credentials`/`client-*`/`web`/`typert`/`api-remotes`/`attachment`/`commands`/`timeout`/`launch-environment`/`app-boot`)全部零功能变化。`dsh.compatibility.dshReleases` 补入 `0.2.1-rc.4`(三条实测记录),`test:engine`/`test:install` 矩阵随之变为 3 个引擎。
25
+
26
+ ### Fixed
27
+
28
+ - **图片预算门不再抢在凭据之前抛错。** 预算门只做会计,凭据解析失败时先报 `MISSING_CREDENTIAL`:避免宿主对一次根本发不出去的请求先做不可逆的持久化图片卸载。预算门与缓存门在凭据与真实通道落定后一次算全(`max(预算, 缓存)`),不再退化成两步卸载,也不会把 cache 溢出报成 budget。(`src/adapter.ts`)
29
+ - **`loaded()` 只认联网来源。** `snapshot()` 灌入的磁盘行与刷新失败后的空目录都不再被当成选择器首屏:冷启动"先聊天再开选择器"不会画出无年龄校验的旧目录,刷新失败后也不会先闪一次空列表。(`src/gateway-facts.ts`)
30
+ - **可见性监听按任意卸载顺序解绑。** `unsubscribe` 句柄从函数局部提升到模块层,启动循环的 surface 先卸载时不再泄漏 `visibilitychange` 监听。(`src/client/panel.ts`)
31
+ - **账户卡片首屏接线补真实渲染测试。** 新增 `tests/accounts-card-render.test.ts`(`react-test-renderer`):首屏必须调 `onRefreshAuto()`、刷新按钮必须调 `onRefresh()`;把组件改回旧接线时该用例失败。此前只有"两个入口存在"的断言,回退后测试仍全绿。
32
+ - **非安全上下文(`http://` 局域网访问)下设置页不再整块空白(issue #102)。** 页面身份原来直接取 `crypto.randomUUID()`,而该 API 只在安全上下文存在:以 `http://<局域网 IP>:3080` 打开时它是 `undefined`,控制器又在 `AccountsCard` 的 `useState(createEnrollment)` 惰性初始化里构造,异常被设置页插槽的错误边界接住——左侧「Command Code」标签正常、右侧内容区空白且无任何提示。新增 `src/client/random-id.ts` 的 `randomId()`:`randomUUID` 可用时照旧,否则退回非安全上下文同样可用的 `crypto.getRandomValues`,按 RFC 4122 拼出同形 v4;`AccountEnrollmentController` 的默认 `newId` 改用它。页面身份只是宿主把开通过程绑到单个浏览器页面的分组键,不参与鉴权,因此不要求 HTTPS。(`src/client/random-id.ts`、`src/client/enrollment.ts`)
33
+
34
+ 用例:`tests/adapter.test.ts` 新增「凭据解析与图片读取并行」「目录已在内存时不再等一次联网刷新」「超时定级复用已序列化的正文」「同一份工具 schema 只归一化一次」「用量卡片与选择器共享同一份账户事实」「凭据缺失时预算门不抢跑」「双超限一次报数」;`tests/gateway-facts.test.ts` 新增 4 条 `loaded()` 来源判据;`tests/panel.test.ts` 与 `tests/usage-client.test.ts` 新增自动/显式刷新、可见性与任意卸载顺序解绑用例;`tests/accounts-card-render.test.ts` 新增 2 条真实渲染接线用例。**验证边界**:`npm run typecheck` 退出码 0;`npm ci` 干净安装后 `npm test` **1125/1125** 通过;`npm run build` 重建 `lib/`。未在真实宿主中人工验证。
35
+
36
+ issue #102 的用例:`tests/random-id.test.ts` 3 条——安全上下文走平台 `randomUUID`(哨兵值分辨分支,不走自行拼装)、非安全上下文退回 `getRandomValues` 时 100 个身份互不相同且为 v4、非安全上下文下构造 `AccountEnrollmentController` 并把页面身份带给宿主。三处定向变异各命中一条:关闭快速路径只失败第一条,把版本位置 0 只失败后两条,把默认 `newId` 改回 `() => crypto.randomUUID()` 只失败第三条。**验证边界**:`npm run typecheck` 退出码 0;`npm ci` 干净安装后 `npm test` **1129/1129** 通过;`npm run build` 重建 `lib/`(`lib/client.js` 含 `randomId`,`lib/index.js` 与 `lib/index.d.ts` 字节不变)。未在真实 NAS/局域网环境人工验证。
37
+
38
+ **本轮(2026-10-27 上游同步)验证**:`npm run typecheck` 退出码 0;`npm test` **1126/1126** 通过、0 失败;`npm run build` 两段 `Build complete`,`lib/index.js` 已重建(含 `gpt-6.1-terra`);`npm run test:engine` **3/3**(`0.2.1-rc.2`/`0.2.1-rc.3`/`0.2.1-rc.4`,宿主准入 preflight 全部放行,末行 `engine-load smoke passed on all 3 declared-compatible engines`);`npm run test:install` **3/3**(pnpm 10.34.5);`npm run test:pack` 16 文件(版本 0.15.4)。未做真实账号登录、付费生成、服务端协议验证与浏览器渲染。
39
+
7
40
  ## [0.15.4] - 2026-10-24
8
41
 
9
42
  ### Changed
package/README.md CHANGED
@@ -61,7 +61,7 @@ Unofficial [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harnes
61
61
  dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@latest
62
62
  ```
63
63
 
64
- This build admits **dsh `>=0.2.1-rc.2 <0.2.2-0`** — every RC of the `0.2.1` channel from `0.2.1-rc.2` onward. `0.2.1-rc.2` is what npm `latest` runs and `0.2.1-rc.3` ships on `next`; both are covered by the full engine matrix (`npm run test:engine`, `npm run test:install`). A later `0.2.1-rc.4` is admitted without a plugin update, but stays unverified until a record is added to `dsh.compatibility.dshReleases`. The next minor is deliberately excluded, `0.2.2-rc.*` included — the upper bound is written `<0.2.2-0` for exactly that reason. A profile on `0.2.0-rc.2`, `0.2.1-alpha.5` or older must upgrade its engine first. Compatibility details — the `engines.dsh` admission rule, package pairings, evidence — live in [engine compatibility](docs/agent-reference/engine-compatibility.md).
64
+ This build admits **dsh `>=0.2.1-rc.2 <0.2.2-0`** — every RC of the `0.2.1` channel from `0.2.1-rc.2` onward. `0.2.1-rc.2` is what npm `latest` runs and `0.2.1-rc.4` ships on `next`; `0.2.1-rc.2`, `0.2.1-rc.3` and `0.2.1-rc.4` are all covered by the full engine matrix (`npm run test:engine`, `npm run test:install`). A later `0.2.1-rc.5` is admitted without a plugin update, but stays unverified until a record is added to `dsh.compatibility.dshReleases`. The next minor is deliberately excluded, `0.2.2-rc.*` included — the upper bound is written `<0.2.2-0` for exactly that reason. A profile on `0.2.0-rc.2`, `0.2.1-alpha.5` or older must upgrade its engine first. Compatibility details — the `engines.dsh` admission rule, package pairings, evidence — live in [engine compatibility](docs/agent-reference/engine-compatibility.md).
65
65
 
66
66
  **Older dsh releases** are no longer supported: the last plugin release for dsh 0.1.7 is `0.11.17`, for 0.1.2–0.1.6 it is `0.11.11`, and for the 0.5.0-era line `0.9.1` — all installed by exact version, none maintained.
67
67
 
package/README.zh-CN.md CHANGED
@@ -61,7 +61,7 @@
61
61
  dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@latest
62
62
  ```
63
63
 
64
- 本构建准入 **dsh `>=0.2.1-rc.2 <0.2.2-0`** —— `0.2.1` 通道中 `0.2.1-rc.2` 及以后的每个 RC。`0.2.1-rc.2` 是 npm `latest` 指向的版本,`0.2.1-rc.3` 挂在 `next`;两者都跑过完整引擎矩阵(`npm run test:engine`、`npm run test:install`)。更晚的 `0.2.1-rc.4` 无需更新插件即可准入,但在 `dsh.compatibility.dshReleases` 补上记录之前不算已验证。下一个 minor 被刻意排除(含 `0.2.2-rc.*`)—— 区间上界写成 `<0.2.2-0` 正是为了这一点。使用 `0.2.0-rc.2`、`0.2.1-alpha.5` 或更早版本的配置需先升级引擎,再安装本构建。兼容性细节 —— `engines.dsh` 准入规则、按引擎配对的包版本、验证证据 —— 见[引擎兼容](docs/agent-reference/engine-compatibility.md)。
64
+ 本构建准入 **dsh `>=0.2.1-rc.2 <0.2.2-0`** —— `0.2.1` 通道中 `0.2.1-rc.2` 及以后的每个 RC。`0.2.1-rc.2` 是 npm `latest` 指向的版本,`0.2.1-rc.4` 挂在 `next`;`0.2.1-rc.2`、`0.2.1-rc.3` 与 `0.2.1-rc.4` 都跑过完整引擎矩阵(`npm run test:engine`、`npm run test:install`)。更晚的 `0.2.1-rc.5` 无需更新插件即可准入,但在 `dsh.compatibility.dshReleases` 补上记录之前不算已验证。下一个 minor 被刻意排除(含 `0.2.2-rc.*`)—— 区间上界写成 `<0.2.2-0` 正是为了这一点。使用 `0.2.0-rc.2`、`0.2.1-alpha.5` 或更早版本的配置需先升级引擎,再安装本构建。兼容性细节 —— `engines.dsh` 准入规则、按引擎配对的包版本、验证证据 —— 见[引擎兼容](docs/agent-reference/engine-compatibility.md)。
65
65
 
66
66
  **更早的 dsh 版本**已不再支持:覆盖 dsh 0.1.7 的最后一个插件版本是 `0.11.17`,0.1.2–0.1.6 线的最后一个版本是 `0.11.11`,0.5.0 时代 Harness 线的最后一个版本是 `0.9.1` —— 三者都按精确版本安装,且都不再维护。
67
67
 
package/lib/client.js CHANGED
@@ -1856,6 +1856,8 @@ window.__ModuleLoader__.load({
1856
1856
  generation = 0;
1857
1857
  inFlight = false;
1858
1858
  disposed = false;
1859
+ /** When the last fetch SETTLED successfully, for {@link USAGE_AUTO_REFRESH_COALESCE_MS}. */
1860
+ settledAt;
1859
1861
  constructor(remote) {
1860
1862
  this.remote = remote;
1861
1863
  }
@@ -1877,9 +1879,16 @@ window.__ModuleLoader__.load({
1877
1879
  /**
1878
1880
  * Fetch (or refetch) the report. Concurrent refreshes collapse onto one
1879
1881
  * request; a superseded fetch's late result is dropped, never published.
1882
+ *
1883
+ * `auto` marks a refresh nobody asked for — a surface appearing, or the
1884
+ * background tick. Those also reuse a result fetched within
1885
+ * {@link USAGE_AUTO_REFRESH_COALESCE_MS}, because two surfaces mounting a
1886
+ * second apart are asking the same question. Every other caller is a user
1887
+ * action and always goes to the wire.
1880
1888
  */
1881
- async refresh() {
1889
+ async refresh(options) {
1882
1890
  if (this.disposed || this.inFlight) return;
1891
+ if (options?.auto === true && this.current.status === "ready" && this.settledAt !== void 0 && Date.now() - this.settledAt < 5e3) return;
1883
1892
  const generation = ++this.generation;
1884
1893
  this.inFlight = true;
1885
1894
  this.current = {
@@ -1891,13 +1900,16 @@ window.__ModuleLoader__.load({
1891
1900
  try {
1892
1901
  const response = await this.remote.report();
1893
1902
  if (this.disposed || generation !== this.generation) return;
1894
- if (response.ok) this.current = {
1895
- status: "ready",
1896
- report: response.value,
1897
- error: void 0,
1898
- fetchedAt: Date.now()
1899
- };
1900
- else this.current = {
1903
+ if (response.ok) {
1904
+ const fetchedAt = Date.now();
1905
+ this.settledAt = fetchedAt;
1906
+ this.current = {
1907
+ status: "ready",
1908
+ report: response.value,
1909
+ error: void 0,
1910
+ fetchedAt
1911
+ };
1912
+ } else this.current = {
1901
1913
  ...this.current,
1902
1914
  status: "error",
1903
1915
  error: response.error.message
@@ -2887,6 +2899,23 @@ window.__ModuleLoader__.load({
2887
2899
  ]
2888
2900
  };
2889
2901
  //#endregion
2902
+ //#region src/client/random-id.ts
2903
+ /**
2904
+ * 页面身份(issue #102)。`crypto.randomUUID` 是安全上下文专属:局域网里用
2905
+ * `http://<ip>:3080` 打开设置页时它是 `undefined`,而这里在控制器构造期就调用,
2906
+ * 异常会落到设置页插槽的错误边界上,整块内容变成空白。`crypto.getRandomValues`
2907
+ * 在非安全上下文同样可用,因此只在 `randomUUID` 不可用时按 RFC 4122 自行拼一个 v4。
2908
+ */
2909
+ function randomId() {
2910
+ const source = globalThis.crypto;
2911
+ if (typeof source.randomUUID === "function") return source.randomUUID();
2912
+ const bytes = source.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
2913
+ bytes[6] = bytes[6] & 15 | 64;
2914
+ bytes[8] = bytes[8] & 63 | 128;
2915
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
2916
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
2917
+ }
2918
+ //#endregion
2890
2919
  //#region src/client/enrollment.ts
2891
2920
  const live = (state) => [
2892
2921
  "creating",
@@ -2912,7 +2941,7 @@ window.__ModuleLoader__.load({
2912
2941
  watch;
2913
2942
  ready;
2914
2943
  openedPage = false;
2915
- constructor(remote, changed, pollMs = 750, newId = () => crypto.randomUUID(), openPage = openLoginPage) {
2944
+ constructor(remote, changed, pollMs = 750, newId = randomId, openPage = openLoginPage) {
2916
2945
  this.remote = remote;
2917
2946
  this.changed = changed;
2918
2947
  this.pollMs = pollMs;
@@ -3243,7 +3272,6 @@ window.__ModuleLoader__.load({
3243
3272
  usageCost: "花费",
3244
3273
  usageCompleted: "成功",
3245
3274
  usageBillingPeriod: "官方账期用量",
3246
- usagePeriodUnknown: "官方用量汇总,统计周期未确认",
3247
3275
  usageTokens: "Token",
3248
3276
  usageTokensIn: "入",
3249
3277
  usageTokensOut: "出",
@@ -3330,7 +3358,7 @@ window.__ModuleLoader__.load({
3330
3358
  accountEnrollmentCleanupRetry: "重试清理",
3331
3359
  accountRefSeparator: ":",
3332
3360
  accountModels: "专用模型",
3333
- accountModelsHint: "选中的模型优先由此账户处理,不可用时仍会自动切换;一个模型只归属一个账户。",
3361
+ accountModelsHint: "选中即专属:优先此账户处理,不可用时自动切换",
3334
3362
  accountModelOwner: "属于 {name}",
3335
3363
  modelPick: "选择模型…",
3336
3364
  modelCount: "已选 {count} 个模型",
@@ -3515,7 +3543,6 @@ window.__ModuleLoader__.load({
3515
3543
  usageCost: "Spend",
3516
3544
  usageCompleted: "Completed",
3517
3545
  usageBillingPeriod: "Official billing-period usage",
3518
- usagePeriodUnknown: "Official usage summary; the billing period is unconfirmed",
3519
3546
  usageTokens: "Tokens",
3520
3547
  usageTokensIn: "in",
3521
3548
  usageTokensOut: "out",
@@ -3602,7 +3629,7 @@ window.__ModuleLoader__.load({
3602
3629
  accountEnrollmentCleanupRetry: "Retry cleanup",
3603
3630
  accountRefSeparator: ": ",
3604
3631
  accountModels: "Dedicated models",
3605
- accountModelsHint: "These models prefer this account and still switch when it is unavailable; a model belongs to one account.",
3632
+ accountModelsHint: "Selected models are dedicated to this account and still switch when unavailable",
3606
3633
  accountModelOwner: "on {name}",
3607
3634
  modelPick: "Choose models…",
3608
3635
  modelCount: "{count} models selected",
@@ -3837,7 +3864,7 @@ window.__ModuleLoader__.load({
3837
3864
  }
3838
3865
  //#endregion
3839
3866
  //#region package.json
3840
- var version = "0.15.4";
3867
+ var version = "0.15.5";
3841
3868
  var repository = {
3842
3869
  "type": "git",
3843
3870
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
@@ -4877,6 +4904,19 @@ window.__ModuleLoader__.load({
4877
4904
  ]
4878
4905
  });
4879
4906
  }
4907
+ /**
4908
+ * Left text of the balance label row: the billing period, stated once per card.
4909
+ * 用量汇总与订阅起点对齐时给完整区间;对不上(或用量汇总没取到)时退回订阅
4910
+ * 自身的截止日,它不依赖对齐也可靠。余额没取到时没有「月额度剩余」可挂,完整
4911
+ * 区间前补回「官方账期用量」,避免一串裸日期没有上下文。
4912
+ */
4913
+ function balanceLabelText(report, t) {
4914
+ const plan = report.plan;
4915
+ const usagePeriod = formatUsageBillingPeriod(report.usage?.periodStart, plan?.currentPeriodStart, plan?.currentPeriodEnd);
4916
+ const periodText = usagePeriod !== "" ? usagePeriod : plan !== void 0 && plan.currentPeriodEnd > 0 ? `${t("usagePeriodEnd")} ${new Date(plan.currentPeriodEnd).toLocaleDateString()}` : "";
4917
+ if (report.credits !== void 0) return periodText !== "" ? `${t("usageMonthlyLeft")} · ${periodText}` : t("usageMonthlyLeft");
4918
+ return usagePeriod !== "" ? `${t("usageBillingPeriod")} ${usagePeriod}` : periodText;
4919
+ }
4880
4920
  /** One pool account's facts (identity, totals, credits, window limits). */
4881
4921
  function AccountReport({ entry, fetchedAt, t, headless = false }) {
4882
4922
  const report = entry.report;
@@ -4886,9 +4926,12 @@ window.__ModuleLoader__.load({
4886
4926
  const plan = report.plan;
4887
4927
  const planName = plan?.name ?? "";
4888
4928
  const planStatus = plan !== void 0 && plan.status !== "" && plan.status !== "active" ? plan.status : "";
4889
- const showPeriod = plan !== void 0 && plan.currentPeriodEnd > 0;
4890
4929
  const showPartial = report.failures.length > 0 && report.blocked === void 0;
4891
- const usagePeriod = formatUsageBillingPeriod(report.usage?.periodStart, plan?.currentPeriodStart, plan?.currentPeriodEnd);
4930
+ const label = balanceLabelText(report, t);
4931
+ const updatedText = fetchedAt === void 0 ? "" : `${t("usageUpdated")} ${new Date(fetchedAt).toLocaleTimeString(void 0, {
4932
+ hour: "2-digit",
4933
+ minute: "2-digit"
4934
+ })}`;
4892
4935
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4893
4936
  className: "cc-accountReport",
4894
4937
  children: [
@@ -4927,26 +4970,33 @@ window.__ModuleLoader__.load({
4927
4970
  children: report.failures.join(" · ")
4928
4971
  }) : null]
4929
4972
  }) : null,
4930
- credits !== void 0 ? (() => {
4931
- const monthlyKnown = credits.monthlyReported !== false;
4932
- const depleted = monthlyKnown && credits.monthlyCredits <= 0;
4973
+ (() => {
4974
+ const monthlyKnown = credits !== void 0 && credits.monthlyReported !== false;
4975
+ const depleted = credits !== void 0 && monthlyKnown && credits.monthlyCredits <= 0;
4976
+ if (label === "" && updatedText === "") return null;
4933
4977
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4934
4978
  className: depleted ? "cc-balance cc-balanceEmpty" : "cc-balance",
4935
4979
  children: [
4936
4980
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4937
- className: "cc-balanceMain",
4981
+ className: "cc-balanceRow",
4938
4982
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4939
4983
  className: "cc-balanceLabel",
4940
- children: t("usageMonthlyLeft")
4941
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4984
+ children: label
4985
+ }), updatedText !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4986
+ className: "cc-balanceLabel",
4987
+ children: updatedText
4988
+ }) : null]
4989
+ }),
4990
+ credits !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4991
+ className: "cc-balanceRow",
4992
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4942
4993
  className: "cc-balanceValue",
4943
4994
  children: monthlyKnown ? formatMoney(Math.max(0, credits.monthlyCredits)) : "—"
4995
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4996
+ className: "cc-balanceSub",
4997
+ children: `${t("usagePurchased")} ${credits.purchasedReported === false ? "—" : formatMoney(credits.purchasedCredits)} · ${t("usageFree")} ${credits.freeReported === false ? "—" : formatMoney(credits.freeCredits)}`
4944
4998
  })]
4945
- }),
4946
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4947
- className: "cc-balanceSub",
4948
- children: `${t("usagePurchased")} ${credits.purchasedReported === false ? "—" : formatMoney(credits.purchasedCredits)} · ${t("usageFree")} ${credits.freeReported === false ? "—" : formatMoney(credits.freeCredits)}`
4949
- }),
4999
+ }) : null,
4950
5000
  depleted ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4951
5001
  className: "cc-balanceNote",
4952
5002
  role: "status",
@@ -4954,11 +5004,7 @@ window.__ModuleLoader__.load({
4954
5004
  }) : null
4955
5005
  ]
4956
5006
  });
4957
- })() : null,
4958
- report.usage !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4959
- className: "cc-usageUpdated",
4960
- children: usagePeriod ? `${t("usageBillingPeriod")} ${usagePeriod}` : t("usagePeriodUnknown")
4961
- }) : null,
5007
+ })(),
4962
5008
  report.usage !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4963
5009
  className: "cc-usageStats cc-usageStatsActivity",
4964
5010
  children: [
@@ -5000,35 +5046,10 @@ window.__ModuleLoader__.load({
5000
5046
  t
5001
5047
  }) : null]
5002
5048
  }) : null,
5003
- showPeriod || showPartial || fetchedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5004
- className: "cc-usageMeta",
5005
- children: [
5006
- showPeriod ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
5007
- className: "cc-usageUpdated",
5008
- children: [
5009
- t("usagePeriodEnd"),
5010
- " ",
5011
- new Date(plan.currentPeriodEnd).toLocaleDateString()
5012
- ]
5013
- }) : null,
5014
- showPartial ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5015
- className: "cc-usagePartial",
5016
- title: report.failures.join("; "),
5017
- children: t("usagePartial")
5018
- }) : null,
5019
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }),
5020
- fetchedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
5021
- className: "cc-usageUpdated",
5022
- children: [
5023
- t("usageUpdated"),
5024
- " ",
5025
- new Date(fetchedAt).toLocaleTimeString(void 0, {
5026
- hour: "2-digit",
5027
- minute: "2-digit"
5028
- })
5029
- ]
5030
- }) : null
5031
- ]
5049
+ showPartial ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5050
+ className: "cc-usagePartial",
5051
+ title: report.failures.join("; "),
5052
+ children: t("usagePartial")
5032
5053
  }) : null
5033
5054
  ]
5034
5055
  });
@@ -6459,8 +6480,13 @@ window.__ModuleLoader__.load({
6459
6480
  * The unified account card: every account the pool rotates through, one row
6460
6481
  * each, with its status and quota inline — so one account's facts no longer
6461
6482
  * have to be cross-read across separate surfaces.
6483
+ *
6484
+ * Exported for the render test (`tests/accounts-card-render.test.ts`) that pins
6485
+ * the first-paint auto-refresh wiring against a real React tree: the page mounts
6486
+ * this card, and a test that only checks the injected function exists cannot see
6487
+ * the card reaching for it.
6462
6488
  */
6463
- function AccountsCard({ t, state, usage, login, disabled, actions, onRefresh, openAnalytics, createEnrollment, expanded, onToggle }) {
6489
+ function AccountsCard({ t, state, usage, login, disabled, actions, onRefresh, onRefreshAuto, openAnalytics, createEnrollment, expanded, onToggle }) {
6464
6490
  const [adding, setAdding] = (0, react.useState)(false);
6465
6491
  const [enrollment] = (0, react.useState)(createEnrollment);
6466
6492
  const snapshot = (0, react.useSyncExternalStore)(enrollment.store.subscribe, enrollment.store.getSnapshot);
@@ -6471,8 +6497,8 @@ window.__ModuleLoader__.load({
6471
6497
  const closeAdd = (0, react.useCallback)(() => setAdding(false), []);
6472
6498
  const { loading, shouldRefresh } = usageCardState(usage);
6473
6499
  (0, react.useEffect)(() => {
6474
- if (shouldRefresh) onRefresh();
6475
- }, [shouldRefresh, onRefresh]);
6500
+ if (shouldRefresh) onRefreshAuto();
6501
+ }, [shouldRefresh, onRefreshAuto]);
6476
6502
  const rows = accountRows(state, usage, t).filter((row) => row.ref === void 0 || !state.accountEnrollmentRefs.includes(row.ref));
6477
6503
  const multi = rows.length > 1;
6478
6504
  const pinnedRow = rows.find((row) => row.id === state.activeAccount);
@@ -6960,6 +6986,7 @@ window.__ModuleLoader__.load({
6960
6986
  disabled,
6961
6987
  actions,
6962
6988
  onRefresh: props.refreshUsage,
6989
+ onRefreshAuto: props.refreshUsageAuto,
6963
6990
  openAnalytics: props.openAnalytics,
6964
6991
  createEnrollment: props.createEnrollment,
6965
6992
  ...foldProps("accounts")
@@ -7899,6 +7926,14 @@ window.__ModuleLoader__.load({
7899
7926
  set: (callback, ms) => setTimeout(callback, ms),
7900
7927
  clear: (handle) => clearTimeout(handle)
7901
7928
  };
7929
+ const DOCUMENT_VISIBILITY = {
7930
+ isVisible: () => typeof document === "undefined" ? true : document.visibilityState !== "hidden",
7931
+ subscribe: (onChange) => {
7932
+ if (typeof document === "undefined") return () => void 0;
7933
+ document.addEventListener("visibilitychange", onChange);
7934
+ return () => document.removeEventListener("visibilitychange", onChange);
7935
+ }
7936
+ };
7902
7937
  /** Live mount count of {@link startPanelAutoRefresh}. */
7903
7938
  let references = 0;
7904
7939
  /** Owner ticket of the running loop, if any. */
@@ -7908,6 +7943,16 @@ window.__ModuleLoader__.load({
7908
7943
  /** Pending timer handle of the running loop. */
7909
7944
  let handle;
7910
7945
  /**
7946
+ * Unsubscribe of the visibility listener owned by the RUNNING loop.
7947
+ *
7948
+ * Module-level on purpose: only the surface that started the loop receives the
7949
+ * subscribe handle, but that surface is not necessarily the last to unmount.
7950
+ * Keeping the handle on the same layer as `activeTicket`/`handle` lets any
7951
+ * teardown order release it; a function-local handle leaked the listener
7952
+ * whenever the starter unmounted first.
7953
+ */
7954
+ let visibilityUnsubscribe;
7955
+ /**
7911
7956
  * Start the shared auto refresh. One fetch when the surface appears (the sidebar
7912
7957
  * row is the point of the panel — it must be current, not wait for a click), then
7913
7958
  * a tick every {@link PANEL_AUTO_REFRESH_MS} while a surface stays mounted.
@@ -7917,22 +7962,37 @@ window.__ModuleLoader__.load({
7917
7962
  * tick goes through `usage.refresh()`, which collapses a concurrent fetch onto
7918
7963
  * the in-flight one. Returns the disposer that drops this surface's reference.
7919
7964
  *
7965
+ * A hidden document is still mounted, so its ticks are SKIPPED: a background
7966
+ * tab must not keep paying for a four-call report nobody reads. The tick is
7967
+ * rescheduled either way, so the loop survives, and becoming visible again
7968
+ * fetches at once — the data on screen is by then older than a tick would have
7969
+ * left it. The first paint still fetches unconditionally: it is the moment the
7970
+ * row appears, and there is nothing to show without it.
7971
+ *
7920
7972
  * @param usage - the shared usage controller.
7921
7973
  * @param isConfigured - whether a credential exists right now (re-read per tick).
7922
7974
  * @param timer - timer seam for tests.
7975
+ * @param visibility - visibility seam; see {@link AutoRefreshVisibility}.
7923
7976
  */
7924
- function startPanelAutoRefresh(usage, isConfigured, timer = REAL_TIMER) {
7977
+ function startPanelAutoRefresh(usage, isConfigured, timer = REAL_TIMER, visibility = DOCUMENT_VISIBILITY) {
7925
7978
  references += 1;
7926
7979
  const ticket = ++ticketSeq;
7980
+ const fetch = () => {
7981
+ if (isConfigured()) usage.refresh({ auto: true });
7982
+ };
7927
7983
  const tick = () => {
7928
7984
  if (activeTicket !== ticket) return;
7929
- if (isConfigured()) usage.refresh();
7985
+ if (visibility.isVisible()) fetch();
7930
7986
  handle = timer.set(tick, PANEL_AUTO_REFRESH_MS);
7931
7987
  };
7932
7988
  if (activeTicket === void 0) {
7933
7989
  activeTicket = ticket;
7934
7990
  handle = timer.set(tick, PANEL_AUTO_REFRESH_MS);
7935
- if (isConfigured()) usage.refresh();
7991
+ fetch();
7992
+ visibilityUnsubscribe = visibility.subscribe?.(() => {
7993
+ if (activeTicket !== ticket || !visibility.isVisible()) return;
7994
+ fetch();
7995
+ });
7936
7996
  }
7937
7997
  let stopped = false;
7938
7998
  return () => {
@@ -7941,6 +8001,8 @@ window.__ModuleLoader__.load({
7941
8001
  references -= 1;
7942
8002
  if (references > 0) return;
7943
8003
  activeTicket = void 0;
8004
+ visibilityUnsubscribe?.();
8005
+ visibilityUnsubscribe = void 0;
7944
8006
  if (handle !== void 0) {
7945
8007
  timer.clear(handle);
7946
8008
  handle = void 0;
@@ -10463,13 +10525,14 @@ window.__ModuleLoader__.load({
10463
10525
  .cc-usageStats{grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;display:grid}
10464
10526
  .cc-usageStatsActivity{grid-template-columns:repeat(4,minmax(0,1fr))}
10465
10527
  @container (max-width:460px){.cc-usageStatsActivity{grid-template-columns:repeat(2,minmax(0,1fr))}}
10466
- .cc-balance{flex-wrap:wrap;justify-content:space-between;align-items:flex-end;gap:4px 16px;display:flex}
10467
- .cc-balanceMain{flex-direction:column;gap:2px;display:flex}
10528
+ .cc-balance{flex-direction:column;gap:2px;display:flex}
10529
+ /* 两行左右分栏:标签行左账期、右更新时间;数值行左剩余、右已购与赠送。 */
10530
+ .cc-balanceRow{flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:4px 16px;display:flex}
10468
10531
  .cc-balanceLabel{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
10469
10532
  .cc-balanceValue{color:var(--dsw-alias-label-primary);font-size:20px;font-weight:600;line-height:28px;font-variant-numeric:tabular-nums}
10470
10533
  .cc-balanceSub{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}
10471
10534
  .cc-balanceEmpty .cc-balanceValue{color:var(--dsw-alias-state-error-primary)}
10472
- .cc-balanceNote{flex-basis:100%;margin:0;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
10535
+ .cc-balanceNote{margin:0;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
10473
10536
  .cc-usageStat{min-width:0;flex-direction:column;gap:2px;display:flex;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-module-platform)}
10474
10537
  .cc-usageStatLabel{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
10475
10538
  .cc-usageStatValue{color:var(--dsw-alias-label-primary);font-size:16px;font-weight:500;line-height:24px;font-variant-numeric:tabular-nums}
@@ -10484,9 +10547,6 @@ window.__ModuleLoader__.load({
10484
10547
  .cc-usageBarFill{background:var(--dsw-alias-brand-primary);border-radius:999px;height:100%;transition:width .3s ease}
10485
10548
  .cc-usageBarFillWarn{background:var(--dsw-alias-state-error-primary)}
10486
10549
  .cc-usageWindowReset{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}
10487
- .cc-usageMeta{align-items:center;gap:8px;display:flex}
10488
- .cc-usageMetaSpacer{flex:1}
10489
- .cc-usageUpdated{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}
10490
10550
  .cc-usagePartial{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}
10491
10551
  .cc-notices{flex-direction:column;gap:8px;display:flex;margin-bottom:12px}
10492
10552
  .cc-section>.cc-notice{margin-top:12px}
@@ -11176,6 +11236,7 @@ window.__ModuleLoader__.load({
11176
11236
  discard: () => controller.discard(),
11177
11237
  refreshCredentials: () => void controller.refreshCredentials(),
11178
11238
  refreshUsage: () => void usageController.refresh(),
11239
+ refreshUsageAuto: () => void usageController.refresh({ auto: true }),
11179
11240
  ...typeof ctx.get("layout")?.selectPanel === "function" ? { openAnalytics } : {},
11180
11241
  beginLogin: (targetRef) => void loginController.begin(targetRef),
11181
11242
  cancelLogin: () => void loginController.cancel(),
package/lib/index.d.ts CHANGED
@@ -382,6 +382,25 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
382
382
  private readonly surfaceOffload;
383
383
  private readonly billingAccess;
384
384
  private readonly billingAccessInflight;
385
+ /**
386
+ * Successful account-endpoint responses, keyed by gateway + key + exact URL.
387
+ *
388
+ * The settings card and the picker's advisory plan filter ask the SAME three
389
+ * endpoints about the SAME account (`/alpha/whoami`, then credits and
390
+ * subscriptions in parallel). Here that is four round trips per surface for
391
+ * two answers — and the card refreshes on a timer, so the picker keeps paying
392
+ * for rows it already has. Sharing the raw responses removes the duplicate
393
+ * without either surface having to know about the other: whoever asks second
394
+ * within {@link ACCOUNT_FACTS_TTL_MS} reads the same record instead of the
395
+ * wire.
396
+ *
397
+ * Only SUCCESSFUL records are kept. A failure must be retried rather than
398
+ * replayed, or an endpoint that recovered a second ago would keep reporting
399
+ * yesterday's outage — and the usage report's per-endpoint `failures` and
400
+ * {@link classifyTotalFailure} verdict would describe a request this process
401
+ * never made.
402
+ */
403
+ private readonly accountJson;
385
404
  private readonly protocolCache;
386
405
  /** Namespace account-derived facts by gateway without exposing the key. */
387
406
  private accountCacheKey;
@@ -488,8 +507,24 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
488
507
  */
489
508
  allowanceTier(): Promise<'go' | 'goat' | 'pro' | undefined>;
490
509
  resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
510
+ /**
511
+ * Resolve and validate the key one account-endpoint call will use.
512
+ *
513
+ * Every account endpoint runs the chat path's own `assertUsableApiKey` check,
514
+ * or a key carrying a character no HTTP header can hold throws inside `fetch`
515
+ * BEFORE any I/O — once per endpoint — and the report reads as "the network is
516
+ * down". The credential reference is read structurally: it belongs to the
517
+ * host's resolved options, not to this adapter's connection type, and it only
518
+ * names the key in the rejection message.
519
+ *
520
+ * Separate from {@link accountHeadersFor} so a caller that also needs to key a
521
+ * cache by the same identity does not have to resolve the key twice.
522
+ */
523
+ private usableAccountKey;
491
524
  /** The headers every authenticated account endpoint shares. */
492
525
  private accountHeaders;
526
+ /** Headers for an already-validated key; see {@link usableAccountKey}. */
527
+ private accountHeadersFor;
493
528
  /**
494
529
  * Fetch one JSON GET and return its HTTP status with a parsed record. The
495
530
  * catalog, billing probe, and usage report each apply their own failure
@@ -524,6 +559,23 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
524
559
  * everything" (fail-open).
525
560
  */
526
561
  private loadBillingAccessForKey;
562
+ /**
563
+ * One account-endpoint GET, served from {@link accountJson} when `share` and
564
+ * the same URL answered successfully within {@link ACCOUNT_FACTS_TTL_MS}.
565
+ *
566
+ * `timeoutMs` is the CALLER's budget; a cache hit spends none of it. Failures
567
+ * are never cached, so this can only ever save a request that would have
568
+ * re-read an answer this process already had.
569
+ *
570
+ * `share: false` marks a VOLATILE endpoint — one whose answer is the number
571
+ * the user is actually looking at, and which must therefore never be replayed
572
+ * from an earlier request. The usage card's summary is exactly that: its
573
+ * headline counts and cost are the reason the card exists, and stamping a
574
+ * cached copy with the current time would misreport when they were read.
575
+ * Identity and billing facts are the opposite: a plan tier or an org id that
576
+ * is 30 s old is still the truth, and those are what the picker duplicates.
577
+ */
578
+ private accountJsonGet;
527
579
  /**
528
580
  * The billing facts behind the picker's plan filter, mirroring the CLI's
529
581
  * `createBilling` flow: whoami yields the org id, then the subscriptions
@@ -1165,10 +1217,9 @@ declare const KNOWN_THINKING_MODELS: ReadonlySet<string>;
1165
1217
  * `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
1166
1218
  * list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
1167
1219
  * Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
1168
- * Provider-tier. Re-verified at command-code@1.88.0 (2026-10-24) against the
1169
- * live `?view=client` catalog: 105 ids, still a strict superset chain. Mistral
1170
- * Large 4
1171
- * (1.75.0) was the only addition at the 1.77.0 check; the 1.78.0/1.79.0 pair
1220
+ * Provider-tier. Re-verified at command-code@1.88.0 (2026-10-27) against the
1221
+ * live `?view=client` catalog: 106 ids, still a strict superset chain. Mistral
1222
+ * Large 4 (1.75.0) was the only addition at the 1.77.0 check; the 1.78.0/1.79.0 pair
1172
1223
  * added Claude Haiku 5.5 at Pro and the free Glyph Cluster preview at Go.
1173
1224
  * 1.79.2 lowers Haiku 5.5's minimum plan to GOAT; 1.80.0 adds Muse Wonder at
1174
1225
  * Max; 1.82.0 adds Gemini 4 Argon at Pro and nothing else moves. The 1.82.4
@@ -1177,7 +1228,8 @@ declare const KNOWN_THINKING_MODELS: ReadonlySet<string>;
1177
1228
  * which left the live catalog. Every other
1178
1229
  * tier is unchanged. The 1.86.1 sync (2026-10-21) adds only the Ornith 1.5 122B
1179
1230
  * launch pair at Go; the 1.87.1 check (2026-10-22) adds no model and moves no
1180
- * tier, and the 2026-10-24 check adds only Kimi K3.1 Pro at Go.
1231
+ * tier, the 2026-10-24 check adds only Kimi K3.1 Pro at Go, and the 2026-10-27
1232
+ * check adds only GPT-6.1 Terra at Pro.
1181
1233
  * The only removals since 1.49.0 are the two retired stealth previews (Pixel
1182
1234
  * Canary at 1.73.1, Space Bunny Alpha at 1.74.3), Ling 3.0 Flash Sante
1183
1235
  * (2026-10-16) and the retired Cat Jam preview (2026-10-20). Per-entry tags
package/lib/index.js CHANGED
@@ -139,7 +139,10 @@ const PLAN_ORDER = Object.fromEntries(PLAN_TIER_ORDER.map((tier, index) => [tier
139
139
  * efforts `low/high/max`, Go tier, `$2/$10/$0.2`). The 1.88.0 artifact does not
140
140
  * name that row yet; the default catalog, `?view=client`, the models docs page
141
141
  * and the pricing page all do, so the capability fields come from the catalog
142
- * as usual. Keeping the snapshot in its own module
142
+ * as usual. The 2026-10-27 check finds the CLI still at 1.88.0 and one more
143
+ * catalog-only addition, GPT-6.1 Terra (`gpt-6.1-terra`): 1.05M context, text +
144
+ * image, the same five-level effort set, Pro tier, `$1/$5` up to a 272K context
145
+ * step and `$2/$7.5` beyond. Keeping the snapshot in its own module
143
146
  * confines those frequent sync diffs here: src/adapter.ts holds only the stable
144
147
  * wire/runtime logic. The read helpers live here too, for the same reason.
145
148
  *
@@ -499,6 +502,13 @@ const KNOWN_EFFORTS = {
499
502
  "xhigh",
500
503
  "max"
501
504
  ],
505
+ "gpt-6.1-terra": [
506
+ "low",
507
+ "medium",
508
+ "high",
509
+ "xhigh",
510
+ "max"
511
+ ],
502
512
  "inclusionai/ling-3.1-flash": [
503
513
  "low",
504
514
  "medium",
@@ -581,6 +591,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
581
591
  "gpt-6-luna",
582
592
  "gpt-6-sol",
583
593
  "gpt-6.1-sol",
594
+ "gpt-6.1-terra",
584
595
  "meta/muse-spark-1.1",
585
596
  "meta/muse-spark-1.2",
586
597
  "meta/muse-spark-1.2-contributor",
@@ -934,10 +945,9 @@ const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
934
945
  * `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
935
946
  * list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
936
947
  * Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
937
- * Provider-tier. Re-verified at command-code@1.88.0 (2026-10-24) against the
938
- * live `?view=client` catalog: 105 ids, still a strict superset chain. Mistral
939
- * Large 4
940
- * (1.75.0) was the only addition at the 1.77.0 check; the 1.78.0/1.79.0 pair
948
+ * Provider-tier. Re-verified at command-code@1.88.0 (2026-10-27) against the
949
+ * live `?view=client` catalog: 106 ids, still a strict superset chain. Mistral
950
+ * Large 4 (1.75.0) was the only addition at the 1.77.0 check; the 1.78.0/1.79.0 pair
941
951
  * added Claude Haiku 5.5 at Pro and the free Glyph Cluster preview at Go.
942
952
  * 1.79.2 lowers Haiku 5.5's minimum plan to GOAT; 1.80.0 adds Muse Wonder at
943
953
  * Max; 1.82.0 adds Gemini 4 Argon at Pro and nothing else moves. The 1.82.4
@@ -946,7 +956,8 @@ const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
946
956
  * which left the live catalog. Every other
947
957
  * tier is unchanged. The 1.86.1 sync (2026-10-21) adds only the Ornith 1.5 122B
948
958
  * launch pair at Go; the 1.87.1 check (2026-10-22) adds no model and moves no
949
- * tier, and the 2026-10-24 check adds only Kimi K3.1 Pro at Go.
959
+ * tier, the 2026-10-24 check adds only Kimi K3.1 Pro at Go, and the 2026-10-27
960
+ * check adds only GPT-6.1 Terra at Pro.
950
961
  * The only removals since 1.49.0 are the two retired stealth previews (Pixel
951
962
  * Canary at 1.73.1, Space Bunny Alpha at 1.74.3), Ling 3.0 Flash Sante
952
963
  * (2026-10-16) and the retired Cat Jam preview (2026-10-20). Per-entry tags
@@ -1043,6 +1054,7 @@ const KNOWN_PLANS = {
1043
1054
  "gpt-5.5": "pro",
1044
1055
  "gpt-5.6-terra": "pro",
1045
1056
  "gpt-6-sol": "pro",
1057
+ "gpt-6.1-terra": "pro",
1046
1058
  "meta/muse-spark-1.1": "pro",
1047
1059
  "claude-fable-5-1": "provider",
1048
1060
  "claude-fable-5": "provider",
@@ -2106,6 +2118,34 @@ const MODEL_PRICE_ROWS = [
2106
2118
  5
2107
2119
  ] }]
2108
2120
  },
2121
+ {
2122
+ id: "gpt-6.1-terra",
2123
+ rates: [
2124
+ 1,
2125
+ 5,
2126
+ .05,
2127
+ 1.25
2128
+ ],
2129
+ allowance: {
2130
+ go: 6,
2131
+ goat: 20,
2132
+ pro: 20
2133
+ },
2134
+ contextTiers: [{
2135
+ "maxContext": 272e3,
2136
+ "rates": [
2137
+ 1,
2138
+ 5,
2139
+ .05,
2140
+ 1.25
2141
+ ]
2142
+ }, { "rates": [
2143
+ 2,
2144
+ 7.5,
2145
+ .1,
2146
+ 2.5
2147
+ ] }]
2148
+ },
2109
2149
  {
2110
2150
  id: "grok-4.5",
2111
2151
  rates: [
@@ -6369,7 +6409,8 @@ var GatewayFacts = class {
6369
6409
  if (this.state?.source !== identity) this.state = {
6370
6410
  source: identity,
6371
6411
  rows: [],
6372
- expiresAt: 0
6412
+ expiresAt: 0,
6413
+ origin: "none"
6373
6414
  };
6374
6415
  return this.state;
6375
6416
  }
@@ -6378,6 +6419,20 @@ var GatewayFacts = class {
6378
6419
  const row = this.forSource(source).rows.find((candidate) => candidate.id === model);
6379
6420
  return row === void 0 ? void 0 : copyRows([row])[0];
6380
6421
  }
6422
+ /**
6423
+ * 本进程**联网成功过**的目录;否则返回 undefined。
6424
+ *
6425
+ * 判据是来源而不是「内存里有行」:`snapshot()` 会把磁盘缓存写进同一份
6426
+ * `state.rows`(生成路径需要它),若只看行数,冷启动先发过一次聊天再打开
6427
+ * 选择器时,首屏拿到的就是没有年龄校验的磁盘副本——正是本方法要排除的东西。
6428
+ * 空目录同样返回 undefined:刷新失败后的空行(`expiresAt` 已进入重试窗口)
6429
+ * 不能让选择器把「暂时读不到」画成「没有模型」,调用方应回落到 `refresh()`。
6430
+ */
6431
+ loaded(source) {
6432
+ const state = this.forSource(source);
6433
+ if (state.origin !== "network" || state.rows.length === 0) return void 0;
6434
+ return copyRows(state.rows);
6435
+ }
6381
6436
  /** 生成只读取本地目录;捕获状态后不再转向实例的新来源。 */
6382
6437
  async snapshot(source) {
6383
6438
  const state = this.forSource(source);
@@ -6385,7 +6440,10 @@ var GatewayFacts = class {
6385
6440
  state.disk ??= readCache(source).catch(() => []);
6386
6441
  const rows = await state.disk;
6387
6442
  if (state.rows.length > 0 || state.expiresAt > 0) return copyRows(state.rows);
6388
- if (state.inflight === void 0) state.rows = rows;
6443
+ if (state.inflight === void 0) {
6444
+ state.rows = rows;
6445
+ if (rows.length > 0 && state.origin === "none") state.origin = "disk";
6446
+ }
6389
6447
  return copyRows(rows);
6390
6448
  }
6391
6449
  async refresh(source, signal) {
@@ -6398,8 +6456,10 @@ var GatewayFacts = class {
6398
6456
  const mayPublish = () => this.state === state && generations.get(source.apiBase) === generation;
6399
6457
  state.inflight = (async () => {
6400
6458
  let rows, ttl = MODEL_CATALOG_TTL_MS;
6459
+ let network = false;
6401
6460
  try {
6402
6461
  rows = parseCatalog$1(await this.fetchCatalog(source), source.apiBase);
6462
+ network = true;
6403
6463
  if (mayPublish()) {
6404
6464
  publish(source.apiBase, rows);
6405
6465
  await writeCache(source, rows, mayPublish).catch(() => void 0);
@@ -6411,6 +6471,8 @@ var GatewayFacts = class {
6411
6471
  }
6412
6472
  state.rows = rows;
6413
6473
  state.expiresAt = Date.now() + ttl;
6474
+ if (network) state.origin = "network";
6475
+ else if (state.origin === "none" && rows.length > 0) state.origin = "disk";
6414
6476
  return rows;
6415
6477
  })().finally(() => {
6416
6478
  delete state.inflight;
@@ -6461,6 +6523,17 @@ const MODELS_TIMEOUT_MS = 1e4;
6461
6523
  /** How long the picker's plan-filter billing facts stay cached before refetching. */
6462
6524
  const BILLING_ACCESS_TTL_MS = 3e5;
6463
6525
  /**
6526
+ * How long the raw account facts captured by one usage-report run stay usable
6527
+ * for the picker's advisory plan filter.
6528
+ *
6529
+ * This is much shorter than {@link BILLING_ACCESS_TTL_MS} because it only ever
6530
+ * removes a DUPLICATE read: the picker and the settings card ask the same three
6531
+ * endpoints about the same key, and whichever ran last already paid for them.
6532
+ * Anything older than this window is simply fetched again — the filter is
6533
+ * fail-open advisory data, so a miss costs one round trip and nothing else.
6534
+ */
6535
+ const ACCOUNT_FACTS_TTL_MS = 3e4;
6536
+ /**
6464
6537
  * How long a learned CLI-only protocol preference stays cached per account.
6465
6538
  * After this TTL a request may probe Provider API again, so an upgraded Go
6466
6539
  * account can recover without a restart.
@@ -6552,9 +6625,13 @@ function fingerprintValue(value) {
6552
6625
  }
6553
6626
  return {
6554
6627
  bytes: Buffer.byteLength(serialized),
6555
- sha256: createHash("sha256").update(serialized).digest("hex")
6628
+ sha256: sha256Hex(serialized)
6556
6629
  };
6557
6630
  }
6631
+ /** Digest of already-serialized text: no second stringify, no retained copy. */
6632
+ function sha256Hex(serialized) {
6633
+ return createHash("sha256").update(serialized).digest("hex");
6634
+ }
6558
6635
  /**
6559
6636
  * Structural request facts for issue #64's cache diagnosis.
6560
6637
  *
@@ -6670,6 +6747,16 @@ function headersTimeoutError(input) {
6670
6747
  /** How deep a root `$ref` / combinator chain is followed while normalizing. */
6671
6748
  const SCHEMA_NORMALIZE_MAX_DEPTH = 4;
6672
6749
  /**
6750
+ * Depth-0 normalization results, keyed by the caller's own `parameters` object.
6751
+ *
6752
+ * {@link toolParametersSchema} only ever reads the root, so a schema that has
6753
+ * already been normalized returns the identical answer however many times it is
6754
+ * asked. The tool list is rebuilt for EVERY attempt — a rotation or a protocol
6755
+ * fallback rebuilds it again — and the objects come from the harness, so they
6756
+ * stay identical for the life of a turn: identity is an exact key.
6757
+ */
6758
+ const TOOL_SCHEMA_CACHE = /* @__PURE__ */ new WeakMap();
6759
+ /**
6673
6760
  * Whether a type-less node is object-shaped enough to be one with the type
6674
6761
  * declared: `properties`/`required`/`additionalProperties` only make sense on
6675
6762
  * an object, and a schema carrying them was meant to be one.
@@ -6715,7 +6802,7 @@ function mergeCombinatorBranches(node, depth) {
6715
6802
  const objectBranches = [];
6716
6803
  for (const branch of branches) {
6717
6804
  if (!isRecord(branch)) continue;
6718
- objectBranches.push(toolParametersSchema(branch, depth + 1));
6805
+ objectBranches.push(normalizeToolParameters(branch, depth + 1));
6719
6806
  }
6720
6807
  if (objectBranches.length === 0) continue;
6721
6808
  sawBranch = true;
@@ -6745,10 +6832,32 @@ function mergeCombinatorBranches(node, depth) {
6745
6832
  * resolved/merged; anything else (an array/scalar root, or no schema at all)
6746
6833
  * degrades to a permissive free-form object, because a request the provider
6747
6834
  * refuses helps no one and the tool's own description is still in the prompt.
6748
- * Only the root is touched and every path returns a copy: the harness may
6749
- * deep-freeze the caller's schema.
6750
- */
6751
- function toolParametersSchema(parameters, depth = 0) {
6835
+ *
6836
+ * Only the ROOT is touched. The object-root fast path deliberately returns the
6837
+ * caller's own object: an already-normalized schema is the common case, the
6838
+ * function has always answered with that same identity, and callers rely on it
6839
+ * to get the same frozen schema they handed in. Everything this function
6840
+ * CONSTRUCTS is a fresh object, so no caller can mutate another's schema.
6841
+ *
6842
+ * The ROOT is memoized per `parameters` object, because every transport rebuilds
6843
+ * its tool list on every attempt — on a rotating or downshifting request the
6844
+ * same handful of tool schemas is walked four, five, six times for an answer
6845
+ * that cannot have changed. The tool objects come from the harness and are
6846
+ * stable for the life of a turn, so a WeakMap keyed on identity is exact: a
6847
+ * genuinely different schema is a different object and misses. Nested
6848
+ * `$ref`/combinator branches are reached through {@link normalizeToolParameters}
6849
+ * and are not memoized; only the root is asked for on a hot path.
6850
+ */
6851
+ function toolParametersSchema(parameters) {
6852
+ if (!isRecord(parameters)) return normalizeToolParameters(parameters, 0);
6853
+ const cached = TOOL_SCHEMA_CACHE.get(parameters);
6854
+ if (cached !== void 0) return cached;
6855
+ const normalized = normalizeToolParameters(parameters, 0);
6856
+ TOOL_SCHEMA_CACHE.set(parameters, normalized);
6857
+ return normalized;
6858
+ }
6859
+ /** Depth-0 normalization of {@link toolParametersSchema}; see its memo there. */
6860
+ function normalizeToolParameters(parameters, depth) {
6752
6861
  if (!isRecord(parameters)) return {
6753
6862
  type: "object",
6754
6863
  properties: {},
@@ -6764,7 +6873,7 @@ function toolParametersSchema(parameters, depth = 0) {
6764
6873
  const target = resolveLocalRef(parameters, parameters.$ref);
6765
6874
  if (target !== void 0) {
6766
6875
  const { $ref: _ref, ...rest } = parameters;
6767
- return toolParametersSchema({
6876
+ return normalizeToolParameters({
6768
6877
  ...target,
6769
6878
  ...rest
6770
6879
  }, depth + 1);
@@ -7000,26 +7109,47 @@ function coreBudget(budget) {
7000
7109
  */
7001
7110
  const imageVersionBytes = (block) => block.attachment.bytes;
7002
7111
  /**
7112
+ * The accounting half of the image gate: project the surface's durable offload
7113
+ * marks and count what the byte budget still needs — WITHOUT throwing.
7114
+ *
7115
+ * The throw has to wait until the credential is known. A request that cannot
7116
+ * authenticate must not trigger a durable (irreversible) image offload before
7117
+ * the caller learns the key is missing; keeping the count pure lets
7118
+ * `streamAttempt()` run it before ANY I/O and report ONE error later, once the
7119
+ * real transport is known.
7120
+ */
7121
+ function projectSurfaceBudget(options, policy, budget) {
7122
+ const marked = policy.projectOffloadedImages(options.messages, (ref) => offloadedImageText(ref));
7123
+ const messages = marked === options.messages ? options.messages : [...marked];
7124
+ return {
7125
+ options: messages === options.messages ? options : {
7126
+ ...options,
7127
+ messages
7128
+ },
7129
+ budgetMissing: policy.requiredImageOffload(messages, coreBudget(budget), imageVersionBytes)
7130
+ };
7131
+ }
7132
+ /**
7003
7133
  * Request options under one budget rung: the surface's durable marks become
7004
7134
  * placeholder text, and an over-budget history FAILS with the count to offload
7005
7135
  * instead of being evicted here. Throws rather than returning, so no caller can
7006
7136
  * accidentally send a body this route already knows is too large.
7007
7137
  *
7138
+ * This is the one-shot form (budget + cache in a single error) used by the 413
7139
+ * downshift and the protocol fallback. `streamAttempt()`'s first pass uses
7140
+ * {@link projectSurfaceBudget} instead, because there the throw must wait for
7141
+ * the credential.
7142
+ *
7008
7143
  * The next rung is applied to the CURRENT projection, never to the raw history —
7009
7144
  * otherwise the 413 retry could reintroduce images the first rung had already
7010
7145
  * evicted.
7011
7146
  */
7012
7147
  function withSurfaceOffload(options, policy, budget, cacheOffloadModel) {
7013
- const marked = policy.projectOffloadedImages(options.messages, (ref) => offloadedImageText(ref));
7014
- const messages = marked === options.messages ? options.messages : [...marked];
7015
- const budgetMissing = policy.requiredImageOffload(messages, coreBudget(budget), imageVersionBytes);
7016
- const cacheMissing = cacheOffloadModel === void 0 ? 0 : seenImageOffloadCount(messages, cacheOffloadModel);
7017
- const missing = Math.max(budgetMissing, cacheMissing);
7018
- if (missing > 0) throw imageOffloadRequired(missing, cacheMissing > budgetMissing ? "cache" : "budget");
7019
- return messages === options.messages ? options : {
7020
- ...options,
7021
- messages
7022
- };
7148
+ const projected = projectSurfaceBudget(options, policy, budget);
7149
+ const cacheMissing = cacheOffloadModel === void 0 ? 0 : seenImageOffloadCount(projected.options.messages, cacheOffloadModel);
7150
+ const missing = Math.max(projected.budgetMissing, cacheMissing);
7151
+ if (missing > 0) throw imageOffloadRequired(missing, cacheMissing > projected.budgetMissing ? "cache" : "budget");
7152
+ return projected.options;
7023
7153
  }
7024
7154
  /**
7025
7155
  * Count retained images already followed by an answer from this same model.
@@ -7977,6 +8107,25 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
7977
8107
  surfaceOffload;
7978
8108
  billingAccess = /* @__PURE__ */ new Map();
7979
8109
  billingAccessInflight = /* @__PURE__ */ new Map();
8110
+ /**
8111
+ * Successful account-endpoint responses, keyed by gateway + key + exact URL.
8112
+ *
8113
+ * The settings card and the picker's advisory plan filter ask the SAME three
8114
+ * endpoints about the SAME account (`/alpha/whoami`, then credits and
8115
+ * subscriptions in parallel). Here that is four round trips per surface for
8116
+ * two answers — and the card refreshes on a timer, so the picker keeps paying
8117
+ * for rows it already has. Sharing the raw responses removes the duplicate
8118
+ * without either surface having to know about the other: whoever asks second
8119
+ * within {@link ACCOUNT_FACTS_TTL_MS} reads the same record instead of the
8120
+ * wire.
8121
+ *
8122
+ * Only SUCCESSFUL records are kept. A failure must be retried rather than
8123
+ * replayed, or an endpoint that recovered a second ago would keep reporting
8124
+ * yesterday's outage — and the usage report's per-endpoint `failures` and
8125
+ * {@link classifyTotalFailure} verdict would describe a request this process
8126
+ * never made.
8127
+ */
8128
+ accountJson = /* @__PURE__ */ new Map();
7980
8129
  protocolCache = /* @__PURE__ */ new Map();
7981
8130
  /** Namespace account-derived facts by gateway without exposing the key. */
7982
8131
  accountCacheKey(apiBase, apiKey) {
@@ -8016,10 +8165,10 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8016
8165
  * @returns the error to surface: unchanged for any other failure, the
8017
8166
  * original timeout below the threshold, and a terminal one at it.
8018
8167
  */
8019
- gradeHeaderTimeout(error, protocol, body, timeoutMs, sessionId) {
8168
+ gradeHeaderTimeout(error, protocol, body, timeoutMs, sessionId, sent) {
8020
8169
  if (!(error instanceof LlmError && error.code === "TIMEOUT")) return error;
8021
8170
  const key = sessionId ?? GLOBAL_HEADER_TIMEOUT_KEY;
8022
- const fingerprint = `${protocol}:${fingerprintValue(body).sha256}`;
8171
+ const fingerprint = `${protocol}:${sent === void 0 ? fingerprintValue(body).sha256 : sha256Hex(sent)}`;
8023
8172
  const previous = this.headerTimeoutStreaks.get(key);
8024
8173
  const count = previous?.fingerprint === fingerprint ? previous.count + 1 : 1;
8025
8174
  if (previous === void 0) {
@@ -8122,7 +8271,9 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8122
8271
  }
8123
8272
  async listModels(provider, opts) {
8124
8273
  const connection = { ...this.deps.options() };
8125
- const catalog = await this.gatewayFacts.refresh(connection);
8274
+ const cached = this.gatewayFacts.loaded(connection);
8275
+ const catalog = cached ?? await this.gatewayFacts.refresh(connection);
8276
+ if (cached !== void 0) this.gatewayFacts.refresh(connection).catch(() => void 0);
8126
8277
  const toInfo = (model) => {
8127
8278
  const vision = KNOWN_IMAGE_MODELS.has(model.id);
8128
8279
  return {
@@ -8187,13 +8338,33 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8187
8338
  })) } } : {}
8188
8339
  };
8189
8340
  }
8190
- /** The headers every authenticated account endpoint shares. */
8191
- async accountHeaders(apiKey) {
8341
+ /**
8342
+ * Resolve and validate the key one account-endpoint call will use.
8343
+ *
8344
+ * Every account endpoint runs the chat path's own `assertUsableApiKey` check,
8345
+ * or a key carrying a character no HTTP header can hold throws inside `fetch`
8346
+ * BEFORE any I/O — once per endpoint — and the report reads as "the network is
8347
+ * down". The credential reference is read structurally: it belongs to the
8348
+ * host's resolved options, not to this adapter's connection type, and it only
8349
+ * names the key in the rejection message.
8350
+ *
8351
+ * Separate from {@link accountHeadersFor} so a caller that also needs to key a
8352
+ * cache by the same identity does not have to resolve the key twice.
8353
+ */
8354
+ async usableAccountKey(apiKey) {
8192
8355
  const connection = this.deps.options();
8193
8356
  const raw = apiKey ?? await this.deps.resolveApiKey(connection);
8194
8357
  const named = connection.apiKeyEnv;
8358
+ return assertUsableApiKey(raw, "llm-commandcode", typeof named === "string" && named !== "" ? named : "the stored credential");
8359
+ }
8360
+ /** The headers every authenticated account endpoint shares. */
8361
+ async accountHeaders(apiKey) {
8362
+ return this.accountHeadersFor(await this.usableAccountKey(apiKey));
8363
+ }
8364
+ /** Headers for an already-validated key; see {@link usableAccountKey}. */
8365
+ accountHeadersFor(key) {
8195
8366
  return {
8196
- Authorization: `Bearer ${assertUsableApiKey(raw, "llm-commandcode", typeof named === "string" && named !== "" ? named : "the stored credential")}`,
8367
+ Authorization: `Bearer ${key}`,
8197
8368
  ...IDENTITY_ENCODING_HEADER,
8198
8369
  "x-command-code-version": COMMAND_CODE_CLI_VERSION,
8199
8370
  "x-cli-environment": "production",
@@ -8313,6 +8484,42 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8313
8484
  return inflight;
8314
8485
  }
8315
8486
  /**
8487
+ * One account-endpoint GET, served from {@link accountJson} when `share` and
8488
+ * the same URL answered successfully within {@link ACCOUNT_FACTS_TTL_MS}.
8489
+ *
8490
+ * `timeoutMs` is the CALLER's budget; a cache hit spends none of it. Failures
8491
+ * are never cached, so this can only ever save a request that would have
8492
+ * re-read an answer this process already had.
8493
+ *
8494
+ * `share: false` marks a VOLATILE endpoint — one whose answer is the number
8495
+ * the user is actually looking at, and which must therefore never be replayed
8496
+ * from an earlier request. The usage card's summary is exactly that: its
8497
+ * headline counts and cost are the reason the card exists, and stamping a
8498
+ * cached copy with the current time would misreport when they were read.
8499
+ * Identity and billing facts are the opposite: a plan tier or an org id that
8500
+ * is 30 s old is still the truth, and those are what the picker duplicates.
8501
+ */
8502
+ async accountJsonGet(apiBase, apiKey, path, timeoutMs, share = true) {
8503
+ const cacheKey = JSON.stringify([
8504
+ apiBase,
8505
+ apiKey,
8506
+ path
8507
+ ]);
8508
+ if (share) {
8509
+ const hit = this.accountJson.get(cacheKey);
8510
+ if (hit !== void 0 && Date.now() - hit.at < ACCOUNT_FACTS_TTL_MS) return {
8511
+ status: 200,
8512
+ record: hit.record
8513
+ };
8514
+ }
8515
+ const answer = await this.fetchJson(`${apiBase}${path}`, this.accountHeadersFor(apiKey), timeoutMs);
8516
+ if (share && answer.record !== void 0) this.accountJson.set(cacheKey, {
8517
+ record: answer.record,
8518
+ at: Date.now()
8519
+ });
8520
+ return answer;
8521
+ }
8522
+ /**
8316
8523
  * The billing facts behind the picker's plan filter, mirroring the CLI's
8317
8524
  * `createBilling` flow: whoami yields the org id, then the subscriptions
8318
8525
  * and credits endpoints answer in parallel. The plan id is honored only
@@ -8323,8 +8530,8 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8323
8530
  */
8324
8531
  async fetchBillingAccess(apiBase, apiKey) {
8325
8532
  try {
8326
- const headers = await this.accountHeaders(apiKey);
8327
- const getJson = async (path) => (await this.fetchJson(`${apiBase}${path}`, headers)).record;
8533
+ const key = await this.usableAccountKey(apiKey);
8534
+ const getJson = async (path) => (await this.accountJsonGet(apiBase, key, path, MODELS_TIMEOUT_MS)).record;
8328
8535
  const whoami = await getJson("/alpha/whoami");
8329
8536
  const orgData = whoami && isRecord(whoami.org) ? whoami.org : void 0;
8330
8537
  const orgId = orgData === void 0 ? void 0 : stringValue(orgData.id);
@@ -8431,9 +8638,9 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8431
8638
  async getUsage(apiKey, capturedConnection) {
8432
8639
  const connection = capturedConnection ?? { ...this.deps.options() };
8433
8640
  const base = connection.apiBase;
8434
- let headers;
8641
+ let key;
8435
8642
  try {
8436
- headers = await this.accountHeaders(apiKey);
8643
+ key = await this.usableAccountKey(apiKey);
8437
8644
  } catch (error) {
8438
8645
  if (error instanceof LlmError && error.code === "INVALID_CREDENTIAL") return {
8439
8646
  failures: [error.message],
@@ -8443,9 +8650,9 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8443
8650
  }
8444
8651
  const failures = [];
8445
8652
  const failed = [];
8446
- const getJson = async (path) => {
8653
+ const getJson = async (path, share = true) => {
8447
8654
  try {
8448
- const { status, record, responseFailure } = await this.fetchJson(`${base}${path}`, headers, connection.requestTimeoutMs);
8655
+ const { status, record, responseFailure } = await this.accountJsonGet(base, key, path, connection.requestTimeoutMs, share);
8449
8656
  if (record === void 0) {
8450
8657
  failures.push(`${path}: HTTP ${status}${responseFailure ? `;${responseFailure}` : ""}`);
8451
8658
  failed.push(responseFailure ? {
@@ -8479,7 +8686,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8479
8686
  const rawStart = subData?.currentPeriodStart;
8480
8687
  const periodStart = periodTimestampValue(rawStart);
8481
8688
  const validStart = periodStart > 0 && Number.isFinite(new Date(periodStart).getTime());
8482
- const totals = parseUsageTotals(await getJson(scopedPath("/alpha/usage/summary", validStart ? String(rawStart) : void 0)));
8689
+ const totals = parseUsageTotals(await getJson(scopedPath("/alpha/usage/summary", validStart ? String(rawStart) : void 0), false));
8483
8690
  if (totals !== void 0) report.usage = totals;
8484
8691
  const limits = parseCreditLimits(credits);
8485
8692
  if (limits !== void 0) report.credits = limits;
@@ -8634,23 +8841,15 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8634
8841
  if (attachments === void 0) throw new LlmError("Command Code image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
8635
8842
  readImage = requestImages(attachments, options.signal, timing);
8636
8843
  }
8637
- const finishCredentials = timing.phase("credentials");
8638
- let apiKey;
8639
- try {
8640
- apiKey = await this.deps.resolveApiKey(connection, options.model);
8641
- } finally {
8642
- finishCredentials();
8643
- }
8644
- options.signal?.throwIfAborted();
8645
8844
  let maxTokens = Math.min(budget.max, learnedOutputLimit(connection.apiBase, options.model));
8845
+ const builtMaxTokens = maxTokens;
8646
8846
  const supported = KNOWN_EFFORTS[options.model];
8647
8847
  const effort = options.purpose === "session-title" && supported?.includes("off") ? "off" : options.reasoningEffort;
8648
8848
  const declared = effort !== void 0 && supported?.includes(effort) ? effort : void 0;
8649
8849
  const reasoningEffort = effort === "off" && declared === void 0 ? void 0 : declared;
8650
8850
  const systemText = [options.system ?? "", ...options.messages.filter((m) => m.role === "system").map((m) => m.content.map(blockText).filter(Boolean).join("\n"))].filter(Boolean).join("\n\n");
8651
- let protocol = this.resolveProtocol(connection, apiKey, options.model, modelEntry);
8652
- let messagesPermissionRejection;
8653
- let requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true && protocol === "cli" ? options.model : void 0);
8851
+ const projected = projectSurfaceBudget(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0]);
8852
+ let requestOptions = projected.options;
8654
8853
  const buildBody = async (target) => {
8655
8854
  const finishBody = timing.phase("body");
8656
8855
  try {
@@ -8668,7 +8867,30 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8668
8867
  finishBody();
8669
8868
  }
8670
8869
  };
8671
- let body = await buildBody(protocol);
8870
+ const provisionalProtocol = connection.protocol === "cli" ? "cli" : this.routesToMessages(options.model, modelEntry) ? "messages" : "openai";
8871
+ const finishCredentials = timing.phase("credentials");
8872
+ const credentials = this.deps.resolveApiKey(connection, options.model);
8873
+ credentials.catch(() => void 0);
8874
+ const provisionalBody = connection.offloadSeenImagesForCache === true || projected.budgetMissing > 0 ? void 0 : buildBody(provisionalProtocol);
8875
+ provisionalBody?.catch(() => void 0);
8876
+ let apiKey;
8877
+ try {
8878
+ apiKey = await credentials;
8879
+ } finally {
8880
+ finishCredentials();
8881
+ }
8882
+ options.signal?.throwIfAborted();
8883
+ maxTokens = Math.min(budget.max, learnedOutputLimit(connection.apiBase, options.model));
8884
+ let protocol = this.resolveProtocol(connection, apiKey, options.model, modelEntry);
8885
+ const cacheMissing = protocol === "cli" && connection.offloadSeenImagesForCache === true ? seenImageOffloadCount(requestOptions.messages, options.model) : 0;
8886
+ const missing = Math.max(projected.budgetMissing, cacheMissing);
8887
+ if (missing > 0) throw imageOffloadRequired(missing, cacheMissing > projected.budgetMissing ? "cache" : "budget");
8888
+ let messagesPermissionRejection;
8889
+ let body = provisionalBody !== void 0 && protocol === provisionalProtocol ? await provisionalBody : await buildBody(protocol);
8890
+ if (maxTokens !== builtMaxTokens) {
8891
+ budget.max = Math.min(budget.max, maxTokens);
8892
+ if (!omitOutputTokens(connection.maxOutputTokens, protocol, options.maxTokens)) assignMaxTokens(body, protocol, maxTokens);
8893
+ }
8672
8894
  const serializedBodies = /* @__PURE__ */ new WeakMap();
8673
8895
  const trace = openStreamTrace(`${options.provider}/${options.model}`);
8674
8896
  const tried = /* @__PURE__ */ new Set();
@@ -8790,7 +9012,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8790
9012
  });
8791
9013
  trace.close();
8792
9014
  if (options.signal?.aborted) throw error;
8793
- throw failureWithRequestId(this.gradeHeaderTimeout(error, protocol, body, connection.requestTimeoutMs, options.sessionId), responseMetadata);
9015
+ throw failureWithRequestId(this.gradeHeaderTimeout(error, protocol, body, connection.requestTimeoutMs, options.sessionId, serializedBodies.get(body)), responseMetadata);
8794
9016
  }
8795
9017
  if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
8796
9018
  const { response, cleanup } = connected;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mars-sea/dsh-commandcode-provider",
3
- "version": "0.15.4",
3
+ "version": "0.15.5",
4
4
  "description": "Unofficial DeepSeek Harness LLM provider plugin for Command Code, ported from pi-commandcode-provider (MIT). Registers the 'commandcode' provider route with a Models-page card and live model catalog.",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",
@@ -24,7 +24,8 @@
24
24
  "compatibility": {
25
25
  "dshReleases": {
26
26
  "0.2.1-rc.2": "compatible",
27
- "0.2.1-rc.3": "compatible"
27
+ "0.2.1-rc.3": "compatible",
28
+ "0.2.1-rc.4": "compatible"
28
29
  },
29
30
  "dsh": ">=0.2.1-rc.2 <0.2.2-0"
30
31
  },
@@ -141,10 +142,15 @@
141
142
  "@deepseek-ai/schemastery": "~3.18.5",
142
143
  "@types/node": "^22.0.0",
143
144
  "@types/react": "^18.3.1",
145
+ "@types/react-test-renderer": "^19.3.0",
144
146
  "parse5": "8.0.1",
145
147
  "react": "^18.2.0",
148
+ "react-test-renderer": "^18.3.1",
146
149
  "tsdown": "^0.22.2",
147
150
  "tsx": "^4.23.12",
148
151
  "typescript": "^6.0.3"
152
+ },
153
+ "overrides": {
154
+ "zod": "^4.6.6"
149
155
  }
150
156
  }