@eddyskywalker/dsh-chatgpt-subscription 0.15.0 → 0.17.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +2 -0
  3. package/lib/client.js +190 -11
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +570 -19
  6. package/lib/types/client/antigravity/AntigravitySection.d.ts.map +1 -1
  7. package/lib/types/client/antigravity/locales.d.ts +47 -2
  8. package/lib/types/client/antigravity/locales.d.ts.map +1 -1
  9. package/lib/types/client/kimi-code/locales.d.ts +2 -2
  10. package/lib/types/client/workbuddy/WorkBuddySection.d.ts +36 -0
  11. package/lib/types/client/workbuddy/WorkBuddySection.d.ts.map +1 -1
  12. package/lib/types/client/workbuddy/locales.d.ts +18 -0
  13. package/lib/types/client/workbuddy/locales.d.ts.map +1 -1
  14. package/lib/types/host/antigravity/account-pool.d.ts +24 -0
  15. package/lib/types/host/antigravity/account-pool.d.ts.map +1 -1
  16. package/lib/types/host/antigravity/adapter.d.ts.map +1 -1
  17. package/lib/types/host/antigravity/cache-stats.d.ts +166 -0
  18. package/lib/types/host/antigravity/cache-stats.d.ts.map +1 -0
  19. package/lib/types/host/antigravity/mapper.d.ts +75 -3
  20. package/lib/types/host/antigravity/mapper.d.ts.map +1 -1
  21. package/lib/types/host/antigravity/routes.d.ts +1 -1
  22. package/lib/types/host/antigravity/routes.d.ts.map +1 -1
  23. package/lib/types/host/workbuddy/client.d.ts +8 -0
  24. package/lib/types/host/workbuddy/client.d.ts.map +1 -1
  25. package/lib/types/host/workbuddy/model-catalog.d.ts +30 -1
  26. package/lib/types/host/workbuddy/model-catalog.d.ts.map +1 -1
  27. package/lib/types/host/workbuddy/routes.d.ts +7 -0
  28. package/lib/types/host/workbuddy/routes.d.ts.map +1 -1
  29. package/lib/types/shared/antigravity-contracts.d.ts +76 -0
  30. package/lib/types/shared/antigravity-contracts.d.ts.map +1 -1
  31. package/lib/types/shared/workbuddy-contracts.d.ts +42 -0
  32. package/lib/types/shared/workbuddy-contracts.d.ts.map +1 -1
  33. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.17.0 - 2026-10-09
