@eddyskywalker/dsh-chatgpt-subscription 0.14.0 → 0.15.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,30 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.15.0 - 2026-10-09
6
+
7
+ - **[WorkBuddy] 补上网关「在服务但从不公布」的模型,并按厂商归拢模型列表**
8
+ - **根因是一处反直觉的实测事实:`/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` 都会落到这一类)。
9
+ - **只加进内置表等于加了个看不见的模型。** 联网时实时目录会**整体替换**内置表,所以这三个 id 单列一份实测表(`UNPUBLISHED_MODELS`),并在**实时目录解析**与**快照反序列化**两处合并进去;已公布的条目永远优先,合并只填目录没提到的 id。国际区因此从 22 个变 25 个。
10
+ - **哪些字段是量出来的、哪些是继承的,界线写在代码里。** 存在性、思考档位、图片支持是**打接口测出来的**(`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` 自己都接受远超其公布上限的值),从请求侧无法反推。三者仍可在设置页逐模型覆盖。
11
+ - **同一份现场实读还修正了国际区三处数值**:`glm-5.3-flash` 国际区也开始服务(此前标为仅国区);`glm-5.3` / `glm-5.2` 国际区输出上限是 48000、`kimi-k2.8-preview` 是 32000。**一条表同时覆盖两区时取小值**:高报会让请求被网关拒绝,低报只是提前压缩,且仍可覆盖。
12
+ - **国区同步**:新增 `hy4-preview-f`、`space-bunny`,`hy4-preview-x` 已下线。
13
+ - **模型列表按厂商分组,不再让网关的排列顺序决定观感。** 网关给 `/v3/config` 的顺序是交错厂商的:一行 DeepSeek、一行 GPT-6-Astra、两行混元、一行 Kimi、再三行 GPT——八个 OpenAI 模型在设置页里从不挨在一起。现在按厂商族分组,族内**新代在前**。
14
+ - **组内排序按版本号比较,而不是照抄目录序。** 这是被实测逼出来的:只做分组时「在服务但不公布」的模型因为追加在已公布列表之后,`gpt-6-sol` 会渲染到 `gpt-5.4` **下面**——最新的沉底。改成比较 id 里的数字段后归位,且数字按**数值**比(`gpt-6.10` 在 `gpt-6.6` 之前),同版本保持目录序(`-flash` 仍领着 `-flash-sg`),读不出版本的 id(`primary-model` 这类别名)保持原位而不瞎猜。
15
+ - **分组放在 host 而不是设置页。** `modelsForRegion` 同样是请求路径解析模型的入口,在那儿排一次,picker 与 adapter 不可能出现两套顺序;客户端一行未改。
16
+ - 厂商归属有个坑:`hy3` / `hy4-preview` / `hunyuan-chat` 是腾讯混元的三种写法,必须归到同一组;没有任何规则命中的 id **自成一族**,而不是被扫进一个共享的 `other` 桶。
17
+ - 测试:新增分组与排序断言(族必须连续、组内新代在前、数值比较、同版本保持目录序),并扩充三个既有目录测试。
18
+ - 验证:`npm run typecheck` 0 错误;全量 `npm test` **2886 passed / 7 skipped**。另加两个一次性探针(`scripts/probe-workbuddy-catalog.probe.ts` 拉实时目录做差异、`scripts/probe-workbuddy-unlisted.probe.ts` 给目录外的 id 测档位/图片/输出上限),本条的每一项数值都是它们的输出。
19
+
20
+ - **[思维保护] 推理坍缩守卫补上第二条独立信号:改写型死循环**
21
+ - **原守卫对「换个说法反复说同一件事」是瞎的。** 唯一率信号只看得见**逐字**重复;一段把同一结论用不同措辞重述的流实测只有 0.48,而阈值是 0.85,于是一路跑到输出上限也没人拦。这不是理论问题——归档样本 `paraphrased-loop-reasoning.txt` 就是这种形态。
22
+ - **第二条信号量的是「复述」而非「新颖」。** 把窗口切成句子,逐句与至少 `semanticLag` 句之前的内容做 3-gram Jaccard 相似度并取最大值。**枚举式推理也会重复,但它是相邻重复**——每一项借用上一项的句式;复述信号**忽略相邻性**,这正是区分两者的关键,所以健康的枚举流不会被误伤。
23
+ - **两条路径,低门槛那条在每一步都更严。** 高门槛路径(原规则,未改动)要求唯一率 ≥ 0.85 且复述 ≥ 0.35;低门槛路径(新)允许唯一率低到 0.6,代价是复述门槛抬到 0.45、**外加一道高路径不需要的新颖度上限**(不同句比例 ≤ 0.47),且需要更多次确认(5 次 vs 3 次)。**用更弱的证据换更多、更长的确认。**
24
+ - **确认是连续窗口计数,不是单窗口触发。** 两个信号必须在同一窗口同时过线,且连续 `semanticConfirmations` 次才动手;只有一方过线、或只有单个异常窗口,一律**只记日志不动作**。断流后连击归零,下一次尝试不会继承它没挣到的确认数。
25
+ - **近失也留痕。** 单信号命中有专门的 alone 日志(每种每轮只报一次,避免数百行刷屏),连击在够数前断掉也有 cleared 日志——否则两段短连击在日志里与一段长连击长得一模一样,确认数事后无从推理。
26
+ - 测试:守卫测试 **39 → 80 例**(源文件 480 → 948 行),新增归档样本 fixture 与真实的改写型坍缩样本。
27
+ - 验证:全量 `npm test` 2886 passed / 7 skipped;守卫专项 80 例全通过。
28
+
5
29
  ## 0.14.0 - 2026-10-08
6
30
 
7
31
  - **[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 字符)——不发送等于静默丢弃模型的思考;
