@eddyskywalker/dsh-chatgpt-subscription 0.14.0 → 0.16.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,44 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.16.0 - 2026-10-09
6
+
7
+ - **[WorkBuddy] 模型行加上消耗倍率提示**
8
+ - **倍率一直是公开的,只是本插件没读。** 网关 `GET /v3/config` 每个模型条目都带一个 `credits` 字符串(国际区 `x6.67`、国区 `x0.79 credits`),就是该模型消耗套餐额度的相对速率;官方 CodeBuddy 客户端把同一个字段渲染成 `6.67x` 显示在模型下拉里(其 web-ui 里对应的就是 `credits.replace(/\s*credits$/i,'').replace(/^[x×]/i,'') + 'x'`)。
9
+ - **倍率必须按区域存,不能一个 id 一个数。** 实测 2026-10-09:`deepseek-v4.1-flash` 国区 `0.11`、国际区 `0.00`——一个标量必然对其中一个区的账号是错的。因此 `credits` 是 `Partial<Record<WorkBuddyRegion, string>>`,**只为该区确实公布过的值写键**;两区共有的 8 个 id 里 7 个数值一致、1 个不一致。
10
+ - **实时目录必须解析这个字段,兜底表也要抄。** 网关可达时**整表替换**内置表,若只在兜底表里写倍率,登录用户反而永远看不到它,而且上游一改价本地就一直是错的。两处都写上。
11
+ - **未公布的倍率一律不出现在界面上,而不是显示 0。** `default-model` 公布的是空字符串;`x0.00` 是**真实声明**(该模型不消耗额度),两者必须区分开——所以在 host 侧就把空串解析成「字段缺席」,`0.00` 原样保留。在一个花钱的数字上猜一个值,比缺一个值更糟。
12
+ - **账号区域已知时只显示该区的值,绝不拿另一个区顶上**;区域未知(还没读回账号)而两区不一致时把两区都写出来(`倍率 0.11x(国区) / 0.00x(国际区)`),一致时折叠成一个值。**两区域串的每一个字都由字典提供**——区域名词、括号字形、连两段之间的分隔符都是模板键,函数里不硬拼任何一个;中英括号字形不同(全角自带留白、半角不带),硬拼必然有一侧把 `(China)/ 0.00x` 粘成一段。跨区调用本来就会被上游以 400 `code 11102` 拒绝,所以借用邻区的数字等于报一个这个账号永远不会付的价。
13
+ - 倍率接在提示行**末尾**(`1M · 图片 · low/high/max · 倍率 0.79x`):容量/图像/档位说的是模型**接受什么**,倍率说的是它**花多少钱**,把花钱那条留在最后,前面的能力信息仍读作一个短语。没有倍率时该项**整个不出现**,不留占位、不产生连续两个 `·`。
14
+ - **区域裁剪放在客户端而不是 host**:`buildModelOptions` 返回的 DTO 也会在没有账号的情况下被读取(harness 在未选定凭据时就解析模型列表),host 侧裁掉会让那种调用方拿到空值。
15
+ - 三处边界由测试钉住:`'×3.31'`/`' x1.62 credits '` 这类拼写、`'x0.00'` 必须保留、`''`/`'0.5x'`/`'x1,000'`/`null`/`42` 必须解析成 null(而不是 0)。另加一条全表不变量:`FALLBACK_MODELS` 里每个 `credits` 键都必须是该条自己 `regions` 声明过的区域。
16
+ - 测试:新增 `creditMultiplierHint` 的逐规则用例(含双语)、`parseConfigModels`/`buildModelOptions` 的透传用例、模型行的渲染级断言与「无倍率不留分隔符」用例。
17
+ - 验证:`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」。
18
+
19
+ ## 0.15.0 - 2026-10-09
20
+
21
+ - **[WorkBuddy] 补上网关「在服务但从不公布」的模型,并按厂商归拢模型列表**
22
+ - **根因是一处反直觉的实测事实:`/v3/config` 不是服务全集。** 国际区实测,`gpt-6-sol`、`gpt-6-luna`、`gemini-3.8-flash` 三个 id 都能正常返回 200 的流式补全,却**完全不在目录里**(该接口只公布 `gpt-6-astra` 一个 GPT-6 成员)。判据只有一条——直接问:`code 11102`「service info not found」是没有这个模型,`code 11133`「provider rejected params」是模型存在、只是参数被拒。**`11133` 不能当存在性证据**:`max_tokens` 给得太小会把目录里的正常模型也波及进来(`gpt-5.5`、`gpt-5.4` 都会落到这一类)。
23
+ - **只加进内置表等于加了个看不见的模型。** 联网时实时目录会**整体替换**内置表,所以这三个 id 单列一份实测表(`UNPUBLISHED_MODELS`),并在**实时目录解析**与**快照反序列化**两处合并进去;已公布的条目永远优先,合并只填目录没提到的 id。国际区因此从 22 个变 25 个。
24
+ - **哪些字段是量出来的、哪些是继承的,界线写在代码里。** 存在性、思考档位、图片支持是**打接口测出来的**(`gpt-6-sol` 精确接受 `low`/`medium`/`high`/`xhigh`/`max`、拒绝 `minimal`,与 `gpt-6-astra` 公布的档位表一致;两者都接受 1×1 PNG)。上下文窗口、输出上限、`canDisableThinking` 则是**继承同族已公布的兄弟模型**(`gpt-6-astra` / `gemini-3.5-flash`)并就地标注——网关对这些 id 不公布任何信息,而 `max_tokens` 也不受窗口约束(连 `gpt-6-astra` 自己都接受远超其公布上限的值),从请求侧无法反推。三者仍可在设置页逐模型覆盖。
25
+ - **同一份现场实读还修正了国际区三处数值**:`glm-5.3-flash` 国际区也开始服务(此前标为仅国区);`glm-5.3` / `glm-5.2` 国际区输出上限是 48000、`kimi-k2.8-preview` 是 32000。**一条表同时覆盖两区时取小值**:高报会让请求被网关拒绝,低报只是提前压缩,且仍可覆盖。
26
+ - **国区同步**:新增 `hy4-preview-f`、`space-bunny`,`hy4-preview-x` 已下线。
27
+ - **模型列表按厂商分组,不再让网关的排列顺序决定观感。** 网关给 `/v3/config` 的顺序是交错厂商的:一行 DeepSeek、一行 GPT-6-Astra、两行混元、一行 Kimi、再三行 GPT——八个 OpenAI 模型在设置页里从不挨在一起。现在按厂商族分组,族内**新代在前**。
28
+ - **组内排序按版本号比较,而不是照抄目录序。** 这是被实测逼出来的:只做分组时「在服务但不公布」的模型因为追加在已公布列表之后,`gpt-6-sol` 会渲染到 `gpt-5.4` **下面**——最新的沉底。改成比较 id 里的数字段后归位,且数字按**数值**比(`gpt-6.10` 在 `gpt-6.6` 之前),同版本保持目录序(`-flash` 仍领着 `-flash-sg`),读不出版本的 id(`primary-model` 这类别名)保持原位而不瞎猜。
29
+ - **分组放在 host 而不是设置页。** `modelsForRegion` 同样是请求路径解析模型的入口,在那儿排一次,picker 与 adapter 不可能出现两套顺序;客户端一行未改。
30
+ - 厂商归属有个坑:`hy3` / `hy4-preview` / `hunyuan-chat` 是腾讯混元的三种写法,必须归到同一组;没有任何规则命中的 id **自成一族**,而不是被扫进一个共享的 `other` 桶。
31
+ - 测试:新增分组与排序断言(族必须连续、组内新代在前、数值比较、同版本保持目录序),并扩充三个既有目录测试。
32
+ - 验证:`npm run typecheck` 0 错误;全量 `npm test` **2886 passed / 7 skipped**。另加两个一次性探针(`scripts/probe-workbuddy-catalog.probe.ts` 拉实时目录做差异、`scripts/probe-workbuddy-unlisted.probe.ts` 给目录外的 id 测档位/图片/输出上限),本条的每一项数值都是它们的输出。
33
+
34
+ - **[思维保护] 推理坍缩守卫补上第二条独立信号:改写型死循环**
35
+ - **原守卫对「换个说法反复说同一件事」是瞎的。** 唯一率信号只看得见**逐字**重复;一段把同一结论用不同措辞重述的流实测只有 0.48,而阈值是 0.85,于是一路跑到输出上限也没人拦。这不是理论问题——归档样本 `paraphrased-loop-reasoning.txt` 就是这种形态。
36
+ - **第二条信号量的是「复述」而非「新颖」。** 把窗口切成句子,逐句与至少 `semanticLag` 句之前的内容做 3-gram Jaccard 相似度并取最大值。**枚举式推理也会重复,但它是相邻重复**——每一项借用上一项的句式;复述信号**忽略相邻性**,这正是区分两者的关键,所以健康的枚举流不会被误伤。
37
+ - **两条路径,低门槛那条在每一步都更严。** 高门槛路径(原规则,未改动)要求唯一率 ≥ 0.85 且复述 ≥ 0.35;低门槛路径(新)允许唯一率低到 0.6,代价是复述门槛抬到 0.45、**外加一道高路径不需要的新颖度上限**(不同句比例 ≤ 0.47),且需要更多次确认(5 次 vs 3 次)。**用更弱的证据换更多、更长的确认。**
38
+ - **确认是连续窗口计数,不是单窗口触发。** 两个信号必须在同一窗口同时过线,且连续 `semanticConfirmations` 次才动手;只有一方过线、或只有单个异常窗口,一律**只记日志不动作**。断流后连击归零,下一次尝试不会继承它没挣到的确认数。
39
+ - **近失也留痕。** 单信号命中有专门的 alone 日志(每种每轮只报一次,避免数百行刷屏),连击在够数前断掉也有 cleared 日志——否则两段短连击在日志里与一段长连击长得一模一样,确认数事后无从推理。
40
+ - 测试:守卫测试 **39 → 80 例**(源文件 480 → 948 行),新增归档样本 fixture 与真实的改写型坍缩样本。
41
+ - 验证:全量 `npm test` 2886 passed / 7 skipped;守卫专项 80 例全通过。
42
+
5
43
  ## 0.14.0 - 2026-10-08
6
44
 
7
45
  - **[Claude] 支持 Claude Haiku 5.5(`claude-haiku-5-5`),并把申报的客户端版本抬到 2.1.293**
package/README.md CHANGED
@@ -153,6 +153,7 @@
153
153
  - 插件托管账号可真正删除;桌面扫描账号只能从本插件隐藏/恢复,永不删除 CodeBuddy 的原文件;
154
154
  - 两区模型清单不同,把模型发到不服务它的区会返回 400 `code 11102`,因此模型选择器**按当前账号区域过滤**;
155
155
  - **模型目录取自网关自己的 `/v3/config`**(官方 CLI 启动时读的就是它):每个模型的真实上下文上限、输出上限、是否接受图片、以及可用的思考档位都在这里,不做任何按模型名猜测。`/v1/models` 在这条线路上是 404,所以此前只能靠内置表——现在内置表只作为离线兜底,且是**从真实 `/v3/config` 转录**的(早期手写版本把 `glm-5.3`、`kimi-k3` 的窗口猜成 200K/256K,实际都是 1M);
156
+ - **目录不是服务全集,模型必须实测**:实测国际区的 `gpt-6-sol`、`gpt-6-luna`、`gemini-3.8-flash` 都能正常返回 200,却**完全不在 `/v3/config` 里**(该接口只公布 `gpt-6-astra`)。而联网时目录会**整体替换**内置表,只加进内置表等于加了个看不见的模型——所以这三个单列一份实测表(`UNPUBLISHED_MODELS`),并合并进实时目录。存在性、思考档位、图片支持是**打接口量出来的**(`gpt-6-sol` 只收 `low`/`medium`/`high`/`xhigh`/`max`,`minimal` 被拒);上下文与输出上限**继承同族已公布的兄弟模型**(`gpt-6-astra` / `gemini-3.5-flash`),因为网关对这些 id 不公布任何信息,而 `max_tokens` 也不受窗口约束(连 `gpt-6-astra` 自己都接受远超其公布上限的值),无从反推。三者仍可在设置页逐模型覆盖;判定一个 id 是否存在只有一条路——直接问:`code 11102` 是没有这个模型,`code 11133` 是模型存在但参数被拒;
156
157
  - 目录同时区分**默认服务的上下文长度**与**模型上限**(如 `deepseek-v4.1-flash` 默认 300K、最大 1M)。本线路不发送显式长度参数,所以 DSH 的压缩与溢出判断按**默认服务长度**计算,不会让请求越过后端实际接受的窗口;
157
158
  - 思考档位**逐模型**取目录声明的档位表,回落顺序为「调用方显式指定 → 用户配置的默认档 → 目录为该模型声明的默认档」;
158
159
  - **最后一档不能省**:上游在请求不带 `reasoning_effort` 时返回**空的 `reasoning_content`**(实测同一提示:不带字段 0 字符,带字段 130–215 字符)——不发送等于静默丢弃模型的思考;
@@ -585,6 +586,8 @@ DSH 设置页的「Subagent」卡片会把勾选的模型写成会话级的允
585
586
 
586
587
  `workbuddy-subscription` 会像其他 Provider 一样出现在 DSH 模型选择器中,并可与名为 `workbuddy` 的自定义 API 同时存在。**上下文窗口**默认取网关 `/v3/config` 声明的**默认服务长度**,可逐模型覆盖(用于 DSH 的压缩与溢出判断,支持 `1M` / `300K` / `200000` 等写法);**默认思考深度**逐模型生效,选项由当前账号各模型**实际声明的档位**取并集(仅部分模型支持的档位会标注数量),档位不在该模型集合内时不会被发送。
587
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
+
588
591
  **凭据目录**按平台解析,可用 `CODEBUDDY_AUTH_DIR` 覆盖(与官方工具链一致):
589
592
 
590
593
  | 平台 | 默认目录 |
package/lib/client.js CHANGED
@@ -5009,6 +5009,9 @@ window.__ModuleLoader__.load({
5009
5009
  unselectAll: "全不选",
5010
5010
  imageSupport: "图片",
5011
5011
  textOnly: "纯文本",
5012
+ creditMultiplier: "倍率",
5013
+ creditMultiplierRegion: "{value}({region})",
5014
+ creditMultiplierPair: "{first} / {second}",
5012
5015
  regionBadge: "区域",
5013
5016
  unavailableHere: "当前区域不可用",
5014
5017
  enhanced: "增强功能",
@@ -5092,6 +5095,9 @@ window.__ModuleLoader__.load({
5092
5095
  unselectAll: "Deselect all",
5093
5096
  imageSupport: "Images",
5094
5097
  textOnly: "Text only",
5098
+ creditMultiplier: "Multiplier",
5099
+ creditMultiplierRegion: "{value} ({region})",
5100
+ creditMultiplierPair: "{first} / {second}",
5095
5101
  regionBadge: "Regions",
5096
5102
  unavailableHere: "Unavailable in this region",
5097
5103
  enhanced: "Enhanced features",
@@ -5648,6 +5654,51 @@ window.__ModuleLoader__.load({
5648
5654
  xhigh: "X-High",
5649
5655
  max: "Max"
5650
5656
  };
5657
+ /**
5658
+ * The consumption rate of one model, as the settings row states it.
5659
+ *
5660
+ * The gateway publishes one rate per region and the official CodeBuddy picker
5661
+ * spells it beside a model as `6.67x`; the row uses the same shape behind the
5662
+ * section's own label. Which rates reach the row depends on what the card knows
5663
+ * about the account, and the two cases are deliberately different:
5664
+ *
5665
+ * - A KNOWN region shows that region's rate and nothing else. Borrowing the
5666
+ * sibling region's number would state a price this account never pays —
5667
+ * measured 2026-10-09, `deepseek-v4.1-flash` is `0.11` on cn against `0.00`
5668
+ * on intl, and the backends reject a model asked of the wrong region anyway.
5669
+ * - An UNKNOWN region (no account read back yet) shows both rates when the
5670
+ * backends disagree, so the row is honest about the range instead of picking
5671
+ * one of them. When they agree there is nothing to disambiguate.
5672
+ *
5673
+ * The two-region form is assembled entirely from the caller's labels: the
5674
+ * qualifier comes from `labels.region(value, key)` and the join from
5675
+ * `labels.pair(first, second)`, so the dictionary owns the region nouns, the
5676
+ * bracket glyphs AND the separator. Nothing about the two-region string is
5677
+ * spelled in this file, which is the point — the glyphs differ per language
5678
+ * (`倍率 0.11x(国区) / 0.00x(国际区)` in Chinese,
5679
+ * `Multiplier 0.11x (China) / 0.00x (International)` in English) and a
5680
+ * separator chosen here would be right in at most one of them.
5681
+ *
5682
+ * `null` means "this row states no rate"; the caller drops it from the hint
5683
+ * array rather than rendering a placeholder, because a guessed rate on a spend
5684
+ * figure is worse than a missing one. `'0.00'` is a declaration — this model
5685
+ * consumes no allowance — so it renders like any other value.
5686
+ */
5687
+ function creditMultiplierHint(credits, region, labels) {
5688
+ const rate = (key) => {
5689
+ const value = credits?.[key];
5690
+ return typeof value === "string" && value !== "" ? value : null;
5691
+ };
5692
+ if (region !== null) {
5693
+ const value = rate(region);
5694
+ return value === null ? null : `${labels.multiplier} ${value}x`;
5695
+ }
5696
+ const cn = rate("cn");
5697
+ const intl = rate("intl");
5698
+ if (cn === null) return intl === null ? null : `${labels.multiplier} ${intl}x`;
5699
+ if (intl === null || intl === cn) return `${labels.multiplier} ${cn}x`;
5700
+ return `${labels.multiplier} ${labels.pair(labels.region(`${cn}x`, "cn"), labels.region(`${intl}x`, "intl"))}`;
5701
+ }
5651
5702
  /** Mask a UIN so the card shows identity without publishing the full number. */
5652
5703
  function maskUin(uin) {
5653
5704
  if (uin === void 0 || uin === null || uin === "") return "—";
@@ -5847,16 +5898,24 @@ window.__ModuleLoader__.load({
5847
5898
  children: t.modelsHint
5848
5899
  }),
5849
5900
  /* @__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
- })),
5901
+ items: (status?.models ?? []).map((model) => {
5902
+ const multiplier = creditMultiplierHint(model.credits, status?.account?.region ?? null, {
5903
+ multiplier: t.creditMultiplier,
5904
+ region: (value, key) => t.creditMultiplierRegion.replace("{value}", value).replace("{region}", key === "cn" ? t.regionCn : t.regionIntl),
5905
+ pair: (first, second) => t.creditMultiplierPair.replace("{first}", first).replace("{second}", second)
5906
+ });
5907
+ return {
5908
+ id: model.id,
5909
+ name: model.name,
5910
+ hint: [
5911
+ formatCapacity(model.contextWindow),
5912
+ model.supportsImage ? t.imageSupport : t.textOnly,
5913
+ ...model.reasoningEfforts && model.reasoningEfforts.length > 0 ? [model.reasoningEfforts.join("/")] : [],
5914
+ ...multiplier === null ? [] : [multiplier]
5915
+ ].join(" · "),
5916
+ enabled: model.enabled
5917
+ };
5918
+ }),
5860
5919
  busy: busy !== null,
5861
5920
  onToggle: (id, enabled) => void toggleModel(id, enabled),
5862
5921
  onToggleAll: (enabled) => void setAllModels(enabled),