6
+
7
+ - **[Antigravity] 缓存卡片改为只报「整段未命中」并给出它之前的空闲时长**
8
+ - **原来的口径把最有用的信息盖掉了。** 卡片报的是「相比上一次请求,三段前缀里哪一段变了」,而每次发新消息都必然改变 `contents`,于是它**每一次都显示「对话内容变化」**——既报了唯一注定要变的那一项,又把真正花钱的那件事藏了起来。这个口径是我上一版写的,属于设计失误。
9
+ - **现在只在整段未命中时发布归因**(`cachedContentTokenCount === 0`,即整个前缀被重算)。部分命中是对话增长的正常状态,每次新回合都必然带入服务端没见过的 token,`freshTokens > 0` 本身不代表任何东西,因此不再上报。
10
+ - **归因改为「先暂存、后发布」**:请求发出前算出的变化只暂存(`pendingBySession`),等响应报回用量后才决定要不要发布。改动的代价是否为零,只有响应的用量能回答。
11
+ - **新增空闲时长**:记录每个会话上一次**发出请求**的时刻,未命中时一并报出间隔。服务端的缓存按空闲时间过期(实测在 76 秒仍命中、183 秒整段失效),而客户端无法读取那个阈值——这是唯一能在本地观测到它的读数。首次请求没有可比较的前值,因此不填该字段(填 0 会被读成「立刻返回」)。
12
+ - **契约与 UI 同步**:`AntigravityCacheStatsDto.lastDriftCause` → `lastMiss: { cause, idleMs?, at }`;卡片显示「最近一次整段未命中」+「未命中前空闲」,时长按 3m 3s / 45s 这样的人类单位渲染;中英文案与段落说明一并重写。
13
+ - 测试:缓存统计 19 → 22 例(新增「命中时不报」「未命中时发布归因」「记录空闲时长」「首次请求不填空闲」),卡片渲染新增 1 例(全部命中时**不出现**任何未命中行,防止退回「每次都有话说」)。既有断言中 3 处依赖旧口径的已按新契约更新。
14
+ - 验证:`npm run typecheck` 0 错误;全量 `npm test` **2956 passed / 7 skipped, 0 failed**(较基线增 4 例)。
15
+
16
+ - **[Antigravity] 让 Gemini 路线的请求前缀保持逐字节稳定,并给这条线路补上缓存观测**
17
+ - **Antigravity 用的是 Google 服务端隐式缓存,客户端没有任何开关可拨。** 二进制里 `cachedContents` / `ContextCacheConfig` / `ttl` 这些字段确实存在,但都属于它链进来的官方 Go SDK,不是它的调用面:它实际只 POST `/v1internal:streamGenerateContent`,那层消息里不含任何缓存控制字段。所以唯一能提高命中率的手段,就是**不修改**请求前缀——前缀一旦在某个位置被改写,从该位置往后全部失效。
18
+ - **Gemini 路线一直在把每个 SSE text delta 回放成独立 part。** 这条修复此前只做给了 Claude(见 `docs/antigravity-claude-cache-audit.md`:385 个 delta 回放成 385 个 part,命中率因此掉到 68.84%),Gemini 走的是同一份 delta 却漏了。现在抽出纯函数 `mergeAdjacentTextParts`,两条路线各自调用;Claude 专属的 thinking/signature 配对逻辑原地保留,只对 Claude 生效。合并只针对「仅含 `text` 一个键」的相邻 part,带 signature、`thought: true`、`functionCall` 的边界一律不跨越。
19
+ - **图片预算原本按存储字节度量,而缩放发生在预算之后**,导致一组图在缩放有机会生效之前就已经超限、最旧的图照样被替换成占位文本——图片数量仍然变化,前缀照旧被改写,缩放等于没接上。现在 `requestImageBytes` 度量的是**实际要发送的尺寸**(有缩放目标时取 `min(存储字节, 目标上限)`),这是 `host/common/request-images.ts` 已记录过的同一个陷阱。预算仍保持 Antigravity 自己的 12MB,**没有**借用 Kimi 的 1.5MB。
20
+ - **这条线路此前完全不可观测**:`request-diagnostics` 的指纹是进程内临时 HMAC、不落盘,而且只看顶层 `tools/system/messages`,看不到 Antigravity 嵌套的 `request.contents/systemInstruction/tools`。新增的 `cache-stats.ts` 按会话记录命中率,并对三段前缀各取一个稳定指纹,把冷却归因到 `contents` / `systemInstruction` / `tools` / `session-id` / `none`。哈希走键排序后的稳定序列化,否则两个结构相同、键序不同的请求会被报成假漂移。统计纯内存、按 `sessionId + accountId` 分作用域、256 作用域 LRU。
21
+ - **已知局限写进了文件头,而不是留在报告里**:前缀快照按 session 分区、token 总计按 session+account 分区,两者作用域不同,所以**会话中途 429 换号会被报成 `'none'`**——读者不能据此读成「客户端什么都没变」。账号是否真的分区服务端缓存始终是 **INFERRED, not measured**,也正因如此没有把 `account` 提升成漂移原因:在假设未验证前把它写进契约,等于把假设当事实陈述。`'session-id'` 那个枚举同理,文案用「会话标识变化」而不承诺缓存语义。
22
+ - **账号调度缺省改为 sticky**,且只改缺省:`parseAntigravityPoolData` 会把缺失值补成 `'sequential'` 并写回池文件,所以那里的解析器也必须一起改,否则 `routes.ts` 的回退分支是死代码。**已存盘的显式选择继续生效**,不为存量用户做迁移——这条改动的前提(账号=缓存命名空间)尚未验证,不适合在未证实收益的情况下改变既有行为。共享 core `host/common/account-pool.ts` 的策略语义未动,其他线路不受影响。
23
+ - 设置页新增缓存卡片(命中率、累计 cached/fresh、最近一次漂移原因),中英文案齐备;未测得用量时显示为空而不是画一个 0% 的假仪表。
24
+ - 验证:`npm run typecheck` 0 错误;全量 `npm test` **2952 passed / 7 skipped, 0 failed**。新用例在改动前的实现上实测 **12 failed**(用 `git stash` 单独暂存 `mapper.ts` 复现),改后全过。
25
+ - **本次没有证明命中率提升**:Google 的隐式缓存是服务端的,本机无法做 A/B,`readImageRequest` 的真实缩放产物在测试里全是 mock。改动依据是「前缀逐字节稳定」这一机制,与 68.84% 那次的成因同构,但没有可报的线上数字。
26
+
27
+ ## 0.16.0 - 2026-10-09
28
+
29
+ - **[WorkBuddy] 模型行加上消耗倍率提示**
30
+ - **倍率一直是公开的,只是本插件没读。** 网关 `GET /v3/config` 每个模型条目都带一个 `credits` 字符串(国际区 `x6.67`、国区 `x0.79 credits`),就是该模型消耗套餐额度的相对速率;官方 CodeBuddy 客户端把同一个字段渲染成 `6.67x` 显示在模型下拉里(其 web-ui 里对应的就是 `credits.replace(/\s*credits$/i,'').replace(/^[x×]/i,'') + 'x'`)。
31
+ - **倍率必须按区域存,不能一个 id 一个数。** 实测 2026-10-09:`deepseek-v4.1-flash` 国区 `0.11`、国际区 `0.00`——一个标量必然对其中一个区的账号是错的。因此 `credits` 是 `Partial<Record<WorkBuddyRegion, string>>`,**只为该区确实公布过的值写键**;两区共有的 8 个 id 里 7 个数值一致、1 个不一致。
32
+ - **实时目录必须解析这个字段,兜底表也要抄。** 网关可达时**整表替换**内置表,若只在兜底表里写倍率,登录用户反而永远看不到它,而且上游一改价本地就一直是错的。两处都写上。
33
+ - **未公布的倍率一律不出现在界面上,而不是显示 0。** `default-model` 公布的是空字符串;`x0.00` 是**真实声明**(该模型不消耗额度),两者必须区分开——所以在 host 侧就把空串解析成「字段缺席」,`0.00` 原样保留。在一个花钱的数字上猜一个值,比缺一个值更糟。
34
+ - **账号区域已知时只显示该区的值,绝不拿另一个区顶上**;区域未知(还没读回账号)而两区不一致时把两区都写出来(`倍率 0.11x(国区) / 0.00x(国际区)`),一致时折叠成一个值。**两区域串的每一个字都由字典提供**——区域名词、括号字形、连两段之间的分隔符都是模板键,函数里不硬拼任何一个;中英括号字形不同(全角自带留白、半角不带),硬拼必然有一侧把 `(China)/ 0.00x` 粘成一段。跨区调用本来就会被上游以 400 `code 11102` 拒绝,所以借用邻区的数字等于报一个这个账号永远不会付的价。
35
+ - 倍率接在提示行**末尾**(`1M · 图片 · low/high/max · 倍率 0.79x`):容量/图像/档位说的是模型**接受什么**,倍率说的是它**花多少钱**,把花钱那条留在最后,前面的能力信息仍读作一个短语。没有倍率时该项**整个不出现**,不留占位、不产生连续两个 `·`。
36
+ - **区域裁剪放在客户端而不是 host**:`buildModelOptions` 返回的 DTO 也会在没有账号的情况下被读取(harness 在未选定凭据时就解析模型列表),host 侧裁掉会让那种调用方拿到空值。
37
+ - 三处边界由测试钉住:`'×3.31'`/`' x1.62 credits '` 这类拼写、`'x0.00'` 必须保留、`''`/`'0.5x'`/`'x1,000'`/`null`/`42` 必须解析成 null(而不是 0)。另加一条全表不变量:`FALLBACK_MODELS` 里每个 `credits` 键都必须是该条自己 `regions` 声明过的区域。
38
+ - 测试:新增 `creditMultiplierHint` 的逐规则用例(含双语)、`parseConfigModels`/`buildModelOptions` 的透传用例、模型行的渲染级断言与「无倍率不留分隔符」用例。
39
+ - 验证:`npm run typecheck` 0 错误;全量 `npm test` **2909 passed / 7 skipped**(较基线 2886 增 23 例,0 failed)。倍率数值本身由一次性探针 `scripts/probe-workbuddy-credits.probe.ts` 从实时 `/v3/config` 读出,`FALLBACK_MODELS` 的 43 行倍率与实测**逐字一致**(另 4 行网关未公布,故无该字段),且探针复核出「两区不一致的只有 `deepseek-v4.1-flash` 一个 id」。
40
+
5
41
  ## 0.15.0 - 2026-10-09