package/lib/index.js CHANGED
@@ -30276,6 +30276,94 @@ function convergeWorkBuddyEffort(effort, ladder) {
30276
30276
  //#endregion
30277
30277
  //#region src/host/workbuddy/model-catalog.ts
30278
30278
  /**
30279
+ * Models the gateway serves but does not publish in `/v3/config`.
30280
+ *
30281
+ * The catalog is a published list, not the served one: measured on the
30282
+ * international backend, `gpt-6-sol`, `gpt-6-luna` and `gemini-3.8-flash` all
30283
+ * answer a normal streaming completion with 200 while being absent from
30284
+ * `/v3/config` entirely, and a reachable gateway *replaces* the shipped table
30285
+ * with that list. Without these rows the picker would simply never show them, so
30286
+ * they are declared here and merged into a live catalog too
30287
+ * ({@link withUnpublishedModels}) rather than living in the fallback alone.
30288
+ *
30289
+ * What each row records, and how it was established, differs by field:
30290
+ *
30291
+ * - **Existence, the reasoning ladder, image support** were measured against the
30292
+ * live endpoint, not inferred from the name. A rejected level answers 400
30293
+ * `code 11133` with `extError` `400002`; `gpt-6-sol` takes exactly the ladder
30294
+ * `gpt-6-astra` publishes (`minimal` refused, `low`/`medium`/`high`/`xhigh`/
30295
+ * `max` accepted) and both accept a 1x1 PNG.
30296
+ * - **Context window, output cap and `canDisableThinking` are inherited from the
30297
+ * published sibling in the same family** — `gpt-6-astra` for `gpt-6-sol` and
30298
+ * `gpt-6-luna`, `gemini-3.5-flash` for `gemini-3.8-flash`. They are marked
30299
+ * here rather than measured because the gateway publishes nothing for these
30300
+ * ids and offers no cheap way to read the real figure: it accepts a
30301
+ * `max_tokens` larger than any published window (even for `gpt-6-astra`, whose
30302
+ * own catalog caps at 128000), so the number is neither enforced nor
30303
+ * discoverable from the request side.
30304
+ *
30305
+ * Every field stays overridable per model from the settings card, and an
30306
+ * inherited window errs low rather than high: over-declaring overflows the
30307
+ * gateway mid-session, while under-declaring only compresses earlier.
30308
+ */
30309
+ const UNPUBLISHED_MODELS = [
30310
+ {
30311
+ id: "gpt-6-sol",
30312
+ name: "GPT-6-Sol",
30313
+ contextWindow: 4e5,
30314
+ maxContextWindow: 1e6,
30315
+ maxTokens: 128e3,
30316
+ regions: ["intl"],
30317
+ supportsImage: true,
30318
+ reasoningEfforts: [
30319
+ "low",
30320
+ "medium",
30321
+ "high",
30322
+ "xhigh",
30323
+ "max"
30324
+ ],
30325
+ defaultReasoningEffort: "high",
30326
+ canDisableThinking: true,
30327
+ description: "OpenAI 旗舰模型,擅长复杂推理与长程任务"
30328
+ },
30329
+ {
30330
+ id: "gpt-6-luna",
30331
+ name: "GPT-6-Luna",
30332
+ contextWindow: 4e5,
30333
+ maxContextWindow: 1e6,
30334
+ maxTokens: 128e3,
30335
+ regions: ["intl"],
30336
+ supportsImage: true,
30337
+ reasoningEfforts: [
30338
+ "low",
30339
+ "medium",
30340
+ "high",
30341
+ "xhigh",
30342
+ "max"
30343
+ ],
30344
+ defaultReasoningEffort: "high",
30345
+ canDisableThinking: true,
30346
+ description: "OpenAI 轻量模型,响应快速,适合日常任务"
30347
+ },
30348
+ {
30349
+ id: "gemini-3.8-flash",
30350
+ name: "Gemini-3.8-Flash",
30351
+ contextWindow: 1e6,
30352
+ maxContextWindow: 1e6,
30353
+ maxTokens: 65536,
30354
+ regions: ["intl"],
30355
+ supportsImage: true,
30356
+ reasoningEfforts: [
30357
+ "low",
30358
+ "high",
30359
+ "max"
30360
+ ],
30361
+ defaultReasoningEffort: "medium",
30362
+ canDisableThinking: false,
30363
+ description: "能力均衡,适合日常使用"
30364
+ }
30365
+ ];
30366
+ /**
30279
30367
  * Offline fallback catalog, transcribed from a live `/v3/config` read.
30280
30368
  *
30281
30369
  * Used when the gateway cannot be reached (first run before sign-in, or a
@@ -30372,6 +30460,7 @@ const FALLBACK_MODELS$1 = [
30372
30460
  canDisableThinking: false,
30373
30461
  description: "深度推理,适合深度分析与难题"
30374
30462
  },
30463
+ ...UNPUBLISHED_MODELS.filter((model) => model.id === "gpt-6-sol" || model.id === "gpt-6-luna"),
30375
30464
  {
30376
30465
  id: "gpt-6-astra",
30377
30466
  name: "GPT-6-Astra",
@@ -30489,7 +30578,7 @@ const FALLBACK_MODELS$1 = [
30489
30578
  name: "GLM-5.3",
30490
30579
  contextWindow: 1e6,
30491
30580
  maxContextWindow: 1e6,
30492
- maxTokens: 64e3,
30581
+ maxTokens: 48e3,
30493
30582
  regions: ["intl", "cn"],
30494
30583
  supportsImage: true,
30495
30584
  reasoningEfforts: [
@@ -30504,10 +30593,10 @@ const FALLBACK_MODELS$1 = [
30504
30593
  {
30505
30594
  id: "glm-5.3-flash",
30506
30595
  name: "GLM-5.3-Flash",
30507
- contextWindow: 1e6,
30596
+ contextWindow: 3e5,
30508
30597
  maxContextWindow: 1e6,
30509
30598
  maxTokens: 32e3,
30510
- regions: ["cn"],
30599
+ regions: ["intl", "cn"],
30511
30600
  supportsImage: true,
30512
30601
  reasoningEfforts: [
30513
30602
  "low",
@@ -30523,7 +30612,7 @@ const FALLBACK_MODELS$1 = [
30523
30612
  name: "GLM-5.2",
30524
30613
  contextWindow: 1e6,
30525
30614
  maxContextWindow: 1e6,
30526
- maxTokens: 64e3,
30615
+ maxTokens: 48e3,
30527
30616
  regions: ["intl", "cn"],
30528
30617
  supportsImage: true,
30529
30618
  reasoningEfforts: ["high", "xhigh"],
@@ -30677,7 +30766,7 @@ const FALLBACK_MODELS$1 = [
30677
30766
  name: "Kimi-K2.8-Preview",
30678
30767
  contextWindow: 3e5,
30679
30768
  maxContextWindow: 1e6,
30680
- maxTokens: 64e3,
30769
+ maxTokens: 32e3,
30681
30770
  regions: ["intl", "cn"],
30682
30771
  supportsImage: true,
30683
30772
  reasoningEfforts: [
@@ -30856,7 +30945,7 @@ const FALLBACK_MODELS$1 = [
30856
30945
  description: "混元思考模型,具有增强的推理能力"
30857
30946
  },
30858
30947
  {
30859
- id: "hy4-preview-x",
30948
+ id: "hy4-preview-f",
30860
30949
  name: "Hy4 preview",
30861
30950
  contextWindow: 1e6,
30862
30951
  maxContextWindow: 1e6,
@@ -30911,6 +31000,25 @@ const FALLBACK_MODELS$1 = [
30911
31000
  canDisableThinking: false,
30912
31001
  description: ""
30913
31002
  },
31003
+ {
31004
+ id: "space-bunny",
31005
+ name: "Space-Bunny",
31006
+ contextWindow: 1e6,
31007
+ maxContextWindow: 1e6,
31008
+ maxTokens: 128e3,
31009
+ regions: ["cn"],
31010
+ supportsImage: true,
31011
+ reasoningEfforts: [
31012
+ "low",
31013
+ "medium",
31014
+ "high",
31015
+ "xhigh",
31016
+ "max"
31017
+ ],
31018
+ defaultReasoningEffort: "max",
31019
+ canDisableThinking: false,
31020
+ description: "推理速度极快,编码能力强劲,并支持原生多模态输入的匿名大模型"
31021
+ },
30914
31022
  {
30915
31023
  id: "minimax-m3",
30916
31024
  name: "MiniMax-M3",
@@ -30962,6 +31070,7 @@ const FALLBACK_MODELS$1 = [
30962
31070
  canDisableThinking: false,
30963
31071
  description: ""
30964
31072
  },
31073
+ ...UNPUBLISHED_MODELS.filter((model) => model.id === "gemini-3.8-flash"),
30965
31074
  {
30966
31075
  id: "gemini-3.5-flash",
30967
31076
  name: "Gemini-3.5-Flash",
@@ -30997,6 +31106,21 @@ const DEFAULT_VISIBLE_MODEL_IDS$1 = [
30997
31106
  /** Every id the fallback knows, in declaration order. */
30998
31107
  const WORKBUDDY_MODEL_IDS = FALLBACK_MODELS$1.map((model) => model.id);
30999
31108
  /**
31109
+ * Add the served-but-unpublished models to a live `/v3/config` catalog.
31110
+ *
31111
+ * A reachable gateway replaces the shipped table with the list it publishes, so
31112
+ * a model it serves without listing ({@link UNPUBLISHED_MODELS}) would drop out
31113
+ * of the picker the moment the catalog loads. Merging here keeps it offered.
31114
+ *
31115
+ * A published entry always wins: the gateway is the authority on anything it
31116
+ * does describe, so these rows only fill ids the payload is silent about.
31117
+ */
31118
+ function withUnpublishedModels(catalog, region) {
31119
+ const known = new Set(catalog.map((model) => model.id));
31120
+ const extra = UNPUBLISHED_MODELS.filter((model) => model.regions.includes(region) && !known.has(model.id));
31121
+ return extra.length > 0 ? [...catalog, ...extra] : [...catalog];
31122
+ }
31123
+ /**
31000
31124
  * Resolve one entry from a catalog.
31001
31125
  *
31002
31126
  * An unknown id resolves to a synthetic text-only entry rather than the first
@@ -31028,9 +31152,131 @@ function resolveWorkBuddyModel(id, catalog = FALLBACK_MODELS$1) {
31028
31152
  description: ""
31029
31153
  };
31030
31154
  }
31031
- /** Models an account in `region` can actually call, in catalog order. */
31155
+ /**
31156
+ * Vendor families, resolved from a model id.
31157
+ *
31158
+ * The gateway orders `/v3/config` the way it feels like, which interleaves
31159
+ * vendors: a DeepSeek row, then a GPT row, then two Hunyuan rows, then a Kimi
31160
+ * row, then three more GPT rows. In a picker that reads as noise — the eye
31161
+ * cannot tell that the GPT entries belong together — so the list is grouped by
31162
+ * family before it is shown.
31163
+ *
31164
+ * The rules are ordered, first match wins. Only families this subscription
31165
+ * actually serves are listed: a rule for a model that cannot appear would be a
31166
+ * guess about a future catalog.
31167
+ *
31168
+ * An id no rule claims is its own family rather than a shared `other` bucket, so
31169
+ * an unrecognised model keeps its place in the order instead of being swept into
31170
+ * a pile of models it has nothing to do with.
31171
+ */
31172
+ const WORKBUDDY_VENDOR_RULES = [
31173
+ {
31174
+ re: /^(default|fast|balanced|primary|deep)(-model)?$/,
31175
+ family: "alias"
31176
+ },
31177
+ {
31178
+ re: /^gpt-/,
31179
+ family: "openai"
31180
+ },
31181
+ {
31182
+ re: /^gemini-/,
31183
+ family: "google"
31184
+ },
31185
+ {
31186
+ re: /^glm-/,
31187
+ family: "zhipu"
31188
+ },
31189
+ {
31190
+ re: /^kimi-/,
31191
+ family: "moonshot"
31192
+ },
31193
+ {
31194
+ re: /^deepseek-/,
31195
+ family: "deepseek"
31196
+ },
31197
+ {
31198
+ re: /^hy[0-9]/,
31199
+ family: "tencent"
31200
+ },
31201
+ {
31202
+ re: /^hunyuan/,
31203
+ family: "tencent"
31204
+ },
31205
+ {
31206
+ re: /^minimax-/,
31207
+ family: "minimax"
31208
+ },
31209
+ {
31210
+ re: /^space-bunny$/,
31211
+ family: "space-bunny"
31212
+ }
31213
+ ];
31214
+ /** The vendor family one model belongs to. */
31215
+ function workBuddyModelVendor(id) {
31216
+ const value = id.toLowerCase();
31217
+ for (const rule of WORKBUDDY_VENDOR_RULES) if (rule.re.test(value)) return rule.family;
31218
+ return id;
31219
+ }
31220
+ /**
31221
+ * Numeric segments of a model id, e.g. `gpt-6-astra` → [6].
31222
+ *
31223
+ * Version numbers are compared as numbers, so GPT-6.0 sorts below GPT-6.6 rather
31224
+ * than above it as it would if the strings were compared.
31225
+ */
31226
+ function versionSegments$1(id) {
31227
+ return [...id.toLowerCase().matchAll(/\d+/g)].map((match) => Number(match[0]));
31228
+ }
31229
+ /**
31230
+ * Order two models of one family, newest generation first.
31231
+ *
31232
+ * Catalog order alone is not good enough. The models served but not published
31233
+ * are appended after the published list, which left `gpt-6-sol` below
31234
+ * `gpt-5.4` — the newest models at the bottom of their own group. Comparing the
31235
+ * numeric segments puts every GPT-6 row together and ahead of every GPT-5 one,
31236
+ * and leaves an id whose version cannot be read (an alias, a nickname) in the
31237
+ * catalog's own order rather than guessing where it belongs.
31238
+ *
31239
+ * Ties keep catalog order, so `deepseek-v4.1-flash` still leads its
31240
+ * `-flash-sg` sibling.
31241
+ */
31242
+ function compareWithinFamily(a, b) {
31243
+ const left = versionSegments$1(a.id);
31244
+ const right = versionSegments$1(b.id);
31245
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
31246
+ const x = left[index];
31247
+ const y = right[index];
31248
+ if (x === void 0) return 1;
31249
+ if (y === void 0) return -1;
31250
+ if (x !== y) return y - x;
31251
+ }
31252
+ return 0;
31253
+ }
31254
+ /**
31255
+ * Group a list so every vendor's models sit together.
31256
+ *
31257
+ * Families appear in the order their first model does — the catalog decides
31258
+ * which vendor leads, not this function — and each family is ordered newest
31259
+ * generation first.
31260
+ */
31261
+ function groupWorkBuddyModelsByVendor(models) {
31262
+ const families = /* @__PURE__ */ new Map();
31263
+ for (const model of models) {
31264
+ const family = workBuddyModelVendor(model.id);
31265
+ const bucket = families.get(family);
31266
+ if (bucket === void 0) families.set(family, [model]);
31267
+ else bucket.push(model);
31268
+ }
31269
+ return [...families.values()].map((bucket) => [...bucket].sort(compareWithinFamily)).flat();
31270
+ }
31271
+ /**
31272
+ * Models an account in `region` can actually call, grouped by vendor.
31273
+ *
31274
+ * Grouped here rather than in the settings card because this is also what the
31275
+ * request path resolves against: one ordering rule, so the picker and the
31276
+ * adapter can never disagree about which models a region offers.
31277
+ */
31032
31278
  function modelsForRegion(region, catalog = FALLBACK_MODELS$1) {
31033
- return catalog.filter((model) => model.regions.includes(region));
31279
+ return groupWorkBuddyModelsByVendor(catalog.filter((model) => model.regions.includes(region)));
31034
31280
  }
