@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/lib/index.js CHANGED
@@ -30217,6 +30217,25 @@ function refreshSourceForDomain(domain) {
30217
30217
  }
30218
30218
  //#endregion
30219
30219
  //#region src/shared/workbuddy-contracts.ts
30220
+ /**
30221
+ * Parse one gateway \`credits\` string into the bare multiplier it names.
30222
+ *
30223
+ * Measured shapes: \`x6.67\` (intl), \`x0.79 credits\` (cn), and \`''\` for a model
30224
+ * whose publisher declares no rate. \`x0.00\` is deliberately kept — it is a real
30225
+ * declaration ("this model does not consume allowance"), not a missing value,
30226
+ * so it must not be confused with the empty string that means nothing is known.
30227
+ *
30228
+ * The return is \`null\` rather than \`0\` for anything unparseable, so a caller can
30229
+ * tell "no rate published" from "a rate of zero".
30230
+ */
30231
+ function parseWorkBuddyCreditMultiplier(raw) {
30232
+ if (typeof raw !== "string") return null;
30233
+ const stripped = raw.replace(/\s*credits\s*$/i, "").trim().replace(/^[x×]/i, "").trim();
30234
+ if (stripped === "") return null;
30235
+ const value = Number(stripped);
30236
+ if (!Number.isFinite(value) || value < 0) return null;
30237
+ return stripped;
30238
+ }
30220
30239
  /** Every level this route can name, in escalating order (the card's sort key). */