6
42
 
7
43
  - **[WorkBuddy] 补上网关「在服务但从不公布」的模型,并按厂商归拢模型列表**
package/README.md CHANGED
@@ -586,6 +586,8 @@ DSH 设置页的「Subagent」卡片会把勾选的模型写成会话级的允
586
586
 
587
587
  `workbuddy-subscription` 会像其他 Provider 一样出现在 DSH 模型选择器中,并可与名为 `workbuddy` 的自定义 API 同时存在。**上下文窗口**默认取网关 `/v3/config` 声明的**默认服务长度**,可逐模型覆盖(用于 DSH 的压缩与溢出判断,支持 `1M` / `300K` / `200000` 等写法);**默认思考深度**逐模型生效,选项由当前账号各模型**实际声明的档位**取并集(仅部分模型支持的档位会标注数量),档位不在该模型集合内时不会被发送。
588
588
 
589
+ **消耗倍率**也来自同一份目录:每个模型在 `/v3/config` 里带一个 `credits` 字符串(如 `x0.79 credits`、`x6.67`),就是该模型消耗套餐额度的相对速率——官方 CodeBuddy 客户端把它渲染成 `6.67x` 显示在模型下拉里。设置页的模型行末尾会以 `倍率 0.79x` 的形式给出,**两区不一致时把两区都写出来**(实测国际区 `deepseek-v4.1-flash` 是 `0.00`、国区是 `0.11`),账号区域已知时只显示该区的值,**绝不拿另一个区的数字顶上**(跨区调用本来就会被上游以 400 `code 11102` 拒绝)。`x0.00` 是真实声明(该模型不消耗额度),照常显示;网关没给倍率的模型(如 `default-model`)则**整项不出现在提示里**,而不是编一个 0。
590
+
589
591
  **凭据目录**按平台解析,可用 `CODEBUDDY_AUTH_DIR` 覆盖(与官方工具链一致):