31035
31281
  /** Whether one model accepts image input. */
31036
31282
  function workBuddyModelSupportsImage(id, catalog = FALLBACK_MODELS$1) {
@@ -31980,7 +32226,7 @@ async function fetchConfigCatalog(credentials, options = {}) {
31980
32226
  signal: timeoutSignal$1(options.signal, DISCOVERY_TIMEOUT_MS$1)
31981
32227
  });
31982
32228
  if (!response.ok) throw new Error(`${PROVIDER_NAME$1} model catalog failed: ${response.status}`);
31983
- return parseConfigModels(await response.json(), credentials.region);
32229
+ return withUnpublishedModels(parseConfigModels(await response.json(), credentials.region), credentials.region);
31984
32230
  }
31985
32231
  /** Cached catalog with a TTL; a failed refresh keeps the previous snapshot.
31986
32232
  *
@@ -32014,7 +32260,7 @@ async function loadConfigCatalog(credentials, options = {}) {
32014
32260
  }, (fetchedAt, models) => {
32015
32261
  cachedCatalog = {
32016
32262
  region: credentials.region,
32017
- models,
32263
+ models: withUnpublishedModels(models, credentials.region),
32018
32264
  fetchedAt
32019
32265
  };
32020
32266
  }, 0) > 0) {
@@ -32540,7 +32786,13 @@ function resolveEnabledModelIds$2(stored, available, enabled = true) {
32540
32786
  const known = new Set(available);
32541
32787
  return stored.filter((id) => known.has(id));
32542
32788
  }
32543
- /** Build the model options the settings card renders, in catalog order. */
32789
+ /**
32790
+ * Build the model options the settings card renders.
32791
+ *
32792
+ * Order comes from the caller, which passes a vendor-grouped list so a family
32793
+ * is not interleaved with the next one; this preserves whatever order it is
32794
+ * given rather than imposing a second one.
32795
+ */
32544
32796
  function buildModelOptions(catalog, available, enabledModelIds, overrides) {
32545
32797
  const enabled = new Set(enabledModelIds);
32546
32798
  return available.map((id) => {
@@ -45550,6 +45802,22 @@ function installRelayProbe(ctx, options) {
45550
45802
  * degenerate stream scores near one. On a hit it stops yielding chunks, so the
45551
45803
  * in-flight request ends, and lets the turn resume on a fresh step.
45552
45804
  *
45805
+ * Uniqueness alone only sees verbatim repetition. A stream can restate one
45806
+ * conclusion over and over in fresh words, score 0.48 where the threshold is
45807
+ * 0.85, and run to the output ceiling untouched. So a second, independent
45808
+ * signal scores the same window for recurrence: how strongly each sentence
45809
+ * repeats something the model already said at least `semanticLag` sentences
45810
+ * earlier. Enumerative reasoning repeats itself too, but adjacently — each item
45811
+ * borrows the previous item's frame — and recurrence ignores adjacency, which
45812
+ * is what separates the two.
45813
+ *
45814
+ * Neither signal stops anything on its own. A break needs both over their
45815
+ * thresholds in the same window, and then `semanticConfirmations` consecutive
45816
+ * such windows: one signal alone, or a single anomalous window, is logged and
45817
+ * released. That gradient is the whole design. A guard that cuts a healthy
45818
+ * answer is worse than one that lets a runaway finish, because the user sees a
45819
+ * turn stop mid-sentence with no explanation, and the fix costs a whole turn.
45820
+ *
45553
45821
  * Seam choice: the guard wraps `llm/stream`, not `agent/assistant-stream`.
45554
45822
  * `agent/assistant-stream` is emit-mode — it can observe but cannot stop a
45555
45823
  * stream — and it does not exist before harness 0.1.5, which would make the
@@ -45586,6 +45854,15 @@ const DEFAULT_GUARD_OPTIONS = {
45586
45854
  maxBreaksPerTurn: 3,
45587
45855
  cooldownMs: 3e4,
45588
45856
  includeModels: [],
45857
+ semanticThreshold: .35,
45858
+ semanticLag: 6,
45859
+ semanticMinChars: 360,
45860
+ semanticMinSentences: 8,
45861
+ semanticConfirmations: 3,
45862
+ semanticLiteralFloor: .6,
45863
+ semanticStrictThreshold: .45,
45864
+ semanticDistinctCeiling: .47,
45865
+ semanticStrictConfirmations: 5,
45589
45866
  now: () => Date.now()
45590
45867
  };
45591
45868
  function pruneUndefined(options) {
@@ -45611,8 +45888,21 @@ function resolveGuardOptions(options = {}) {
45611
45888
  if (!Number.isFinite(resolved.threshold) || resolved.threshold <= 0 || resolved.threshold >= 1) throw new Error(`reasoning-collapse-guard: invalid threshold ${String(resolved.threshold)} — must be strictly between 0 and 1`);
45612
45889
  if (!Number.isInteger(resolved.maxBreaksPerTurn) || resolved.maxBreaksPerTurn < 1) throw new Error(`reasoning-collapse-guard: invalid maxBreaksPerTurn ${String(resolved.maxBreaksPerTurn)} — must be an integer >= 1`);
45613
45890
  if (!Number.isInteger(resolved.cooldownMs) || resolved.cooldownMs < 0) throw new Error(`reasoning-collapse-guard: invalid cooldownMs ${String(resolved.cooldownMs)} — must be an integer >= 0`);
45891
+ if (!Number.isFinite(resolved.semanticThreshold) || resolved.semanticThreshold <= 0 || resolved.semanticThreshold >= 1) throw new Error(`reasoning-collapse-guard: invalid semanticThreshold ${String(resolved.semanticThreshold)} — must be strictly between 0 and 1`);
45892
+ if (!Number.isInteger(resolved.semanticLag) || resolved.semanticLag < 1) throw new Error(`reasoning-collapse-guard: invalid semanticLag ${String(resolved.semanticLag)} — must be an integer >= 1`);
45893
+ if (!Number.isInteger(resolved.semanticMinChars) || resolved.semanticMinChars < 1) throw new Error(`reasoning-collapse-guard: invalid semanticMinChars ${String(resolved.semanticMinChars)} — must be an integer >= 1`);
45894
+ if (resolved.semanticMinChars > resolved.windowChars) throw new Error(`reasoning-collapse-guard: invalid semanticMinChars ${resolved.semanticMinChars} — must not exceed windowChars ${resolved.windowChars}`);
45895
+ if (!Number.isInteger(resolved.semanticMinSentences) || resolved.semanticMinSentences < 2) throw new Error(`reasoning-collapse-guard: invalid semanticMinSentences ${String(resolved.semanticMinSentences)} — must be an integer >= 2`);
45896
+ if (!Number.isInteger(resolved.semanticConfirmations) || resolved.semanticConfirmations < 1) throw new Error(`reasoning-collapse-guard: invalid semanticConfirmations ${String(resolved.semanticConfirmations)} — must be an integer >= 1`);
45897
+ if (!Number.isFinite(resolved.semanticLiteralFloor) || resolved.semanticLiteralFloor <= 0 || resolved.semanticLiteralFloor >= 1) throw new Error(`reasoning-collapse-guard: invalid semanticLiteralFloor ${String(resolved.semanticLiteralFloor)} — must be strictly between 0 and 1`);
45898
+ if (!Number.isFinite(resolved.semanticStrictThreshold) || resolved.semanticStrictThreshold <= 0 || resolved.semanticStrictThreshold >= 1) throw new Error(`reasoning-collapse-guard: invalid semanticStrictThreshold ${String(resolved.semanticStrictThreshold)} — must be strictly between 0 and 1`);
45899
+ if (!Number.isFinite(resolved.semanticDistinctCeiling) || resolved.semanticDistinctCeiling <= 0 || resolved.semanticDistinctCeiling >= 1) throw new Error(`reasoning-collapse-guard: invalid semanticDistinctCeiling ${String(resolved.semanticDistinctCeiling)} — must be strictly between 0 and 1`);
45900
+ if (!Number.isInteger(resolved.semanticStrictConfirmations) || resolved.semanticStrictConfirmations < 1) throw new Error(`reasoning-collapse-guard: invalid semanticStrictConfirmations ${String(resolved.semanticStrictConfirmations)} — must be an integer >= 1`);
45614
45901
  if (!Array.isArray(resolved.includeModels) || resolved.includeModels.some((entry) => typeof entry !== "string")) throw new Error("reasoning-collapse-guard: includeModels must be an array of strings");
45615
45902
  if (typeof resolved.now !== "function") throw new Error("reasoning-collapse-guard: now must be a function");
45903
+ resolved.semanticStrictThreshold = Math.max(resolved.semanticStrictThreshold, resolved.semanticThreshold);
45904
+ resolved.semanticStrictConfirmations = Math.max(resolved.semanticStrictConfirmations, resolved.semanticConfirmations + 1);
45905
+ resolved.semanticLiteralFloor = Math.min(resolved.semanticLiteralFloor, resolved.threshold);
45616
45906
  return resolved;
45617
45907
  }
45618
45908
  /**
@@ -45635,6 +45925,92 @@ function collapseScore(window, n) {
45635
45925
  if (total === 0) return 0;
45636
45926
  return 1 - grams.size / total;
45637
45927
  }
45928
+ /**
45929
+ * Split reasoning text into sentences, for mixed CJK and Latin streams.
45930
+ *
45931
+ * A Chinese full stop ends a sentence whatever follows it. An ASCII `.!?` only
45932
+ * ends one when whitespace or the end of the text follows, because the same
45933
+ * characters carry out decimals, file names and version numbers
45934
+ * (`selectors.test.ts:745`, `3.14`) that must not become sentence
45935
+ * edges. Whitespace runs collapse and case folds, so a difference in spacing
45936
+ * or capitalisation cannot read as a difference in wording.
45937
+ *
45938
+ * @param text - reasoning text, typically one trailing window.
45939
+ * @returns non-empty normalized sentences, in order.
45940
+ */
45941
+ function splitSentences(text) {
45942
+ const parts = [];
45943
+ let start = 0;
45944
+ for (let i = 0; i < text.length; i++) {
45945
+ const char = text[i];
45946
+ const next = text[i + 1];
45947
+ const cjk = CJK_TERMINATORS.has(char);
45948
+ const latin = LATIN_TERMINATORS.has(char) && (next === void 0 || TRAILING_SPACE.test(next));
45949
+ if (!cjk && !latin) continue;
45950
+ parts.push(text.slice(start, i + 1));
45951
+ start = i + 1;
45952
+ }
45953
+ parts.push(text.slice(start));
45954
+ const sentences = [];
45955
+ for (const part of parts) {
45956
+ const normalized = part.replace(WHITESPACE_RUN, " ").trim().toLowerCase();
45957
+ if (normalized.length > 0) sentences.push(normalized);
45958
+ }
45959
+ return sentences;
45960
+ }
45961
+ /** The three Chinese sentence terminators, which need no trailing space. */
45962
+ const CJK_TERMINATORS = /* @__PURE__ */ new Set([
45963
+ "。",
45964
+ "!",
45965
+ "?"
45966
+ ]);
45967
+ /** The three ASCII terminators, which only close a sentence before whitespace. */
45968
+ const LATIN_TERMINATORS = /* @__PURE__ */ new Set([
45969
+ ".",
45970
+ "!",
45971
+ "?"
45972
+ ]);
45973
+ const TRAILING_SPACE = /\s/;
45974
+ const WHITESPACE_RUN = /\s+/g;
45975
+ /** Character n-grams of one normalized sentence, as a set. */
45976
+ function sentenceGrams(sentence) {
45977
+ const grams = /* @__PURE__ */ new Set();
45978
+ for (let i = 0; i + SEMANTIC_NGRAM_SIZE <= sentence.length; i++) grams.add(sentence.slice(i, i + SEMANTIC_NGRAM_SIZE));
45979
+ return grams;
45980
+ }
45981
+ function gramJaccard(a, b) {
45982
+ if (a.size === 0 || b.size === 0) return 0;
45983
+ let shared = 0;
45984
+ for (const gram of a) if (b.has(gram)) shared++;
45985
+ return shared / (a.size + b.size - shared);
45986
+ }
45987
+ /** {@link semanticRecurrence} over sentences the caller has already split. */
45988
+ function sentenceRecurrence(sentences, lag) {
45989
+ const grams = sentences.map(sentenceGrams);
45990
+ if (grams.length <= lag) return 0;
45991
+ let total = 0;
45992
+ let counted = 0;
45993
+ for (let i = lag; i < grams.length; i++) {
45994
+ const current = grams[i];
45995
+ let best = 0;
45996
+ for (let j = 0; j <= i - lag; j++) {
45997
+ const earlier = grams[j];
45998
+ if (Math.min(current.size, earlier.size) / Math.max(current.size, earlier.size) <= best) continue;
45999
+ const similarity = gramJaccard(current, earlier);
46000
+ if (similarity > best) best = similarity;
46001
+ }
46002
+ total += best;
46003
+ counted++;
46004
+ }
46005
+ return counted === 0 ? 0 : total / counted;
46006
+ }
46007
+ /** Character n-gram size both halves of the semantic signal are built on. */
46008
+ const SEMANTIC_NGRAM_SIZE = 3;
46009
+ /** {@link distinctSentenceRatio} over sentences the caller already split. */
46010
+ function distinctRatioOf(sentences) {
46011
+ if (sentences.length < 2) return 1;
46012
+ return new Set(sentences).size / sentences.length;
46013
+ }
45638
46014
  const RESUME_HINT = "Your previous reasoning became repetitive and was stopped before it finished. Resume from what you already established instead of restarting the analysis, and keep the rest of this response short.";
45639
46015
  /** The second resume message, used once a resume has also collapsed. */
45640
46016
  const RESUME_HINT_STRICT = "Your reasoning has now repeated across two attempts. Do not re-derive the analysis a third time. Either state the conclusion you already reached, or make the single next tool call that moves the task forward. Keep this response to a few sentences.";
@@ -45643,7 +46019,11 @@ function createBreakerState() {
45643
46019
  breaksThisTurn: 0,
45644
46020
  lastBreakAt: -Infinity,
45645
46021
  exhausted: false,
45646
- resuming: false
46022
+ resuming: false,
46023
+ dualStreak: 0,
46024
+ lowStreak: 0,
46025
+ notedLiteralOnly: false,
46026
+ notedSemanticOnly: false
45647
46027
  };
45648
46028
  }
45649
46029
  /**
@@ -45798,10 +46178,12 @@ function installReasoningCollapseGuard(ctx, options = {}) {
45798
46178
  * point — and the log says so, because a break that cannot resume is
45799
46179
  * indistinguishable from a broken turn to the user.
45800
46180
  */
45801
- function breakOff(state, score, agent, sessionId) {
46181
+ function breakOff(state, literal, semantic, path, agent, sessionId) {
45802
46182
  state.breaksThisTurn++;
45803
46183
  state.lastBreakAt = resolved.now();
45804
- ctx.logger?.warn(`reasoning-collapse-guard: stopped degenerate reasoning (score ${score.toFixed(3)}, break ${state.breaksThisTurn}/${resolved.maxBreaksPerTurn}, session ${sessionId ?? "unidentified"}${agent === void 0 ? ", no agent to resume" : ""})`);
46184
+ state.dualStreak = 0;
46185
+ state.lowStreak = 0;
46186
+ ctx.logger?.warn(`reasoning-collapse-guard: stopped degenerate reasoning on the ${path} path after ${path === "high-literal" ? resolved.semanticConfirmations : resolved.semanticStrictConfirmations} confirmed windows (literal ${literal.toFixed(3)}, semantic ${semantic.toFixed(3)}, break ${state.breaksThisTurn}/${resolved.maxBreaksPerTurn}, session ${sessionId ?? "unidentified"}${agent === void 0 ? ", no agent to resume" : ""})`);
45805
46187
  if (agent === void 0) return;
45806
46188
  agent.cancel?.({
45807
46189
  kind: "hook",
@@ -45819,6 +46201,48 @@ function installReasoningCollapseGuard(ctx, options = {}) {
45819
46201
  }
45820
46202
  });
45821
46203
  }
46204
+ /**
46205
+ * Report one signal holding without the other.
46206
+ *
46207
+ * Reported once per kind per turn, not once per window: a runaway can hold
46208
+ * one signal for hundreds of windows, and a line each would bury the report
46209
+ * of the one that mattered. This is the diagnostic half of the gradient —
46210
+ * the reason a near miss is still diagnosable after the fact.
46211
+ */
46212
+ function noteLoneSignal(state, kind, literal, semantic, sessionId) {
46213
+ if (kind === "literal" ? state.notedLiteralOnly : state.notedSemanticOnly) return;
46214
+ if (kind === "literal") state.notedLiteralOnly = true;
46215
+ else state.notedSemanticOnly = true;
46216
+ ctx.logger?.warn(`reasoning-collapse-guard: observed the ${kind} signal alone (literal ${literal === void 0 ? "not scored" : literal.toFixed(3)}, semantic ${semantic === void 0 ? "not scored" : semantic.toFixed(3)}, session ${sessionId ?? "unidentified"}) — the other signal has not agreed, not acting`);
46217
+ }
46218
+ /**
46219
+ * Report a run of agreeing windows that ended before it reached the count.
46220
+ *
46221
+ * Without this, two short runs of agreeing windows look in the log exactly
46222
+ * like one long one that simply stopped short, and the confirmation count
46223
+ * becomes impossible to reason about after the fact.
46224
+ */
46225
+ function noteStreakCleared(state, reached, sessionId) {
46226
+ ctx.logger?.warn(`reasoning-collapse-guard: the signals stopped agreeing after ${reached} of ${resolved.semanticConfirmations} windows (session ${sessionId ?? "unidentified"}) — streak cleared, not acting`);
46227
+ }
46228
+ /** Report the low-literal run that ended before it reached its longer count. */
46229
+ function noteLowStreakCleared(reached, sessionId) {
46230
+ ctx.logger?.warn(`reasoning-collapse-guard: the low-literal bars stopped holding after ${reached} of ${resolved.semanticStrictConfirmations} windows (session ${sessionId ?? "unidentified"}) — streak cleared, not acting`);
46231
+ }
46232
+ /** Report a low-literal window that qualifies but has not been confirmed enough. */
46233
+ function notePendingLowConfirmation(state, literal, semantic, distinct, sessionId) {
46234
+ ctx.logger?.warn(`reasoning-collapse-guard: low-literal bars hold in window ${state.lowStreak}/${resolved.semanticStrictConfirmations} (literal ${literal.toFixed(3)} >= ${resolved.semanticLiteralFloor}, semantic ${semantic.toFixed(3)} >= ${resolved.semanticStrictThreshold}, distinct ${distinct.toFixed(3)} <= ${resolved.semanticDistinctCeiling}, session ${sessionId ?? "unidentified"}) — not acting yet`);
46235
+ }
46236
+ /**
46237
+ * Report a window where both signals held but the streak is still short.
46238
+ *
46239
+ * `semanticConfirmations` is what makes a break a decision rather than a
46240
+ * reflex, so each pending confirmation is named, and the log then says how
46241
+ * close a stream came to being cut by a guard that did not cut it.
46242
+ */
46243
+ function notePendingConfirmation(state, literal, semantic, sessionId) {
46244
+ ctx.logger?.warn(`reasoning-collapse-guard: both signals hold in window ${state.dualStreak}/${resolved.semanticConfirmations} (literal ${literal.toFixed(3)}, semantic ${semantic.toFixed(3)}, session ${sessionId ?? "unidentified"}) — not acting yet`);
46245
+ }
45822
46246
  function guardStream(request, next) {
45823
46247
  const state = breakerFor(request.signal ?? STATIC_KEY);
45824
46248
  if (state.exhausted || !watchedModel(request.model) || !conversationCall(request)) return next();
@@ -45829,13 +46253,33 @@ function installReasoningCollapseGuard(ctx, options = {}) {
45829
46253
  if (chunk.type === "block-start") window = "";
45830
46254
  if (chunk.type === "reasoning-delta" && typeof chunk.text === "string") {
45831
46255
  window = appendWindow(window, chunk.text, resolved.windowChars);
45832
- if (window.length >= resolved.minWindowChars && resolved.now() - state.lastBreakAt >= resolved.cooldownMs) {
45833
- const score = collapseScore(window, resolved.ngramSize);
45834
- if (score >= resolved.threshold) if (state.breaksThisTurn >= resolved.maxBreaksPerTurn) state.exhausted = true;
46256
+ if (resolved.now() - state.lastBreakAt >= resolved.cooldownMs) {
46257
+ const literal = window.length >= resolved.minWindowChars ? collapseScore(window, resolved.ngramSize) : void 0;
46258
+ const sentences = window.length >= resolved.semanticMinChars ? splitSentences(window) : [];
46259
+ const semantic = sentences.length >= resolved.semanticMinSentences ? sentenceRecurrence(sentences, resolved.semanticLag) : void 0;
46260
+ const distinct = sentences.length >= resolved.semanticMinSentences ? distinctRatioOf(sentences) : void 0;
46261
+ const high = literal !== void 0 && literal >= resolved.threshold && semantic !== void 0 && semantic >= resolved.semanticThreshold;
46262
+ const low = literal !== void 0 && literal >= resolved.semanticLiteralFloor && literal < resolved.threshold && semantic !== void 0 && semantic >= resolved.semanticStrictThreshold && distinct !== void 0 && distinct <= resolved.semanticDistinctCeiling;
46263
+ const reachedHigh = state.dualStreak;
46264
+ const reachedLow = state.lowStreak;
46265
+ state.dualStreak = high ? reachedHigh + 1 : 0;
46266
+ state.lowStreak = low ? reachedLow + 1 : 0;
46267
+ if (reachedHigh !== 0 && state.dualStreak === 0) noteStreakCleared(state, reachedHigh, request.sessionId);
46268
+ if (reachedLow !== 0 && state.lowStreak === 0) noteLowStreakCleared(reachedLow, request.sessionId);
46269
+ if (high && state.dualStreak >= resolved.semanticConfirmations) if (state.breaksThisTurn >= resolved.maxBreaksPerTurn) state.exhausted = true;
46270
+ else {
46271
+ breakOff(state, literal, semantic, "high-literal", agent, request.sessionId);
46272
+ return;
46273
+ }
46274
+ else if (low && state.lowStreak >= resolved.semanticStrictConfirmations) if (state.breaksThisTurn >= resolved.maxBreaksPerTurn) state.exhausted = true;
45835
46275
  else {
45836
- breakOff(state, score, agent, request.sessionId);
46276
+ breakOff(state, literal, semantic, "low-literal", agent, request.sessionId);
45837
46277
  return;
45838
46278
  }
46279
+ else if (high) notePendingConfirmation(state, literal, semantic, request.sessionId);
46280
+ else if (low) notePendingLowConfirmation(state, literal, semantic, distinct, request.sessionId);
46281
+ else if (literal !== void 0 && literal >= resolved.threshold) noteLoneSignal(state, "literal", literal, semantic, request.sessionId);
46282
+ else if (semantic !== void 0 && semantic >= resolved.semanticThreshold) noteLoneSignal(state, "semantic", literal, semantic, request.sessionId);
45839
46283
  }
45840
46284
  }
45841
46285
  yield chunk;
@@ -12,6 +12,22 @@
12
12
  * degenerate stream scores near one. On a hit it stops yielding chunks, so the
13
13
  * in-flight request ends, and lets the turn resume on a fresh step.
14
14
  *
15
+ * Uniqueness alone only sees verbatim repetition. A stream can restate one
16
+ * conclusion over and over in fresh words, score 0.48 where the threshold is
17
+ * 0.85, and run to the output ceiling untouched. So a second, independent
18
+ * signal scores the same window for recurrence: how strongly each sentence
19
+ * repeats something the model already said at least `semanticLag` sentences
20
+ * earlier. Enumerative reasoning repeats itself too, but adjacently — each item
21
+ * borrows the previous item's frame — and recurrence ignores adjacency, which
22
+ * is what separates the two.
23
+ *
24
+ * Neither signal stops anything on its own. A break needs both over their
25
+ * thresholds in the same window, and then `semanticConfirmations` consecutive
26
+ * such windows: one signal alone, or a single anomalous window, is logged and
27
+ * released. That gradient is the whole design. A guard that cuts a healthy
28
+ * answer is worse than one that lets a runaway finish, because the user sees a
29
+ * turn stop mid-sentence with no explanation, and the fix costs a whole turn.
30
+ *
15
31
  * Seam choice: the guard wraps `llm/stream`, not `agent/assistant-stream`.
16
32
  * `agent/assistant-stream` is emit-mode — it can observe but cannot stop a
17
33
  * stream — and it does not exist before harness 0.1.5, which would make the
@@ -120,6 +136,91 @@ export interface GuardOptions {
120
136
  cooldownMs?: number;
121
137
  /** Model ids to watch; empty watches every model. */
122
138
  includeModels?: string[];
139
+ /**
140
+ * Recurrence score at or above which the semantic signal is considered to
141
+ * hold (default 0.35).
142
+ *
143
+ * Measured on the trailing window: the three healthy fixtures peak at 0.19,
144
+ * 0.17 and 0.17 across every streaming prefix, while a paraphrased loop and
145
+ * the verbatim loop score 0.75 and 0.99. 0.35 sits near the geometric mean
146
+ * of that gap, roughly 1.8x above the worst healthy prefix.
147
+ */
148
+ semanticThreshold?: number;
149
+ /**
150
+ * Sentences a claim must be separated from to count as recurrence
151
+ * (default 6).
152
+ *
153
+ * Zero would make healthy enumeration look degenerate: item N of a list
154
+ * legitimately shares most of its wording with item N-1. Requiring the
155
+ * earlier sentence to be at least this far back keeps the measurement on
156
+ * "the model said this before" rather than "the model is still on this point".
157
+ */
158
+ semanticLag?: number;
159
+ /**
160
+ * Characters required before the semantic signal is scored (default 360).
161
+ *
162
+ * Deliberately its own floor, well below `minWindowChars`: a real trace
163
+ * emitted 16 steps of which only one crossed 1500 characters, so the
164
+ * literal signal saw a single step of a whole turn. The two floors are
165
+ * separate knobs because they answer different questions — the literal one
166
+ * is about how much text a duplicate needs to be visible in, the semantic
167
+ * one about how much text a recurrence needs to be measurable in.
168
+ */
169
+ semanticMinChars?: number;
170
+ /** Sentences required before recurrence is scored (default 8). */
171
+ semanticMinSentences?: number;
172
+ /**
173
+ * Consecutive windows in which both signals must hold before a break
174
+ * (default 3).
175
+ *
176
+ * Counted independently of the per-turn break budget, which bounds how often
177
+ * the guard may act, not how sure it must be. A stream delivers a scoring
178
+ * window every few dozen characters, so three confirmations cost a few
179
+ * hundred wasted tokens against a runaway measured in the tens of thousands.
180
+ */
181
+ semanticConfirmations?: number;
182
+ /**
183
+ * Lowest literal score the low-literal path will act on (default 0.6).
184
+ *
185
+ * A paraphrased loop sits far below `threshold` — measured at 0.676 for a
186
+ * model restating twelve conclusions in fresh words, against 0.85 for
187
+ * verbatim repetition. So a strong semantic signal has to be allowed to
188
+ * carry a decision the literal signal cannot make on its own.
189
+ *
190
+ * The floor exists because a low score is not the same as a high one: 0.6 is
191
+ * evidence that something is repeating, 0.2 is a short trace that has not
192
+ * repeated long enough to say anything. Measured healthy literal peaks at
193
+ * 0.276, so 0.6 keeps a 2.2x margin over every healthy prefix.
194
+ */
195
+ semanticLiteralFloor?: number;
196
+ /**
197
+ * Semantic score the low-literal path demands (default 0.45).
198
+ *
199
+ * Strictly greater than `semanticThreshold`, and validated as such. The literal
200
+ * evidence on this path is weaker by construction, so the semantic evidence
201
+ * has to be stronger: 0.45 against a measured healthy peak of 0.194 is a
202
+ * 2.3x margin, while a paraphrase loop measures 0.77.
203
+ */
204
+ semanticStrictThreshold?: number;
205
+ /**
206
+ * Most of a window's sentences may be distinct and still be eligible for the
207
+ * low-literal path (default 0.47).
208
+ *
209
+ * This is the dimension that separates a model circling from a model
210
+ * listing. Both reuse a frame heavily and both score high on recurrence, but
211
+ * a list writes a sentence that has never been written before on every line
212
+ * — measured at 1.000 distinct, against 0.42 for a paraphrase loop and 0.075
213
+ * for a verbatim one. Recurrence says the wording comes back; this says
214
+ * whether anything at all was new.
215
+ */
216
+ semanticDistinctCeiling?: number;
217
+ /**
218
+ * Consecutive windows the low-literal path needs (default 5).
219
+ *
220
+ * Strictly greater than `semanticConfirmations`, and validated as such. The path is
221
+ * acting on weaker evidence, so it also waits longer before it acts.
222
+ */
223
+ semanticStrictConfirmations?: number;
123
224
  /** Injectable clock, so the cooldown is testable without a timer. */
124
225
  now?: () => number;
125
226
  }
@@ -132,6 +233,15 @@ export interface ResolvedGuardOptions {
132
233
  maxBreaksPerTurn: number;
133
234
  cooldownMs: number;
134
235
  includeModels: string[];
236
+ semanticThreshold: number;
237
+ semanticLag: number;
238
+ semanticMinChars: number;
239
+ semanticMinSentences: number;
240
+ semanticConfirmations: number;
241
+ semanticLiteralFloor: number;
242
+ semanticStrictThreshold: number;
243
+ semanticDistinctCeiling: number;
244
+ semanticStrictConfirmations: number;
135
245
  now: () => number;
136
246
  }
137
247
  export declare const DEFAULT_GUARD_OPTIONS: ResolvedGuardOptions;
@@ -151,6 +261,62 @@ export declare function resolveGuardOptions(options?: GuardOptions): ResolvedGua
151
261
  * @returns collapse score in [0,1]; 0 means every n-gram is unique.
152
262
  */
153
263
  export declare function collapseScore(window: string, n: number): number;
264
+ /**
265
+ * Split reasoning text into sentences, for mixed CJK and Latin streams.
266
+ *
267
+ * A Chinese full stop ends a sentence whatever follows it. An ASCII `.!?` only
268
+ * ends one when whitespace or the end of the text follows, because the same
269
+ * characters carry out decimals, file names and version numbers
270
+ * (`selectors.test.ts:745`, `3.14`) that must not become sentence
271
+ * edges. Whitespace runs collapse and case folds, so a difference in spacing
272
+ * or capitalisation cannot read as a difference in wording.
273
+ *
274
+ * @param text - reasoning text, typically one trailing window.
275
+ * @returns non-empty normalized sentences, in order.
276
+ */
277
+ export declare function splitSentences(text: string): string[];
278
+ /**
279
+ * How strongly a window keeps returning to sentences it already stated.
280
+ *
281
+ * Each sentence is compared against every sentence at least `lag` positions earlier
282
+ * and keeps the best match; the score is the mean of those best matches. The
283
+ * comparison is normalized (Jaccard), so two sentences match on shared wording
284
+ * rather than on shared length, and character n-grams are used rather than words
285
+ * because no tokenizer is available here, and none would survive a CJK/Latin
286
+ * mix without segmenting Chinese first.
287
+ *
288
+ * Two properties matter more than the exact number. The score saturates — a
289
+ * twelve-sentence loop and a four-hundred-sentence one both approach one — so
290
+ * it does not drift with length the way a mean pairwise similarity does, and
291
+ * it does not reward a long answer merely for containing more pairs. And the
292
+ * lag is what makes it safe to ship: healthy enumeration reuses its framing
293
+ * from the sentence immediately before, which this metric cannot see.
294
+ *
295
+ * @param window - trailing reasoning characters.
296
+ * @param lag - sentences a claim must be separated from to count as recurrence.
297
+ * @returns recurrence score in [0,1]; 0 means nothing recurs, or too little text to tell.
298
+ */
299
+ export declare function semanticRecurrence(window: string, lag: number): number;
300
+ /**
301
+ * The share of a window's sentences that have never been said before.
302
+ *
303
+ * Recurrence cannot tell a model that is circling from a model that is
304
+ * listing, because a list reuses its frame as heavily as a loop does and
305
+ * scores just as high. The difference is that a list writes a new sentence on
306
+ * every line. Measured over a trailing window: 0.075 for the verbatim loop,
307
+ * 0.42 for a paraphrase loop, 1.000 for every healthy fixture and for every
308
+ * list shape tried.
309
+ *
310
+ * Equality is exact and on the normalized sentence, which is what keeps the
311
+ * lists out. Two rows differing only by a row number are 0.86 similar by
312
+ * `semanticRecurrence` and 1.000 distinct here, and that gap is the whole point: the
313
+ * fuzzy measure asks whether the wording came back, this one asks whether
314
+ * anything was new.
315
+ *
316
+ * @param window - trailing reasoning characters.
317
+ * @returns distinct share in (0,1]; 1 when every sentence is unique.
318
+ */
319
+ export declare function distinctSentenceRatio(window: string): number;
154
320
  export declare const RESUME_HINT: string;
155
321
  /** The second resume message, used once a resume has also collapsed. */
156
322
  export declare const RESUME_HINT_STRICT: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/host/reasoning-collapse-guard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAKH;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;IACzC,4DAA4D;IAC5D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACtC;AAED,mFAAmF;AACnF,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACnC;AAED,qFAAqF;AACrF,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,EAAE,CAAC,EAAE,OAAO,CAAA;IACrB,MAAM,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAA;IACzF,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;CAC/B;AAED,+DAA+D;AAC/D,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,MAAM,aAAa,CAAC,UAAU,CAAC,KAClC,aAAa,CAAC,UAAU,CAAC,CAAA;AAE9B,2EAA2E;AAC3E,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,CAAA;AAE3E,mEAAmE;AACnE,MAAM,MAAM,aAAa,GAAG,mBAAmB,GAAG,uBAAuB,CAAA;AAEzE,0DAA0D;AAC1D,MAAM,WAAW,oBAAoB;IACnC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,EAAE,CAAC,EAAE,CACH,KAAK,EAAE,YAAY,GAAG,eAAe,GAAG,gBAAgB,EACxD,QAAQ,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,oBAAoB,KAC3B,OAAO,CAAA;IACZ,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,SAAS,CAAA;IACpD;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAAA;KAAE,GAAG,SAAS,CAAA;CAC5D;AAED,+DAA+D;AAC/D,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qDAAqD;IACrD,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,qEAAqE;IACrE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,0CAA0C;AAC1C,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,GAAG,EAAE,MAAM,MAAM,CAAA;CAClB;AAED,eAAO,MAAM,qBAAqB,EAAE,oBASnC,CAAA;AAUD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,YAAiB,GAAG,oBAAoB,CAiCpF;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAU/D;AACD,eAAO,MAAM,WAAW,QAGuB,CAAA;AAE/C,wEAAwE;AACxE,eAAO,MAAM,kBAAkB,QAIG,CAAA;AA0FlC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,wBAAwB,EAC7B,OAAO,GAAE,YAAiB,GACzB,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CA4J1B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/host/reasoning-collapse-guard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAKH;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;IACzC,4DAA4D;IAC5D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACtC;AAED,mFAAmF;AACnF,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACnC;AAED,qFAAqF;AACrF,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,EAAE,CAAC,EAAE,OAAO,CAAA;IACrB,MAAM,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAA;IACzF,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;CAC/B;AAED,+DAA+D;AAC/D,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,MAAM,aAAa,CAAC,UAAU,CAAC,KAClC,aAAa,CAAC,UAAU,CAAC,CAAA;AAE9B,2EAA2E;AAC3E,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,CAAA;AAE3E,mEAAmE;AACnE,MAAM,MAAM,aAAa,GAAG,mBAAmB,GAAG,uBAAuB,CAAA;AAEzE,0DAA0D;AAC1D,MAAM,WAAW,oBAAoB;IACnC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,EAAE,CAAC,EAAE,CACH,KAAK,EAAE,YAAY,GAAG,eAAe,GAAG,gBAAgB,EACxD,QAAQ,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,oBAAoB,KAC3B,OAAO,CAAA;IACZ,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,SAAS,CAAA;IACpD;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAAA;KAAE,GAAG,SAAS,CAAA;CAC5D;AAED,+DAA+D;AAC/D,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qDAAqD;IACrD,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,kEAAkE;IAClE,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B;;;;;;;;OAQG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B;;;;;;;;;;;;OAYG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B;;;;;;;OAOG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC;;;;;;;;;;OAUG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC;;;;;OAKG;IACH,2BAA2B,CAAC,EAAE,MAAM,CAAA;IACpC,qEAAqE;IACrE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,0CAA0C;AAC1C,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,WAAW,EAAE,MAAM,CAAA;IACnB,gBAAgB,EAAE,MAAM,CAAA;IACxB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,qBAAqB,EAAE,MAAM,CAAA;IAC7B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,uBAAuB,EAAE,MAAM,CAAA;IAC/B,uBAAuB,EAAE,MAAM,CAAA;IAC/B,2BAA2B,EAAE,MAAM,CAAA;IACnC,GAAG,EAAE,MAAM,MAAM,CAAA;CAClB;AAED,eAAO,MAAM,qBAAqB,EAAE,oBAkBnC,CAAA;AAUD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,YAAiB,GAAG,oBAAoB,CAiFpF;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAU/D;AACD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAmBrD;AA2BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEtE;AA6BD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE5D;AAQD,eAAO,MAAM,WAAW,QAGuB,CAAA;AAE/C,wEAAwE;AACxE,eAAO,MAAM,kBAAkB,QAIG,CAAA;AAmHlC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,wBAAwB,EAC7B,OAAO,GAAE,YAAiB,GACzB,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAoT1B"}
@@ -2,7 +2,7 @@ import type { WorkBuddyAccount, WorkBuddyAccountQuota, WorkBuddyMeter } from '..
2
2
  import { PROVIDER_ID, PROVIDER_NAME } from './types.ts';
3
3
  import { FileCredentialStore, type WorkBuddyCredentials } from './token-store.ts';
4
4
  import type { WorkBuddyTokenIdentity } from './identity.ts';
5
- import type { WorkBuddyModelEntry } from './model-catalog.ts';
5
+ import { type WorkBuddyModelEntry } from './model-catalog.ts';
6
6
  export interface WorkBuddyRequestOptions {
7
7
  fetchFn?: typeof fetch;
8
8
  signal?: AbortSignal;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../src/host/workbuddy/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../src/host/workbuddy/client.ts"],"names":[],"mappings":"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"}
@@ -11,9 +11,21 @@
11
11
  * The fallback is a verbatim transcription of a live `/v3/config` read, not
12
12
  * hand-written guesses: an earlier revision of this file guessed context
13
13
  * windows from vendor marketing and was wrong in both directions (`glm-5.3`
14
- * and `kimi-k3` are 1M, not 200K/256K). Where the two regions disagree, the
15
- * larger maximum is kept while the served default comes from whichever region
16
- * reported one.
14
+ * and `kimi-k3` are 1M, not 200K/256K).
15
+ *
16
+ * One entry serves both regions, so a model the regions disagree about needs a
17
+ * merged figure. The rule is asymmetric on purpose: the **larger maximum** is
18
+ * kept, while the **served default** and the **output cap** take the **smaller**
19
+ * of the two. Over-declaring either one is a hard failure — DSH would send a
20
+ * request the gateway rejects — whereas under-declaring only compresses a
21
+ * conversation earlier than needed, and both remain overridable per model from
22
+ * the settings card. Measured disagreements (`glm-5.3-flash`, `glm-5.3`,
23
+ * `glm-5.2`, `kimi-k2.8-preview` served 300000/48000/48000/32000 by intl
24
+ * against 1000000/64000/64000/64000 by cn) are reconciled that way.
25
+ *
26
+ * The regions also do not serve the same ids. intl added `glm-5.3-flash` and
27
+ * keeps `gemini-3.5-flash`, `kimi-k3` and the `*-model` aliases to itself; cn
28
+ * keeps `glm-5.3-flash`, `kimi-k3-1` and its own `-x` variants.
17
29
  *
18
30
  * `regions` is a routing fact, not a preference: asking a region for a model it
19
31
  * does not serve answers 400 `code 11102`.
@@ -67,6 +79,38 @@ export interface WorkBuddyModelEntry {
67
79
  /** One-line note shown beside the model in the settings card. */
68
80
  description: string;
69
81
  }
82
+ /**
83
+ * Models the gateway serves but does not publish in `/v3/config`.
84
+ *
85
+ * The catalog is a published list, not the served one: measured on the
86
+ * international backend, `gpt-6-sol`, `gpt-6-luna` and `gemini-3.8-flash` all
87
+ * answer a normal streaming completion with 200 while being absent from
88
+ * `/v3/config` entirely, and a reachable gateway *replaces* the shipped table
89
+ * with that list. Without these rows the picker would simply never show them, so
90
+ * they are declared here and merged into a live catalog too
91
+ * ({@link withUnpublishedModels}) rather than living in the fallback alone.
92
+ *
93
+ * What each row records, and how it was established, differs by field:
94
+ *
95
+ * - **Existence, the reasoning ladder, image support** were measured against the
96
+ * live endpoint, not inferred from the name. A rejected level answers 400
97
+ * `code 11133` with `extError` `400002`; `gpt-6-sol` takes exactly the ladder
98
+ * `gpt-6-astra` publishes (`minimal` refused, `low`/`medium`/`high`/`xhigh`/
99
+ * `max` accepted) and both accept a 1x1 PNG.
100
+ * - **Context window, output cap and `canDisableThinking` are inherited from the
101
+ * published sibling in the same family** — `gpt-6-astra` for `gpt-6-sol` and
102
+ * `gpt-6-luna`, `gemini-3.5-flash` for `gemini-3.8-flash`. They are marked
103
+ * here rather than measured because the gateway publishes nothing for these
104
+ * ids and offers no cheap way to read the real figure: it accepts a
105
+ * `max_tokens` larger than any published window (even for `gpt-6-astra`, whose
106
+ * own catalog caps at 128000), so the number is neither enforced nor
107
+ * discoverable from the request side.
108
+ *
109
+ * Every field stays overridable per model from the settings card, and an
110
+ * inherited window errs low rather than high: over-declaring overflows the
111
+ * gateway mid-session, while under-declaring only compresses earlier.
112
+ */
113
+ export declare const UNPUBLISHED_MODELS: readonly WorkBuddyModelEntry[];
70
114
  /**
71
115
  * Offline fallback catalog, transcribed from a live `/v3/config` read.
72
116
  *
@@ -85,6 +129,17 @@ export declare const FALLBACK_MODELS: readonly WorkBuddyModelEntry[];
85
129
  export declare const DEFAULT_VISIBLE_MODEL_IDS: readonly string[];
86
130
  /** Every id the fallback knows, in declaration order. */
87
131
  export declare const WORKBUDDY_MODEL_IDS: readonly string[];
132
+ /**
133
+ * Add the served-but-unpublished models to a live `/v3/config` catalog.
134
+ *
135
+ * A reachable gateway replaces the shipped table with the list it publishes, so
136
+ * a model it serves without listing ({@link UNPUBLISHED_MODELS}) would drop out
137
+ * of the picker the moment the catalog loads. Merging here keeps it offered.
138
+ *
139
+ * A published entry always wins: the gateway is the authority on anything it
140
+ * does describe, so these rows only fill ids the payload is silent about.
141
+ */
142
+ export declare function withUnpublishedModels(catalog: readonly WorkBuddyModelEntry[], region: WorkBuddyRegion): WorkBuddyModelEntry[];
88
143
  /**
89
144
  * Resolve one entry from a catalog.
90
145
  *
@@ -95,7 +150,25 @@ export declare const WORKBUDDY_MODEL_IDS: readonly string[];
95
150
  export declare function resolveWorkBuddyModel(id: string, catalog?: readonly WorkBuddyModelEntry[]): WorkBuddyModelEntry;
96
151
  /** Whether one catalog knows this exact id. */
97
152
  export declare function isWorkBuddyModelId(id: unknown, catalog?: readonly WorkBuddyModelEntry[]): id is string;
98
- /** Models an account in `region` can actually call, in catalog order. */
153
+ /** The vendor family one model belongs to. */
154
+ export declare function workBuddyModelVendor(id: string): string;
155
+ /**
156
+ * Group a list so every vendor's models sit together.
157
+ *
158
+ * Families appear in the order their first model does — the catalog decides
159
+ * which vendor leads, not this function — and each family is ordered newest
160
+ * generation first.
161
+ */
162
+ export declare function groupWorkBuddyModelsByVendor<T extends {
163
+ id: string;
164
+ }>(models: readonly T[]): T[];
165
+ /**
166
+ * Models an account in `region` can actually call, grouped by vendor.
167
+ *
168
+ * Grouped here rather than in the settings card because this is also what the
169
+ * request path resolves against: one ordering rule, so the picker and the
170
+ * adapter can never disagree about which models a region offers.
171
+ */
99
172
  export declare function modelsForRegion(region: WorkBuddyRegion, catalog?: readonly WorkBuddyModelEntry[]): WorkBuddyModelEntry[];
100
173
  /** Whether one model accepts image input. */
101
174
  export declare function workBuddyModelSupportsImage(id: string, catalog?: readonly WorkBuddyModelEntry[]): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"model-catalog.d.ts","sourceRoot":"","sources":["../../../../src/host/workbuddy/model-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAA;AAI1E,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;;OAOG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB,sEAAsE;IACtE,gBAAgB,EAAE,MAAM,CAAA;IACxB,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAA;IACjB,4CAA4C;IAC5C,OAAO,EAAE,SAAS,eAAe,EAAE,CAAA;IACnC,6CAA6C;IAC7C,aAAa,EAAE,OAAO,CAAA;IACtB,2EAA2E;IAC3E,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,2DAA2D;IAC3D,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,gEAAgE;IAChE,kBAAkB,EAAE,OAAO,CAAA;IAC3B,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAA;CACpB;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,mBAAmB,EAgjBzD,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EAKtD,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAA6C,CAAA;AAE9F;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,mBAAmB,CA0BrB;AAED,+CAA+C;AAC/C,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,OAAO,EACX,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,EAAE,IAAI,MAAM,CAEd;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAC7B,MAAM,EAAE,eAAe,EACvB,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,mBAAmB,EAAE,CAEvB;AAED,6CAA6C;AAC7C,wBAAgB,2BAA2B,CACzC,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,OAAO,CAET;AAED,0CAA0C;AAC1C,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,SAAS,MAAM,EAAE,CAEnB;AAED,sDAAsD;AACtD,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,MAAM,CAER;AAED,wDAAwD;AACxD,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,MAAM,CAER"}
1
+ {"version":3,"file":"model-catalog.d.ts","sourceRoot":"","sources":["../../../../src/host/workbuddy/model-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAA;AAI1E,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;;OAOG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB,sEAAsE;IACtE,gBAAgB,EAAE,MAAM,CAAA;IACxB,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAA;IACjB,4CAA4C;IAC5C,OAAO,EAAE,SAAS,eAAe,EAAE,CAAA;IACnC,6CAA6C;IAC7C,aAAa,EAAE,OAAO,CAAA;IACtB,2EAA2E;IAC3E,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,2DAA2D;IAC3D,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,gEAAgE;IAChE,kBAAkB,EAAE,OAAO,CAAA;IAC3B,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,mBAAmB,EAwC5D,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,mBAAmB,EAwkBzD,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EAKtD,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAA6C,CAAA;AAE9F;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,SAAS,mBAAmB,EAAE,EACvC,MAAM,EAAE,eAAe,GACtB,mBAAmB,EAAE,CAMvB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,mBAAmB,CA0BrB;AAED,+CAA+C;AAC/C,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,OAAO,EACX,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,EAAE,IAAI,MAAM,CAEd;AAoCD,8CAA8C;AAC9C,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAMvD;AAwCD;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,EAAE,CAWhG;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,eAAe,EACvB,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,mBAAmB,EAAE,CAEvB;AAED,6CAA6C;AAC7C,wBAAgB,2BAA2B,CACzC,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,OAAO,CAET;AAED,0CAA0C;AAC1C,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,SAAS,MAAM,EAAE,CAEnB;AAED,sDAAsD;AACtD,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,MAAM,CAER;AAED,wDAAwD;AACxD,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,SAAS,mBAAmB,EAAoB,GACxD,MAAM,CAER"}
@@ -15,7 +15,13 @@ import type { WorkBuddyAccountPool } from './account-pool.ts';
15
15
  * behind an unedited default. Any explicit edit is honoured exactly.
16
16
  */
17
17
  export declare function resolveEnabledModelIds(stored: readonly string[], available: readonly string[], enabled?: boolean): string[];
18
- /** Build the model options the settings card renders, in catalog order. */
18
+ /**
19
+ * Build the model options the settings card renders.
20
+ *
21
+ * Order comes from the caller, which passes a vendor-grouped list so a family
22
+ * is not interleaved with the next one; this preserves whatever order it is
23
+ * given rather than imposing a second one.
24
+ */
19
25
  export declare function buildModelOptions(catalog: readonly WorkBuddyModelEntry[], available: readonly string[], enabledModelIds: readonly string[], overrides: Record<string, number>): WorkBuddyModelOption[];
20
26
  export interface WorkBuddyStatusOptions {
21
27
  fetchFn?: typeof fetch;
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../../src/host/workbuddy/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAIlD,OAAO,EAGL,sBAAsB,EACtB,WAAW,EACX,aAAa,EAEd,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EAGtB,KAAK,wBAAwB,EAC9B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAKL,cAAc,EAKf,MAAM,aAAa,CAAA;AACpB,OAAO,EASL,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAIV,oBAAoB,EACpB,kBAAkB,EACnB,MAAM,qCAAqC,CAAA;AAC5C,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAG3D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AA6C7D;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,OAAO,UAAO,GACb,MAAM,EAAE,CASV;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,SAAS,mBAAmB,EAAE,EACvC,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,eAAe,EAAE,SAAS,MAAM,EAAE,EAClC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAChC,oBAAoB,EAAE,CAqBxB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAA;IACtB,sFAAsF;IACtF,OAAO,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;IACnC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC,CAAA;IAChD,4FAA4F;IAC5F,WAAW,CAAC,EAAE,oBAAoB,CAAA;IAClC,6EAA6E;IAC7E,OAAO,CAAC,EAAE,IAAI,CAAC,uBAAuB,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAC5D;AAMD,mFAAmF;AACnF,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,mBAAmB,EAC1B,aAAa,EAAE,sBAAsB,EACrC,WAAW,CAAC,EAAE,wBAAwB,EACtC,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,kBAAkB,CAAC,CA2E7B;AAED,qEAAqE;AACrE,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,mBAAmB,EAC1B,aAAa,EAAE,sBAAsB,EACrC,WAAW,CAAC,EAAE,wBAAwB,EACtC,OAAO,GAAE,sBAA2B,GACnC,MAAM,IAAI,CAkXZ;AAED,OAAO,EACL,WAAW,EACX,aAAa,EACb,sBAAsB,EACtB,cAAc,EACd,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,GACxB,CAAA"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../../src/host/workbuddy/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAIlD,OAAO,EAGL,sBAAsB,EACtB,WAAW,EACX,aAAa,EAEd,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EAGtB,KAAK,wBAAwB,EAC9B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAKL,cAAc,EAKf,MAAM,aAAa,CAAA;AACpB,OAAO,EASL,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAIV,oBAAoB,EACpB,kBAAkB,EACnB,MAAM,qCAAqC,CAAA;AAC5C,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAG3D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AA6C7D;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,OAAO,UAAO,GACb,MAAM,EAAE,CASV;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,SAAS,mBAAmB,EAAE,EACvC,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,eAAe,EAAE,SAAS,MAAM,EAAE,EAClC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAChC,oBAAoB,EAAE,CAqBxB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAA;IACtB,sFAAsF;IACtF,OAAO,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;IACnC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC,CAAA;IAChD,4FAA4F;IAC5F,WAAW,CAAC,EAAE,oBAAoB,CAAA;IAClC,6EAA6E;IAC7E,OAAO,CAAC,EAAE,IAAI,CAAC,uBAAuB,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAC5D;AAMD,mFAAmF;AACnF,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,mBAAmB,EAC1B,aAAa,EAAE,sBAAsB,EACrC,WAAW,CAAC,EAAE,wBAAwB,EACtC,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,kBAAkB,CAAC,CA2E7B;AAED,qEAAqE;AACrE,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,mBAAmB,EAC1B,aAAa,EAAE,sBAAsB,EACrC,WAAW,CAAC,EAAE,wBAAwB,EACtC,OAAO,GAAE,sBAA2B,GACnC,MAAM,IAAI,CAkXZ;AAED,OAAO,EACL,WAAW,EACX,aAAa,EACb,sBAAsB,EACtB,cAAc,EACd,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,GACxB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eddyskywalker/dsh-chatgpt-subscription",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "DSH provider plugin for Codex access through a ChatGPT subscription.",
5
5
  "author": {
6
6
  "name": "eddyskywalker",