30221
30240
  const WORKBUDDY_REASONING_EFFORTS = [
30222
30241
  "minimal",
@@ -30276,6 +30295,94 @@ function convergeWorkBuddyEffort(effort, ladder) {
30276
30295
  //#endregion
30277
30296
  //#region src/host/workbuddy/model-catalog.ts
30278
30297
  /**
30298
+ * Models the gateway serves but does not publish in `/v3/config`.
30299
+ *
30300
+ * The catalog is a published list, not the served one: measured on the
30301
+ * international backend, `gpt-6-sol`, `gpt-6-luna` and `gemini-3.8-flash` all
30302
+ * answer a normal streaming completion with 200 while being absent from
30303
+ * `/v3/config` entirely, and a reachable gateway *replaces* the shipped table
30304
+ * with that list. Without these rows the picker would simply never show them, so
30305
+ * they are declared here and merged into a live catalog too
30306
+ * ({@link withUnpublishedModels}) rather than living in the fallback alone.
30307
+ *
30308
+ * What each row records, and how it was established, differs by field:
30309
+ *
30310
+ * - **Existence, the reasoning ladder, image support** were measured against the
30311
+ * live endpoint, not inferred from the name. A rejected level answers 400
30312
+ * `code 11133` with `extError` `400002`; `gpt-6-sol` takes exactly the ladder
30313
+ * `gpt-6-astra` publishes (`minimal` refused, `low`/`medium`/`high`/`xhigh`/
30314
+ * `max` accepted) and both accept a 1x1 PNG.
30315
+ * - **Context window, output cap and `canDisableThinking` are inherited from the
30316
+ * published sibling in the same family** — `gpt-6-astra` for `gpt-6-sol` and
30317
+ * `gpt-6-luna`, `gemini-3.5-flash` for `gemini-3.8-flash`. They are marked
30318
+ * here rather than measured because the gateway publishes nothing for these
30319
+ * ids and offers no cheap way to read the real figure: it accepts a
30320
+ * `max_tokens` larger than any published window (even for `gpt-6-astra`, whose
30321
+ * own catalog caps at 128000), so the number is neither enforced nor
30322
+ * discoverable from the request side.
30323
+ *
30324
+ * Every field stays overridable per model from the settings card, and an
30325
+ * inherited window errs low rather than high: over-declaring overflows the
30326
+ * gateway mid-session, while under-declaring only compresses earlier.
30327
+ */
30328
+ const UNPUBLISHED_MODELS = [
30329
+ {
30330
+ id: "gpt-6-sol",
30331
+ name: "GPT-6-Sol",
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: "gpt-6-luna",
30350
+ name: "GPT-6-Luna",
30351
+ contextWindow: 4e5,
30352
+ maxContextWindow: 1e6,
30353
+ maxTokens: 128e3,
30354
+ regions: ["intl"],
30355
+ supportsImage: true,
30356
+ reasoningEfforts: [
30357
+ "low",
30358
+ "medium",
30359
+ "high",
30360
+ "xhigh",
30361
+ "max"
30362
+ ],
30363
+ defaultReasoningEffort: "high",
30364
+ canDisableThinking: true,
30365
+ description: "OpenAI 轻量模型,响应快速,适合日常任务"
30366
+ },
30367
+ {
30368
+ id: "gemini-3.8-flash",
30369
+ name: "Gemini-3.8-Flash",
30370
+ contextWindow: 1e6,
30371
+ maxContextWindow: 1e6,
30372
+ maxTokens: 65536,
30373
+ regions: ["intl"],
30374
+ supportsImage: true,
30375
+ reasoningEfforts: [
30376
+ "low",
30377
+ "high",
30378
+ "max"
30379
+ ],
30380
+ defaultReasoningEffort: "medium",
30381
+ canDisableThinking: false,
30382
+ description: "能力均衡,适合日常使用"
30383
+ }
30384
+ ];
30385
+ /**
30279
30386
  * Offline fallback catalog, transcribed from a live `/v3/config` read.
30280
30387
  *
30281
30388
  * Used when the gateway cannot be reached (first run before sign-in, or a
@@ -30302,6 +30409,7 @@ const FALLBACK_MODELS$1 = [
30302
30409
  maxContextWindow: 56e3,
30303
30410
  maxTokens: 24e3,
30304
30411
  regions: ["cn"],
30412
+ credits: { cn: "2.00" },
30305
30413
  supportsImage: false,
30306
30414
  reasoningEfforts: [],
30307
30415
  defaultReasoningEffort: null,
@@ -30315,6 +30423,7 @@ const FALLBACK_MODELS$1 = [
30315
30423
  maxContextWindow: 2e5,
30316
30424
  maxTokens: 32e3,
30317
30425
  regions: ["intl"],
30426
+ credits: { intl: "0.34" },
30318
30427
  supportsImage: true,
30319
30428
  reasoningEfforts: [
30320
30429
  "low",
@@ -30332,6 +30441,7 @@ const FALLBACK_MODELS$1 = [
30332
30441
  maxContextWindow: 256e3,
30333
30442
  maxTokens: 32e3,
30334
30443
  regions: ["intl"],
30444
+ credits: { intl: "0.59" },
30335
30445
  supportsImage: true,
30336
30446
  reasoningEfforts: [
30337
30447
  "low",
@@ -30349,6 +30459,7 @@ const FALLBACK_MODELS$1 = [
30349
30459
  maxContextWindow: 272e3,
30350
30460
  maxTokens: 72e3,
30351
30461
  regions: ["intl"],
30462
+ credits: { intl: "3.31" },
30352
30463
  supportsImage: true,
30353
30464
  reasoningEfforts: [
30354
30465
  "low",
@@ -30366,12 +30477,14 @@ const FALLBACK_MODELS$1 = [
30366
30477
  maxContextWindow: 2e5,
30367
30478
  maxTokens: 24e3,
30368
30479
  regions: ["intl"],
30480
+ credits: { intl: "3.33" },
30369
30481
  supportsImage: true,
30370
30482
  reasoningEfforts: [],
30371
30483
  defaultReasoningEffort: null,
30372
30484
  canDisableThinking: false,
30373
30485
  description: "深度推理,适合深度分析与难题"
30374
30486
  },
30487
+ ...UNPUBLISHED_MODELS.filter((model) => model.id === "gpt-6-sol" || model.id === "gpt-6-luna"),
30375
30488
  {
30376
30489
  id: "gpt-6-astra",
30377
30490
  name: "GPT-6-Astra",
@@ -30379,6 +30492,7 @@ const FALLBACK_MODELS$1 = [
30379
30492
  maxContextWindow: 1e6,
30380
30493
  maxTokens: 128e3,
30381
30494
  regions: ["intl"],
30495
+ credits: { intl: "6.67" },
30382
30496
  supportsImage: true,
30383
30497
  reasoningEfforts: [
30384
30498
  "low",
@@ -30398,6 +30512,7 @@ const FALLBACK_MODELS$1 = [
30398
30512
  maxContextWindow: 1e6,
30399
30513
  maxTokens: 128e3,
30400
30514
  regions: ["intl"],
30515
+ credits: { intl: "3.47" },
30401
30516
  supportsImage: true,
30402
30517
  reasoningEfforts: [
30403
30518
  "low",
@@ -30417,6 +30532,7 @@ const FALLBACK_MODELS$1 = [
30417
30532
  maxContextWindow: 1e6,
30418
30533
  maxTokens: 128e3,
30419
30534
  regions: ["intl"],
30535
+ credits: { intl: "1.39" },
30420
30536
  supportsImage: true,
30421
30537
  reasoningEfforts: [
30422
30538
  "low",
@@ -30436,6 +30552,7 @@ const FALLBACK_MODELS$1 = [
30436
30552
  maxContextWindow: 1e6,
30437
30553
  maxTokens: 128e3,
30438
30554
  regions: ["intl"],
30555
+ credits: { intl: "0.14" },
30439
30556
  supportsImage: true,
30440
30557
  reasoningEfforts: [
30441
30558
  "low",
@@ -30455,6 +30572,7 @@ const FALLBACK_MODELS$1 = [
30455
30572
  maxContextWindow: 1e6,
30456
30573
  maxTokens: 128e3,
30457
30574
  regions: ["intl"],
30575
+ credits: { intl: "3.31" },
30458
30576
  supportsImage: true,
30459
30577
  reasoningEfforts: [
30460
30578
  "low",
@@ -30473,6 +30591,7 @@ const FALLBACK_MODELS$1 = [
30473
30591
  maxContextWindow: 272e3,
30474
30592
  maxTokens: 72e3,
30475
30593
  regions: ["intl"],
30594
+ credits: { intl: "1.65" },
30476
30595
  supportsImage: true,
30477
30596
  reasoningEfforts: [
30478
30597
  "low",
@@ -30489,8 +30608,12 @@ const FALLBACK_MODELS$1 = [
30489
30608
  name: "GLM-5.3",
30490
30609
  contextWindow: 1e6,
30491
30610
  maxContextWindow: 1e6,
30492
- maxTokens: 64e3,
30611
+ maxTokens: 48e3,
30493
30612
  regions: ["intl", "cn"],
30613
+ credits: {
30614
+ cn: "0.79",
30615
+ intl: "0.79"
30616
+ },
30494
30617
  supportsImage: true,
30495
30618
  reasoningEfforts: [
30496
30619
  "low",
@@ -30504,10 +30627,14 @@ const FALLBACK_MODELS$1 = [
30504
30627
  {
30505
30628
  id: "glm-5.3-flash",
30506
30629
  name: "GLM-5.3-Flash",
30507
- contextWindow: 1e6,
30630
+ contextWindow: 3e5,
30508
30631
  maxContextWindow: 1e6,
30509
30632
  maxTokens: 32e3,
30510
- regions: ["cn"],
30633
+ regions: ["intl", "cn"],
30634
+ credits: {
30635
+ cn: "0.06",
30636
+ intl: "0.06"
30637
+ },
30511
30638
  supportsImage: true,
30512
30639
  reasoningEfforts: [
30513
30640
  "low",
@@ -30523,8 +30650,12 @@ const FALLBACK_MODELS$1 = [
30523
30650
  name: "GLM-5.2",
30524
30651
  contextWindow: 1e6,
30525
30652
  maxContextWindow: 1e6,
30526
- maxTokens: 64e3,
30653
+ maxTokens: 48e3,
30527
30654
  regions: ["intl", "cn"],
30655
+ credits: {
30656
+ cn: "0.79",
30657
+ intl: "0.79"
30658
+ },
30528
30659
  supportsImage: true,
30529
30660
  reasoningEfforts: ["high", "xhigh"],
30530
30661
  defaultReasoningEffort: "high",
@@ -30538,6 +30669,7 @@ const FALLBACK_MODELS$1 = [
30538
30669
  maxContextWindow: 2e5,
30539
30670
  maxTokens: 48e3,
30540
30671
  regions: ["cn"],
30672
+ credits: { cn: "0.79" },
30541
30673
  supportsImage: true,
30542
30674
  reasoningEfforts: [
30543
30675
  "low",
@@ -30555,6 +30687,7 @@ const FALLBACK_MODELS$1 = [
30555
30687
  maxContextWindow: 2e5,
30556
30688
  maxTokens: 64e3,
30557
30689
  regions: ["cn"],
30690
+ credits: { cn: "0.71" },
30558
30691
  supportsImage: true,
30559
30692
  reasoningEfforts: [
30560
30693
  "low",
@@ -30572,6 +30705,7 @@ const FALLBACK_MODELS$1 = [
30572
30705
  maxContextWindow: 2e5,
30573
30706
  maxTokens: 48e3,
30574
30707
  regions: ["cn"],
30708
+ credits: { cn: "0.80" },
30575
30709
  supportsImage: false,
30576
30710
  reasoningEfforts: [],
30577
30711
  defaultReasoningEffort: null,
@@ -30585,6 +30719,7 @@ const FALLBACK_MODELS$1 = [
30585
30719
  maxContextWindow: 2e5,
30586
30720
  maxTokens: 48e3,
30587
30721
  regions: ["cn"],
30722
+ credits: { cn: "0.95" },
30588
30723
  supportsImage: false,
30589
30724
  reasoningEfforts: [
30590
30725
  "low",
@@ -30602,6 +30737,7 @@ const FALLBACK_MODELS$1 = [
30602
30737
  maxContextWindow: 2e5,
30603
30738
  maxTokens: 48e3,
30604
30739
  regions: ["cn"],
30740
+ credits: { cn: "0.23" },
30605
30741
  supportsImage: false,
30606
30742
  reasoningEfforts: [],
30607
30743
  defaultReasoningEffort: null,
@@ -30615,6 +30751,7 @@ const FALLBACK_MODELS$1 = [
30615
30751
  maxContextWindow: 128e3,
30616
30752
  maxTokens: 32e3,
30617
30753
  regions: ["cn"],
30754
+ credits: { cn: "0.11" },
30618
30755
  supportsImage: true,
30619
30756
  reasoningEfforts: [
30620
30757
  "low",
@@ -30632,6 +30769,7 @@ const FALLBACK_MODELS$1 = [
30632
30769
  maxContextWindow: 168e3,
30633
30770
  maxTokens: 32e3,
30634
30771
  regions: ["cn"],
30772
+ credits: { cn: "0.23" },
30635
30773
  supportsImage: false,
30636
30774
  reasoningEfforts: [],
30637
30775
  defaultReasoningEffort: null,
@@ -30645,6 +30783,7 @@ const FALLBACK_MODELS$1 = [
30645
30783
  maxContextWindow: 1e6,
30646
30784
  maxTokens: 32e3,
30647
30785
  regions: ["intl"],
30786
+ credits: { intl: "1.62" },
30648
30787
  supportsImage: true,
30649
30788
  reasoningEfforts: [
30650
30789
  "low",
@@ -30662,6 +30801,7 @@ const FALLBACK_MODELS$1 = [
30662
30801
  maxContextWindow: 1e6,
30663
30802
  maxTokens: 32e3,
30664
30803
  regions: ["cn"],
30804
+ credits: { cn: "1.62" },
30665
30805
  supportsImage: true,
30666
30806
  reasoningEfforts: [
30667
30807
  "low",
@@ -30677,8 +30817,12 @@ const FALLBACK_MODELS$1 = [
30677
30817
  name: "Kimi-K2.8-Preview",
30678
30818
  contextWindow: 3e5,
30679
30819
  maxContextWindow: 1e6,
30680
- maxTokens: 64e3,
30820
+ maxTokens: 32e3,
30681
30821
  regions: ["intl", "cn"],
30822
+ credits: {
30823
+ cn: "0.77",
30824
+ intl: "0.77"
30825
+ },
30682
30826
  supportsImage: true,
30683
30827
  reasoningEfforts: [
30684
30828
  "low",
@@ -30696,6 +30840,7 @@ const FALLBACK_MODELS$1 = [
30696
30840
  maxContextWindow: 256e3,
30697
30841
  maxTokens: 32e3,
30698
30842
  regions: ["cn"],
30843
+ credits: { cn: "0.57" },
30699
30844
  supportsImage: true,
30700
30845
  reasoningEfforts: [
30701
30846
  "low",
@@ -30713,6 +30858,10 @@ const FALLBACK_MODELS$1 = [
30713
30858
  maxContextWindow: 256e3,
30714
30859
  maxTokens: 32e3,
30715
30860
  regions: ["intl", "cn"],
30861
+ credits: {
30862
+ cn: "0.52",
30863
+ intl: "0.52"
30864
+ },
30716
30865
  supportsImage: true,
30717
30866
  reasoningEfforts: [
30718
30867
  "low",
@@ -30730,6 +30879,7 @@ const FALLBACK_MODELS$1 = [
30730
30879
  maxContextWindow: 164e3,
30731
30880
  maxTokens: 32e3,
30732
30881
  regions: ["cn"],
30882
+ credits: { cn: "0.45" },
30733
30883
  supportsImage: true,
30734
30884
  reasoningEfforts: [
30735
30885
  "low",
@@ -30747,6 +30897,7 @@ const FALLBACK_MODELS$1 = [
30747
30897
  maxContextWindow: 164e3,
30748
30898
  maxTokens: 32e3,
30749
30899
  regions: ["cn"],
30900
+ credits: { cn: "0.54" },
30750
30901
  supportsImage: false,
30751
30902
  reasoningEfforts: [
30752
30903
  "low",
@@ -30764,6 +30915,10 @@ const FALLBACK_MODELS$1 = [
30764
30915
  maxContextWindow: 1e6,
30765
30916
  maxTokens: 128e3,
30766
30917
  regions: ["intl", "cn"],
30918
+ credits: {
30919
+ cn: "0.11",
30920
+ intl: "0.00"
30921
+ },
30767
30922
  supportsImage: true,
30768
30923
  reasoningEfforts: [
30769
30924
  "low",
@@ -30781,6 +30936,7 @@ const FALLBACK_MODELS$1 = [
30781
30936
  maxContextWindow: 1e6,
30782
30937
  maxTokens: 128e3,
30783
30938
  regions: ["intl"],
30939
+ credits: { intl: "0.03" },
30784
30940
  supportsImage: true,
30785
30941
  reasoningEfforts: [
30786
30942
  "low",
@@ -30798,6 +30954,7 @@ const FALLBACK_MODELS$1 = [
30798
30954
  maxContextWindow: 1e6,
30799
30955
  maxTokens: 128e3,
30800
30956
  regions: ["cn"],
30957
+ credits: { cn: "0.51" },
30801
30958
  supportsImage: true,
30802
30959
  reasoningEfforts: [
30803
30960
  "low",
@@ -30815,6 +30972,7 @@ const FALLBACK_MODELS$1 = [
30815
30972
  maxContextWindow: 1e6,
30816
30973
  maxTokens: 5e4,
30817
30974
  regions: ["cn"],
30975
+ credits: { cn: "0.17" },
30818
30976
  supportsImage: true,
30819
30977
  reasoningEfforts: [
30820
30978
  "low",
@@ -30832,6 +30990,7 @@ const FALLBACK_MODELS$1 = [
30832
30990
  maxContextWindow: 96e3,
30833
30991
  maxTokens: 32e3,
30834
30992
  regions: ["cn"],
30993
+ credits: { cn: "0.29" },
30835
30994
  supportsImage: false,
30836
30995
  reasoningEfforts: [
30837
30996
  "low",
@@ -30849,6 +31008,10 @@ const FALLBACK_MODELS$1 = [
30849
31008
  maxContextWindow: 1e6,
30850
31009
  maxTokens: 64e3,
30851
31010
  regions: ["intl", "cn"],
31011
+ credits: {
31012
+ cn: "0.29",
31013
+ intl: "0.29"
31014
+ },
30852
31015
  supportsImage: true,
30853
31016
  reasoningEfforts: ["high"],
30854
31017
  defaultReasoningEffort: "high",
@@ -30856,12 +31019,13 @@ const FALLBACK_MODELS$1 = [
30856
31019
  description: "混元思考模型,具有增强的推理能力"
30857
31020
  },
30858
31021
  {
30859
- id: "hy4-preview-x",
31022
+ id: "hy4-preview-f",
30860
31023
  name: "Hy4 preview",
30861
31024
  contextWindow: 1e6,
30862
31025
  maxContextWindow: 1e6,
30863
31026
  maxTokens: 64e3,
30864
31027
  regions: ["cn"],
31028
+ credits: { cn: "0.00" },
30865
31029
  supportsImage: true,
30866
31030
  reasoningEfforts: ["high"],
30867
31031
  defaultReasoningEffort: "high",
@@ -30875,6 +31039,10 @@ const FALLBACK_MODELS$1 = [
30875
31039
  maxContextWindow: 192e3,
30876
31040
  maxTokens: 64e3,
30877
31041
  regions: ["intl", "cn"],
31042
+ credits: {
31043
+ cn: "0.00",
31044
+ intl: "0.00"
31045
+ },
30878
31046
  supportsImage: true,
30879
31047
  reasoningEfforts: ["low", "high"],
30880
31048
  defaultReasoningEffort: "high",
@@ -30888,6 +31056,7 @@ const FALLBACK_MODELS$1 = [
30888
31056
  maxContextWindow: 192e3,
30889
31057
  maxTokens: 64e3,
30890
31058
  regions: ["cn"],
31059
+ credits: { cn: "0.05" },
30891
31060
  supportsImage: true,
30892
31061
  reasoningEfforts: [
30893
31062
  "low",
@@ -30905,12 +31074,33 @@ const FALLBACK_MODELS$1 = [
30905
31074
  maxContextWindow: 2e5,
30906
31075
  maxTokens: 8192,
30907
31076
  regions: ["cn"],
31077
+ credits: { cn: "0.10" },
30908
31078
  supportsImage: false,
30909
31079
  reasoningEfforts: [],
30910
31080
  defaultReasoningEffort: null,
30911
31081
  canDisableThinking: false,
30912
31082
  description: ""
30913
31083
  },
31084
+ {
31085
+ id: "space-bunny",
31086
+ name: "Space-Bunny",
31087
+ contextWindow: 1e6,
31088
+ maxContextWindow: 1e6,
31089
+ maxTokens: 128e3,
31090
+ regions: ["cn"],
31091
+ credits: { cn: "0.08" },
31092
+ supportsImage: true,
31093
+ reasoningEfforts: [
31094
+ "low",
31095
+ "medium",
31096
+ "high",
31097
+ "xhigh",
31098
+ "max"
31099
+ ],
31100
+ defaultReasoningEffort: "max",
31101
+ canDisableThinking: false,
31102
+ description: "推理速度极快,编码能力强劲,并支持原生多模态输入的匿名大模型"
31103
+ },
30914
31104
  {
30915
31105
  id: "minimax-m3",
30916
31106
  name: "MiniMax-M3",
@@ -30918,6 +31108,7 @@ const FALLBACK_MODELS$1 = [
30918
31108
  maxContextWindow: 512e3,
30919
31109
  maxTokens: 64e3,
30920
31110
  regions: ["cn"],
31111
+ credits: { cn: "0.25" },
30921
31112
  supportsImage: true,
30922
31113
  reasoningEfforts: [
30923
31114
  "low",
@@ -30935,6 +31126,7 @@ const FALLBACK_MODELS$1 = [
30935
31126
  maxContextWindow: 2e5,
30936
31127
  maxTokens: 48e3,
30937
31128
  regions: ["cn"],
31129
+ credits: { cn: "0.19" },
30938
31130
  supportsImage: true,
30939
31131
  reasoningEfforts: [
30940
31132
  "low",
@@ -30952,6 +31144,7 @@ const FALLBACK_MODELS$1 = [
30952
31144
  maxContextWindow: 2e5,
30953
31145
  maxTokens: 48e3,
30954
31146
  regions: ["cn"],
31147
+ credits: { cn: "0.18" },
30955
31148
  supportsImage: false,
30956
31149
  reasoningEfforts: [
30957
31150
  "low",
@@ -30962,6 +31155,7 @@ const FALLBACK_MODELS$1 = [
30962
31155
  canDisableThinking: false,
30963
31156
  description: ""
30964
31157
  },
31158
+ ...UNPUBLISHED_MODELS.filter((model) => model.id === "gemini-3.8-flash"),
30965
31159
  {
30966
31160
  id: "gemini-3.5-flash",
30967
31161
  name: "Gemini-3.5-Flash",
@@ -30969,6 +31163,7 @@ const FALLBACK_MODELS$1 = [
30969
31163
  maxContextWindow: 1e6,
30970
31164
  maxTokens: 65536,
30971
31165
  regions: ["intl"],
31166
+ credits: { intl: "0.99" },
30972
31167
  supportsImage: true,
30973
31168
  reasoningEfforts: [
30974
31169
  "low",
@@ -30997,6 +31192,21 @@ const DEFAULT_VISIBLE_MODEL_IDS$1 = [
30997
31192
  /** Every id the fallback knows, in declaration order. */
30998
31193
  const WORKBUDDY_MODEL_IDS = FALLBACK_MODELS$1.map((model) => model.id);
30999
31194
  /**
31195
+ * Add the served-but-unpublished models to a live `/v3/config` catalog.
31196
+ *
31197
+ * A reachable gateway replaces the shipped table with the list it publishes, so
31198
+ * a model it serves without listing ({@link UNPUBLISHED_MODELS}) would drop out
31199
+ * of the picker the moment the catalog loads. Merging here keeps it offered.
31200
+ *
31201
+ * A published entry always wins: the gateway is the authority on anything it
31202
+ * does describe, so these rows only fill ids the payload is silent about.
31203
+ */
31204
+ function withUnpublishedModels(catalog, region) {
31205
+ const known = new Set(catalog.map((model) => model.id));
31206
+ const extra = UNPUBLISHED_MODELS.filter((model) => model.regions.includes(region) && !known.has(model.id));
31207
+ return extra.length > 0 ? [...catalog, ...extra] : [...catalog];
31208
+ }
31209
+ /**
31000
31210
  * Resolve one entry from a catalog.
31001
31211
  *
31002
31212
  * An unknown id resolves to a synthetic text-only entry rather than the first
@@ -31021,6 +31231,7 @@ function resolveWorkBuddyModel(id, catalog = FALLBACK_MODELS$1) {
31021
31231
  maxContextWindow: DEFAULT_CONTEXT_WINDOW$1,
31022
31232
  maxTokens: FALLBACK_MAX_TOKENS,
31023
31233
  regions: ["cn", "intl"],
31234
+ credits: {},
31024
31235
  supportsImage: false,
31025
31236
  reasoningEfforts: [],
31026
31237
  defaultReasoningEffort: null,
@@ -31028,9 +31239,131 @@ function resolveWorkBuddyModel(id, catalog = FALLBACK_MODELS$1) {
31028
31239
  description: ""
31029
31240
  };
31030
31241
  }
31031
- /** Models an account in `region` can actually call, in catalog order. */
31242
+ /**
31243
+ * Vendor families, resolved from a model id.
31244
+ *
31245
+ * The gateway orders `/v3/config` the way it feels like, which interleaves
31246
+ * vendors: a DeepSeek row, then a GPT row, then two Hunyuan rows, then a Kimi
31247
+ * row, then three more GPT rows. In a picker that reads as noise — the eye
31248
+ * cannot tell that the GPT entries belong together — so the list is grouped by
31249
+ * family before it is shown.
31250
+ *
31251
+ * The rules are ordered, first match wins. Only families this subscription
31252
+ * actually serves are listed: a rule for a model that cannot appear would be a
31253
+ * guess about a future catalog.
31254
+ *
31255
+ * An id no rule claims is its own family rather than a shared `other` bucket, so
31256
+ * an unrecognised model keeps its place in the order instead of being swept into
31257
+ * a pile of models it has nothing to do with.
31258
+ */
31259
+ const WORKBUDDY_VENDOR_RULES = [
31260
+ {
31261
+ re: /^(default|fast|balanced|primary|deep)(-model)?$/,
31262
+ family: "alias"
31263
+ },
31264
+ {
31265
+ re: /^gpt-/,
31266
+ family: "openai"
31267
+ },
31268
+ {
31269
+ re: /^gemini-/,
31270
+ family: "google"
31271
+ },
31272
+ {
31273
+ re: /^glm-/,
31274
+ family: "zhipu"
31275
+ },
31276
+ {
31277
+ re: /^kimi-/,
31278
+ family: "moonshot"
31279
+ },
31280
+ {
31281
+ re: /^deepseek-/,
31282
+ family: "deepseek"
31283
+ },
31284
+ {
31285
+ re: /^hy[0-9]/,
31286
+ family: "tencent"
31287
+ },
31288
+ {
31289
+ re: /^hunyuan/,
31290
+ family: "tencent"
31291
+ },
31292
+ {
31293
+ re: /^minimax-/,
31294
+ family: "minimax"
31295
+ },
31296
+ {
31297
+ re: /^space-bunny$/,
31298
+ family: "space-bunny"
31299
+ }
31300
+ ];
31301
+ /** The vendor family one model belongs to. */
31302
+ function workBuddyModelVendor(id) {
31303
+ const value = id.toLowerCase();
31304
+ for (const rule of WORKBUDDY_VENDOR_RULES) if (rule.re.test(value)) return rule.family;
31305
+ return id;
31306
+ }
31307
+ /**
31308
+ * Numeric segments of a model id, e.g. `gpt-6-astra` → [6].
31309
+ *
31310
+ * Version numbers are compared as numbers, so GPT-6.0 sorts below GPT-6.6 rather
31311
+ * than above it as it would if the strings were compared.
31312
+ */
31313
+ function versionSegments$1(id) {
31314
+ return [...id.toLowerCase().matchAll(/\d+/g)].map((match) => Number(match[0]));
31315
+ }
31316
+ /**
31317
+ * Order two models of one family, newest generation first.
31318
+ *
31319
+ * Catalog order alone is not good enough. The models served but not published
31320
+ * are appended after the published list, which left `gpt-6-sol` below
31321
+ * `gpt-5.4` — the newest models at the bottom of their own group. Comparing the
31322
+ * numeric segments puts every GPT-6 row together and ahead of every GPT-5 one,
31323
+ * and leaves an id whose version cannot be read (an alias, a nickname) in the
31324
+ * catalog's own order rather than guessing where it belongs.
31325
+ *
31326
+ * Ties keep catalog order, so `deepseek-v4.1-flash` still leads its
31327
+ * `-flash-sg` sibling.
31328
+ */
31329
+ function compareWithinFamily(a, b) {
31330
+ const left = versionSegments$1(a.id);
31331
+ const right = versionSegments$1(b.id);
31332
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
31333
+ const x = left[index];
31334
+ const y = right[index];
31335
+ if (x === void 0) return 1;
31336
+ if (y === void 0) return -1;
31337
+ if (x !== y) return y - x;
31338
+ }
31339
+ return 0;
31340
+ }
31341
+ /**
31342
+ * Group a list so every vendor's models sit together.
31343
+ *
31344
+ * Families appear in the order their first model does — the catalog decides
31345
+ * which vendor leads, not this function — and each family is ordered newest
31346
+ * generation first.
31347
+ */
31348
+ function groupWorkBuddyModelsByVendor(models) {
31349
+ const families = /* @__PURE__ */ new Map();
31350
+ for (const model of models) {
31351
+ const family = workBuddyModelVendor(model.id);
31352
+ const bucket = families.get(family);
31353
+ if (bucket === void 0) families.set(family, [model]);
31354
+ else bucket.push(model);
31355
+ }
31356
+ return [...families.values()].map((bucket) => [...bucket].sort(compareWithinFamily)).flat();
31357
+ }
31358
+ /**
31359
+ * Models an account in `region` can actually call, grouped by vendor.
31360
+ *
31361
+ * Grouped here rather than in the settings card because this is also what the
31362
+ * request path resolves against: one ordering rule, so the picker and the
31363
+ * adapter can never disagree about which models a region offers.
31364
+ */
31032
31365
  function modelsForRegion(region, catalog = FALLBACK_MODELS$1) {
31033
- return catalog.filter((model) => model.regions.includes(region));
31366
+ return groupWorkBuddyModelsByVendor(catalog.filter((model) => model.regions.includes(region)));
31034
31367
  }
31035
31368
  /** Whether one model accepts image input. */
31036
31369
  function workBuddyModelSupportsImage(id, catalog = FALLBACK_MODELS$1) {
@@ -31905,6 +32238,14 @@ async function fetchAccountIdentity$1(credentials, options = {}) {
31905
32238
  * inferred from the model's name. Image-generation tools (`tags` naming
31906
32239
  * `text-to-image`/`image-to-image`) are skipped: they are not chat models and
31907
32240
  * carry no context window.
32241
+ *
32242
+ * `credits` is read here rather than left to the shipped table because a
32243
+ * reachable gateway **replaces** that table entirely: a multiplier transcribed
32244
+ * offline would be the only one a signed-in user ever saw, and it would stay
32245
+ * wrong the moment the subscription repriced a model. A string this route cannot
32246
+ * turn into a number (`''`, which `default-model` publishes, or a value a
32247
+ * future gateway invents) leaves the field off the entry altogether instead of
32248
+ * writing a plausible-looking `0` — see `parseWorkBuddyCreditMultiplier`.
31908
32249
  */
31909
32250
  function parseConfigModels(payload, region) {
31910
32251
  const root = asRecord$4(payload) ?? {};
@@ -31926,6 +32267,7 @@ function parseConfigModels(payload, region) {
31926
32267
  const declaredDefault = asString$5(reasoning.defaultEffort) ?? single ?? null;
31927
32268
  const modelDefault = declaredDefault === null || efforts.includes(declaredDefault) ? declaredDefault : convergeWorkBuddyEffort(declaredDefault, efforts);
31928
32269
  const servedDefault = asNumber$1(asRecord$4(record.contextWindow)?.defaultLength);
32270
+ const multiplier = parseWorkBuddyCreditMultiplier(record.credits);
31929
32271
  models.push({
31930
32272
  id,
31931
32273
  name: asString$5(record.name) ?? id,
@@ -31933,6 +32275,7 @@ function parseConfigModels(payload, region) {
31933
32275
  maxContextWindow,
31934
32276
  maxTokens: asNumber$1(record.maxOutputTokens) ?? 32768,
31935
32277
  regions: [region],
32278
+ ...multiplier === null ? {} : { credits: { [region]: multiplier } },
31936
32279
  supportsImage: record.supportsImages === true,
31937
32280
  reasoningEfforts: efforts,
31938
32281
  defaultReasoningEffort: modelDefault,
@@ -31980,7 +32323,7 @@ async function fetchConfigCatalog(credentials, options = {}) {
31980
32323
  signal: timeoutSignal$1(options.signal, DISCOVERY_TIMEOUT_MS$1)
31981
32324
  });
31982
32325
  if (!response.ok) throw new Error(`${PROVIDER_NAME$1} model catalog failed: ${response.status}`);
31983
- return parseConfigModels(await response.json(), credentials.region);
32326
+ return withUnpublishedModels(parseConfigModels(await response.json(), credentials.region), credentials.region);
31984
32327
  }
31985
32328
  /** Cached catalog with a TTL; a failed refresh keeps the previous snapshot.
31986
32329
  *
@@ -32014,7 +32357,7 @@ async function loadConfigCatalog(credentials, options = {}) {
32014
32357
  }, (fetchedAt, models) => {
32015
32358
  cachedCatalog = {
32016
32359
  region: credentials.region,
32017
- models,
32360
+ models: withUnpublishedModels(models, credentials.region),
32018
32361
  fetchedAt
32019
32362
  };
32020
32363
  }, 0) > 0) {
@@ -32540,7 +32883,20 @@ function resolveEnabledModelIds$2(stored, available, enabled = true) {
32540
32883
  const known = new Set(available);
32541
32884
  return stored.filter((id) => known.has(id));
32542
32885
  }
32543
- /** Build the model options the settings card renders, in catalog order. */
32886
+ /**
32887
+ * Build the model options the settings card renders.
32888
+ *
32889
+ * Order comes from the caller, which passes a vendor-grouped list so a family
32890
+ * is not interleaved with the next one; this preserves whatever order it is
32891
+ * given rather than imposing a second one.
32892
+ *
32893
+ * `credits` is passed through whole, every region the entry carries. Trimming
32894
+ * it to one account's region is tempting and wrong: this DTO is also read with
32895
+ * no account in hand (the harness resolves a model list before a credential is
32896
+ * chosen), and such a caller would be told nothing rather than everything. The
32897
+ * region key the reader actually has is already on each entry as `regions`, so
32898
+ * picking a value is the renderer's job, not this one's.
32899
+ */
32544
32900
  function buildModelOptions(catalog, available, enabledModelIds, overrides) {
32545
32901
  const enabled = new Set(enabledModelIds);
32546
32902
  return available.map((id) => {
@@ -32557,6 +32913,7 @@ function buildModelOptions(catalog, available, enabledModelIds, overrides) {
32557
32913
  reasoningEfforts: [...workBuddyReasoningEfforts(id, catalog)],
32558
32914
  supportsImage: workBuddyModelSupportsImage(id, catalog),
32559
32915
  regions: [...entry.regions],
32916
+ ...entry.credits === void 0 || Object.keys(entry.credits).length === 0 ? {} : { credits: { ...entry.credits } },
32560
32917
  description: entry.description
32561
32918
  };
32562
32919
  });
@@ -45550,6 +45907,22 @@ function installRelayProbe(ctx, options) {
45550
45907
  * degenerate stream scores near one. On a hit it stops yielding chunks, so the
45551
45908
  * in-flight request ends, and lets the turn resume on a fresh step.
45552
45909
  *
45910
+ * Uniqueness alone only sees verbatim repetition. A stream can restate one
45911
+ * conclusion over and over in fresh words, score 0.48 where the threshold is
45912
+ * 0.85, and run to the output ceiling untouched. So a second, independent
45913
+ * signal scores the same window for recurrence: how strongly each sentence
45914
+ * repeats something the model already said at least `semanticLag` sentences
45915
+ * earlier. Enumerative reasoning repeats itself too, but adjacently — each item
45916
+ * borrows the previous item's frame — and recurrence ignores adjacency, which
45917
+ * is what separates the two.
45918
+ *
45919
+ * Neither signal stops anything on its own. A break needs both over their
45920
+ * thresholds in the same window, and then `semanticConfirmations` consecutive
45921
+ * such windows: one signal alone, or a single anomalous window, is logged and
45922
+ * released. That gradient is the whole design. A guard that cuts a healthy
45923
+ * answer is worse than one that lets a runaway finish, because the user sees a
45924
+ * turn stop mid-sentence with no explanation, and the fix costs a whole turn.
45925
+ *
45553
45926
  * Seam choice: the guard wraps `llm/stream`, not `agent/assistant-stream`.
45554
45927
  * `agent/assistant-stream` is emit-mode — it can observe but cannot stop a
45555
45928
  * stream — and it does not exist before harness 0.1.5, which would make the
@@ -45586,6 +45959,15 @@ const DEFAULT_GUARD_OPTIONS = {
45586
45959
  maxBreaksPerTurn: 3,
45587
45960
  cooldownMs: 3e4,
45588
45961
  includeModels: [],
45962
+ semanticThreshold: .35,
45963
+ semanticLag: 6,
45964
+ semanticMinChars: 360,
45965
+ semanticMinSentences: 8,
45966
+ semanticConfirmations: 3,
45967
+ semanticLiteralFloor: .6,
45968
+ semanticStrictThreshold: .45,
45969
+ semanticDistinctCeiling: .47,
45970
+ semanticStrictConfirmations: 5,
45589
45971
  now: () => Date.now()
45590
45972
  };
45591
45973
  function pruneUndefined(options) {
@@ -45611,8 +45993,21 @@ function resolveGuardOptions(options = {}) {
45611
45993
  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
45994
  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
45995
  if (!Number.isInteger(resolved.cooldownMs) || resolved.cooldownMs < 0) throw new Error(`reasoning-collapse-guard: invalid cooldownMs ${String(resolved.cooldownMs)} — must be an integer >= 0`);
45996
+ 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`);
45997
+ if (!Number.isInteger(resolved.semanticLag) || resolved.semanticLag < 1) throw new Error(`reasoning-collapse-guard: invalid semanticLag ${String(resolved.semanticLag)} — must be an integer >= 1`);
45998
+ if (!Number.isInteger(resolved.semanticMinChars) || resolved.semanticMinChars < 1) throw new Error(`reasoning-collapse-guard: invalid semanticMinChars ${String(resolved.semanticMinChars)} — must be an integer >= 1`);
45999
+ if (resolved.semanticMinChars > resolved.windowChars) throw new Error(`reasoning-collapse-guard: invalid semanticMinChars ${resolved.semanticMinChars} — must not exceed windowChars ${resolved.windowChars}`);
46000
+ if (!Number.isInteger(resolved.semanticMinSentences) || resolved.semanticMinSentences < 2) throw new Error(`reasoning-collapse-guard: invalid semanticMinSentences ${String(resolved.semanticMinSentences)} — must be an integer >= 2`);
46001
+ if (!Number.isInteger(resolved.semanticConfirmations) || resolved.semanticConfirmations < 1) throw new Error(`reasoning-collapse-guard: invalid semanticConfirmations ${String(resolved.semanticConfirmations)} — must be an integer >= 1`);
46002
+ 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`);
46003
+ 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`);
46004
+ 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`);
46005
+ 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
46006
  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
46007
  if (typeof resolved.now !== "function") throw new Error("reasoning-collapse-guard: now must be a function");
46008
+ resolved.semanticStrictThreshold = Math.max(resolved.semanticStrictThreshold, resolved.semanticThreshold);
46009
+ resolved.semanticStrictConfirmations = Math.max(resolved.semanticStrictConfirmations, resolved.semanticConfirmations + 1);
46010
+ resolved.semanticLiteralFloor = Math.min(resolved.semanticLiteralFloor, resolved.threshold);
45616
46011
  return resolved;
45617
46012
  }
45618
46013
  /**
@@ -45635,6 +46030,92 @@ function collapseScore(window, n) {
45635
46030
  if (total === 0) return 0;
45636
46031
  return 1 - grams.size / total;
45637
46032
  }
46033
+ /**
46034
+ * Split reasoning text into sentences, for mixed CJK and Latin streams.
46035
+ *
46036
+ * A Chinese full stop ends a sentence whatever follows it. An ASCII `.!?` only
46037
+ * ends one when whitespace or the end of the text follows, because the same
46038
+ * characters carry out decimals, file names and version numbers
46039
+ * (`selectors.test.ts:745`, `3.14`) that must not become sentence
46040
+ * edges. Whitespace runs collapse and case folds, so a difference in spacing
46041
+ * or capitalisation cannot read as a difference in wording.
46042
+ *
46043
+ * @param text - reasoning text, typically one trailing window.
46044
+ * @returns non-empty normalized sentences, in order.
46045
+ */
46046
+ function splitSentences(text) {
46047
+ const parts = [];
46048
+ let start = 0;
46049
+ for (let i = 0; i < text.length; i++) {
46050
+ const char = text[i];
46051
+ const next = text[i + 1];
46052
+ const cjk = CJK_TERMINATORS.has(char);
46053
+ const latin = LATIN_TERMINATORS.has(char) && (next === void 0 || TRAILING_SPACE.test(next));
46054
+ if (!cjk && !latin) continue;
46055
+ parts.push(text.slice(start, i + 1));
46056
+ start = i + 1;
46057
+ }
46058
+ parts.push(text.slice(start));
46059
+ const sentences = [];
46060
+ for (const part of parts) {
46061
+ const normalized = part.replace(WHITESPACE_RUN, " ").trim().toLowerCase();
46062
+ if (normalized.length > 0) sentences.push(normalized);
46063
+ }
46064
+ return sentences;
46065
+ }
46066
+ /** The three Chinese sentence terminators, which need no trailing space. */
46067
+ const CJK_TERMINATORS = /* @__PURE__ */ new Set([
46068
+ "。",
46069
+ "!",
46070
+ "?"
46071
+ ]);
46072
+ /** The three ASCII terminators, which only close a sentence before whitespace. */
46073
+ const LATIN_TERMINATORS = /* @__PURE__ */ new Set([
46074
+ ".",
46075
+ "!",
46076
+ "?"
46077
+ ]);
46078
+ const TRAILING_SPACE = /\s/;
46079
+ const WHITESPACE_RUN = /\s+/g;
46080
+ /** Character n-grams of one normalized sentence, as a set. */
46081
+ function sentenceGrams(sentence) {
46082
+ const grams = /* @__PURE__ */ new Set();
46083
+ for (let i = 0; i + SEMANTIC_NGRAM_SIZE <= sentence.length; i++) grams.add(sentence.slice(i, i + SEMANTIC_NGRAM_SIZE));
46084
+ return grams;
46085
+ }
46086
+ function gramJaccard(a, b) {
46087
+ if (a.size === 0 || b.size === 0) return 0;
46088
+ let shared = 0;
46089
+ for (const gram of a) if (b.has(gram)) shared++;
46090
+ return shared / (a.size + b.size - shared);
46091
+ }
46092
+ /** {@link semanticRecurrence} over sentences the caller has already split. */
46093
+ function sentenceRecurrence(sentences, lag) {
46094
+ const grams = sentences.map(sentenceGrams);
46095
+ if (grams.length <= lag) return 0;
46096
+ let total = 0;
46097
+ let counted = 0;
46098
+ for (let i = lag; i < grams.length; i++) {
46099
+ const current = grams[i];
46100
+ let best = 0;
46101
+ for (let j = 0; j <= i - lag; j++) {
46102
+ const earlier = grams[j];
46103
+ if (Math.min(current.size, earlier.size) / Math.max(current.size, earlier.size) <= best) continue;
46104
+ const similarity = gramJaccard(current, earlier);
46105
+ if (similarity > best) best = similarity;
46106
+ }
46107
+ total += best;
46108
+ counted++;
46109
+ }
46110
+ return counted === 0 ? 0 : total / counted;
46111
+ }
46112
+ /** Character n-gram size both halves of the semantic signal are built on. */
46113
+ const SEMANTIC_NGRAM_SIZE = 3;
46114
+ /** {@link distinctSentenceRatio} over sentences the caller already split. */
46115
+ function distinctRatioOf(sentences) {
46116
+ if (sentences.length < 2) return 1;
46117
+ return new Set(sentences).size / sentences.length;
46118
+ }
45638
46119
  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
46120
  /** The second resume message, used once a resume has also collapsed. */
45640
46121
  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 +46124,11 @@ function createBreakerState() {
45643
46124
  breaksThisTurn: 0,
45644
46125
  lastBreakAt: -Infinity,
45645
46126
  exhausted: false,
45646
- resuming: false
46127
+ resuming: false,
46128
+ dualStreak: 0,
46129
+ lowStreak: 0,
46130
+ notedLiteralOnly: false,
46131
+ notedSemanticOnly: false
45647
46132
  };
45648
46133
  }
45649
46134
  /**
@@ -45798,10 +46283,12 @@ function installReasoningCollapseGuard(ctx, options = {}) {
45798
46283
  * point — and the log says so, because a break that cannot resume is
45799
46284
  * indistinguishable from a broken turn to the user.
45800
46285
  */
45801
- function breakOff(state, score, agent, sessionId) {
46286
+ function breakOff(state, literal, semantic, path, agent, sessionId) {
45802
46287
  state.breaksThisTurn++;
45803
46288
  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" : ""})`);
46289
+ state.dualStreak = 0;
46290
+ state.lowStreak = 0;
46291
+ 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
46292
  if (agent === void 0) return;
45806
46293
  agent.cancel?.({
45807
46294
  kind: "hook",
@@ -45819,6 +46306,48 @@ function installReasoningCollapseGuard(ctx, options = {}) {
45819
46306
  }
45820
46307
  });
45821
46308
  }
46309
+ /**
46310
+ * Report one signal holding without the other.
46311
+ *
46312
+ * Reported once per kind per turn, not once per window: a runaway can hold
46313
+ * one signal for hundreds of windows, and a line each would bury the report
46314
+ * of the one that mattered. This is the diagnostic half of the gradient —
46315
+ * the reason a near miss is still diagnosable after the fact.
46316
+ */
46317
+ function noteLoneSignal(state, kind, literal, semantic, sessionId) {
46318
+ if (kind === "literal" ? state.notedLiteralOnly : state.notedSemanticOnly) return;
46319
+ if (kind === "literal") state.notedLiteralOnly = true;
46320
+ else state.notedSemanticOnly = true;
46321
+ 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`);
46322
+ }
46323
+ /**
46324
+ * Report a run of agreeing windows that ended before it reached the count.
46325
+ *
46326
+ * Without this, two short runs of agreeing windows look in the log exactly
46327
+ * like one long one that simply stopped short, and the confirmation count
46328
+ * becomes impossible to reason about after the fact.
46329
+ */
46330
+ function noteStreakCleared(state, reached, sessionId) {
46331
+ ctx.logger?.warn(`reasoning-collapse-guard: the signals stopped agreeing after ${reached} of ${resolved.semanticConfirmations} windows (session ${sessionId ?? "unidentified"}) — streak cleared, not acting`);
46332
+ }
46333
+ /** Report the low-literal run that ended before it reached its longer count. */
46334
+ function noteLowStreakCleared(reached, sessionId) {
46335
+ 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`);
46336
+ }
46337
+ /** Report a low-literal window that qualifies but has not been confirmed enough. */
46338
+ function notePendingLowConfirmation(state, literal, semantic, distinct, sessionId) {
46339
+ 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`);
46340
+ }
46341
+ /**
46342
+ * Report a window where both signals held but the streak is still short.
46343
+ *
46344
+ * `semanticConfirmations` is what makes a break a decision rather than a
46345
+ * reflex, so each pending confirmation is named, and the log then says how
46346
+ * close a stream came to being cut by a guard that did not cut it.
46347
+ */
46348
+ function notePendingConfirmation(state, literal, semantic, sessionId) {
46349
+ 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`);
46350
+ }
45822
46351
  function guardStream(request, next) {
45823
46352
  const state = breakerFor(request.signal ?? STATIC_KEY);
45824
46353
  if (state.exhausted || !watchedModel(request.model) || !conversationCall(request)) return next();
@@ -45829,13 +46358,33 @@ function installReasoningCollapseGuard(ctx, options = {}) {
45829
46358
  if (chunk.type === "block-start") window = "";
45830
46359
  if (chunk.type === "reasoning-delta" && typeof chunk.text === "string") {
45831
46360
  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;
46361
+ if (resolved.now() - state.lastBreakAt >= resolved.cooldownMs) {
46362
+ const literal = window.length >= resolved.minWindowChars ? collapseScore(window, resolved.ngramSize) : void 0;
46363
+ const sentences = window.length >= resolved.semanticMinChars ? splitSentences(window) : [];
46364
+ const semantic = sentences.length >= resolved.semanticMinSentences ? sentenceRecurrence(sentences, resolved.semanticLag) : void 0;
46365
+ const distinct = sentences.length >= resolved.semanticMinSentences ? distinctRatioOf(sentences) : void 0;
46366
+ const high = literal !== void 0 && literal >= resolved.threshold && semantic !== void 0 && semantic >= resolved.semanticThreshold;
46367
+ const low = literal !== void 0 && literal >= resolved.semanticLiteralFloor && literal < resolved.threshold && semantic !== void 0 && semantic >= resolved.semanticStrictThreshold && distinct !== void 0 && distinct <= resolved.semanticDistinctCeiling;
46368
+ const reachedHigh = state.dualStreak;
46369
+ const reachedLow = state.lowStreak;
46370
+ state.dualStreak = high ? reachedHigh + 1 : 0;
46371
+ state.lowStreak = low ? reachedLow + 1 : 0;
46372
+ if (reachedHigh !== 0 && state.dualStreak === 0) noteStreakCleared(state, reachedHigh, request.sessionId);
46373
+ if (reachedLow !== 0 && state.lowStreak === 0) noteLowStreakCleared(reachedLow, request.sessionId);
46374
+ if (high && state.dualStreak >= resolved.semanticConfirmations) if (state.breaksThisTurn >= resolved.maxBreaksPerTurn) state.exhausted = true;
46375
+ else {
46376
+ breakOff(state, literal, semantic, "high-literal", agent, request.sessionId);
46377
+ return;
46378
+ }
46379
+ else if (low && state.lowStreak >= resolved.semanticStrictConfirmations) if (state.breaksThisTurn >= resolved.maxBreaksPerTurn) state.exhausted = true;
45835
46380
  else {
45836
- breakOff(state, score, agent, request.sessionId);
46381
+ breakOff(state, literal, semantic, "low-literal", agent, request.sessionId);
45837
46382
  return;
45838
46383
  }
46384
+ else if (high) notePendingConfirmation(state, literal, semantic, request.sessionId);
46385
+ else if (low) notePendingLowConfirmation(state, literal, semantic, distinct, request.sessionId);
46386
+ else if (literal !== void 0 && literal >= resolved.threshold) noteLoneSignal(state, "literal", literal, semantic, request.sessionId);
46387
+ else if (semantic !== void 0 && semantic >= resolved.semanticThreshold) noteLoneSignal(state, "semantic", literal, semantic, request.sessionId);
45839
46388
  }
45840
46389
  }
45841
46390
  yield chunk;