590
592
 
591
593
  | 平台 | 默认目录 |
package/lib/client.js CHANGED
@@ -2510,6 +2510,21 @@ window.__ModuleLoader__.load({
2510
2510
  save: "保存",
2511
2511
  saved: "已保存",
2512
2512
  saving: "保存中...",
2513
+ cacheSection: "缓存",
2514
+ cacheDesc: "Antigravity 的缓存由服务端隐式命中,客户端不发送任何缓存控制字段,因此这里用实际读缓存的比例反映效果。下面只报「整段未命中」的那一次,并给出它之前的空闲时长——缓存是按空闲时间过期的,而每次新对话都必然改变内容,把每次变化都报出来等于什么都没说。统计只保存在内存中,并按会话隔离。",
2515
+ cacheEmpty: "本进程还没有带用量回报的请求。",
2516
+ cacheHitRatio: "缓存命中率",
2517
+ cacheCached: "命中 tokens",
2518
+ cacheFresh: "新增 tokens",
2519
+ cacheRequests: "请求数",
2520
+ cacheMiss: "最近一次整段未命中",
2521
+ cacheMissIdle: "未命中前空闲",
2522
+ cacheDriftNone: "前缀未变化",
2523
+ cacheDriftContents: "对话内容变化",
2524
+ cacheDriftSystemInstruction: "系统指令变化",
2525
+ cacheDriftTools: "工具列表变化",
2526
+ cacheDriftSessionId: "会话标识变化",
2527
+ cacheDriftNewSession: "本次会话的首次请求",
2513
2528
  quotaSection: "用量与配额",
2514
2529
  quotaDesc: "数据来自 Google Cloud Code Assist 配额服务,页面可见时最多每 60 秒刷新一次。",
2515
2530
  refreshQuota: "刷新用量",
@@ -2573,6 +2588,21 @@ window.__ModuleLoader__.load({
2573
2588
  save: "Save",
2574
2589
  saved: "Saved",
2575
2590
  saving: "Saving...",
2591
+ cacheSection: "Cache",
2592
+ cacheDesc: "Antigravity's cache is matched implicitly by the service — the client sends no cache-control field — so the served-from-cache share is the only evidence it is working. Only a total miss is reported below, beside how long the client had been idle: the cache expires on idle time, and every new turn changes the conversation, so reporting each change would report the same thing every time. The totals are in-memory and scoped per session.",
2593
+ cacheEmpty: "No request in this process has reported usage yet.",
2594
+ cacheHitRatio: "Cache hit ratio",
2595
+ cacheCached: "Cached tokens",
2596
+ cacheFresh: "Fresh tokens",
2597
+ cacheRequests: "Requests",
2598
+ cacheMiss: "Last total miss",
2599
+ cacheMissIdle: "Idle before that miss",
2600
+ cacheDriftNone: "Prefix unchanged",
2601
+ cacheDriftContents: "Conversation contents changed",
2602
+ cacheDriftSystemInstruction: "System instruction changed",
2603
+ cacheDriftTools: "Tool list changed",
2604
+ cacheDriftSessionId: "Session identifier changed",
2605
+ cacheDriftNewSession: "This session's first request",
2576
2606
  quotaSection: "Usage & Quota",
2577
2607
  quotaDesc: "Data from Google Cloud Code Assist quota service; refreshes up to once every 60 seconds when visible.",
2578
2608
  refreshQuota: "Refresh quota",
@@ -2599,6 +2629,26 @@ window.__ModuleLoader__.load({
2599
2629
  //#endregion
2600
2630
  //#region src/client/antigravity/AntigravitySection.tsx
2601
2631
  const api$11 = createLineApi("/antigravity/api", "Antigravity");
2632
+ /** A gap in human units, so the idle reading is legible without arithmetic. */
2633
+ function idleLabel(ms) {
2634
+ const seconds = Math.round(ms / 1e3);
2635
+ if (seconds < 60) return `${seconds}s`;
2636
+ const minutes = Math.floor(seconds / 60);
2637
+ const rest = seconds % 60;
2638
+ return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
2639
+ }
2640
+ /** Human label for the cause of the last request that lost the whole cache. */
2641
+ function missLabel(cause, t) {
2642
+ switch (cause) {
2643
+ case "none": return t.cacheDriftNone;
2644
+ case "contents": return t.cacheDriftContents;
2645
+ case "systemInstruction": return t.cacheDriftSystemInstruction;
2646
+ case "tools": return t.cacheDriftTools;
2647
+ case "session-id": return t.cacheDriftSessionId;
2648
+ case "new-session": return t.cacheDriftNewSession;
2649
+ default: return String(cause);
2650
+ }
2651
+ }
2602
2652
  function AntigravitySection({ onModelChange, loadModelDirectory }) {
2603
2653
  const [status, setStatus] = (0, react.useState)(null);
2604
2654
  const [loading, setLoading] = (0, react.useState)(true);
@@ -2903,7 +2953,7 @@ window.__ModuleLoader__.load({
2903
2953
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountPoolSection, {
2904
2954
  accounts: status?.accounts ?? [],
2905
2955
  activeAccountId: status?.activeAccountId,
2906
- rotationStrategy: status?.rotationStrategy ?? "sequential",
2956
+ rotationStrategy: status?.rotationStrategy ?? "sticky",
2907
2957
  busy,
2908
2958
  labels: t,
2909
2959
  loginBusyLabel: busy === "login" ? loginProgress || t.signingIn : void 0,
@@ -3027,6 +3077,76 @@ window.__ModuleLoader__.load({
3027
3077
  })
3028
3078
  ]
3029
3079
  }),
3080
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3081
+ className: "dsha-group",
3082
+ children: [
3083
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3084
+ className: "dsha-grouphead",
3085
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t.cacheSection })
3086
+ }),
3087
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3088
+ className: "dsha-muted",
3089
+ children: t.cacheDesc
3090
+ }),
3091
+ status?.cache == null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3092
+ className: "dsha-empty",
3093
+ children: t.cacheEmpty
3094
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3095
+ className: "dsha-quota-card",
3096
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3097
+ className: "dsha-meter-wrap",
3098
+ children: [
3099
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3100
+ className: "dsha-meter-label",
3101
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t.cacheHitRatio }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: status.cache.hitRatio === null ? "—" : `${(status.cache.hitRatio * 100).toFixed(1)}%` })]
3102
+ }),
3103
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3104
+ className: `dsha-meter ${(status.cache.hitRatio ?? 0) >= .9 ? "dsha-meter-green" : "dsha-meter-cyan"}`,
3105
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { width: `${Math.round((status.cache.hitRatio ?? 0) * 100)}%` } })
3106
+ }),
3107
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3108
+ className: "dsha-meter-meta",
3109
+ children: [
3110
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
3111
+ t.cacheCached,
3112
+ ": ",
3113
+ status.cache.cachedTokens.toLocaleString()
3114
+ ] }),
3115
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
3116
+ t.cacheFresh,
3117
+ ": ",
3118
+ status.cache.freshTokens.toLocaleString()
3119
+ ] }),
3120
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
3121
+ t.cacheRequests,
3122
+ ": ",
3123
+ status.cache.requests
3124
+ ] })
3125
+ ]
3126
+ })
3127
+ ]
3128
+ }), status.cache.lastMiss !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3129
+ className: "dsha-row",
3130
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3131
+ className: "dsha-label",
3132
+ children: t.cacheMiss
3133
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3134
+ className: "dsha-value",
3135
+ children: missLabel(status.cache.lastMiss.cause, t)
3136
+ })]
3137
+ }), status.cache.lastMiss.idleMs !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3138
+ className: "dsha-row",
3139
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3140
+ className: "dsha-label",
3141
+ children: t.cacheMissIdle
3142
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3143
+ className: "dsha-value",
3144
+ children: idleLabel(status.cache.lastMiss.idleMs)
3145
+ })]
3146
+ })] })]
3147
+ })
3148
+ ]
3149
+ }),
3030
3150
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3031
3151
  className: "dsha-group",
3032
3152
  children: [
@@ -5009,6 +5129,9 @@ window.__ModuleLoader__.load({
5009
5129
  unselectAll: "全不选",
5010
5130
  imageSupport: "图片",
5011
5131
  textOnly: "纯文本",
5132
+ creditMultiplier: "倍率",
5133
+ creditMultiplierRegion: "{value}({region})",
5134
+ creditMultiplierPair: "{first} / {second}",
5012
5135
  regionBadge: "区域",
5013
5136
  unavailableHere: "当前区域不可用",
5014
5137
  enhanced: "增强功能",
@@ -5092,6 +5215,9 @@ window.__ModuleLoader__.load({
5092
5215
  unselectAll: "Deselect all",
5093
5216
  imageSupport: "Images",
5094
5217
  textOnly: "Text only",
5218
+ creditMultiplier: "Multiplier",
5219
+ creditMultiplierRegion: "{value} ({region})",
5220
+ creditMultiplierPair: "{first} / {second}",
5095
5221
  regionBadge: "Regions",
5096
5222
  unavailableHere: "Unavailable in this region",
5097
5223
  enhanced: "Enhanced features",
@@ -5648,6 +5774,51 @@ window.__ModuleLoader__.load({
5648
5774
  xhigh: "X-High",
5649
5775
  max: "Max"
5650
5776
  };
5777
+ /**
5778
+ * The consumption rate of one model, as the settings row states it.
5779
+ *
5780
+ * The gateway publishes one rate per region and the official CodeBuddy picker
5781
+ * spells it beside a model as `6.67x`; the row uses the same shape behind the
5782
+ * section's own label. Which rates reach the row depends on what the card knows
5783
+ * about the account, and the two cases are deliberately different:
5784
+ *
5785
+ * - A KNOWN region shows that region's rate and nothing else. Borrowing the
5786
+ * sibling region's number would state a price this account never pays —
5787
+ * measured 2026-10-09, `deepseek-v4.1-flash` is `0.11` on cn against `0.00`
5788
+ * on intl, and the backends reject a model asked of the wrong region anyway.
5789
+ * - An UNKNOWN region (no account read back yet) shows both rates when the
5790
+ * backends disagree, so the row is honest about the range instead of picking
5791
+ * one of them. When they agree there is nothing to disambiguate.
5792
+ *
5793
+ * The two-region form is assembled entirely from the caller's labels: the
5794
+ * qualifier comes from `labels.region(value, key)` and the join from
5795
+ * `labels.pair(first, second)`, so the dictionary owns the region nouns, the
5796
+ * bracket glyphs AND the separator. Nothing about the two-region string is
5797
+ * spelled in this file, which is the point — the glyphs differ per language
5798
+ * (`倍率 0.11x(国区) / 0.00x(国际区)` in Chinese,
5799
+ * `Multiplier 0.11x (China) / 0.00x (International)` in English) and a
5800
+ * separator chosen here would be right in at most one of them.
5801
+ *
5802
+ * `null` means "this row states no rate"; the caller drops it from the hint
5803
+ * array rather than rendering a placeholder, because a guessed rate on a spend
5804
+ * figure is worse than a missing one. `'0.00'` is a declaration — this model
5805
+ * consumes no allowance — so it renders like any other value.
5806
+ */
5807
+ function creditMultiplierHint(credits, region, labels) {
5808
+ const rate = (key) => {
5809
+ const value = credits?.[key];
5810
+ return typeof value === "string" && value !== "" ? value : null;
5811
+ };
5812
+ if (region !== null) {
5813
+ const value = rate(region);
5814
+ return value === null ? null : `${labels.multiplier} ${value}x`;
5815
+ }
5816
+ const cn = rate("cn");
5817
+ const intl = rate("intl");
5818
+ if (cn === null) return intl === null ? null : `${labels.multiplier} ${intl}x`;
5819
+ if (intl === null || intl === cn) return `${labels.multiplier} ${cn}x`;
5820
+ return `${labels.multiplier} ${labels.pair(labels.region(`${cn}x`, "cn"), labels.region(`${intl}x`, "intl"))}`;
5821
+ }
5651
5822
  /** Mask a UIN so the card shows identity without publishing the full number. */
5652
5823
  function maskUin(uin) {
5653
5824
  if (uin === void 0 || uin === null || uin === "") return "—";
@@ -5847,16 +6018,24 @@ window.__ModuleLoader__.load({
5847
6018
  children: t.modelsHint
5848
6019
  }),
5849
6020
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelChecklist, {
5850
- items: (status?.models ?? []).map((model) => ({
5851
- id: model.id,
5852
- name: model.name,
5853
- hint: [
5854
- formatCapacity(model.contextWindow),
5855
- model.supportsImage ? t.imageSupport : t.textOnly,
5856
- ...model.reasoningEfforts && model.reasoningEfforts.length > 0 ? [model.reasoningEfforts.join("/")] : []
5857
- ].join(" · "),
5858
- enabled: model.enabled
5859
- })),
6021
+ items: (status?.models ?? []).map((model) => {
6022
+ const multiplier = creditMultiplierHint(model.credits, status?.account?.region ?? null, {
6023
+ multiplier: t.creditMultiplier,
6024
+ region: (value, key) => t.creditMultiplierRegion.replace("{value}", value).replace("{region}", key === "cn" ? t.regionCn : t.regionIntl),
6025
+ pair: (first, second) => t.creditMultiplierPair.replace("{first}", first).replace("{second}", second)
6026
+ });
6027
+ return {
6028
+ id: model.id,
6029
+ name: model.name,
6030
+ hint: [
6031
+ formatCapacity(model.contextWindow),
6032
+ model.supportsImage ? t.imageSupport : t.textOnly,
6033
+ ...model.reasoningEfforts && model.reasoningEfforts.length > 0 ? [model.reasoningEfforts.join("/")] : [],
6034
+ ...multiplier === null ? [] : [multiplier]
6035
+ ].join(" · "),
6036
+ enabled: model.enabled
6037
+ };
6038
+ }),
5860
6039
  busy: busy !== null,
5861
6040
  onToggle: (id, enabled) => void toggleModel(id, enabled),
5862
6041
  onToggleAll: (enabled) => void setAllModels(enabled),