@mars-sea/dsh-commandcode-provider 0.11.17 → 0.12.1

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
@@ -6,12 +6,96 @@ import { CONTEXT_WINDOW_EXCEEDED_CODE, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter,
6
6
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
7
7
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
8
8
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
9
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
9
+ import { appendFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
10
10
  import { createHash, randomBytes, randomUUID } from "node:crypto";
11
11
  import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
12
12
  import { createServer } from "node:http";
13
13
  import { createServer as createServer$1 } from "node:net";
14
14
  import { WebError } from "@deepseek-ai/dsh-web";
15
+ //#region src/cost-facts.ts
16
+ const zeroCostTokens = () => ({
17
+ uncachedInputTokens: 0,
18
+ outputTokens: 0,
19
+ cacheReadTokens: 0,
20
+ cacheWriteTokens: 0
21
+ });
22
+ const COST_TOKEN_KEYS = [
23
+ "uncachedInputTokens",
24
+ "outputTokens",
25
+ "cacheReadTokens",
26
+ "cacheWriteTokens"
27
+ ];
28
+ function peakHour(at, windows) {
29
+ const time = new Date(at);
30
+ return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
31
+ }
32
+ /**
33
+ * All prompt billing buckets determine a request's band, never cumulative
34
+ * session input.
35
+ *
36
+ * A tiered model returns the matching band and does NOT then apply `peak`: the
37
+ * page publishes those two dimensions independently and no row carries both
38
+ * today, so the combination becoming real is visible before it reaches a user.
39
+ * This ordering is a latent choice, not a verified upstream rule.
40
+ */
41
+ function requestRates(price, at, contextTokens, table) {
42
+ const tier = price.contextTiers?.find((tier) => tier.maxContext === void 0 || contextTokens <= tier.maxContext);
43
+ if (tier !== void 0) return tier;
44
+ return price.peak !== void 0 && at !== null && peakHour(at, table.peakHours) ? price.peak : price;
45
+ }
46
+ /** Cache version and wire guard: changing bands/rates must refold historical groups. */
47
+ function pricingKey(table) {
48
+ const rates = (r) => [
49
+ r.inputCost,
50
+ r.outputCost,
51
+ r.cacheReadCost,
52
+ r.cacheWriteCost ?? null
53
+ ];
54
+ const canonical = JSON.stringify([
55
+ "commandCodeCost-v1",
56
+ table.peakHours,
57
+ table.models.map((p) => [
58
+ p.id,
59
+ p.slug,
60
+ p.free === true,
61
+ rates(p),
62
+ p.peak ? rates(p.peak) : null,
63
+ p.contextTiers?.map((t) => [t.maxContext ?? null, rates(t)]) ?? null
64
+ ])
65
+ ]);
66
+ let hash = 2166136261;
67
+ for (const c of canonical) hash = Math.imul(hash ^ c.charCodeAt(0), 16777619) >>> 0;
68
+ return hash;
69
+ }
70
+ /** Only safe to merge requests whose billing classification is identical. */
71
+ function costGroupKey(group, table) {
72
+ const price = table.models.find((p) => p.id === group.model || p.slug === group.model);
73
+ return JSON.stringify([
74
+ group.provider,
75
+ group.model,
76
+ price === void 0 ? null : requestRates(price, group.at, group.contextTokens, table),
77
+ group.at === null
78
+ ]);
79
+ }
80
+ //#endregion
81
+ //#region src/plan-tiers.ts
82
+ /** Shared plan-tier presentation facts; safe to inline into the browser bundle. */
83
+ const PLAN_TIER_ORDER = [
84
+ "go",
85
+ "goat",
86
+ "pro",
87
+ "provider",
88
+ "max"
89
+ ];
90
+ const PLAN_LABELS = {
91
+ go: "Go",
92
+ goat: "GOAT",
93
+ pro: "Pro",
94
+ provider: "Provider",
95
+ max: "Max"
96
+ };
97
+ const PLAN_ORDER = Object.fromEntries(PLAN_TIER_ORDER.map((tier, index) => [tier, index]));
98
+ //#endregion
15
99
  //#region src/capabilities.ts
16
100
  /**
17
101
  * Static capability snapshot for the Command Code provider: model →
@@ -19,7 +103,7 @@ import { WebError } from "@deepseek-ai/dsh-web";
19
103
  * subscription-plan labels, deals, and hourly (peak/off-peak) pricing.
20
104
  *
21
105
  * Everything here is synced from official sources — the command-code CLI
22
- * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.66.0) and
106
+ * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.72.1) and
23
107
  * the official plan/pricing/model docs; see the dsh-commandcode-upstream skill
24
108
  * for the extraction procedures. Keeping the snapshot in its own module
25
109
  * confines those frequent sync diffs here: src/adapter.ts holds only the stable
@@ -103,6 +187,13 @@ const KNOWN_EFFORTS = {
103
187
  "xhigh",
104
188
  "max"
105
189
  ],
190
+ "claude-sonnet-5-5": [
191
+ "low",
192
+ "medium",
193
+ "high",
194
+ "xhigh",
195
+ "max"
196
+ ],
106
197
  "deepseek/deepseek-v4-flash-fast": [
107
198
  "low",
108
199
  "high",
@@ -113,6 +204,11 @@ const KNOWN_EFFORTS = {
113
204
  "high",
114
205
  "max"
115
206
  ],
207
+ "deepseek/deepseek-v4.1-flash-fast": [
208
+ "low",
209
+ "high",
210
+ "max"
211
+ ],
116
212
  "deepseek/deepseek-v4-flash": ["high", "max"],
117
213
  "deepseek/deepseek-v4-flash-vision-exp": ["high", "max"],
118
214
  "deepseek/deepseek-v4-pro": ["high", "max"],
@@ -312,6 +408,18 @@ const KNOWN_EFFORTS = {
312
408
  "low",
313
409
  "medium",
314
410
  "xhigh"
411
+ ],
412
+ "gpt-6.1-sol": [
413
+ "low",
414
+ "medium",
415
+ "high",
416
+ "xhigh",
417
+ "max"
418
+ ],
419
+ "inclusionai/ling-3.1-flash:free": [
420
+ "low",
421
+ "medium",
422
+ "high"
315
423
  ]
316
424
  };
317
425
  /**
@@ -348,8 +456,10 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
348
456
  "claude-opus-5",
349
457
  "claude-sonnet-4-6",
350
458
  "claude-sonnet-5",
459
+ "claude-sonnet-5-5",
351
460
  "deepseek/deepseek-v4-flash-vision-exp",
352
461
  "deepseek/deepseek-v4.1-flash",
462
+ "deepseek/deepseek-v4.1-flash-fast",
353
463
  "google/gemini-3.1-flash-lite",
354
464
  "google/gemini-3.5-flash",
355
465
  "google/gemini-3.5-flash-lite",
@@ -366,6 +476,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
366
476
  "gpt-6-astra",
367
477
  "gpt-6-luna",
368
478
  "gpt-6-sol",
479
+ "gpt-6.1-sol",
369
480
  "meta/muse-spark-1.1",
370
481
  "meta/muse-spark-1.2",
371
482
  "meta/muse-spark-1.2-contributor",
@@ -395,12 +506,13 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
395
506
  ]);
396
507
  /**
397
508
  * Models WITHOUT a zero-data-retention upstream, per the official CLI's own
398
- * registry (`command-code@1.66.0` `dist/cli.mjs`): `modelSupportsZdr(id)` is
509
+ * registry (`command-code@1.72.1` `dist/cli.mjs`): `modelSupportsZdr(id)` is
399
510
  * exactly `!nonZdrSet.has(canonicalize(id))`, and `knownModelSupportsZdr`
400
511
  * carries the same membership in the sibling route table — the UNION of both
401
512
  * is this set. Reading only the sibling route table would drop `meituan/
402
- * LongCat-2.0` and `stealth/pixel-canary`, which each appear in
403
- * `modelSupportsZdr` alone. The official docs (commandcode.ai/docs/resources/
513
+ * LongCat-2.0`, which appears in `modelSupportsZdr` alone (re-read from the
514
+ * 1.66.0 / 1.67.0 / 1.68.0 artifacts on 2026-09-29: the two stealth-preview
515
+ * rows sit in BOTH sets, so LongCat is the only divergence). The official docs (commandcode.ai/docs/resources/
404
516
  * zdr) put it in prose — "99% of our models have ZDR-capable upstreams … only
405
517
  * a small handful of models are affected" — so the CLI's exclusion list is
406
518
  * the only per-model evidence there is; a ZDR request naming one of these
@@ -422,12 +534,15 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
422
534
  * (its `zdr:{only:[…]}` provider routes and the per-provider `zdr`/`noTraining`
423
535
  * flags) is upstream-internal routing, not a per-model contract, so this table
424
536
  * is the snapshot of the exclusion set and nothing more. It is a rare change:
425
- * 20 members held across 1.62.0 → 1.64.0, and 1.65.0 and 1.66.0 each added
426
- * exactly one (the two stealth-preview models below).
537
+ * 20 members held across 1.62.0 → 1.64.0, 1.65.0 and 1.66.0 each added exactly
538
+ * one (the two stealth-preview models below), 1.67.0 added one
539
+ * (`deepseek/deepseek-v4.1-flash-fast`), and 1.68.0 changed nothing — 23
540
+ * members as of 2026-09-29.
427
541
  */
428
542
  const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
429
543
  "MiniMaxAI/MiniMax-M3",
430
544
  "Qwen/Qwen3.8-Max-0902",
545
+ "deepseek/deepseek-v4.1-flash-fast",
431
546
  "meituan/LongCat-2.0",
432
547
  "meta/muse-spark-1.1",
433
548
  "meta/muse-spark-1.2",
@@ -492,16 +607,22 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
492
607
  /**
493
608
  * Catalog models the Provider API serves ONLY through `/provider/v1/messages`
494
609
  * (Anthropic Messages shape). Every other model in the catalog answers on
495
- * `/provider/v1/chat/completions`; these reject it outright with HTTP 400
496
- * `Model "<id>" must be called via /provider/v1/messages (Anthropic Messages
497
- * shape)`.
610
+ * `/provider/v1/chat/completions`.
498
611
  *
499
- * Measured 2026-09-16 against the live catalog (command-code@1.54.0): all 69
500
- * models were posted to `/provider/v1/chat/completions`; exactly these eight
501
- * — the whole Claude family then — refused, and every one of them is routed
502
- * normally by `/alpha/generate` (a lower-plan key gets the ordinary
503
- * `MODEL_NOT_IN_PLAN` 403 there, never a routing error). So the CLI transport
504
- * is a complete fallback and the adapter does not need a Messages transport.
612
+ * Measured 2026-09-16 (command-code@1.54.0, 69 models posted to
613
+ * `/provider/v1/chat/completions`): exactly the then-entire Claude family
614
+ * refused with HTTP 400 `Model "<id>" must be called via
615
+ * /provider/v1/messages (Anthropic Messages shape)`, and every one of them is
616
+ * routed normally by `/alpha/generate`.
617
+ *
618
+ * Re-measured 2026-09-29 against the 84-model catalog: the same ten
619
+ * `claude-*` ids are the only ones carrying `supported_endpoints:
620
+ * ["/messages"]`, and posting a non-Claude model to the endpoint answers 400
621
+ * `Model "<id>" is not supported on this endpoint. Use
622
+ * /provider/v1/chat/completions for OpenAI and OSS models.` The adapter routes
623
+ * on that field when the catalog supplies it, so this set is the FALLBACK for
624
+ * a cached or hand-built catalog, not the primary source — see
625
+ * `requiresMessagesEndpoint()`.
505
626
  *
506
627
  * Note what this list is NOT: it is not a plan gate. `claude-sonnet-5` is
507
628
  * Pro-tier and `claude-opus-4-8` Provider-tier, so the accounts entitled to
@@ -510,20 +631,21 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
510
631
  * request before this snapshot existed.
511
632
  *
512
633
  * `requiresMessagesEndpoint()` additionally treats any `claude-*` id as
513
- * Messages-only, so a Claude model added upstream (`claude-opus-5-5` in
514
- * command-code@1.64.0 was the first this snapshot missed) is still routed
515
- * correctly by an un-updated plugin instead of hard-failing with the 400
516
- * above. The worst case of that rule going stale the other way (upstream
517
- * teaching `/provider/v1/chat/completions` to serve Claude) is one model
518
- * riding the CLI transport it already works on.
634
+ * Messages-only, so a Claude model added upstream is still routed correctly
635
+ * by an un-updated plugin instead of hard-failing with the 400 above. The
636
+ * worst case of that rule going stale the other way (upstream teaching
637
+ * `/provider/v1/chat/completions` to serve Claude) is one model riding a
638
+ * transport it already works on.
519
639
  *
520
640
  * Keep in sync when models ship (see the dsh-commandcode-upstream skill).
521
641
  */
522
642
  const MESSAGES_ONLY_MODELS = /* @__PURE__ */ new Set([
643
+ "claude-sonnet-5-5",
523
644
  "claude-sonnet-5",
524
645
  "claude-sonnet-4-6",
525
646
  "claude-fable-5-1",
526
647
  "claude-fable-5",
648
+ "claude-opus-5-5",
527
649
  "claude-opus-5",
528
650
  "claude-opus-4-8",
529
651
  "claude-opus-4-7",
@@ -534,13 +656,140 @@ function requiresMessagesEndpoint(modelId) {
534
656
  return MESSAGES_ONLY_MODELS.has(modelId) || modelId.startsWith("claude-");
535
657
  }
536
658
  /**
659
+ * Per-model output ceilings, which `/provider/v1/models` does NOT publish (it
660
+ * carries `context_length` and `supported_endpoints` only) while every endpoint
661
+ * refuses a larger `max_tokens` per model. Sending the context window as the
662
+ * output budget is what produced issue #71: `claude-sonnet-5-5` has a
663
+ * 1 000 000-token window and a 128 000-token ceiling, the adapter derived
664
+ * 131072, and the endpoint refused every request of a fresh process.
665
+ *
666
+ * **Generated — do not hand-edit.** `scripts/sync-output-limits.mjs` rewrites
667
+ * this literal from models.dev; run it after a catalog sync, and use its
668
+ * `--check` in CI. Command Code resells these models through their VENDOR's own
669
+ * API, so the vendor's published ceiling is the value recorded: 76 of the 86
670
+ * catalog ids as of 2026-09-30. Each row carries a comment naming the entry it
671
+ * came from.
672
+ *
673
+ * Two of those 76 rest on more than a vendor lookup. `gpt-6.1-sol` (128000) comes
674
+ * from OpenAI's own row and is corroborated by every other provider carrying it.
675
+ * `inclusionai/ling-3.1-flash:free` (32768) is hand-recorded in the script's
676
+ * `MANUAL_CEILINGS`: models.dev has no `ling-3.1` entry yet, so it takes the Ling
677
+ * family's uniform ceiling. Both are still correctable — a stricter gateway costs
678
+ * one refused request, and `streamRequest()` remembers what the endpoint said.
679
+ *
680
+ * The 10 unlisted models are ones no vendor publishes and the aggregators
681
+ * disagree about. They are NOT a gap: a refusal states the ceiling in full, and
682
+ * `streamRequest()` reads it off, retries once and remembers it for the model
683
+ * (issue #71). A vendor figure the gateway turns out to be stricter than costs
684
+ * one round trip; a conservative guess costs half of every answer, forever —
685
+ * which is why the unlisted path defers to the endpoint rather than to a
686
+ * fallback here.
687
+ *
688
+ * An unlisted Messages-route model still falls back to
689
+ * {@link DEFAULT_MESSAGES_MAX_TOKENS} before any request is made, so a ceiling
690
+ * is never absent when the body is built.
691
+ */
692
+ const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
693
+ ["claude-fable-5", 128e3],
694
+ ["claude-fable-5-1", 128e3],
695
+ ["claude-haiku-4-5-20251001", 64e3],
696
+ ["claude-opus-4-7", 128e3],
697
+ ["claude-opus-4-8", 128e3],
698
+ ["claude-opus-5", 128e3],
699
+ ["claude-opus-5-5", 128e3],
700
+ ["claude-sonnet-4-6", 128e3],
701
+ ["claude-sonnet-5", 128e3],
702
+ ["claude-sonnet-5-5", 128e3],
703
+ ["deepseek/deepseek-v4-flash", 393216],
704
+ ["deepseek/deepseek-v4-flash-vision-exp", 393216],
705
+ ["deepseek/deepseek-v4-pro", 393216],
706
+ ["google/gemini-3.1-flash-lite", 65536],
707
+ ["google/gemini-3.5-flash", 65536],
708
+ ["google/gemini-3.5-flash-lite", 65536],
709
+ ["google/gemini-3.6-flash", 65536],
710
+ ["google/gemini-3.7-flash", 65536],
711
+ ["google/gemini-3.8-flash", 65536],
712
+ ["gpt-5.3-codex", 128e3],
713
+ ["gpt-5.4", 128e3],
714
+ ["gpt-5.4-mini", 128e3],
715
+ ["gpt-5.5", 128e3],
716
+ ["gpt-5.6-luna", 128e3],
717
+ ["gpt-5.6-sol", 128e3],
718
+ ["gpt-5.6-terra", 128e3],
719
+ ["gpt-6-astra", 128e3],
720
+ ["gpt-6-luna", 128e3],
721
+ ["gpt-6-sol", 128e3],
722
+ ["gpt-6.1-sol", 128e3],
723
+ ["inclusionai/ling-3.0-flash-sante:free", 32768],
724
+ ["inclusionai/ling-3.1-flash:free", 32768],
725
+ ["meituan/LongCat-2.0", 131072],
726
+ ["meta/muse-spark-1.1", 131072],
727
+ ["meta/muse-spark-1.2", 131072],
728
+ ["meta/muse-spark-1.2-contributor", 131072],
729
+ ["meta/muse-spark-1.3", 131072],
730
+ ["meta/muse-spark-1.3-contributor", 131072],
731
+ ["MiniMaxAI/MiniMax-M2.5", 131072],
732
+ ["MiniMaxAI/MiniMax-M2.7", 131072],
733
+ ["MiniMaxAI/MiniMax-M3", 512e3],
734
+ ["moonshotai/Kimi-K2.6", 262144],
735
+ ["moonshotai/Kimi-K2.7-Code", 262144],
736
+ ["moonshotai/Kimi-K2.7-Code-Highspeed", 262144],
737
+ ["moonshotai/Kimi-K3", 1048576],
738
+ ["nvidia/nemotron-3-ultra-550b-a55b", 65536],
739
+ ["poolside/laguna-s-2.1-free", 32768],
740
+ ["Qwen/Qwen3.6-Max-Preview", 65536],
741
+ ["Qwen/Qwen3.6-Plus", 65536],
742
+ ["Qwen/Qwen3.7-Flash", 131072],
743
+ ["Qwen/Qwen3.7-Max", 131072],
744
+ ["Qwen/Qwen3.7-Plus", 131072],
745
+ ["Qwen/Qwen3.8-Flash", 131072],
746
+ ["Qwen/Qwen3.8-Max", 131072],
747
+ ["Qwen/Qwen3.8-Omni-Flash", 131072],
748
+ ["sakana/fugu-ultra", 1e6],
749
+ ["stealth/pixel-canary", 131072],
750
+ ["stealth/space-bunny-alpha", 524288],
751
+ ["stepfun/Step-3.5-Flash", 256e3],
752
+ ["stepfun/Step-3.7-Flash", 256e3],
753
+ ["stepfun/Step-5-Preview", 65536],
754
+ ["thinkingmachines/inkling", 65536],
755
+ ["xai/grok-4.5", 5e5],
756
+ ["xai/grok-4.6", 5e5],
757
+ ["xai/grok-4.7", 5e5],
758
+ ["xiaomi/mimo-v2.5", 131072],
759
+ ["xiaomi/mimo-v2.5-pro", 131072],
760
+ ["xiaomi/mimo-v2.6-flash", 131072],
761
+ ["xiaomi/mimo-v2.6-pro", 131072],
762
+ ["xiaomi/mimo-v2.6-pro-ultraspeed", 131072],
763
+ ["z-ai/glm-5.3-flash", 131072],
764
+ ["z-ai/glm-5.3-flashx", 131072],
765
+ ["zai-org/GLM-5", 131072],
766
+ ["zai-org/GLM-5.1", 131072],
767
+ ["zai-org/GLM-5.2", 131072],
768
+ ["zai-org/GLM-5.3", 131072]
769
+ ]);
770
+ /**
771
+ * Output ceiling for a Messages-route model that {@link MODEL_OUTPUT_TOKEN_LIMITS}
772
+ * does not carry — today only models no vendor publishes yet. Deliberately
773
+ * conservative: a 400 on `max_tokens` is a wasted round trip on every request,
774
+ * whereas a lower ceiling only shortens an answer, and the endpoint states the
775
+ * real one the first time it refuses, at which point the runtime overrides this
776
+ * anyway (issue #71).
777
+ *
778
+ * 64 000 is not arbitrary: it is `claude-haiku-4-5-20251001`'s actual ceiling,
779
+ * the lowest in the Claude family. A Claude model added to the catalog before
780
+ * its row exists here therefore answers at a size the family really supports,
781
+ * and is corrected upward on its first rejection rather than clamped here
782
+ * forever.
783
+ */
784
+ const DEFAULT_MESSAGES_MAX_TOKENS = 64e3;
785
+ /**
537
786
  * The minimum subscription plan a model is included in, per the official plan
538
787
  * pages (`/docs/plans/go`, `/docs/plans/goat`, `/docs/plans/pro`,
539
788
  * `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
540
789
  * list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
541
790
  * Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
542
- * Provider-tier. Re-verified at command-code@1.66.0 (2026-09-27): 82 catalog
543
- * ids at 52/60/74/82 cumulative, a strict superset chain — every release since
791
+ * Provider-tier. Re-verified at command-code@1.72.1 (2026-09-30): 86 catalog
792
+ * ids at 53/62/76/84 cumulative, a strict superset chain — every release since
544
793
  * 1.49.0 has been additive with no tier move, and per-entry tags below name the
545
794
  * release that added each row.
546
795
  *
@@ -568,6 +817,7 @@ const KNOWN_PLANS = {
568
817
  "Qwen/Qwen3.8-Omni-Flash": "go",
569
818
  "deepseek/deepseek-v4-flash-fast": "go",
570
819
  "deepseek/deepseek-v4.1-flash": "go",
820
+ "deepseek/deepseek-v4.1-flash-fast": "go",
571
821
  "deepseek/deepseek-v4-flash": "go",
572
822
  "deepseek/deepseek-v4-flash-vision-exp": "go",
573
823
  "deepseek/deepseek-v4-pro": "go",
@@ -575,6 +825,7 @@ const KNOWN_PLANS = {
575
825
  "gpt-6-luna": "go",
576
826
  "meituan/LongCat-2.0": "go",
577
827
  "inclusionai/ling-3.0-flash-sante:free": "go",
828
+ "inclusionai/ling-3.1-flash:free": "go",
578
829
  "meta/muse-spark-1.2-contributor": "go",
579
830
  "meta/muse-spark-1.3-contributor": "go",
580
831
  "moonshotai/Kimi-K2.5": "go",
@@ -605,6 +856,7 @@ const KNOWN_PLANS = {
605
856
  "zai-org/GLM-5.2": "go",
606
857
  "zai-org/GLM-5.2-Fast": "go",
607
858
  "zai-org/GLM-5.3": "go",
859
+ "claude-sonnet-5-5": "goat",
608
860
  "google/gemini-3.7-flash": "goat",
609
861
  "google/gemini-3.8-flash": "goat",
610
862
  "gpt-5.6-sol": "goat",
@@ -634,26 +886,8 @@ const KNOWN_PLANS = {
634
886
  "claude-opus-5-5": "provider",
635
887
  "claude-opus-5": "provider",
636
888
  "gpt-6-astra": "provider",
637
- "sakana/fugu-ultra": "provider"
638
- };
639
- /** Official display labels for each plan tier. */
640
- const PLAN_LABELS = {
641
- go: "Go",
642
- goat: "GOAT",
643
- pro: "Pro",
644
- provider: "Provider",
645
- max: "Max"
646
- };
647
- /**
648
- * Plan-tier sort weights, low to high. Models outside the snapshot (unknown
649
- * plans) sort after every known tier, keeping known models predictable.
650
- */
651
- const PLAN_ORDER = {
652
- go: 0,
653
- goat: 1,
654
- pro: 2,
655
- provider: 3,
656
- max: 4
889
+ "sakana/fugu-ultra": "provider",
890
+ "gpt-6.1-sol": "max"
657
891
  };
658
892
  /**
659
893
  * Whether a model is free (requests cost no credits), per the pricing page's
@@ -682,7 +916,7 @@ function compareByPlan(a, b) {
682
916
  /**
683
917
  * Subscription plan table, synced from the official CLI bundle's plan maps
684
918
  * (located by the `"individual-go"` key in `dist/cli.mjs`, re-verified unchanged
685
- * through command-code@1.66.0): subscription `planId` prefix → display name and
919
+ * through command-code@1.72.1): subscription `planId` prefix → display name and
686
920
  * the plan's monthly credit total. This is the account's own subscription
687
921
  * (from `/alpha/billing/subscriptions`) — distinct from {@link KNOWN_PLANS},
688
922
  * which maps catalog models to their minimum tier.
@@ -697,6 +931,11 @@ const KNOWN_SUBSCRIPTION_PLANS = {
697
931
  monthlyCredits: 10,
698
932
  tierWeight: 0
699
933
  },
934
+ "individual-go-v1": {
935
+ name: "Go",
936
+ monthlyCredits: 10,
937
+ tierWeight: 0
938
+ },
700
939
  "individual-goat": {
701
940
  name: "GOAT",
702
941
  monthlyCredits: 70,
@@ -785,10 +1024,6 @@ const KNOWN_DEALS = {
785
1024
  "MiniMaxAI/MiniMax-M3": { label: "50% off" },
786
1025
  "xiaomi/mimo-v2.5-pro": { label: "99% off" },
787
1026
  "xiaomi/mimo-v2.5": { label: "98% off" },
788
- "xai/grok-4.7": {
789
- label: "40% off",
790
- expiresAt: "2026-09-27T23:59:59.999Z"
791
- },
792
1027
  "poolside/laguna-s-2.1-free": {
793
1028
  label: "FREE",
794
1029
  free: true
@@ -797,6 +1032,10 @@ const KNOWN_DEALS = {
797
1032
  label: "FREE",
798
1033
  free: true
799
1034
  },
1035
+ "inclusionai/ling-3.1-flash:free": {
1036
+ label: "FREE",
1037
+ free: true
1038
+ },
800
1039
  "stealth/space-bunny-alpha": {
801
1040
  label: "FREE",
802
1041
  free: true
@@ -812,7 +1051,7 @@ const KNOWN_DEALS = {
812
1051
  * charges by the hour: peak hours are 01:00–04:00 and 06:00–10:00 UTC (7h per
813
1052
  * weekday, full price) **Monday to Friday only**; the other 17 hours of a
814
1053
  * weekday and every hour of Saturday/Sunday (UTC) are off-peak at half price.
815
- * Exactly four models carry the page's `timeOfDay` block (the four rows below).
1054
+ * Exactly five models carry the page's `timeOfDay` block (the five rows below).
816
1055
  * The picker shows the *current* state as a compact label (`Peak`/`Half`)
817
1056
  * matching the English noun style of the other markers (`Image`, `FREE`), so a
818
1057
  * developer can tell at a glance whether calling the model right now is cheap
@@ -837,7 +1076,8 @@ const KNOWN_PEAK_PRICING = /* @__PURE__ */ new Set([
837
1076
  "deepseek/deepseek-v4-pro",
838
1077
  "deepseek/deepseek-v4-flash",
839
1078
  "deepseek/deepseek-v4-flash-vision-exp",
840
- "deepseek/deepseek-v4.1-flash"
1079
+ "deepseek/deepseek-v4.1-flash",
1080
+ "deepseek/deepseek-v4.1-flash-fast"
841
1081
  ]);
842
1082
  /**
843
1083
  * Peak hours (UTC, hour-of-day range end-exclusive): 01–03 and 06–09.
@@ -860,11 +1100,7 @@ const PEAK_HOUR_RANGES = [[1, 4], [6, 10]];
860
1100
  * the one in {@link KNOWN_PEAK_PRICING} still gets the right half of the day.
861
1101
  */
862
1102
  function isPeakPricingHour(now = Date.now()) {
863
- const at = new Date(now);
864
- const day = at.getUTCDay();
865
- if (day === 0 || day === 6) return false;
866
- const hour = at.getUTCHours();
867
- return PEAK_HOUR_RANGES.some(([start, end]) => hour >= start && hour < end);
1103
+ return peakHour(now, PEAK_HOUR_RANGES);
868
1104
  }
869
1105
  /**
870
1106
  * As {@link isPeakPricingHour}, plus the {@link KNOWN_PEAK_PRICING} membership
@@ -945,6 +1181,27 @@ function capabilityDescription(modelId, contextWindow, now = Date.now()) {
945
1181
  //#endregion
946
1182
  //#region src/model-prices.ts
947
1183
  /**
1184
+ * The plan tiers a per-model allowance exists for, cheapest first. Ordering
1185
+ * matters: {@link allowanceTierForWeight} matches a tier weight against these
1186
+ * rather than against hard-coded numbers, so this table and the subscription
1187
+ * table cannot drift apart silently.
1188
+ */
1189
+ const ALLOWANCE_TIERS = ["goat", "pro"];
1190
+ /**
1191
+ * Which allowance bracket a plan-tier weight belongs to, or undefined when that
1192
+ * plan has no per-model allowance.
1193
+ *
1194
+ * Weights come from `KNOWN_SUBSCRIPTION_PLANS` (go 0 · goat 1 · pro 2 ·
1195
+ * provider 3 · max/ultra 4) and {@link PLAN_ORDER} is the same scale, which is
1196
+ * what makes the lookup exact: Go, Provider, Max and Ultra have no published
1197
+ * allowance, and answering with a neighbouring tier's figure would be a
1198
+ * fabricated number rather than a missing one.
1199
+ */
1200
+ function allowanceTierForWeight(tierWeight) {
1201
+ if (tierWeight === void 0) return void 0;
1202
+ return ALLOWANCE_TIERS.find((tier) => PLAN_ORDER[tier] === tierWeight);
1203
+ }
1204
+ /**
948
1205
  * Every price row the page publishes, ordered by its own slug.
949
1206
  *
950
1207
  * Note on the GPT rows: `gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini` and
@@ -964,7 +1221,11 @@ const MODEL_PRICE_ROWS = [
964
1221
  50,
965
1222
  1,
966
1223
  12.5
967
- ]
1224
+ ],
1225
+ allowance: {
1226
+ goat: 20,
1227
+ pro: 20
1228
+ }
968
1229
  },
969
1230
  {
970
1231
  id: "claude-fable-5-1",
@@ -973,7 +1234,11 @@ const MODEL_PRICE_ROWS = [
973
1234
  50,
974
1235
  .25,
975
1236
  12.5
976
- ]
1237
+ ],
1238
+ allowance: {
1239
+ goat: 20,
1240
+ pro: 20
1241
+ }
977
1242
  },
978
1243
  {
979
1244
  id: "claude-haiku-4-5",
@@ -982,7 +1247,11 @@ const MODEL_PRICE_ROWS = [
982
1247
  5,
983
1248
  .1,
984
1249
  1.25
985
- ]
1250
+ ],
1251
+ allowance: {
1252
+ goat: 20,
1253
+ pro: 20
1254
+ }
986
1255
  },
987
1256
  {
988
1257
  id: "claude-opus-4-6",
@@ -991,7 +1260,11 @@ const MODEL_PRICE_ROWS = [
991
1260
  25,
992
1261
  .5,
993
1262
  6.25
994
- ]
1263
+ ],
1264
+ allowance: {
1265
+ goat: 20,
1266
+ pro: 20
1267
+ }
995
1268
  },
996
1269
  {
997
1270
  id: "claude-opus-4-7",
@@ -1000,7 +1273,11 @@ const MODEL_PRICE_ROWS = [
1000
1273
  25,
1001
1274
  .5,
1002
1275
  6.25
1003
- ]
1276
+ ],
1277
+ allowance: {
1278
+ goat: 20,
1279
+ pro: 20
1280
+ }
1004
1281
  },
1005
1282
  {
1006
1283
  id: "claude-opus-4-8",
@@ -1009,7 +1286,11 @@ const MODEL_PRICE_ROWS = [
1009
1286
  25,
1010
1287
  .5,
1011
1288
  6.25
1012
- ]
1289
+ ],
1290
+ allowance: {
1291
+ goat: 20,
1292
+ pro: 20
1293
+ }
1013
1294
  },
1014
1295
  {
1015
1296
  id: "claude-opus-5",
@@ -1018,7 +1299,11 @@ const MODEL_PRICE_ROWS = [
1018
1299
  25,
1019
1300
  .5,
1020
1301
  6.25
1021
- ]
1302
+ ],
1303
+ allowance: {
1304
+ goat: 20,
1305
+ pro: 20
1306
+ }
1022
1307
  },
1023
1308
  {
1024
1309
  id: "claude-opus-5-5",
@@ -1027,7 +1312,11 @@ const MODEL_PRICE_ROWS = [
1027
1312
  20,
1028
1313
  .2,
1029
1314
  5
1030
- ]
1315
+ ],
1316
+ allowance: {
1317
+ goat: 20,
1318
+ pro: 20
1319
+ }
1031
1320
  },
1032
1321
  {
1033
1322
  id: "claude-sonnet-4-6",
@@ -1036,7 +1325,11 @@ const MODEL_PRICE_ROWS = [
1036
1325
  15,
1037
1326
  .3,
1038
1327
  3.75
1039
- ]
1328
+ ],
1329
+ allowance: {
1330
+ goat: 20,
1331
+ pro: 20
1332
+ }
1040
1333
  },
1041
1334
  {
1042
1335
  id: "claude-sonnet-5",
@@ -1045,7 +1338,24 @@ const MODEL_PRICE_ROWS = [
1045
1338
  10,
1046
1339
  .2,
1047
1340
  2.5
1048
- ]
1341
+ ],
1342
+ allowance: {
1343
+ goat: 20,
1344
+ pro: 20
1345
+ }
1346
+ },
1347
+ {
1348
+ id: "claude-sonnet-5-5",
1349
+ rates: [
1350
+ 2,
1351
+ 10,
1352
+ .2,
1353
+ 2.5
1354
+ ],
1355
+ allowance: {
1356
+ goat: 10,
1357
+ pro: 20
1358
+ }
1049
1359
  },
1050
1360
  {
1051
1361
  id: "deepseek-v4-flash",
@@ -1058,7 +1368,11 @@ const MODEL_PRICE_ROWS = [
1058
1368
  .3,
1059
1369
  1.2,
1060
1370
  .006
1061
- ]
1371
+ ],
1372
+ allowance: {
1373
+ goat: 60,
1374
+ pro: 70
1375
+ }
1062
1376
  },
1063
1377
  {
1064
1378
  id: "deepseek-v4-flash-fast",
@@ -1066,7 +1380,11 @@ const MODEL_PRICE_ROWS = [
1066
1380
  .28,
1067
1381
  .56,
1068
1382
  .07
1069
- ]
1383
+ ],
1384
+ allowance: {
1385
+ goat: 20,
1386
+ pro: 30
1387
+ }
1070
1388
  },
1071
1389
  {
1072
1390
  id: "deepseek-v4-flash-vision-exp",
@@ -1079,7 +1397,11 @@ const MODEL_PRICE_ROWS = [
1079
1397
  .3,
1080
1398
  1.2,
1081
1399
  .006
1082
- ]
1400
+ ],
1401
+ allowance: {
1402
+ goat: 20,
1403
+ pro: 30
1404
+ }
1083
1405
  },
1084
1406
  {
1085
1407
  id: "deepseek-v4-pro",
@@ -1092,7 +1414,11 @@ const MODEL_PRICE_ROWS = [
1092
1414
  1.32,
1093
1415
  3.96,
1094
1416
  .044
1095
- ]
1417
+ ],
1418
+ allowance: {
1419
+ goat: 20,
1420
+ pro: 30
1421
+ }
1096
1422
  },
1097
1423
  {
1098
1424
  id: "deepseek-v4.1-flash",
@@ -1105,7 +1431,28 @@ const MODEL_PRICE_ROWS = [
1105
1431
  .3,
1106
1432
  1.2,
1107
1433
  .006
1108
- ]
1434
+ ],
1435
+ allowance: {
1436
+ goat: 60,
1437
+ pro: 70
1438
+ }
1439
+ },
1440
+ {
1441
+ id: "deepseek-v4.1-flash-fast",
1442
+ rates: [
1443
+ .16,
1444
+ .58,
1445
+ .016
1446
+ ],
1447
+ peak: [
1448
+ .32,
1449
+ 1.16,
1450
+ .032
1451
+ ],
1452
+ allowance: {
1453
+ goat: 60,
1454
+ pro: 70
1455
+ }
1109
1456
  },
1110
1457
  {
1111
1458
  id: "fugu-ultra",
@@ -1113,7 +1460,11 @@ const MODEL_PRICE_ROWS = [
1113
1460
  5,
1114
1461
  30,
1115
1462
  .5
1116
- ]
1463
+ ],
1464
+ allowance: {
1465
+ goat: 20,
1466
+ pro: 20
1467
+ }
1117
1468
  },
1118
1469
  {
1119
1470
  id: "gemini-3.1-flash-lite",
@@ -1121,7 +1472,11 @@ const MODEL_PRICE_ROWS = [
1121
1472
  .25,
1122
1473
  1.5,
1123
1474
  .03
1124
- ]
1475
+ ],
1476
+ allowance: {
1477
+ goat: 20,
1478
+ pro: 20
1479
+ }
1125
1480
  },
1126
1481
  {
1127
1482
  id: "gemini-3.5-flash",
@@ -1129,7 +1484,11 @@ const MODEL_PRICE_ROWS = [
1129
1484
  1.5,
1130
1485
  9,
1131
1486
  .15
1132
- ]
1487
+ ],
1488
+ allowance: {
1489
+ goat: 20,
1490
+ pro: 20
1491
+ }
1133
1492
  },
1134
1493
  {
1135
1494
  id: "gemini-3.5-flash-lite",
@@ -1137,7 +1496,11 @@ const MODEL_PRICE_ROWS = [
1137
1496
  .3,
1138
1497
  2.5,
1139
1498
  .03
1140
- ]
1499
+ ],
1500
+ allowance: {
1501
+ goat: 20,
1502
+ pro: 20
1503
+ }
1141
1504
  },
1142
1505
  {
1143
1506
  id: "gemini-3.6-flash",
@@ -1145,7 +1508,11 @@ const MODEL_PRICE_ROWS = [
1145
1508
  1.5,
1146
1509
  7.5,
1147
1510
  .15
1148
- ]
1511
+ ],
1512
+ allowance: {
1513
+ goat: 20,
1514
+ pro: 20
1515
+ }
1149
1516
  },
1150
1517
  {
1151
1518
  id: "gemini-3.7-flash",
@@ -1154,7 +1521,11 @@ const MODEL_PRICE_ROWS = [
1154
1521
  7.5,
1155
1522
  .15,
1156
1523
  .08334
1157
- ]
1524
+ ],
1525
+ allowance: {
1526
+ goat: 40,
1527
+ pro: 50
1528
+ }
1158
1529
  },
1159
1530
  {
1160
1531
  id: "gemini-3.8-flash",
@@ -1162,7 +1533,11 @@ const MODEL_PRICE_ROWS = [
1162
1533
  1.5,
1163
1534
  7.5,
1164
1535
  .15
1165
- ]
1536
+ ],
1537
+ allowance: {
1538
+ goat: 40,
1539
+ pro: 50
1540
+ }
1166
1541
  },
1167
1542
  {
1168
1543
  id: "glm-5",
@@ -1170,7 +1545,11 @@ const MODEL_PRICE_ROWS = [
1170
1545
  1,
1171
1546
  3.2,
1172
1547
  .2
1173
- ]
1548
+ ],
1549
+ allowance: {
1550
+ goat: 20,
1551
+ pro: 30
1552
+ }
1174
1553
  },
1175
1554
  {
1176
1555
  id: "glm-5.1",
@@ -1178,7 +1557,11 @@ const MODEL_PRICE_ROWS = [
1178
1557
  1.4,
1179
1558
  4.4,
1180
1559
  .26
1181
- ]
1560
+ ],
1561
+ allowance: {
1562
+ goat: 20,
1563
+ pro: 30
1564
+ }
1182
1565
  },
1183
1566
  {
1184
1567
  id: "glm-5.2",
@@ -1186,7 +1569,11 @@ const MODEL_PRICE_ROWS = [
1186
1569
  1.4,
1187
1570
  4.4,
1188
1571
  .26
1189
- ]
1572
+ ],
1573
+ allowance: {
1574
+ goat: 70,
1575
+ pro: 80
1576
+ }
1190
1577
  },
1191
1578
  {
1192
1579
  id: "glm-5.2-fast",
@@ -1194,7 +1581,11 @@ const MODEL_PRICE_ROWS = [
1194
1581
  3,
1195
1582
  10.25,
1196
1583
  .5
1197
- ]
1584
+ ],
1585
+ allowance: {
1586
+ goat: 20,
1587
+ pro: 30
1588
+ }
1198
1589
  },
1199
1590
  {
1200
1591
  id: "glm-5.3",
@@ -1202,7 +1593,11 @@ const MODEL_PRICE_ROWS = [
1202
1593
  1.4,
1203
1594
  4.4,
1204
1595
  .26
1205
- ]
1596
+ ],
1597
+ allowance: {
1598
+ goat: 20,
1599
+ pro: 30
1600
+ }
1206
1601
  },
1207
1602
  {
1208
1603
  id: "glm-5.3-flash",
@@ -1210,7 +1605,11 @@ const MODEL_PRICE_ROWS = [
1210
1605
  .15,
1211
1606
  .5,
1212
1607
  .03
1213
- ]
1608
+ ],
1609
+ allowance: {
1610
+ goat: 40,
1611
+ pro: 50
1612
+ }
1214
1613
  },
1215
1614
  {
1216
1615
  id: "glm-5.3-flashx",
@@ -1218,7 +1617,11 @@ const MODEL_PRICE_ROWS = [
1218
1617
  .37,
1219
1618
  1.25,
1220
1619
  .075
1221
- ]
1620
+ ],
1621
+ allowance: {
1622
+ goat: 20,
1623
+ pro: 30
1624
+ }
1222
1625
  },
1223
1626
  {
1224
1627
  id: "gpt-5.3-codex",
@@ -1227,7 +1630,11 @@ const MODEL_PRICE_ROWS = [
1227
1630
  8,
1228
1631
  .5,
1229
1632
  0
1230
- ]
1633
+ ],
1634
+ allowance: {
1635
+ goat: 20,
1636
+ pro: 20
1637
+ }
1231
1638
  },
1232
1639
  {
1233
1640
  id: "gpt-5.4",
@@ -1236,7 +1643,11 @@ const MODEL_PRICE_ROWS = [
1236
1643
  15,
1237
1644
  .25,
1238
1645
  0
1239
- ]
1646
+ ],
1647
+ allowance: {
1648
+ goat: 20,
1649
+ pro: 20
1650
+ }
1240
1651
  },
1241
1652
  {
1242
1653
  id: "gpt-5.4-mini",
@@ -1245,7 +1656,11 @@ const MODEL_PRICE_ROWS = [
1245
1656
  4.5,
1246
1657
  .075,
1247
1658
  0
1248
- ]
1659
+ ],
1660
+ allowance: {
1661
+ goat: 20,
1662
+ pro: 20
1663
+ }
1249
1664
  },
1250
1665
  {
1251
1666
  id: "gpt-5.5",
@@ -1254,7 +1669,11 @@ const MODEL_PRICE_ROWS = [
1254
1669
  30,
1255
1670
  .5,
1256
1671
  0
1257
- ]
1672
+ ],
1673
+ allowance: {
1674
+ goat: 20,
1675
+ pro: 20
1676
+ }
1258
1677
  },
1259
1678
  {
1260
1679
  id: "gpt-5.6-luna",
@@ -1264,6 +1683,10 @@ const MODEL_PRICE_ROWS = [
1264
1683
  .02,
1265
1684
  .25
1266
1685
  ],
1686
+ allowance: {
1687
+ goat: 20,
1688
+ pro: 30
1689
+ },
1267
1690
  contextTiers: [{
1268
1691
  "maxContext": 272e3,
1269
1692
  "rates": [
@@ -1287,6 +1710,10 @@ const MODEL_PRICE_ROWS = [
1287
1710
  .5,
1288
1711
  6.25
1289
1712
  ],
1713
+ allowance: {
1714
+ goat: 70,
1715
+ pro: 80
1716
+ },
1290
1717
  contextTiers: [{
1291
1718
  "maxContext": 272e3,
1292
1719
  "rates": [
@@ -1310,6 +1737,10 @@ const MODEL_PRICE_ROWS = [
1310
1737
  .2,
1311
1738
  2.5
1312
1739
  ],
1740
+ allowance: {
1741
+ goat: 20,
1742
+ pro: 20
1743
+ },
1313
1744
  contextTiers: [{
1314
1745
  "maxContext": 272e3,
1315
1746
  "rates": [
@@ -1333,6 +1764,10 @@ const MODEL_PRICE_ROWS = [
1333
1764
  1,
1334
1765
  12.5
1335
1766
  ],
1767
+ allowance: {
1768
+ goat: 20,
1769
+ pro: 20
1770
+ },
1336
1771
  contextTiers: [{
1337
1772
  "maxContext": 272e3,
1338
1773
  "rates": [
@@ -1348,6 +1783,33 @@ const MODEL_PRICE_ROWS = [
1348
1783
  25
1349
1784
  ] }]
1350
1785
  },
1786
+ {
1787
+ id: "gpt-6.1-sol",
1788
+ rates: [
1789
+ 2,
1790
+ 10,
1791
+ .1,
1792
+ 2.5
1793
+ ],
1794
+ allowance: {
1795
+ goat: 20,
1796
+ pro: 20
1797
+ },
1798
+ contextTiers: [{
1799
+ "maxContext": 272e3,
1800
+ "rates": [
1801
+ 2,
1802
+ 10,
1803
+ .1,
1804
+ 2.5
1805
+ ]
1806
+ }, { "rates": [
1807
+ 4,
1808
+ 15,
1809
+ .2,
1810
+ 5
1811
+ ] }]
1812
+ },
1351
1813
  {
1352
1814
  id: "gpt-6-luna",
1353
1815
  rates: [
@@ -1356,6 +1818,10 @@ const MODEL_PRICE_ROWS = [
1356
1818
  .01,
1357
1819
  .125
1358
1820
  ],
1821
+ allowance: {
1822
+ goat: 20,
1823
+ pro: 30
1824
+ },
1359
1825
  contextTiers: [{
1360
1826
  "maxContext": 272e3,
1361
1827
  "rates": [
@@ -1379,6 +1845,10 @@ const MODEL_PRICE_ROWS = [
1379
1845
  .2,
1380
1846
  2.5
1381
1847
  ],
1848
+ allowance: {
1849
+ goat: 20,
1850
+ pro: 20
1851
+ },
1382
1852
  contextTiers: [{
1383
1853
  "maxContext": 272e3,
1384
1854
  "rates": [
@@ -1400,7 +1870,11 @@ const MODEL_PRICE_ROWS = [
1400
1870
  2,
1401
1871
  6,
1402
1872
  .5
1403
- ]
1873
+ ],
1874
+ allowance: {
1875
+ goat: 20,
1876
+ pro: 30
1877
+ }
1404
1878
  },
1405
1879
  {
1406
1880
  id: "grok-4.6",
@@ -1409,6 +1883,10 @@ const MODEL_PRICE_ROWS = [
1409
1883
  6,
1410
1884
  .5
1411
1885
  ],
1886
+ allowance: {
1887
+ goat: 20,
1888
+ pro: 30
1889
+ },
1412
1890
  contextTiers: [{
1413
1891
  "maxContext": 2e5,
1414
1892
  "rates": [
@@ -1425,21 +1903,25 @@ const MODEL_PRICE_ROWS = [
1425
1903
  {
1426
1904
  id: "grok-4.7",
1427
1905
  rates: [
1428
- 1.2,
1429
- 3.6,
1430
- .3
1906
+ 2,
1907
+ 6,
1908
+ .5
1431
1909
  ],
1910
+ allowance: {
1911
+ goat: 20,
1912
+ pro: 30
1913
+ },
1432
1914
  contextTiers: [{
1433
1915
  "maxContext": 2e5,
1434
1916
  "rates": [
1435
- 1.2,
1436
- 3.6,
1437
- .3
1917
+ 2,
1918
+ 6,
1919
+ .5
1438
1920
  ]
1439
1921
  }, { "rates": [
1440
- 2.4,
1441
- 7.2,
1442
- .6
1922
+ 4,
1923
+ 12,
1924
+ 1
1443
1925
  ] }]
1444
1926
  },
1445
1927
  {
@@ -1448,7 +1930,11 @@ const MODEL_PRICE_ROWS = [
1448
1930
  1,
1449
1931
  4.05,
1450
1932
  .17
1451
- ]
1933
+ ],
1934
+ allowance: {
1935
+ goat: 20,
1936
+ pro: 30
1937
+ }
1452
1938
  },
1453
1939
  {
1454
1940
  id: "inkling-small",
@@ -1456,7 +1942,11 @@ const MODEL_PRICE_ROWS = [
1456
1942
  .5,
1457
1943
  1.2,
1458
1944
  .1
1459
- ]
1945
+ ],
1946
+ allowance: {
1947
+ goat: 20,
1948
+ pro: 30
1949
+ }
1460
1950
  },
1461
1951
  {
1462
1952
  id: "kimi-k2.5",
@@ -1464,7 +1954,11 @@ const MODEL_PRICE_ROWS = [
1464
1954
  .6,
1465
1955
  3,
1466
1956
  .1
1467
- ]
1957
+ ],
1958
+ allowance: {
1959
+ goat: 20,
1960
+ pro: 30
1961
+ }
1468
1962
  },
1469
1963
  {
1470
1964
  id: "kimi-k2.6",
@@ -1472,7 +1966,11 @@ const MODEL_PRICE_ROWS = [
1472
1966
  .95,
1473
1967
  4,
1474
1968
  .16
1475
- ]
1969
+ ],
1970
+ allowance: {
1971
+ goat: 20,
1972
+ pro: 30
1973
+ }
1476
1974
  },
1477
1975
  {
1478
1976
  id: "kimi-k2.7-code",
@@ -1480,7 +1978,11 @@ const MODEL_PRICE_ROWS = [
1480
1978
  .95,
1481
1979
  4,
1482
1980
  .19
1483
- ]
1981
+ ],
1982
+ allowance: {
1983
+ goat: 60,
1984
+ pro: 70
1985
+ }
1484
1986
  },
1485
1987
  {
1486
1988
  id: "kimi-k2.7-code-highspeed",
@@ -1488,7 +1990,11 @@ const MODEL_PRICE_ROWS = [
1488
1990
  1.9,
1489
1991
  8,
1490
1992
  .38
1491
- ]
1993
+ ],
1994
+ allowance: {
1995
+ goat: 20,
1996
+ pro: 30
1997
+ }
1492
1998
  },
1493
1999
  {
1494
2000
  id: "kimi-k3",
@@ -1496,7 +2002,11 @@ const MODEL_PRICE_ROWS = [
1496
2002
  3,
1497
2003
  15,
1498
2004
  .3
1499
- ]
2005
+ ],
2006
+ allowance: {
2007
+ goat: 20,
2008
+ pro: 30
2009
+ }
1500
2010
  },
1501
2011
  {
1502
2012
  id: "longcat-2.0",
@@ -1504,7 +2014,11 @@ const MODEL_PRICE_ROWS = [
1504
2014
  .3,
1505
2015
  1.2,
1506
2016
  .006
1507
- ]
2017
+ ],
2018
+ allowance: {
2019
+ goat: 50,
2020
+ pro: 60
2021
+ }
1508
2022
  },
1509
2023
  {
1510
2024
  id: "mimo-v2.5",
@@ -1512,7 +2026,16 @@ const MODEL_PRICE_ROWS = [
1512
2026
  .14,
1513
2027
  .28,
1514
2028
  .0028
1515
- ]
2029
+ ],
2030
+ listRates: [
2031
+ .8,
2032
+ 4,
2033
+ .16
2034
+ ],
2035
+ allowance: {
2036
+ goat: 30,
2037
+ pro: 40
2038
+ }
1516
2039
  },
1517
2040
  {
1518
2041
  id: "mimo-v2.5-pro",
@@ -1520,7 +2043,16 @@ const MODEL_PRICE_ROWS = [
1520
2043
  .435,
1521
2044
  .87,
1522
2045
  .0036
1523
- ]
2046
+ ],
2047
+ listRates: [
2048
+ 2,
2049
+ 6,
2050
+ .4
2051
+ ],
2052
+ allowance: {
2053
+ goat: 20,
2054
+ pro: 30
2055
+ }
1524
2056
  },
1525
2057
  {
1526
2058
  id: "mimo-v2.6-flash",
@@ -1528,7 +2060,11 @@ const MODEL_PRICE_ROWS = [
1528
2060
  .14,
1529
2061
  .28,
1530
2062
  .0028
1531
- ]
2063
+ ],
2064
+ allowance: {
2065
+ goat: 20,
2066
+ pro: 30
2067
+ }
1532
2068
  },
1533
2069
  {
1534
2070
  id: "mimo-v2.6-pro",
@@ -1536,7 +2072,11 @@ const MODEL_PRICE_ROWS = [
1536
2072
  .435,
1537
2073
  .87,
1538
2074
  .0036
1539
- ]
2075
+ ],
2076
+ allowance: {
2077
+ goat: 20,
2078
+ pro: 30
2079
+ }
1540
2080
  },
1541
2081
  {
1542
2082
  id: "mimo-v2.6-pro-ultraspeed",
@@ -1544,7 +2084,11 @@ const MODEL_PRICE_ROWS = [
1544
2084
  4.35,
1545
2085
  8.7,
1546
2086
  .036
1547
- ]
2087
+ ],
2088
+ allowance: {
2089
+ goat: 10,
2090
+ pro: 20
2091
+ }
1548
2092
  },
1549
2093
  {
1550
2094
  id: "minimax-m2.5",
@@ -1552,7 +2096,11 @@ const MODEL_PRICE_ROWS = [
1552
2096
  .3,
1553
2097
  1.2,
1554
2098
  .03
1555
- ]
2099
+ ],
2100
+ allowance: {
2101
+ goat: 20,
2102
+ pro: 30
2103
+ }
1556
2104
  },
1557
2105
  {
1558
2106
  id: "minimax-m2.7",
@@ -1560,7 +2108,11 @@ const MODEL_PRICE_ROWS = [
1560
2108
  .3,
1561
2109
  1.2,
1562
2110
  .06
1563
- ]
2111
+ ],
2112
+ allowance: {
2113
+ goat: 20,
2114
+ pro: 30
2115
+ }
1564
2116
  },
1565
2117
  {
1566
2118
  id: "minimax-m3",
@@ -1568,7 +2120,16 @@ const MODEL_PRICE_ROWS = [
1568
2120
  .3,
1569
2121
  1.2,
1570
2122
  .06
1571
- ]
2123
+ ],
2124
+ listRates: [
2125
+ .6,
2126
+ 2.4,
2127
+ .12
2128
+ ],
2129
+ allowance: {
2130
+ goat: 47,
2131
+ pro: 57
2132
+ }
1572
2133
  },
1573
2134
  {
1574
2135
  id: "muse-spark-1.1",
@@ -1576,7 +2137,11 @@ const MODEL_PRICE_ROWS = [
1576
2137
  1.25,
1577
2138
  4.25,
1578
2139
  .15
1579
- ]
2140
+ ],
2141
+ allowance: {
2142
+ goat: 20,
2143
+ pro: 20
2144
+ }
1580
2145
  },
1581
2146
  {
1582
2147
  id: "muse-spark-1.2",
@@ -1584,7 +2149,11 @@ const MODEL_PRICE_ROWS = [
1584
2149
  1.25,
1585
2150
  4.25,
1586
2151
  .15
1587
- ]
2152
+ ],
2153
+ allowance: {
2154
+ goat: 20,
2155
+ pro: 30
2156
+ }
1588
2157
  },
1589
2158
  {
1590
2159
  id: "muse-spark-1.2-contributor",
@@ -1592,7 +2161,11 @@ const MODEL_PRICE_ROWS = [
1592
2161
  .1,
1593
2162
  .2,
1594
2163
  .002
1595
- ]
2164
+ ],
2165
+ allowance: {
2166
+ goat: 20,
2167
+ pro: 30
2168
+ }
1596
2169
  },
1597
2170
  {
1598
2171
  id: "muse-spark-1.3",
@@ -1600,7 +2173,11 @@ const MODEL_PRICE_ROWS = [
1600
2173
  1.25,
1601
2174
  4.25,
1602
2175
  .15
1603
- ]
2176
+ ],
2177
+ allowance: {
2178
+ goat: 20,
2179
+ pro: 30
2180
+ }
1604
2181
  },
1605
2182
  {
1606
2183
  id: "muse-spark-1.3-contributor",
@@ -1608,7 +2185,11 @@ const MODEL_PRICE_ROWS = [
1608
2185
  .1,
1609
2186
  .2,
1610
2187
  .002
1611
- ]
2188
+ ],
2189
+ allowance: {
2190
+ goat: 20,
2191
+ pro: 30
2192
+ }
1612
2193
  },
1613
2194
  {
1614
2195
  id: "nemotron-3-ultra",
@@ -1616,7 +2197,11 @@ const MODEL_PRICE_ROWS = [
1616
2197
  .6,
1617
2198
  2.4,
1618
2199
  .12
1619
- ]
2200
+ ],
2201
+ allowance: {
2202
+ goat: 20,
2203
+ pro: 30
2204
+ }
1620
2205
  },
1621
2206
  {
1622
2207
  id: "qwen-3.6-max",
@@ -1625,7 +2210,11 @@ const MODEL_PRICE_ROWS = [
1625
2210
  7.8,
1626
2211
  .26,
1627
2212
  1.63
1628
- ]
2213
+ ],
2214
+ allowance: {
2215
+ goat: 20,
2216
+ pro: 30
2217
+ }
1629
2218
  },
1630
2219
  {
1631
2220
  id: "qwen-3.6-plus",
@@ -1634,6 +2223,10 @@ const MODEL_PRICE_ROWS = [
1634
2223
  3,
1635
2224
  .1
1636
2225
  ],
2226
+ allowance: {
2227
+ goat: 33,
2228
+ pro: 43
2229
+ },
1637
2230
  contextTiers: [{
1638
2231
  "maxContext": 256e3,
1639
2232
  "rates": [
@@ -1655,6 +2248,10 @@ const MODEL_PRICE_ROWS = [
1655
2248
  .006,
1656
2249
  .038
1657
2250
  ],
2251
+ allowance: {
2252
+ goat: 20,
2253
+ pro: 30
2254
+ },
1658
2255
  contextTiers: [
1659
2256
  {
1660
2257
  "maxContext": 32e3,
@@ -1689,7 +2286,11 @@ const MODEL_PRICE_ROWS = [
1689
2286
  7.5,
1690
2287
  .5,
1691
2288
  3.13
1692
- ]
2289
+ ],
2290
+ allowance: {
2291
+ goat: 33,
2292
+ pro: 43
2293
+ }
1693
2294
  },
1694
2295
  {
1695
2296
  id: "qwen-3.7-plus",
@@ -1699,6 +2300,10 @@ const MODEL_PRICE_ROWS = [
1699
2300
  .08,
1700
2301
  .5
1701
2302
  ],
2303
+ allowance: {
2304
+ goat: 33,
2305
+ pro: 43
2306
+ },
1702
2307
  contextTiers: [{
1703
2308
  "maxContext": 256e3,
1704
2309
  "rates": [
@@ -1720,7 +2325,11 @@ const MODEL_PRICE_ROWS = [
1720
2325
  .4,
1721
2326
  3,
1722
2327
  .04
1723
- ]
2328
+ ],
2329
+ allowance: {
2330
+ goat: 70,
2331
+ pro: 80
2332
+ }
1724
2333
  },
1725
2334
  {
1726
2335
  id: "qwen-3.8-flash",
@@ -1728,7 +2337,11 @@ const MODEL_PRICE_ROWS = [
1728
2337
  .16,
1729
2338
  .47,
1730
2339
  .016
1731
- ]
2340
+ ],
2341
+ allowance: {
2342
+ goat: 20,
2343
+ pro: 30
2344
+ }
1732
2345
  },
1733
2346
  {
1734
2347
  id: "qwen-3.8-max",
@@ -1737,7 +2350,11 @@ const MODEL_PRICE_ROWS = [
1737
2350
  6,
1738
2351
  .25,
1739
2352
  2.5
1740
- ]
2353
+ ],
2354
+ allowance: {
2355
+ goat: 20,
2356
+ pro: 30
2357
+ }
1741
2358
  },
1742
2359
  {
1743
2360
  id: "qwen-3.8-max-0902",
@@ -1745,7 +2362,11 @@ const MODEL_PRICE_ROWS = [
1745
2362
  2,
1746
2363
  6,
1747
2364
  .25
1748
- ]
2365
+ ],
2366
+ allowance: {
2367
+ goat: 20,
2368
+ pro: 30
2369
+ }
1749
2370
  },
1750
2371
  {
1751
2372
  id: "qwen-3.8-omni-flash",
@@ -1753,7 +2374,11 @@ const MODEL_PRICE_ROWS = [
1753
2374
  .15,
1754
2375
  .47,
1755
2376
  .016
1756
- ]
2377
+ ],
2378
+ allowance: {
2379
+ goat: 20,
2380
+ pro: 30
2381
+ }
1757
2382
  },
1758
2383
  {
1759
2384
  id: "step-3.5-flash",
@@ -1761,7 +2386,11 @@ const MODEL_PRICE_ROWS = [
1761
2386
  .09,
1762
2387
  .3,
1763
2388
  .02
1764
- ]
2389
+ ],
2390
+ allowance: {
2391
+ goat: 20,
2392
+ pro: 30
2393
+ }
1765
2394
  },
1766
2395
  {
1767
2396
  id: "step-3.7-flash",
@@ -1769,7 +2398,11 @@ const MODEL_PRICE_ROWS = [
1769
2398
  .2,
1770
2399
  1.15,
1771
2400
  .04
1772
- ]
2401
+ ],
2402
+ allowance: {
2403
+ goat: 20,
2404
+ pro: 30
2405
+ }
1773
2406
  },
1774
2407
  {
1775
2408
  id: "step-5-preview",
@@ -1777,7 +2410,11 @@ const MODEL_PRICE_ROWS = [
1777
2410
  1,
1778
2411
  2.7,
1779
2412
  .05
1780
- ]
2413
+ ],
2414
+ allowance: {
2415
+ goat: 20,
2416
+ pro: 30
2417
+ }
1781
2418
  },
1782
2419
  {
1783
2420
  id: "tencent/hy3-paid",
@@ -1785,7 +2422,11 @@ const MODEL_PRICE_ROWS = [
1785
2422
  .14,
1786
2423
  .58,
1787
2424
  .035
1788
- ]
2425
+ ],
2426
+ allowance: {
2427
+ goat: 70,
2428
+ pro: 80
2429
+ }
1789
2430
  },
1790
2431
  {
1791
2432
  id: "tencent/hy4-preview",
@@ -1793,7 +2434,11 @@ const MODEL_PRICE_ROWS = [
1793
2434
  .834,
1794
2435
  2.501,
1795
2436
  .042
1796
- ]
2437
+ ],
2438
+ allowance: {
2439
+ goat: 20,
2440
+ pro: 30
2441
+ }
1797
2442
  },
1798
2443
  {
1799
2444
  id: "typesafe/jev",
@@ -1801,7 +2446,11 @@ const MODEL_PRICE_ROWS = [
1801
2446
  .042,
1802
2447
  0,
1803
2448
  0
1804
- ]
2449
+ ],
2450
+ allowance: {
2451
+ goat: 20,
2452
+ pro: 30
2453
+ }
1805
2454
  }
1806
2455
  ];
1807
2456
  /**
@@ -1854,16 +2503,23 @@ function ratesOf(values) {
1854
2503
  if (values[3] !== void 0) rates.cacheWriteCost = values[3];
1855
2504
  return rates;
1856
2505
  }
1857
- /** Build the full price row the browser prices a session with. */
1858
- function wireRow(id, slug, row) {
2506
+ /**
2507
+ * Build the full price row the browser prices a session with.
2508
+ *
2509
+ * Every figure switches together: once the deal stops being live, the base
2510
+ * rates AND each context band come from the pre-discount set, so a session
2511
+ * priced after a promotion ends can never mix the two.
2512
+ */
2513
+ function wireRow(id, slug, row, now) {
2514
+ const promotional = dealLabel(id, now) !== void 0;
1859
2515
  const price = {
1860
2516
  id,
1861
2517
  slug,
1862
- ...ratesOf(row.rates)
2518
+ ...ratesOf(promotional ? row.rates : row.listRates ?? row.rates)
1863
2519
  };
1864
2520
  if (row.peak !== void 0) price.peak = ratesOf(row.peak);
1865
2521
  if (row.contextTiers !== void 0) price.contextTiers = row.contextTiers.map((tier) => ({
1866
- ...ratesOf(tier.rates),
2522
+ ...ratesOf(promotional ? tier.rates : tier.listRates ?? tier.rates),
1867
2523
  ...tier.maxContext === void 0 ? {} : { maxContext: tier.maxContext }
1868
2524
  }));
1869
2525
  return price;
@@ -1878,8 +2534,13 @@ function wireRow(id, slug, row) {
1878
2534
  * explicitly at zero so the composer can say so instead of showing nothing; the
1879
2535
  * peak windows travel with the table so the browser applies the very schedule
1880
2536
  * this snapshot knows instead of restating it.
2537
+ *
2538
+ * `now` (defaults to `Date.now()`) resolves lapsed promotions: a discounted row
2539
+ * whose `expiresAt` has passed is served at its pre-discount `listRates`, the
2540
+ * same instant {@link dealLabel} stops showing its badge. The table is rebuilt
2541
+ * per request, so that switch needs no redeploy.
1881
2542
  */
1882
- function modelPriceTable() {
2543
+ function modelPriceTable(now = Date.now()) {
1883
2544
  const bySlug = new Map(MODEL_PRICE_ROWS.map((row) => [row.id, row]));
1884
2545
  const known = new Set(bySlug.keys());
1885
2546
  const models = [];
@@ -1901,86 +2562,56 @@ function modelPriceTable() {
1901
2562
  const row = bySlug.get(slug);
1902
2563
  if (row === void 0) continue;
1903
2564
  claimed.add(slug);
1904
- models.push(wireRow(catalogId, slug, row));
2565
+ models.push(wireRow(catalogId, slug, row, now));
1905
2566
  }
1906
2567
  for (const row of MODEL_PRICE_ROWS) {
1907
2568
  if (claimed.has(row.id)) continue;
1908
- models.push(wireRow(row.id, row.id, row));
2569
+ models.push(wireRow(row.id, row.id, row, now));
1909
2570
  }
1910
2571
  return {
1911
2572
  models,
1912
2573
  peakHours: PEAK_HOUR_RANGES.map(([start, end]) => [start, end])
1913
2574
  };
1914
2575
  }
1915
- //#endregion
1916
- //#region src/cost-facts.ts
1917
- const zeroCostTokens = () => ({
1918
- uncachedInputTokens: 0,
1919
- outputTokens: 0,
1920
- cacheReadTokens: 0,
1921
- cacheWriteTokens: 0
1922
- });
1923
- const COST_TOKEN_KEYS = [
1924
- "uncachedInputTokens",
1925
- "outputTokens",
1926
- "cacheReadTokens",
1927
- "cacheWriteTokens"
1928
- ];
1929
- function peakHour(at, windows) {
1930
- const time = new Date(at);
1931
- return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
1932
- }
2576
+ /** Slug-keyed view of the rows, built once: neither table nor allowance mutates. */
2577
+ const ROWS_BY_SLUG = new Map(MODEL_PRICE_ROWS.map((row) => [row.id, row]));
2578
+ const KNOWN_SLUGS = new Set(ROWS_BY_SLUG.keys());
1933
2579
  /**
1934
- * All prompt billing buckets determine a request's band, never cumulative
1935
- * session input.
2580
+ * The per-model monthly allowance for a CATALOG model id, or undefined when the
2581
+ * page carries none (a model without a price row, or the free stealth previews
2582
+ * the estimator array omits).
1936
2583
  *
1937
- * A tiered model returns the matching band and does NOT then apply `peak`: the
1938
- * page publishes those two dimensions independently and no row carries both
1939
- * today, so the combination becoming real is visible before it reaches a user.
1940
- * This ordering is a latent choice, not a verified upstream rule.
1941
- */
1942
- function requestRates(price, at, contextTokens, table) {
1943
- const tier = price.contextTiers?.find((tier) => tier.maxContext === void 0 || contextTokens <= tier.maxContext);
1944
- if (tier !== void 0) return tier;
1945
- return price.peak !== void 0 && at !== null && peakHour(at, table.peakHours) ? price.peak : price;
2584
+ * Resolution rides the same slug rules as the price table, so
2585
+ * `MiniMaxAI/MiniMax-M3` finds the row the page files under `minimax-m3` — the
2586
+ * settings page must not grow a second mapping that can drift from this one.
2587
+ * The settings page asks for a model only while rendering a list it already
2588
+ * has, so this stays a pure lookup with no clock: an allowance is not
2589
+ * time-dependent (a plan's figure does not lapse the way a promotional rate
2590
+ * does; when the page re-prices it, the next sync carries it).
2591
+ */
2592
+ function modelAllowanceFor(modelId) {
2593
+ const slug = priceSlugFor(modelId, KNOWN_SLUGS);
2594
+ if (slug === void 0) return void 0;
2595
+ return ROWS_BY_SLUG.get(slug)?.allowance;
2596
+ }
2597
+ //#endregion
2598
+ //#region src/wire-guards.ts
2599
+ /** Non-throwing value guards for untrusted provider payloads. */
2600
+ function isRecord(value) {
2601
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1946
2602
  }
1947
- /** Cache version and wire guard: changing bands/rates must refold historical groups. */
1948
- function pricingKey(table) {
1949
- const rates = (r) => [
1950
- r.inputCost,
1951
- r.outputCost,
1952
- r.cacheReadCost,
1953
- r.cacheWriteCost ?? null
1954
- ];
1955
- const canonical = JSON.stringify([
1956
- "commandCodeCost-v1",
1957
- table.peakHours,
1958
- table.models.map((p) => [
1959
- p.id,
1960
- p.slug,
1961
- p.free === true,
1962
- rates(p),
1963
- p.peak ? rates(p.peak) : null,
1964
- p.contextTiers?.map((t) => [t.maxContext ?? null, rates(t)]) ?? null
1965
- ])
1966
- ]);
1967
- let hash = 2166136261;
1968
- for (const c of canonical) hash = Math.imul(hash ^ c.charCodeAt(0), 16777619) >>> 0;
1969
- return hash;
2603
+ function stringValue(value) {
2604
+ return typeof value === "string" ? value : void 0;
1970
2605
  }
1971
- /** Only safe to merge requests whose billing classification is identical. */
1972
- function costGroupKey(group, table) {
1973
- const price = table.models.find((p) => p.id === group.model || p.slug === group.model);
1974
- return JSON.stringify([
1975
- group.provider,
1976
- group.model,
1977
- price === void 0 ? null : requestRates(price, group.at, group.contextTokens, table),
1978
- group.at === null
1979
- ]);
2606
+ function numberValue(value) {
2607
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2608
+ }
2609
+ function booleanValue(value) {
2610
+ return typeof value === "boolean" ? value : void 0;
1980
2611
  }
1981
2612
  //#endregion
1982
2613
  //#region src/cost-projection.ts
1983
- const record$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
2614
+ const record$2 = (v) => isRecord(v) ? v : void 0;
1984
2615
  const finite = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
1985
2616
  function tokensOf(value) {
1986
2617
  const u = record$2(value);
@@ -2069,11 +2700,18 @@ function createCostProjection(table = modelPriceTable()) {
2069
2700
  ...g,
2070
2701
  tokens: { ...g.tokens }
2071
2702
  }));
2703
+ const byKey = /* @__PURE__ */ new Map();
2704
+ groups.forEach((g, i) => {
2705
+ const key = costGroupKey(g, table);
2706
+ if (!byKey.has(key)) byKey.set(key, i);
2707
+ });
2708
+ const groupKey = costGroupKey(group, table);
2072
2709
  if (previous) {
2073
- const old = groups.find((g) => costGroupKey(g, table) === costGroupKey(previous, table));
2074
- if (old) for (const k of COST_TOKEN_KEYS) old.tokens[k] -= previous.tokens[k];
2710
+ const old = byKey.get(costGroupKey(previous, table));
2711
+ if (old !== void 0) for (const k of COST_TOKEN_KEYS) groups[old].tokens[k] -= previous.tokens[k];
2075
2712
  }
2076
- let target = groups.find((g) => costGroupKey(g, table) === costGroupKey(group, table));
2713
+ const targetIdx = byKey.get(groupKey);
2714
+ let target = targetIdx === void 0 ? void 0 : groups[targetIdx];
2077
2715
  if (!target) {
2078
2716
  target = {
2079
2717
  ...group,
@@ -2122,6 +2760,27 @@ function installCostProjection(ctx) {
2122
2760
  * JSON, SSE, and pre-stream error bodies undecoded.
2123
2761
  */
2124
2762
  const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
2763
+ const THROTTLED_CODE = "THROTTLED";
2764
+ const boundedCodes = /* @__PURE__ */ new Set([
2765
+ "EMPTY_RESPONSE",
2766
+ "SERVER",
2767
+ "TIMEOUT",
2768
+ THROTTLED_CODE
2769
+ ]);
2770
+ const counts$1 = /* @__PURE__ */ new WeakMap();
2771
+ function absorbTransientFailure(agent, code) {
2772
+ if (!boundedCodes.has(code)) return "ignored";
2773
+ const used = counts$1.get(agent) ?? 0;
2774
+ if (used >= 3) return "exhausted";
2775
+ counts$1.set(agent, used + 1);
2776
+ return "retry";
2777
+ }
2778
+ function resetTransientFailures(agent) {
2779
+ counts$1.delete(agent);
2780
+ }
2781
+ function transientBudgetMessage(code, message) {
2782
+ return `Command Code 请求在 3 次自动重试后仍失败,已停止重试(${code})。请稍后重新发送;若反复出现,请检查服务状态或请求耗时诊断。 原因:${message}`;
2783
+ }
2125
2784
  //#endregion
2126
2785
  //#region src/accounts.ts
2127
2786
  /**
@@ -2143,24 +2802,18 @@ const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
2143
2802
  * @module dsh-commandcode-provider/accounts
2144
2803
  */
2145
2804
  /**
2146
- * Upper bound on the retry wait this pool attaches to the all-exhausted
2147
- * `RATE_LIMIT` error. Must equal the `backoff.maxDelayMs` in the adapter's
2148
- * `providerRetryPolicy` (which imports it from here): dsh-llm-retry honors a
2149
- * provider-specified wait verbatim only at or below that cap — a LONGER one
2150
- * makes the executor abandon the retry instead of falling back to local backoff
2151
- * (in normal mode), turning "poll until the window opens" into "fail now".
2805
+ * Upper bound on the retry wait attached to the all-exhausted `RATE_LIMIT`.
2806
+ * Must equal the adapter's `backoff.maxDelayMs`: dsh-llm-retry honors a
2807
+ * provider wait verbatim only at or below that cap — a longer one abandons the
2808
+ * retry instead of falling back to local backoff, turning "poll until the
2809
+ * window opens" into "fail now".
2152
2810
  */
2153
2811
  const RETRY_MAX_DELAY_MS = 9e5;
2154
2812
  /**
2155
2813
  * How long an explicitly selected account's rate-limit mark may stand before
2156
- * its window is probed again.
2157
- *
2158
- * A 429 whose reset was never learned marks the key `unknown`, which
2159
- * {@link accountUsable} refuses — for a key nobody asked for that is correct
2160
- * (only the all-marked pass clears it, and it costs one billing call), but for
2161
- * the user's own pinned account a single 429 would demote it until the process
2162
- * restarts (issue #51). Once per interval per key bounds a probe endpoint that
2163
- * keeps failing; a successful probe resolves the mark for good.
2814
+ * its window is probed again. Without this, a single 429 on the user's pinned
2815
+ * account demotes it until the process restarts (issue #51); probing at most
2816
+ * once per interval per key bounds a probe endpoint that keeps failing.
2164
2817
  */
2165
2818
  const EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS = 6e4;
2166
2819
  /** A labeled, human-readable clock reading for error messages. */
@@ -2265,26 +2918,18 @@ var CommandCodeAccountPool = class {
2265
2918
  return out;
2266
2919
  }
2267
2920
  /**
2268
- * Hand out the key for a request: the model-routed account when the request's
2269
- * model matches a rule (and that account is usable), else the manually
2270
- * preferred account when usable, else the first usable account in rotation
2271
- * order. Returns `undefined` when no account resolves any key at all, or when
2272
- * an account this request already used is still UNMARKED — that rejection is
2273
- * the honest answer, so the caller's own rejection surfaces as itself. Throws
2274
- * `RATE_LIMIT` (naming the earliest window reset) or `INVALID_CREDENTIAL`
2275
- * when accounts exist but every one of them is marked.
2921
+ * Hand out the key for a request: the model-routed account when usable, else
2922
+ * the manually preferred account when usable, else the first usable account
2923
+ * in rotation order. Returns `undefined` when no account resolves any key,
2924
+ * or when an account this request already used is still UNMARKED — the
2925
+ * caller's own rejection is the honest answer then. Throws `RATE_LIMIT` /
2926
+ * `INVALID_CREDENTIAL` when every account is marked.
2276
2927
  *
2277
- * `options.model` is the request's model id; routing rules re-read per
2278
- * resolution, so a settings change applies live. `options.tried` lists the
2279
- * keys this request has already used — the just-rejected one included — and
2280
- * they are removed from the resolution entirely, which is what lets one
2281
- * request walk a four-account pool: a rejection that does not mark the key
2282
- * (no credits, a model outside the account's plan) would otherwise be offered
2283
- * again on every attempt and the accounts behind it never reached.
2284
- *
2285
- * An explicit selection (the pin or a model rule) that a rate-limit mark would
2286
- * demote is probed before the fallback serves, so "falls back while exhausted"
2287
- * never becomes "stays demoted until the process restarts" (issue #51).
2928
+ * `options.tried` lists the keys this request already used; they are removed
2929
+ * from the resolution entirely, which is what lets one request walk a
2930
+ * four-account pool. An explicit selection (pin or model rule) that a
2931
+ * rate-limit mark would demote is probed first, so a fallback never becomes
2932
+ * permanent (issue #51). Rules re-read per resolution, so settings apply live.
2288
2933
  */
2289
2934
  async resolveKey(options) {
2290
2935
  const accounts = await this.resolvedAccounts();
@@ -2320,18 +2965,7 @@ var CommandCodeAccountPool = class {
2320
2965
  this.states.delete(account.key);
2321
2966
  return;
2322
2967
  }
2323
- if (probe.resetAt > 0) this.states.set(account.key, {
2324
- kind: "cooldown",
2325
- cause: "window",
2326
- reason: account.state?.reason ?? "usage window exhausted (429)",
2327
- until: probe.resetAt
2328
- });
2329
- else if (account.state?.kind !== "cooldown") this.states.set(account.key, {
2330
- kind: "unknown",
2331
- cause: "window",
2332
- reason: account.state?.reason ?? "usage window exhausted (429)",
2333
- until: 0
2334
- });
2968
+ this.stampWindowMark(account, probe);
2335
2969
  }));
2336
2970
  const latestAccounts = await this.resolvedAccounts();
2337
2971
  const revived = selectActiveAccount(tried === void 0 || tried.length === 0 ? latestAccounts : latestAccounts.filter((account) => !tried.includes(account.key)), preferred);
@@ -2464,6 +3098,18 @@ var CommandCodeAccountPool = class {
2464
3098
  state: void 0
2465
3099
  };
2466
3100
  }
3101
+ this.stampWindowMark(account, probe);
3102
+ }
3103
+ /**
3104
+ * Stamp a probe-read window onto one key. A known reset becomes a cooldown
3105
+ * that expires by itself; without one the mark stays `unknown`, so a later
3106
+ * probe can still learn it — and a mark that already carries a cooldown
3107
+ * keeps it, never trading a known reset for an unknown one. The cause is
3108
+ * `window` either way: a probe that read a window as exceeded IS window
3109
+ * evidence, whatever the key was marked for. An `until: 0` cooldown would
3110
+ * read as "never usable again" to {@link accountUsable}, so `unknown` it is.
3111
+ */
3112
+ stampWindowMark(account, probe) {
2467
3113
  if (probe.resetAt > 0) this.states.set(account.key, {
2468
3114
  kind: "cooldown",
2469
3115
  cause: "window",
@@ -2512,7 +3158,7 @@ var CommandCodeAccountPool = class {
2512
3158
  function allAccountsUnusable(accounts) {
2513
3159
  if (accounts.filter((account) => account.state?.kind === "disabled").length === accounts.length) return new LlmError(`llm-commandcode: every configured Command Code account (${accounts.length}) was rejected with 401 — check the stored API keys (Models page / settings) or the auth file;已配置的 ${accounts.length} 个 Command Code 账户密钥均被拒绝(401)——请在设置页检查存储的 API 密钥,或重新运行 command-code login`, "INVALID_CREDENTIAL");
2514
3160
  const windowMarked = accounts.map((account) => account.state).filter((state) => state !== void 0).filter((state) => state.cause === "window");
2515
- if (windowMarked.length === 0) return new LlmError(`llm-commandcode: all ${accounts.length} Command Code account(s) are rate limited (429) — the provider did not report an exhausted usage window; retrying;全部 ${accounts.length} 个 Command Code 账户被限流(429)——服务商未报告用量窗口用尽,正在重试`, "RATE_LIMIT");
3161
+ if (windowMarked.length === 0) return new LlmError(`llm-commandcode: all ${accounts.length} Command Code account(s) are rate limited (429) — the provider did not report an exhausted usage window; retrying;全部 ${accounts.length} 个 Command Code 账户被限流(429)——服务商未报告用量窗口用尽,正在重试`, THROTTLED_CODE);
2516
3162
  const resets = windowMarked.filter((state) => state.kind === "cooldown" && state.until > 0).map((state) => state.until);
2517
3163
  const earliest = resets.length > 0 ? Math.min(...resets) : 0;
2518
3164
  const wait = earliest > 0 ? Math.max(1e3, earliest - Date.now()) : 0;
@@ -2647,12 +3293,12 @@ function commandCodeImageTokens(modelId, width, height) {
2647
3293
  return imageTokenCost(imageTokenFamily(modelId), width, height);
2648
3294
  }
2649
3295
  /**
2650
- * Anthropic: the image is first scaled so its long edge is at most 1568 px
2651
- * (never enlarged), then charged roughly one token per 750 pixels.
3296
+ * Anthropic: the image is first scaled to the request long edge (never
3297
+ * enlarged), then charged roughly one token per 750 pixels.
2652
3298
  * Source: Anthropic vision documentation, "Image size and token usage".
2653
3299
  */
2654
3300
  function anthropicImageTokens(width, height) {
2655
- const projected = longEdgeDimensions(width, height, 1568);
3301
+ const projected = longEdgeDimensions(width, height, REQUEST_IMAGE_MAX_LONG_EDGE);
2656
3302
  return Math.max(1, Math.ceil(projected.width * projected.height / 750));
2657
3303
  }
2658
3304
  /**
@@ -2933,6 +3579,95 @@ function openStreamTrace(scope, options = {}) {
2933
3579
  };
2934
3580
  }
2935
3581
  //#endregion
3582
+ //#region src/request-timing.ts
3583
+ /** 请求耗时只记录阶段、计数和状态;不记录消息、凭据、请求头或错误正文。 */
3584
+ const REQUEST_TIMING_ENV = "DSH_COMMANDCODE_TIMING";
3585
+ /** 每次生成独立计时;可注入时钟验证顺序,不依赖真实等待。 */
3586
+ var RequestTiming = class {
3587
+ sink;
3588
+ now;
3589
+ started;
3590
+ summary;
3591
+ constructor(model, sink, now = () => performance.now()) {
3592
+ this.sink = sink;
3593
+ this.now = now;
3594
+ this.started = now();
3595
+ this.summary = {
3596
+ version: 1,
3597
+ startedAt: Date.now(),
3598
+ model,
3599
+ outcome: "cancelled",
3600
+ totalMs: 0,
3601
+ phasesMs: {
3602
+ credentials: 0,
3603
+ images: 0,
3604
+ body: 0,
3605
+ serialization: 0,
3606
+ headers: 0
3607
+ },
3608
+ attempts: []
3609
+ };
3610
+ }
3611
+ /** 返回结束函数,确保成功和异常都计入同一阶段。阶段可嵌套,不能直接相加。 */
3612
+ phase(name) {
3613
+ if (!this.sink) return () => {};
3614
+ const start = this.now();
3615
+ return () => {
3616
+ this.summary.phasesMs[name] += this.now() - start;
3617
+ };
3618
+ }
3619
+ attempt(protocol, bodyBytes) {
3620
+ if (!this.sink) return () => {};
3621
+ const start = this.now();
3622
+ const attempt = {
3623
+ protocol,
3624
+ bodyBytes,
3625
+ headersMs: 0
3626
+ };
3627
+ this.summary.attempts.push(attempt);
3628
+ return (status) => {
3629
+ Object.assign(attempt, {
3630
+ headersMs: this.now() - start,
3631
+ ...status === void 0 ? {} : { status }
3632
+ });
3633
+ this.summary.phasesMs.headers += attempt.headersMs;
3634
+ };
3635
+ }
3636
+ first(kind) {
3637
+ if (this.sink && this.summary[kind] === void 0) this.summary[kind] = this.now() - this.started;
3638
+ }
3639
+ async close(outcome, errorCode) {
3640
+ if (!this.sink) return;
3641
+ this.summary.outcome = outcome;
3642
+ this.summary.totalMs = this.now() - this.started;
3643
+ if (errorCode !== void 0) this.summary.errorCode = errorCode;
3644
+ try {
3645
+ await this.sink(this.summary);
3646
+ } catch {}
3647
+ }
3648
+ };
3649
+ /** 与原始流诊断独立开关,便于仅收集数值耗时。 */
3650
+ function openRequestTiming(model, sink) {
3651
+ if (sink) return new RequestTiming(model, sink);
3652
+ const raw = process.env[REQUEST_TIMING_ENV]?.trim();
3653
+ if (!raw || [
3654
+ "0",
3655
+ "false",
3656
+ "no",
3657
+ "off"
3658
+ ].includes(raw.toLowerCase())) return new RequestTiming(model);
3659
+ const path = [
3660
+ "1",
3661
+ "true",
3662
+ "yes",
3663
+ "on"
3664
+ ].includes(raw.toLowerCase()) ? join(tmpdir(), "dsh-commandcode-timing.jsonl") : raw;
3665
+ return new RequestTiming(model, async (summary) => {
3666
+ await mkdir(dirname(path), { recursive: true });
3667
+ await appendFile(path, `${JSON.stringify(summary)}\n`, { mode: 384 });
3668
+ });
3669
+ }
3670
+ //#endregion
2936
3671
  //#region src/adapter.ts
2937
3672
  /**
2938
3673
  * DeepSeek Harness LLM adapter for the Command Code Provider API.
@@ -2941,21 +3676,151 @@ function openStreamTrace(scope, options = {}) {
2941
3676
  * community-maintained integration; you need your own Command Code account
2942
3677
  * and API key or subscription, and Command Code's terms apply.
2943
3678
  *
2944
- * Owns the two chat transports (`/alpha/generate`, `/provider/v1/chat/completions`),
3679
+ * Owns the three chat transports (`/alpha/generate`, `/provider/v1/chat/completions`,
3680
+ * `/provider/v1/messages`),
2945
3681
  * message conversion, SSE/JSONL parsing, the catalog + its on-disk cache, and
2946
3682
  * the pre-stream account rotation loop. The wire protocol is
2947
- * reverse-engineered (command-code@1.28.4, re-verified through 1.66.0);
2948
- * AGENTS.md holds the full protocol record.
3683
+ * reverse-engineered (command-code@1.28.4, re-verified through 1.72.1);
3684
+ * docs/agent-reference/adapter-protocol.md holds the full protocol record.
2949
3685
  *
2950
3686
  * The adapter is deliberately free of cordis/schemastery: it receives a
2951
3687
  * per-request options thunk and an API-key resolver from the plugin entry
2952
3688
  * (src/index.ts), so a settings change reaches the very next request.
2953
3689
  */
2954
- const COMMAND_CODE_CLI_VERSION = "1.66.0";
3690
+ /**
3691
+ * Output ceilings the endpoint has named in a live rejection, per model.
3692
+ *
3693
+ * The snapshot in ./capabilities.ts carries the vendor-published ceiling for
3694
+ * 74 of the 84 catalog models (see scripts/sync-output-limits.mjs). The other
3695
+ * 10 are models no vendor publishes yet, and a gateway is free to sit below
3696
+ * the vendor figure even where one exists — so this map is the correction
3697
+ * channel, not a fallback of last resort: the endpoint states the model's own
3698
+ * ceiling in its rejection, so one wasted round trip is enough to know it for
3699
+ * the rest of the process.
3700
+ *
3701
+ * Shared by adapters for the same gateway, but not across apiBase values: a
3702
+ * proxy can enforce a smaller ceiling for the same model id. Only ever tightens
3703
+ * within that gateway, so a stale catalog row cannot undo a live refusal.
3704
+ */
3705
+ const learnedOutputLimits = /* @__PURE__ */ new Map();
3706
+ /**
3707
+ * Output ceilings the CATALOG published, per model.
3708
+ *
3709
+ * `/provider/v1/models` carries `max_output_tokens` in its schema and the
3710
+ * official pi provider already prefers it, but the endpoint does not send it
3711
+ * yet — so this is empty today and fills in on its own the day it does. It sits
3712
+ * above the models.dev snapshot and below what a live rejection proved, because
3713
+ * that ordering is also the order of trust: the gateway knows its own ceiling,
3714
+ * a rejection proves it, and models.dev is a third party describing the same
3715
+ * model. Restored from the on-disk catalog cache so a published ceiling
3716
+ * survives a restart without waiting for the next catalog fetch.
3717
+ */
3718
+ const publishedOutputLimits = /* @__PURE__ */ new Map();
3719
+ /** Keep model facts from one gateway from constraining another gateway. */
3720
+ function outputLimitsFor(store, apiBase) {
3721
+ let limits = store.get(apiBase);
3722
+ if (limits === void 0) {
3723
+ limits = /* @__PURE__ */ new Map();
3724
+ store.set(apiBase, limits);
3725
+ }
3726
+ return limits;
3727
+ }
3728
+ /**
3729
+ * The output ceiling this gateway route sends for `modelId`.
3730
+ *
3731
+ * `/provider/v1/models` publishes `context_length` and `supported_endpoints`
3732
+ * but no output cap today, while each endpoint refuses a larger `max_tokens` per
3733
+ * model — the Messages endpoint names its own in the rejection (`max_tokens:
3734
+ * 200000 > 128000, which is the maximum allowed number of output tokens for
3735
+ * claude-sonnet-5-5`). What the endpoint has already named wins, then the
3736
+ * catalog's own figure once it publishes one, then the snapshot, then — for
3737
+ * the Messages-route models only — the conservative value, because the ceilings
3738
+ * actually observed are below the global default and assuming otherwise would
3739
+ * fail every request.
3740
+ */
3741
+ function modelOutputTokenLimit(apiBase, modelId) {
3742
+ const learned = outputLimitsFor(learnedOutputLimits, apiBase).get(modelId);
3743
+ const known = outputLimitsFor(publishedOutputLimits, apiBase).get(modelId) ?? MODEL_OUTPUT_TOKEN_LIMITS.get(modelId);
3744
+ if (learned !== void 0 && (known === void 0 || learned < known)) return learned;
3745
+ if (known !== void 0) return known;
3746
+ return requiresMessagesEndpoint(modelId) ? DEFAULT_MESSAGES_MAX_TOKENS : DEFAULT_MAX_OUTPUT_TOKENS;
3747
+ }
3748
+ /**
3749
+ * Record a ceiling the endpoint just stated, if it is one this route can act
3750
+ * on: a real number, and one below what the next request would otherwise send.
3751
+ * A ceiling at or above the current budget describes a request that was not
3752
+ * refused for being too large, and acting on it would be a guess.
3753
+ */
3754
+ function noteOutputCeiling(apiBase, modelId, ceiling) {
3755
+ if (!Number.isSafeInteger(ceiling) || ceiling <= 0) return false;
3756
+ if (ceiling >= modelOutputTokenLimit(apiBase, modelId)) return false;
3757
+ outputLimitsFor(learnedOutputLimits, apiBase).set(modelId, ceiling);
3758
+ return true;
3759
+ }
3760
+ /**
3761
+ * The ceiling to retry this turn with, when `error` is a request-side output
3762
+ * refusal the route can act on — else undefined, meaning surface it.
3763
+ *
3764
+ * Read from the message, so it covers both shapes the same refusal arrives in:
3765
+ * an HTTP 400 (`PROVIDER_HTTP_ERROR`) and an in-stream error frame
3766
+ * (`PROVIDER_STREAM_ERROR`). Both carry the endpoint's wording, and both are
3767
+ * already classified as output refusals rather than context overflows by the
3768
+ * time they reach here.
3769
+ *
3770
+ * Recording is what makes the retry pay off: the ceiling is kept for this
3771
+ * gateway and model, so a refused turn leaves its next request correct.
3772
+ */
3773
+ function outputCeilingRefusal(apiBase, modelId, error) {
3774
+ if (!(error instanceof LlmError)) return void 0;
3775
+ const detail = error.message;
3776
+ if (!OUTPUT_LIMIT_REJECTION.test(detail)) return void 0;
3777
+ const stated = parseOutputCeiling(detail);
3778
+ if (stated === void 0) return void 0;
3779
+ return noteOutputCeiling(apiBase, modelId, stated) ? stated : void 0;
3780
+ }
3781
+ /** Keep the two user-facing languages and error metadata together. */
3782
+ function bilingual(code, en, zh, options) {
3783
+ return new LlmError(`${en};${zh}`, code, options);
3784
+ }
3785
+ const COMMAND_CODE_CLI_VERSION = "1.72.1";
2955
3786
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
2956
- const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
2957
- const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
3787
+ /**
3788
+ * The output budget this bundle asks for, and the ceiling it will never exceed.
3789
+ *
3790
+ * 131 072, chosen against a measured provider limit rather than a round guess.
3791
+ * Probing `/alpha/generate` directly on 2026-09-28: 64 000 → 200, 131 072 → 200,
3792
+ * 196 608 → 200, **200 000 → 200, 200 704 → 400**, 1 000 000 → 400. The server
3793
+ * therefore caps `max_tokens` at 200 000, and it does so identically for a
3794
+ * 262 144-window model (`stealth/pixel-canary`) and a 1 000 000-window one
3795
+ * (`stealth/space-bunny-alpha`) — the cap is global, not per model. 131 072
3796
+ * keeps a 35 % margin under it while covering the real output ceiling of the
3797
+ * models that have one (Claude 64 K, Gemini 64 K, several at 128 K).
3798
+ *
3799
+ * The old 64 000 was never a provider requirement: it cost nothing in latency
3800
+ * (a 64 000 request answered headers in 1.28 s against 1.57 s for 131 072) but
3801
+ * it did cap what a model could ever say in one turn.
3802
+ *
3803
+ * This is only an UPPER bound. `requestContextBudget` lowers it per request
3804
+ * from the prompt actually being sent, so raising the ceiling cannot push a
3805
+ * large session over its window.
3806
+ */
3807
+ const DEFAULT_GENERATE_MAX_TOKENS = 131072;
3808
+ /**
3809
+ * The fallback when the catalog carries no output ceiling for a model.
3810
+ *
3811
+ * `/provider/v1/models` publishes only `context_length` — no output limit — so
3812
+ * the catalog cannot learn one per model and derives a stand-in from the
3813
+ * window instead. Kept equal to {@link DEFAULT_GENERATE_MAX_TOKENS} on purpose:
3814
+ * a lower value here would silently re-cap every request through
3815
+ * `Math.min(contextLength, this)` before the ceiling above is ever consulted.
3816
+ */
3817
+ const DEFAULT_MAX_OUTPUT_TOKENS = 131072;
3818
+ /** Preserve a small usable answer budget when the heuristic overshoots. */
3819
+ const MIN_CONTEXT_OUTPUT_TOKENS = 1024;
2958
3820
  const MODELS_TIMEOUT_MS = 1e4;
3821
+ /** 目录短期复用;刷新失败只短暂退避,避免反复阻塞模型选择器。 */
3822
+ const MODEL_CATALOG_TTL_MS = 3e5;
3823
+ const MODEL_CATALOG_RETRY_MS = 1e4;
2959
3824
  /** How long the picker's plan-filter billing facts stay cached before refetching. */
2960
3825
  const BILLING_ACCESS_TTL_MS = 3e5;
2961
3826
  /** The entry-plan tier weight (`individual-go` in KNOWN_SUBSCRIPTION_PLANS). */
@@ -2971,19 +3836,70 @@ const ACTIVE_SUBSCRIPTION_STATUSES = /* @__PURE__ */ new Set([
2971
3836
  "trialing",
2972
3837
  "past_due"
2973
3838
  ]);
2974
- /** Head-of-request timeout: how long to wait for the first response byte. */
2975
- const DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
3839
+ /**
3840
+ * Head-of-request timeout: how long to wait for the first response byte.
3841
+ *
3842
+ * 300 s, and that number is the official CLI's, not a round choice. Decompiled
3843
+ * from `command-code@1.66.0`: its `createNodeTransport` fetch passes only the
3844
+ * caller's `signal` and sets no timeout at all, so the model request inherits
3845
+ * Node/undici's own default — measured at 301.0 s against a local server that
3846
+ * accepts the connection and never sends headers. The previous 60 s default was
3847
+ * five times stricter than the CLI it emulates, and being stricter only ever
3848
+ * costs legitimate requests: a slow upstream first token on the Provider API
3849
+ * route can outlast 60 s, and the official CLI simply waits it out. Measured
3850
+ * headers (2026-09-28, 26 250-token prompt, `max_tokens: 16` to separate the
3851
+ * header from the generation): `/alpha/generate` 2.12 / 2.56 / 2.75 s,
3852
+ * `/provider/v1/chat/completions` 5.52 / 2.98 / 3.70 s — a ~1.1 s median gap
3853
+ * with the Provider API route varying over a 2.5 s span against the CLI route's
3854
+ * 0.6 s. A gateway that is genuinely dead now takes longer to name itself,
3855
+ * which is what the consecutive-timeout streak exists to bound (three
3856
+ * attempts, then a diagnosis instead of another re-send).
3857
+ */
3858
+ const DEFAULT_REQUEST_TIMEOUT_MS = 3e5;
2976
3859
  /** Stream idle timeout: a generation that stalls this long is a dead connection. */
2977
3860
  const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
2978
- const MODEL_CACHE_VERSION = 1;
2979
- function isRecord(value) {
2980
- return typeof value === "object" && value !== null && !Array.isArray(value);
2981
- }
2982
- function stringValue(value) {
2983
- return typeof value === "string" ? value : void 0;
2984
- }
2985
- function numberValue(value) {
2986
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3861
+ /** Version 3 binds cached catalog facts to their gateway; older files lack a safe source. */
3862
+ const MODEL_CACHE_VERSION = 3;
3863
+ /**
3864
+ * A lone UTF-16 surrogate half: a high surrogate not followed by a low one, or
3865
+ * a low surrogate not preceded by a high one. Such a half is not a character —
3866
+ * it only arises from a truncated slice, a lossy decode, or a lone `\uD83D`
3867
+ * escape — and it survives `JSON.stringify` as an escape (`"\ud83d"`) rather
3868
+ * than failing locally, so the request leaves the process carrying a string no
3869
+ * UTF-8 consumer can round-trip. Providers running strict decoders reject it.
3870
+ *
3871
+ * This is the same expression `@earendil-works/pi-ai` applies to every outgoing
3872
+ * string on this endpoint. Valid astral characters (emoji and anything else
3873
+ * outside the BMP) are PAIRED surrogates and pass through untouched.
3874
+ */
3875
+ const LONE_SURROGATE_PATTERN = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
3876
+ /**
3877
+ * A cheap pre-test for "this string contains any surrogate code unit at all",
3878
+ * paired or not. The exact pattern above needs lookaround, which V8 cannot
3879
+ * optimize into a fast scan; this one is a plain range test. Real prompts are
3880
+ * overwhelmingly ASCII-plus-CJK with no surrogates, so the common case pays one
3881
+ * linear scan instead of the lookaround pass. Measured on a 412 KB request
3882
+ * body: 1.40 ms with the exact pattern alone against 0.77 ms with this gate,
3883
+ * where a bare `JSON.stringify` is 0.59 ms.
3884
+ */
3885
+ const ANY_SURROGATE_PATTERN = /[\uD800-\uDFFF]/;
3886
+ /**
3887
+ * Drop lone surrogate halves from one outgoing string.
3888
+ *
3889
+ * Applied at the single point a request body is serialized, so every field of
3890
+ * every transport is covered — system text, message content, tool schemas and
3891
+ * tool results alike — instead of enumerating the fields that can carry text
3892
+ * and missing one. The value is returned unchanged whenever it holds no
3893
+ * surrogate at all, which is the ordinary case.
3894
+ *
3895
+ * The `test()` call deliberately uses the flagless {@link ANY_SURROGATE_PATTERN}:
3896
+ * a global regex is stateful, so testing with one would advance `lastIndex` and
3897
+ * make consecutive calls on the same string alternate between true and false.
3898
+ * `replace()` resets `lastIndex` itself, so the global pattern is safe there.
3899
+ */
3900
+ function sanitizeSurrogates(value) {
3901
+ if (typeof value !== "string") return value;
3902
+ return ANY_SURROGATE_PATTERN.test(value) ? value.replace(LONE_SURROGATE_PATTERN, "") : value;
2987
3903
  }
2988
3904
  /** Hash one request value without retaining or logging its contents. */
2989
3905
  function fingerprintValue(value) {
@@ -3076,9 +3992,6 @@ function failureWithRequestId(error, metadata) {
3076
3992
  ...error.cause === void 0 ? {} : { cause: error.cause }
3077
3993
  });
3078
3994
  }
3079
- function booleanValue(value) {
3080
- return typeof value === "boolean" ? value : void 0;
3081
- }
3082
3995
  /**
3083
3996
  * Thinking text carried by an OpenRouter-shaped `reasoning_details` array,
3084
3997
  * concatenated in the order the gateway sent it.
@@ -3160,6 +4073,109 @@ const CLI_CONTEXT_OVERFLOW_PATTERN = /prompt is too long|context.*(length|window
3160
4073
  function isContextOverflowDetail(detail) {
3161
4074
  return isContextWindowExceededError(detail) || CLI_CONTEXT_OVERFLOW_PATTERN.test(detail);
3162
4075
  }
4076
+ /** A gateway refusal observed for an over-reserved request, but not unique to that cause. */
4077
+ const AMBIGUOUS_SIZE_REJECTION = /\bmodel could not complete the request\b/i;
4078
+ /**
4079
+ * A request-side `max_tokens` refusal, told apart from a context overflow that
4080
+ * mentions the same field. Kept narrow on purpose: the overflow pattern also
4081
+ * matches a bare `max_tokens`, so this must name the specific shapes the
4082
+ * endpoint uses — a ceiling comparison, its prose about output tokens, or a
4083
+ * request-validation error on the field.
4084
+ */
4085
+ const OUTPUT_LIMIT_REJECTION = new RegExp([
4086
+ "max_tokens:\\s*\\d+\\s*>\\s*\\d+",
4087
+ "maximum allowed number of output tokens",
4088
+ "invalid input[^\\n]*max_tokens",
4089
+ "expected number[^\\n]*at max_tokens"
4090
+ ].join("|"), "i");
4091
+ /**
4092
+ * The output ceiling the endpoint states in a `max_tokens` refusal, read off
4093
+ * its own comparison: `max_tokens: 131072 > 128000` yields 128000.
4094
+ *
4095
+ * Only the comparison form is parsed. The other shapes
4096
+ * {@link OUTPUT_LIMIT_REJECTION} matches — a bare validation error, or the
4097
+ * prose without the numbers — name no ceiling, and guessing one would be worse
4098
+ * than failing: a fabricated value is indistinguishable from a real one at the
4099
+ * point where it decides the next request's budget.
4100
+ */
4101
+ function parseOutputCeiling(detail) {
4102
+ const stated = /max_tokens:\s*\d+\s*>\s*(\d+)/i.exec(detail)?.[1];
4103
+ if (stated === void 0) return void 0;
4104
+ const ceiling = Number(stated);
4105
+ return Number.isSafeInteger(ceiling) && ceiling > 0 ? ceiling : void 0;
4106
+ }
4107
+ /**
4108
+ * The advice floor for a Provider-API timeout: doubling a 60 s budget is still
4109
+ * short for a slow upstream first token, so the suggestion never goes below 5
4110
+ * minutes. Capped by `RETRY_MAX_DELAY_MS` rather than the schema's timer
4111
+ * ceiling on purpose — a single header wait that outlasts the whole retry
4112
+ * backoff is not a budget, and clamping to the engine constant would also pull
4113
+ * a runtime dependency into the bundle.
4114
+ */
4115
+ const MIN_SUGGESTED_TIMEOUT_MS = 3e5;
4116
+ /**
4117
+ * Why one attempt saw no response headers, and what the user can do about it.
4118
+ *
4119
+ * The TRANSPORT is the reliable signal; the request size is not — and the
4120
+ * mechanism is read straight out of the official CLI rather than inferred from
4121
+ * a timing. `createNodeTransport` passes only the caller's `signal` and sets no
4122
+ * timeout, so the two routes differ in WHO answers the headers:
4123
+ * `/alpha/generate` has the gateway answer them before the model starts, while
4124
+ * `/provider/v1/chat/completions` cannot answer until its upstream emits a
4125
+ * first token, so whatever that upstream is doing is charged to the header
4126
+ * wait. Measured 2026-09-28 (26 250-token prompt, `max_tokens: 16`):
4127
+ * `/alpha/generate` 2.12 / 2.56 / 2.75 s, `/provider/v1/chat/completions`
4128
+ * 5.52 / 2.98 / 3.70 s — a ~1.1 s median gap, with the Provider API route
4129
+ * swinging across 2.5 s where the CLI route moved 0.6 s. A larger prompt moves
4130
+ * neither, which is why an earlier draft that branched on a 1 MiB body
4131
+ * threshold was wrong: size is not the variable here. Only the route decides.
4132
+ *
4133
+ * @param protocol - the transport the attempt used.
4134
+ * @param timeoutMs - the header budget that just expired.
4135
+ */
4136
+ function headersTimeoutAdvice(protocol, timeoutMs) {
4137
+ if (protocol === "openai") return {
4138
+ cause: "upstream-first-token",
4139
+ suggestionMs: Math.min(RETRY_MAX_DELAY_MS, Math.max(timeoutMs * 2, MIN_SUGGESTED_TIMEOUT_MS))
4140
+ };
4141
+ return { cause: "gateway" };
4142
+ }
4143
+ /**
4144
+ * How many consecutive header timeouts on ONE unchanged payload stop being a
4145
+ * blip. The retry policy whitelists `TIMEOUT` with `maxRetries: 1000` and a
4146
+ * 500 ms → 15 min backoff, which is right for a provider that says when to
4147
+ * come back but wrong for a gateway that simply never answers: the reporter
4148
+ * saw the identical multi-megabyte body re-sent for hours (issue #67) instead
4149
+ * of a diagnosis. Three is where a 60 s budget has already spent three
4150
+ * minutes waiting, so the cost of being wrong is bounded and the user gets an
4151
+ * answer either way.
4152
+ */
4153
+ const MAX_CONSECUTIVE_HEADER_TIMEOUTS = 3;
4154
+ /** Streak key for a session-less call, so those keep the old single-slot semantics instead of never escalating. */
4155
+ const GLOBAL_HEADER_TIMEOUT_KEY = "\0global";
4156
+ /** UTF-8 byte length, computed only on the timeout path so a large body is encoded once, not per attempt. */
4157
+ function utf8ByteLength(text) {
4158
+ return new TextEncoder().encode(text).length;
4159
+ }
4160
+ /**
4161
+ * The thrown headers timeout, worded for the cause it actually has.
4162
+ *
4163
+ * Every earlier version of this message said "通常是网络或代理问题" for both
4164
+ * transports, which is actively wrong for a large `/provider/v1/chat/completions`
4165
+ * prompt: there the model simply needs longer than any budget, and a user sent
4166
+ * to check their proxy never finds anything. The code stays `TIMEOUT` (still
4167
+ * retryable) in both branches — only the diagnosis and the advice change.
4168
+ */
4169
+ function headersTimeoutError(input) {
4170
+ const { protocol, endpoint, timeoutMs, cause } = input;
4171
+ const megabytes = `${(utf8ByteLength(input.body) / 1048576).toFixed(1)} MB`;
4172
+ const advice = headersTimeoutAdvice(protocol, timeoutMs);
4173
+ if (advice.cause === "upstream-first-token") {
4174
+ const suggestion = advice.suggestionMs ?? timeoutMs;
4175
+ return bilingual("TIMEOUT", `Command Code API request to ${endpoint} did not respond within ${timeoutMs}ms: this route withholds response headers until the upstream model emits its first token, and it did not start within the budget. Raise "Request timeout" in the plugin's advanced settings to ${suggestion}ms, or compact the session or start a new one. The request body was ${megabytes}, but size is not the cause here — a slow upstream is. ${errorChain(cause)}`, `Command Code API 请求在 ${timeoutMs} 毫秒内未收到响应:该路由要等上游模型吐出第一个 token 才返回响应头,而上游在预算内没有开始作答。可在插件「高级」设置里把「请求超时」调到 ${suggestion} 毫秒,或压缩上下文 / 新开会话。本次请求体约 ${megabytes},但体积不是原因——慢的是上游。(${errorChain(cause)})`, { cause });
4176
+ }
4177
+ return bilingual("TIMEOUT", `Command Code API request to ${endpoint} did not respond within ${timeoutMs}ms: the gateway returned no response headers at all — it answers headers itself before the model replies. This is usually provider queueing, a Go-plan capacity limit, or a network/proxy path problem; a longer timeout will not help, but retrying usually recovers. The request body was ${megabytes}, and size is not the cause on this route. ${errorChain(cause)}`, `Command Code API 请求在 ${timeoutMs} 毫秒内未收到响应:网关完全没有返回响应头——它会在模型开始回答前先返回响应头。这通常是服务商排队、Go 套餐容量限制,或网络/代理链路问题;调大超时不会有帮助,重试通常可恢复。本次请求体约 ${megabytes},而在该路由上体积不是原因。(${errorChain(cause)})`, { cause });
4178
+ }
3163
4179
  /**
3164
4180
  * Whether one rejection says "ZDR requested, no ZDR-capable upstream": the
3165
4181
  * provider spells the code `cmd_zdr_no_providers` in JSON error bodies and
@@ -3203,9 +4219,21 @@ function streamErrorToLlmError(value, fallbackMessage) {
3203
4219
  message
3204
4220
  ].filter((part) => part !== void 0 && part !== "").join(" ");
3205
4221
  const statusOption = statusCode !== void 0 ? { status: statusCode } : void 0;
3206
- if (isContextOverflowDetail(detail)) return new LlmError(`Command Code stream error: ${message};Command Code API 拒绝了这次请求:内容超出模型上下文窗口。正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, CONTEXT_WINDOW_EXCEEDED_CODE, statusOption);
4222
+ if (OUTPUT_LIMIT_REJECTION.test(detail)) {
4223
+ const stated = /max_tokens:\s*\d+\s*>\s*\d+/i.exec(detail)?.[0];
4224
+ return bilingual("PROVIDER_STREAM_ERROR", `Command Code refused the request's max_tokens for this model` + (stated !== void 0 ? ` (${stated})` : "") + " — the session is not oversized, so compacting it will not help; lower \"Max output tokens\" or pick a model with a larger output ceiling", `Command Code 拒绝了本次请求的 max_tokens(该模型的输出上限更低)` + (stated !== void 0 ? `(${stated})` : "") + "——会话内容并未超长,压缩上下文无效;请调低「最大输出 token」或改用输出上限更大的模型", statusOption);
4225
+ }
4226
+ if (isContextOverflowDetail(detail)) return bilingual(CONTEXT_WINDOW_EXCEEDED_CODE, `Command Code stream error: ${message}`, "Command Code API 拒绝了这次请求:内容超出模型上下文窗口。正在压缩上下文后重试;如仍失败,请新建会话或减少上下文", statusOption);
3207
4227
  const terminal = hasTerminalStreamMarker(detail);
3208
4228
  if (!(isRetryable === true || (isRetryable === false || terminal ? false : statusCode !== void 0 ? statusCode !== void 0 && (statusCode === 429 || statusCode >= 500) : true))) return new LlmError(`Command Code stream error: ${message}`, "PROVIDER_STREAM_ERROR", statusOption);
4229
+ const rejection = classifyAccountRejection(statusCode ?? 0, JSON.stringify(value) ?? message);
4230
+ if (rejection?.reason === "rate-limit" || rejection?.reason === "throttled") {
4231
+ const wait = rejection.resetAtMs === void 0 ? 0 : Math.min(RETRY_MAX_DELAY_MS, Math.max(1e3, rejection.resetAtMs - Date.now()));
4232
+ return bilingual(rejection.reason === "rate-limit" ? "RATE_LIMIT" : THROTTLED_CODE, `Command Code stream error: ${message}`, rejection.reason === "rate-limit" ? "Command Code 用量窗口已限流,等待恢复" : "Command Code 请求暂时被限流,正在重试", {
4233
+ ...statusOption,
4234
+ ...wait > 0 ? { providerRetryAfterMs: wait } : {}
4235
+ });
4236
+ }
3209
4237
  return new LlmError(`Command Code stream error: ${message}`, "SERVER", statusOption);
3210
4238
  }
3211
4239
  function recordOrEmpty(value) {
@@ -3370,8 +4398,9 @@ function resolveAuthFileApiKey() {
3370
4398
  return apiKeyFromCredentialRecord(parsed.commandcode) ?? apiKeyFromCredentialRecord(parsed["command-code"]);
3371
4399
  } catch {}
3372
4400
  }
3373
- function parseCatalogResponse(value) {
4401
+ function parseCatalogResponse(value, apiBase) {
3374
4402
  if (!isRecord(value) || value.object !== "list" || !Array.isArray(value.data)) throw new LlmError("Unexpected Command Code models response shape", "PROVIDER_PROTOCOL_ERROR");
4403
+ publishedOutputLimits.set(apiBase, /* @__PURE__ */ new Map());
3375
4404
  const models = [];
3376
4405
  for (const entry of value.data) {
3377
4406
  if (!isRecord(entry)) continue;
@@ -3379,33 +4408,52 @@ function parseCatalogResponse(value) {
3379
4408
  const name = stringValue(entry.name);
3380
4409
  const contextLength = numberValue(entry.context_length);
3381
4410
  if (!id || !name || !contextLength || contextLength <= 0) continue;
4411
+ const endpoints = Array.isArray(entry.supported_endpoints) ? entry.supported_endpoints.filter((item) => typeof item === "string") : [];
4412
+ const published = numberValue(entry.max_output_tokens);
4413
+ if (published !== void 0 && published > 0) outputLimitsFor(publishedOutputLimits, apiBase).set(id, published);
3382
4414
  models.push({
3383
4415
  id,
3384
4416
  name,
3385
4417
  contextWindow: contextLength,
3386
- maxTokens: Math.min(contextLength, DEFAULT_MAX_OUTPUT_TOKENS)
4418
+ maxTokens: Math.min(contextLength, modelOutputTokenLimit(apiBase, id)),
4419
+ ...published !== void 0 && published > 0 ? { publishedMaxTokens: published } : {},
4420
+ supportedEndpoints: endpoints
3387
4421
  });
3388
4422
  }
3389
4423
  if (models.length === 0) throw new LlmError("Command Code returned an empty model catalog", "PROVIDER_PROTOCOL_ERROR");
3390
4424
  return models;
3391
4425
  }
3392
- async function readModelsCache(cachePath) {
4426
+ async function readModelsCache(cachePath, apiBase) {
3393
4427
  const parsed = JSON.parse(await readFile(cachePath, "utf-8"));
3394
- if (!isRecord(parsed) || parsed.version !== MODEL_CACHE_VERSION || !Array.isArray(parsed.models)) throw new Error(`Invalid model cache at ${cachePath}`);
3395
- return parsed.models;
4428
+ if (!isRecord(parsed) || parsed.version !== MODEL_CACHE_VERSION || parsed.apiBase !== apiBase || !Array.isArray(parsed.models)) throw new Error(`Invalid model cache at ${cachePath}`);
4429
+ publishedOutputLimits.set(apiBase, /* @__PURE__ */ new Map());
4430
+ return parsed.models.map((entry) => {
4431
+ const model = isRecord(entry) ? entry : {};
4432
+ const endpoints = Array.isArray(model.supportedEndpoints) ? model.supportedEndpoints.filter((item) => typeof item === "string") : [];
4433
+ const id = stringValue(model.id);
4434
+ const published = numberValue(model.publishedMaxTokens);
4435
+ if (id !== void 0 && published !== void 0 && published > 0) outputLimitsFor(publishedOutputLimits, apiBase).set(id, published);
4436
+ const contextWindow = numberValue(model.contextWindow);
4437
+ return {
4438
+ ...model,
4439
+ supportedEndpoints: endpoints,
4440
+ maxTokens: id === void 0 || contextWindow === void 0 ? numberValue(model.maxTokens) ?? 131072 : Math.min(contextWindow, modelOutputTokenLimit(apiBase, id))
4441
+ };
4442
+ });
3396
4443
  }
3397
- async function writeModelsCache(cachePath, models) {
4444
+ async function writeModelsCache(cachePath, apiBase, models, mayCommit) {
3398
4445
  await mkdir(dirname(cachePath), { recursive: true });
3399
- const tmp = `${cachePath}.${process.pid}.tmp`;
4446
+ const tmp = `${cachePath}.${process.pid}.${randomUUID()}.tmp`;
3400
4447
  try {
3401
4448
  await writeFile(tmp, `${JSON.stringify({
3402
4449
  version: MODEL_CACHE_VERSION,
4450
+ apiBase,
3403
4451
  models
3404
4452
  }, null, 2)}\n`, {
3405
4453
  encoding: "utf-8",
3406
4454
  mode: 384
3407
4455
  });
3408
- await rename(tmp, cachePath);
4456
+ if (mayCommit()) await rename(tmp, cachePath);
3409
4457
  } finally {
3410
4458
  await rm(tmp, { force: true }).catch(() => void 0);
3411
4459
  }
@@ -3581,9 +4629,9 @@ const ENGINE_IMAGE_POLICY = {
3581
4629
  * dsh-llm-retry. On a profile that does not mount that plugin the failure is
3582
4630
  * terminal — every shipped profile reaches it through dsh-base.
3583
4631
  */
3584
- function imageOffloadRequired(missing) {
4632
+ function imageOffloadRequired(missing, reason = "budget") {
3585
4633
  const options = { offloadImages: missing };
3586
- return new LlmError(`commandcode request images exceed the route budget; ${missing} more oldest occurrence(s) must be offloaded.`, IMAGE_OFFLOAD_REQUIRED_CODE, options);
4634
+ return new LlmError(reason === "cache" ? `commandcode cache-aware image history: ${missing} already-seen oldest image occurrence(s) must be offloaded before replay.` : `commandcode request images exceed the route budget; ${missing} more oldest occurrence(s) must be offloaded.`, IMAGE_OFFLOAD_REQUIRED_CODE, options);
3587
4635
  }
3588
4636
  /** The budget in the shape the core counter takes (representation + the rung). */
3589
4637
  function coreBudget(budget) {
@@ -3609,17 +4657,40 @@ const imageVersionBytes = (block) => block.attachment.bytes;
3609
4657
  * otherwise the 413 retry could reintroduce images the first rung had already
3610
4658
  * evicted.
3611
4659
  */
3612
- function withSurfaceOffload(options, policy, budget) {
4660
+ function withSurfaceOffload(options, policy, budget, cacheOffloadModel) {
3613
4661
  const marked = policy.projectOffloadedImages(options.messages, (ref) => offloadedImageText(ref));
3614
4662
  const messages = marked === options.messages ? options.messages : [...marked];
3615
- const missing = policy.requiredImageOffload(messages, coreBudget(budget), imageVersionBytes);
3616
- if (missing > 0) throw imageOffloadRequired(missing);
4663
+ const budgetMissing = policy.requiredImageOffload(messages, coreBudget(budget), imageVersionBytes);
4664
+ const cacheMissing = cacheOffloadModel === void 0 ? 0 : seenImageOffloadCount(messages, cacheOffloadModel);
4665
+ const missing = Math.max(budgetMissing, cacheMissing);
4666
+ if (missing > 0) throw imageOffloadRequired(missing, cacheMissing > budgetMissing ? "cache" : "budget");
3617
4667
  return messages === options.messages ? options : {
3618
4668
  ...options,
3619
4669
  messages
3620
4670
  };
3621
4671
  }
3622
4672
  /**
4673
+ * Count retained images already followed by an answer from this same model.
4674
+ * The current tool result's image has not yet been seen by the model and must
4675
+ * stay on the wire. The engine can only offload durable user/tool input nodes;
4676
+ * assistant output and request-only input have no selectable surface event.
4677
+ */
4678
+ function seenImageOffloadCount(messages, model) {
4679
+ let lastAnswer = -1;
4680
+ for (let index = 0; index < messages.length; index++) {
4681
+ const message = messages[index];
4682
+ if (message.role === "assistant" && message.source.kind === "model" && message.source.provider === "commandcode" && message.source.model === model) lastAnswer = index;
4683
+ }
4684
+ if (lastAnswer < 0) return 0;
4685
+ let count = 0;
4686
+ for (let index = 0; index < lastAnswer; index++) {
4687
+ const message = messages[index];
4688
+ if (message.role !== "user" && message.role !== "tool" || message.id === void 0) continue;
4689
+ for (const block of message.content) if (block.type === "image") count++;
4690
+ }
4691
+ return count;
4692
+ }
4693
+ /**
3623
4694
  * One image's bytes for the wire: the attachment service's REQUEST VERSION.
3624
4695
  *
3625
4696
  * `readImage()` answers the stored original, which is what admission accepted —
@@ -3630,7 +4701,39 @@ function withSurfaceOffload(options, policy, budget) {
3630
4701
  * the point here, the encoded bytes).
3631
4702
  */
3632
4703
  async function readRequestImage(attachments, ref) {
3633
- return (await attachments.readImageRequest(ref, requestImageTarget(ref))).data;
4704
+ return attachments.readImageRequest(ref, requestImageTarget(ref));
4705
+ }
4706
+ /** 同一次生成及其协议切换共享图片读取,最多同时准备四张,避免内存峰值失控。 */
4707
+ function requestImages(attachments, signal, timing) {
4708
+ const cache = /* @__PURE__ */ new Map();
4709
+ const read = (ref) => {
4710
+ signal?.throwIfAborted();
4711
+ const key = JSON.stringify([ref.attachmentId, requestImageTarget(ref)]);
4712
+ let pending = cache.get(key);
4713
+ if (!pending) {
4714
+ pending = readRequestImage(attachments, ref);
4715
+ cache.set(key, pending);
4716
+ }
4717
+ return pending;
4718
+ };
4719
+ read.prepare = async (refs) => {
4720
+ if (refs.length === 0) return;
4721
+ const finishImages = timing.phase("images");
4722
+ let cursor = 0;
4723
+ try {
4724
+ const workers = Array.from({ length: Math.min(4, refs.length) }, async () => {
4725
+ while (cursor < refs.length) {
4726
+ signal?.throwIfAborted();
4727
+ await read(refs[cursor++]);
4728
+ }
4729
+ });
4730
+ const failure = (await Promise.allSettled(workers)).find((result) => result.status === "rejected");
4731
+ if (failure?.status === "rejected") throw failure.reason;
4732
+ } finally {
4733
+ finishImages();
4734
+ }
4735
+ };
4736
+ return read;
3634
4737
  }
3635
4738
  /**
3636
4739
  * Price one request occurrence.
@@ -3661,31 +4764,58 @@ function priceRequestImage(block, model, imageCapable) {
3661
4764
  };
3662
4765
  }
3663
4766
  /**
3664
- * Convert one image reference to the Command Code wire format, as the official
3665
- * CLI does: `{ type: 'image', source: { type: 'base64', media_type, data } }`.
3666
- * Bytes come from the durable attachment service; the media type is the one
3667
- * verified at save time.
4767
+ * Convert one image reference to the final `/alpha/generate` wire format.
4768
+ * The official CLI accepts a base64 source internally, then toWireMessages()
4769
+ * sends a data URL plus mimeType. Use the request version's media type because
4770
+ * readImageRequest() can transcode the stored image to JPEG or WebP.
3668
4771
  */
3669
4772
  async function imageToCommandCode(ref, readImage) {
3670
- const data = await readImage(ref);
4773
+ const version = await readImage(ref);
3671
4774
  return {
3672
4775
  type: "image",
3673
- source: {
3674
- type: "base64",
3675
- media_type: ref.mediaType,
3676
- data: Buffer.from(data).toString("base64")
3677
- }
4776
+ image: `data:${version.mediaType};base64,${Buffer.from(version.data).toString("base64")}`,
4777
+ mimeType: version.mediaType
3678
4778
  };
3679
4779
  }
3680
- async function messagesToCC(messages, readImage) {
4780
+ /**
4781
+ * The signature recorded for the reasoning block at `position` of a
4782
+ * historical assistant message, or undefined when the message was produced
4783
+ * by another route or an older plugin version.
4784
+ */
4785
+ function messagesSignatureAt(message, position) {
4786
+ const replay = message.source?.replayState;
4787
+ if (!isRecord(replay) || !Array.isArray(replay.blocks)) return void 0;
4788
+ const entry = replay.blocks[position];
4789
+ return isRecord(entry) ? stringValue(entry.signature) : void 0;
4790
+ }
4791
+ /**
4792
+ * The common pairing and image-carrier ordering for both transports. Each
4793
+ * codec owns its wire shape: CLI content blocks and Chat Completions
4794
+ * reasoning/tool calls differ beyond the image envelope.
4795
+ */
4796
+ async function convertMessages(messages, readImage, codec) {
3681
4797
  const out = [];
3682
4798
  const { ids: paired, names: toolNames } = pairedToolCalls(messages);
4799
+ if (readImage?.prepare) {
4800
+ const refs = [];
4801
+ for (const message of messages) {
4802
+ const result = toolResultOf(message);
4803
+ if (message.role === "user" && !result) {
4804
+ for (const block of message.content) if (block.type === "image") refs.push(block.attachment);
4805
+ } else if (result && paired.has(result.toolCallId)) refs.push(...toolResultMedia(result).images);
4806
+ }
4807
+ await readImage.prepare(refs);
4808
+ }
3683
4809
  const wireIds = wireToolCallIds(paired);
3684
4810
  const pendingImages = [];
3685
4811
  const flushPendingImages = () => {
3686
4812
  for (const message of pendingImages) out.push(message);
3687
4813
  pendingImages.length = 0;
3688
4814
  };
4815
+ const encodeImage = (ref) => {
4816
+ if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
4817
+ return codec.encodeImage(ref, readImage);
4818
+ };
3689
4819
  for (const message of messages) {
3690
4820
  if (message.role === "system") continue;
3691
4821
  const result = toolResultOf(message);
@@ -3697,86 +4827,89 @@ async function messagesToCC(messages, readImage) {
3697
4827
  type: "text",
3698
4828
  text: block.text
3699
4829
  });
3700
- if (block.type === "image") {
3701
- if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
3702
- parts.push(await imageToCommandCode(block.attachment, readImage));
3703
- }
4830
+ if (block.type === "image") parts.push(await encodeImage(block.attachment));
3704
4831
  }
3705
- if (parts.length === 0) continue;
3706
- out.push({
3707
- role: "user",
3708
- content: parts
3709
- });
4832
+ if (parts.length > 0) out.push(codec.encodeUser(parts));
3710
4833
  continue;
3711
4834
  }
3712
4835
  if (message.role === "assistant") {
3713
4836
  flushPendingImages();
3714
- const parts = [];
3715
- for (const block of message.content) if (block.type === "text") parts.push({
3716
- type: "text",
3717
- text: block.text
3718
- });
3719
- else if (block.type === "reasoning") parts.push({
3720
- type: "reasoning",
3721
- text: block.text
3722
- });
3723
- else if (block.type === "tool-call" && paired.has(block.id)) parts.push({
3724
- type: "tool-call",
3725
- toolCallId: wireIds.get(block.id) ?? block.id,
3726
- toolName: block.name,
3727
- input: recordOrEmpty(block.arguments)
3728
- });
3729
- if (parts.length > 0) out.push({
3730
- role: "assistant",
3731
- content: parts
3732
- });
4837
+ const assistant = codec.encodeAssistant(message, paired, wireIds);
4838
+ if (assistant !== void 0) out.push(assistant);
3733
4839
  continue;
3734
4840
  }
3735
- if (result) {
3736
- if (!paired.has(result.toolCallId)) continue;
3737
- const media = toolResultMedia(result);
3738
- const wireToolCallId = wireIds.get(result.toolCallId) ?? result.toolCallId;
3739
- out.push({
3740
- role: "tool",
3741
- content: [{
3742
- type: "tool-result",
3743
- toolCallId: wireToolCallId,
3744
- toolName: toolNames.get(result.toolCallId) || "unknown",
3745
- output: result.isError ? {
3746
- type: "error-text",
3747
- value: toolResultTextForWire(media)
3748
- } : {
3749
- type: "text",
3750
- value: toolResultTextForWire(media)
3751
- }
3752
- }]
3753
- });
3754
- if (media.images.length > 0) {
3755
- if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
3756
- const carried = [{
3757
- type: "text",
3758
- text: toolResultImageNote(media, wireToolCallId)
3759
- }];
3760
- for (const attachment of media.images) carried.push(await imageToCommandCode(attachment, readImage));
3761
- pendingImages.push({
3762
- role: "user",
3763
- content: carried
3764
- });
3765
- }
4841
+ if (!result || !paired.has(result.toolCallId)) continue;
4842
+ const media = toolResultMedia(result);
4843
+ const wireId = wireIds.get(result.toolCallId) ?? result.toolCallId;
4844
+ out.push(await codec.encodeTool(result, media, wireId, toolNames.get(result.toolCallId) || "unknown", readImage));
4845
+ if (!codec.carriesToolResultMedia && media.images.length > 0) {
4846
+ const carried = [{
4847
+ type: "text",
4848
+ text: toolResultImageNote(media, wireId)
4849
+ }];
4850
+ for (const attachment of media.images) carried.push(await encodeImage(attachment));
4851
+ pendingImages.push(codec.encodeUser(carried));
3766
4852
  }
3767
4853
  }
3768
4854
  flushPendingImages();
3769
4855
  return out;
3770
4856
  }
4857
+ const ccMessageCodec = {
4858
+ carriesToolResultMedia: false,
4859
+ encodeImage: imageToCommandCode,
4860
+ encodeUser: (parts) => ({
4861
+ role: "user",
4862
+ content: parts
4863
+ }),
4864
+ encodeAssistant: (message, paired, wireIds) => {
4865
+ const parts = [];
4866
+ for (const block of message.content) if (block.type === "text") parts.push({
4867
+ type: "text",
4868
+ text: block.text
4869
+ });
4870
+ else if (block.type === "reasoning") parts.push({
4871
+ type: "reasoning",
4872
+ text: block.text
4873
+ });
4874
+ else if (block.type === "tool-call" && paired.has(block.id)) parts.push({
4875
+ type: "tool-call",
4876
+ toolCallId: wireIds.get(block.id) ?? block.id,
4877
+ toolName: block.name,
4878
+ input: recordOrEmpty(block.arguments)
4879
+ });
4880
+ return parts.length > 0 ? {
4881
+ role: "assistant",
4882
+ content: parts
4883
+ } : void 0;
4884
+ },
4885
+ encodeTool: (result, media, wireId, toolName) => ({
4886
+ role: "tool",
4887
+ content: [{
4888
+ type: "tool-result",
4889
+ toolCallId: wireId,
4890
+ toolName,
4891
+ output: result.isError ? {
4892
+ type: "error-text",
4893
+ value: toolResultTextForWire(media)
4894
+ } : {
4895
+ type: "text",
4896
+ value: toolResultTextForWire(media)
4897
+ }
4898
+ }]
4899
+ })
4900
+ };
4901
+ async function messagesToCC(messages, readImage) {
4902
+ return convertMessages(messages, readImage, ccMessageCodec);
4903
+ }
3771
4904
  /**
3772
4905
  * Convert one image reference to the OpenAI Chat Completions wire format:
3773
4906
  * `{ type: 'image_url', image_url: { url: 'data:...;base64,...' } }`.
3774
4907
  */
3775
4908
  async function imageToOpenAI(ref, readImage) {
3776
- const data = await readImage(ref);
4909
+ const version = await readImage(ref);
3777
4910
  return {
3778
4911
  type: "image_url",
3779
- image_url: { url: `data:${ref.mediaType};base64,${Buffer.from(data).toString("base64")}` }
4912
+ image_url: { url: `data:${version.mediaType};base64,${Buffer.from(version.data).toString("base64")}` }
3780
4913
  };
3781
4914
  }
3782
4915
  /**
@@ -3789,89 +4922,159 @@ async function imageToOpenAI(ref, readImage) {
3789
4922
  * on assistant messages. Only tool calls with a paired tool result are
3790
4923
  * replayed (same policy as the CLI path).
3791
4924
  */
4925
+ const openAiMessageCodec = {
4926
+ carriesToolResultMedia: false,
4927
+ encodeImage: imageToOpenAI,
4928
+ encodeUser: (parts) => {
4929
+ return !parts.some((part) => part.type === "image_url") && parts.length === 1 ? {
4930
+ role: "user",
4931
+ content: parts[0].text
4932
+ } : {
4933
+ role: "user",
4934
+ content: parts
4935
+ };
4936
+ },
4937
+ encodeAssistant: (message, paired, wireIds) => {
4938
+ const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
4939
+ const reasoning = message.content.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
4940
+ const toolCalls = message.content.filter((block) => block.type === "tool-call" && paired.has(block.id)).map((block) => ({
4941
+ id: wireIds.get(block.id) ?? block.id,
4942
+ type: "function",
4943
+ function: {
4944
+ name: block.name,
4945
+ arguments: block.arguments
4946
+ }
4947
+ }));
4948
+ if (text === "" && reasoning === "" && toolCalls.length === 0) return void 0;
4949
+ const assistant = {
4950
+ role: "assistant",
4951
+ content: text === "" ? null : text
4952
+ };
4953
+ if (reasoning !== "") assistant.reasoning_content = reasoning;
4954
+ if (toolCalls.length > 0) assistant.tool_calls = toolCalls;
4955
+ return assistant;
4956
+ },
4957
+ encodeTool: (_result, media, wireId) => ({
4958
+ role: "tool",
4959
+ tool_call_id: wireId,
4960
+ content: toolResultTextForWire(media)
4961
+ })
4962
+ };
3792
4963
  async function messagesToOpenAI(messages, readImage) {
3793
- const out = [];
3794
- const { ids: paired } = pairedToolCalls(messages);
3795
- const wireIds = wireToolCallIds(paired);
3796
- const pendingImages = [];
3797
- const flushPendingImages = () => {
3798
- for (const message of pendingImages) out.push(message);
3799
- pendingImages.length = 0;
4964
+ return convertMessages(messages, readImage, openAiMessageCodec);
4965
+ }
4966
+ /**
4967
+ * Convert one image reference to the Messages wire shape. Unlike the other two
4968
+ * transports this one keeps the native media type rather than a data URL, and
4969
+ * it may be used INSIDE a tool result — `tool_result.content` accepts image
4970
+ * blocks, which is why this codec sets `carriesToolResultMedia`.
4971
+ */
4972
+ async function imageToMessages(ref, readImage) {
4973
+ const version = await readImage(ref);
4974
+ return {
4975
+ type: "image",
4976
+ source: {
4977
+ type: "base64",
4978
+ media_type: version.mediaType,
4979
+ data: Buffer.from(version.data).toString("base64")
4980
+ }
3800
4981
  };
3801
- for (const message of messages) {
3802
- if (message.role === "system") continue;
3803
- const result = toolResultOf(message);
3804
- if (message.role === "user" && !result) {
3805
- flushPendingImages();
3806
- const parts = [];
3807
- for (const block of message.content) {
3808
- if (block.type === "text") parts.push({
4982
+ }
4983
+ /** A tool call's `arguments` is a raw JSON string in the harness, an object here. */
4984
+ function parseToolArguments(raw) {
4985
+ try {
4986
+ const parsed = JSON.parse(raw);
4987
+ return isRecord(parsed) ? parsed : {};
4988
+ } catch {
4989
+ return {};
4990
+ }
4991
+ }
4992
+ /**
4993
+ * Convert harness messages to the Anthropic Messages request shape.
4994
+ *
4995
+ * Differences from the other two transports, all measured against the live
4996
+ * endpoint on 2026-09-29:
4997
+ *
4998
+ * - Content is always a block array; there is no string shorthand, so a
4999
+ * single-text user turn is still `{ role: 'user', content: [{type:'text'…}] }`.
5000
+ * - Tool results live in a `user` turn as `tool_result` blocks (not a
5001
+ * `role: 'tool'` message) and may carry images natively, so no image is
5002
+ * split into a separate user message here.
5003
+ * - A replayed `thinking` block is only valid WITH its `signature`: the
5004
+ * endpoint answers `thinking.signature: Field required` without it, and
5005
+ * `each thinking block must contain thinking` when the text is empty and
5006
+ * the signature is blank. A reasoning block with no recorded signature is
5007
+ * therefore dropped rather than sent — replaying it would fail the request,
5008
+ * while omitting it is accepted (verified 200 both ways).
5009
+ * - The conversation must end with a user turn; the endpoint refuses an
5010
+ * assistant prefill (`This model does not support assistant message
5011
+ * prefill`), which `convertMessages` already guarantees for a tool loop.
5012
+ */
5013
+ const messagesMessageCodec = {
5014
+ carriesToolResultMedia: true,
5015
+ encodeImage: imageToMessages,
5016
+ encodeUser: (parts) => ({
5017
+ role: "user",
5018
+ content: parts
5019
+ }),
5020
+ encodeAssistant: (message, paired, wireIds) => {
5021
+ const parts = [];
5022
+ let position = 0;
5023
+ for (const block of message.content) {
5024
+ if (block.type === "text") {
5025
+ parts.push({
3809
5026
  type: "text",
3810
5027
  text: block.text
3811
5028
  });
3812
- if (block.type === "image") {
3813
- if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
3814
- parts.push(await imageToOpenAI(block.attachment, readImage));
3815
- }
5029
+ position++;
5030
+ continue;
3816
5031
  }
3817
- if (parts.length === 0) continue;
3818
- if (!parts.some((part) => part.type === "image_url") && parts.length === 1) out.push({
3819
- role: "user",
3820
- content: parts[0].text
3821
- });
3822
- else out.push({
3823
- role: "user",
3824
- content: parts
3825
- });
3826
- continue;
3827
- }
3828
- if (message.role === "assistant") {
3829
- flushPendingImages();
3830
- const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
3831
- const reasoning = message.content.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
3832
- const toolCalls = message.content.filter((block) => block.type === "tool-call" && paired.has(block.id)).map((block) => ({
3833
- id: wireIds.get(block.id) ?? block.id,
3834
- type: "function",
3835
- function: {
5032
+ if (block.type === "reasoning") {
5033
+ const signature = messagesSignatureAt(message, position);
5034
+ if (signature !== void 0) parts.push({
5035
+ type: "thinking",
5036
+ thinking: block.text,
5037
+ signature
5038
+ });
5039
+ position++;
5040
+ continue;
5041
+ }
5042
+ if (block.type === "tool-call" && paired.has(block.id)) {
5043
+ parts.push({
5044
+ type: "tool_use",
5045
+ id: wireIds.get(block.id) ?? block.id,
3836
5046
  name: block.name,
3837
- arguments: block.arguments
3838
- }
3839
- }));
3840
- if (text === "" && reasoning === "" && toolCalls.length === 0) continue;
3841
- const assistant = {
3842
- role: "assistant",
3843
- content: text === "" ? null : text
3844
- };
3845
- if (reasoning !== "") assistant.reasoning_content = reasoning;
3846
- if (toolCalls.length > 0) assistant.tool_calls = toolCalls;
3847
- out.push(assistant);
3848
- continue;
3849
- }
3850
- if (result) {
3851
- if (!paired.has(result.toolCallId)) continue;
3852
- const media = toolResultMedia(result);
3853
- const wireToolCallId = wireIds.get(result.toolCallId) ?? result.toolCallId;
3854
- out.push({
3855
- role: "tool",
3856
- tool_call_id: wireToolCallId,
3857
- content: toolResultTextForWire(media)
3858
- });
3859
- if (media.images.length > 0) {
3860
- if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
3861
- const carried = [{
3862
- type: "text",
3863
- text: toolResultImageNote(media, wireToolCallId)
3864
- }];
3865
- for (const attachment of media.images) carried.push(await imageToOpenAI(attachment, readImage));
3866
- pendingImages.push({
3867
- role: "user",
3868
- content: carried
5047
+ input: parseToolArguments(block.arguments)
3869
5048
  });
5049
+ position++;
3870
5050
  }
3871
5051
  }
5052
+ return parts.length > 0 ? {
5053
+ role: "assistant",
5054
+ content: parts
5055
+ } : void 0;
5056
+ },
5057
+ encodeTool: async (result, media, wireId, _toolName, readImage) => {
5058
+ const content = [];
5059
+ if (readImage) for (const attachment of media.images) content.push(await imageToMessages(attachment, readImage));
5060
+ content.push({
5061
+ type: "text",
5062
+ text: toolResultTextForWire(media)
5063
+ });
5064
+ const toolResult = {
5065
+ type: "tool_result",
5066
+ tool_use_id: wireId,
5067
+ content
5068
+ };
5069
+ if (result.isError) toolResult.is_error = true;
5070
+ return {
5071
+ role: "user",
5072
+ content: [toolResult]
5073
+ };
3872
5074
  }
3873
- flushPendingImages();
3874
- return out;
5075
+ };
5076
+ async function messagesToMessages(messages, readImage) {
5077
+ return convertMessages(messages, readImage, messagesMessageCodec);
3875
5078
  }
3876
5079
  /** Account endpoints fetched by one `getUsage()` run (see the classification there). */
3877
5080
  const USAGE_ENDPOINT_COUNT = 4;
@@ -4000,6 +5203,50 @@ function cliSystem(systemText) {
4000
5203
  cache_control: { type: "ephemeral" }
4001
5204
  }];
4002
5205
  }
5206
+ /**
5207
+ * The Anthropic ephemeral cache marker, shared by every breakpoint this route
5208
+ * sets. One object is reused across positions because it is only ever
5209
+ * serialized, never mutated.
5210
+ *
5211
+ * No `ttl` is sent: the default 5-minute entry covers a continuously advancing
5212
+ * session, and `ttl: "1h"` is a separate billable tier whose read/write prices
5213
+ * are identical — it would only lengthen survival, not reduce cost.
5214
+ */
5215
+ const EPHEMERAL_CACHE_CONTROL = { type: "ephemeral" };
5216
+ /**
5217
+ * Content-block types that may carry the rolling breakpoint. Matches what the
5218
+ * official Command Code provider for pi marks through
5219
+ * `@earendil-works/pi-ai`'s `anthropic-messages` transport (its own list also
5220
+ * holds pi-internal `tool_addition` / `tool_removal`, which this route never
5221
+ * emits). Assistant-side blocks are deliberately excluded: a breakpoint on a
5222
+ * `thinking` or `tool_use` block caches a prefix the next turn invalidates.
5223
+ */
5224
+ const CACHEABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
5225
+ "text",
5226
+ "image",
5227
+ "tool_result"
5228
+ ]);
5229
+ /**
5230
+ * Mark the last user turn as the rolling cache breakpoint.
5231
+ *
5232
+ * Anthropic caching is explicit — a prefix is stored only when a
5233
+ * `cache_control` marker names it — so an unmarked route re-prefills the whole
5234
+ * replayed history at the full input price on every turn. Measured cost of that
5235
+ * omission on a 10-turn session: $0.5648, all of it at the undiscounted
5236
+ * $2/M input rate, against $0.1869 with the three breakpoints this route sets.
5237
+ *
5238
+ * The final user message is the rolling boundary: DSH resends the whole
5239
+ * history each turn, so a marker here grows with the conversation, whereas a
5240
+ * fixed `system`-only marker would leave every later turn's history
5241
+ * uncached. {@link messagesMessageCodec} renders both user input and tool
5242
+ * results as `role: 'user'`, so this covers a tool loop's trailing turn too.
5243
+ */
5244
+ function markLastUserCacheControl(messages) {
5245
+ const last = messages[messages.length - 1];
5246
+ if (!isRecord(last) || last.role !== "user" || !Array.isArray(last.content)) return;
5247
+ const lastBlock = last.content[last.content.length - 1];
5248
+ if (isRecord(lastBlock) && typeof lastBlock.type === "string" && CACHEABLE_BLOCK_TYPES.has(lastBlock.type)) lastBlock.cache_control = EPHEMERAL_CACHE_CONTROL;
5249
+ }
4003
5250
  /** Build the legacy CLI (`/alpha/generate`) request body for one call. */
4004
5251
  async function buildCliBody(options, connection, facts) {
4005
5252
  return {
@@ -4060,6 +5307,108 @@ async function buildOpenAIBody(options, facts) {
4060
5307
  };
4061
5308
  }
4062
5309
  /**
5310
+ * Build the Anthropic Messages request body for one call.
5311
+ *
5312
+ * Measured deviations from DeepSeek's own Messages API (the shape
5313
+ * `dsh-llm-deepseek` implements), all confirmed on 2026-09-29:
5314
+ *
5315
+ * - `thinking` accepts ONLY `{ type: 'adaptive' }` here. `enabled` and
5316
+ * `disabled` are refused with `"thinking.type.enabled" is not supported for
5317
+ * this model. Use "thinking.type.adaptive" and "output_config.effort" to
5318
+ * control thinking behavior.`, and a `between_tools` value fails validation
5319
+ * outright. The model reasons on its own; this switch only admits it.
5320
+ * - Reasoning strength travels in `output_config.effort`, whose accepted
5321
+ * values are `low | medium | high | xhigh | max` — there is no `none`, and
5322
+ * `xhigh` is absent from DeepSeek's own type.
5323
+ * - `stop_sequences` is a real field here, but this adapter still rejects
5324
+ * `GenerateOptions.stop` in `stream()` because Command Code documents no
5325
+ * equivalent for the other two transports.
5326
+ * - **`temperature` is never sent.** Adaptive thinking constrains it to 1:
5327
+ * `` `temperature` may only be set to 1 when thinking is enabled or in
5328
+ * adaptive mode `` (measured 2026-09-29 — every request carrying the other
5329
+ * transports' 0.3 default was refused with HTTP 400). Omitting the field
5330
+ * is the only way to honour both the thinking switch and a caller that
5331
+ * asked for a temperature, so Claude loses sampling control on this route
5332
+ * because the endpoint, not this adapter, removed it.
5333
+ * - `system` travels as a one-block ARRAY carrying `cache_control`, not as the
5334
+ * bare string the other two transports send. Anthropic's caching is opt-in
5335
+ * per block, and this route is the only Claude path, so the marker is what
5336
+ * makes a session's replayed history bill at the cache-read rate instead of
5337
+ * the full input rate. See {@link EPHEMERAL_CACHE_CONTROL}.
5338
+ */
5339
+ async function buildMessagesBody(options, facts) {
5340
+ const body = {
5341
+ model: options.model,
5342
+ max_tokens: facts.maxTokens,
5343
+ stream: true,
5344
+ messages: await messagesToMessages(options.messages, facts.readImage)
5345
+ };
5346
+ if (facts.selectableEffort) body.thinking = { type: "adaptive" };
5347
+ if (facts.systemText) body.system = [{
5348
+ type: "text",
5349
+ text: facts.systemText,
5350
+ cache_control: EPHEMERAL_CACHE_CONTROL
5351
+ }];
5352
+ if (facts.reasoningEffort) body.output_config = { effort: facts.reasoningEffort };
5353
+ const tools = (options.tools ?? []).map((tool) => ({
5354
+ name: tool.name,
5355
+ description: tool.description,
5356
+ input_schema: toolParametersSchema(tool.parameters)
5357
+ }));
5358
+ if (tools.length > 0) {
5359
+ tools[tools.length - 1].cache_control = EPHEMERAL_CACHE_CONTROL;
5360
+ body.tools = tools;
5361
+ }
5362
+ markLastUserCacheControl(body.messages);
5363
+ return body;
5364
+ }
5365
+ /**
5366
+ * Estimate the text the selected transport actually sends. JSON framing and
5367
+ * punctuation each get a token; ordinary ASCII words get one, with an extra
5368
+ * allowance for long identifiers. UTF-8 bytes / 4 tracks the measured CJK
5369
+ * case better than the engine's UTF-16 characters / 4. This is a heuristic,
5370
+ * never proof that a prompt cannot fit: a valid long English prompt can still
5371
+ * be larger under this estimate than under the provider's tokenizer.
5372
+ */
5373
+ function estimateWireInputTokens(protocol, body, messages, model) {
5374
+ const params = protocol === "cli" ? recordOrEmpty(body.params) : body;
5375
+ const wire = JSON.stringify(params, (_key, value) => typeof value === "string" && /^data:[^,]*;base64,/.test(value) ? "[image]" : value);
5376
+ let tokens = 0;
5377
+ for (const match of wire.matchAll(/[A-Za-z0-9_]+|[^\x00-\x7f]+|[^\s]/gu)) {
5378
+ const part = match[0];
5379
+ if (/^[A-Za-z0-9_]+$/.test(part)) tokens += 1 + Math.ceil(Math.max(0, part.length - 12) / 4);
5380
+ else if (part.charCodeAt(0) > 127) tokens += Math.ceil(Buffer.byteLength(part) / 4);
5381
+ else tokens += 1;
5382
+ }
5383
+ for (const message of messages) for (const block of message.content) if (block.type === "image") tokens += priceRequestImage(block, model, true).visualTokens;
5384
+ return tokens;
5385
+ }
5386
+ function requestContextBudget(protocol, body, messages, model, contextWindow, requestedMaxTokens) {
5387
+ if (contextWindow === void 0) return {
5388
+ maxTokens: requestedMaxTokens,
5389
+ sizePressure: false
5390
+ };
5391
+ const estimatedInput = estimateWireInputTokens(protocol, body, messages, model);
5392
+ const headroom = Math.min(16384, Math.max(2048, Math.ceil(contextWindow * .02)));
5393
+ const available = Math.floor(contextWindow - estimatedInput - headroom);
5394
+ const floor = Math.min(requestedMaxTokens, MIN_CONTEXT_OUTPUT_TOKENS, Math.max(1, Math.floor(contextWindow / 8)));
5395
+ return {
5396
+ maxTokens: Math.min(requestedMaxTokens, Math.max(floor, available)),
5397
+ sizePressure: estimatedInput >= contextWindow / 4 && estimatedInput + requestedMaxTokens + headroom >= contextWindow
5398
+ };
5399
+ }
5400
+ /**
5401
+ * One connect attempt's inputs: everything `connectGenerate` needs beyond
5402
+ * the per-attempt key and protocol. Passed explicitly (not closed over) so
5403
+ * the rotation loop's mutation of `protocol`/`body` stays visible at the
5404
+ * call site.
5405
+ */
5406
+ /** The generate endpoint one protocol posts to. */
5407
+ function protocolEndpoint(protocol, apiBase) {
5408
+ if (protocol === "cli") return `${apiBase}/alpha/generate`;
5409
+ return protocol === "messages" ? `${apiBase}/provider/v1/messages` : `${apiBase}/provider/v1/chat/completions`;
5410
+ }
5411
+ /**
4063
5412
  * One pre-stream connect attempt: POST the body and wait for response
4064
5413
  * headers only (`requestTimeoutMs` must never bound the body stream). Returns
4065
5414
  * the live response plus its cleanup, or the rejection facts for the rotation
@@ -4067,11 +5416,11 @@ async function buildOpenAIBody(options, facts) {
4067
5416
  * on success the caller-abort listener outlives the connect phase (it aborts a
4068
5417
  * stalled body read), so the streaming tail calls cleanup.
4069
5418
  */
4070
- async function connectGenerate(deps, key, protocol, body) {
5419
+ async function connectGenerate(deps, key, protocol, serializedBody) {
4071
5420
  const { options, connection, fetchImpl } = deps;
4072
5421
  const connectAbort = new AbortController();
4073
5422
  let connectTimedOut = false;
4074
- const endpoint = protocol === "cli" ? `${connection.apiBase}/alpha/generate` : `${connection.apiBase}/provider/v1/chat/completions`;
5423
+ const endpoint = protocolEndpoint(protocol, connection.apiBase);
4075
5424
  const connectTimer = setTimeout(() => {
4076
5425
  connectTimedOut = true;
4077
5426
  connectAbort.abort(new DOMException(`Command Code API request to ${endpoint} did not respond within ${connection.requestTimeoutMs}ms`, "TimeoutError"));
@@ -4089,38 +5438,46 @@ async function connectGenerate(deps, key, protocol, body) {
4089
5438
  };
4090
5439
  let response;
4091
5440
  const zdr = connection.zdr === true;
4092
- const headers = protocol === "cli" ? {
5441
+ const sharedHeaders = {
4093
5442
  "Content-Type": "application/json",
4094
5443
  ...IDENTITY_ENCODING_HEADER,
4095
5444
  Authorization: `Bearer ${key}`,
5445
+ ...zdr ? { "x-cmd-zdr": "1" } : {},
5446
+ ...attributionHeaders()
5447
+ };
5448
+ const headers = protocol === "cli" ? {
5449
+ ...sharedHeaders,
4096
5450
  "x-command-code-version": COMMAND_CODE_CLI_VERSION,
4097
5451
  "x-cli-environment": "production",
4098
5452
  "x-project-slug": projectSlugFromPath(connection.workingDir),
4099
5453
  "x-taste-learning": "true",
4100
- "x-co-flag": "false",
4101
- ...zdr ? { "x-cmd-zdr": "1" } : {},
4102
- ...attributionHeaders()
5454
+ "x-co-flag": "false"
4103
5455
  } : {
4104
- "Content-Type": "application/json",
4105
- ...IDENTITY_ENCODING_HEADER,
4106
- Authorization: `Bearer ${key}`,
4107
- Accept: "text/event-stream",
4108
- ...zdr ? { "x-cmd-zdr": "1" } : {},
4109
- ...attributionHeaders()
5456
+ ...sharedHeaders,
5457
+ Accept: "text/event-stream"
4110
5458
  };
5459
+ const finishHeaders = deps.timing.attempt(protocol, Buffer.byteLength(serializedBody));
4111
5460
  try {
4112
5461
  response = await fetchImpl(endpoint, {
4113
5462
  method: "POST",
4114
5463
  headers,
4115
- body: JSON.stringify(body),
5464
+ body: serializedBody,
4116
5465
  signal: connectAbort.signal
4117
5466
  });
5467
+ finishHeaders(response.status);
4118
5468
  clearTimeout(connectTimer);
4119
5469
  } catch (error) {
5470
+ finishHeaders();
4120
5471
  cleanup();
4121
5472
  if (options.signal?.aborted) throw error;
4122
- if (connectTimedOut || error instanceof DOMException && error.name === "TimeoutError") throw new LlmError(`Command Code API request to ${endpoint} did not respond within ${connection.requestTimeoutMs}ms: ${errorChain(error)};Command Code API 请求在 ${connection.requestTimeoutMs} 毫秒内未收到响应——通常是网络或代理问题,请检查后重试`, "TIMEOUT", { cause: error });
4123
- throw new LlmError(`Command Code API request to ${endpoint} failed: ${errorChain(error)};Command Code API 请求连接失败——通常是网络或代理问题,请检查网络或代理设置后重试`, "TRANSPORT", { cause: error });
5473
+ if (connectTimedOut || error instanceof DOMException && error.name === "TimeoutError") throw headersTimeoutError({
5474
+ protocol,
5475
+ endpoint,
5476
+ body: serializedBody,
5477
+ timeoutMs: connection.requestTimeoutMs,
5478
+ cause: error
5479
+ });
5480
+ throw bilingual("TRANSPORT", `Command Code API request to ${endpoint} failed: ${errorChain(error)}`, "Command Code API 请求连接失败——通常是网络或代理问题,请检查网络或代理设置后重试", { cause: error });
4124
5481
  }
4125
5482
  deps.onResponse(response);
4126
5483
  if (!response.ok) {
@@ -4152,7 +5509,9 @@ function createBlockAssembler() {
4152
5509
  sawContent: false,
4153
5510
  finishReason: void 0,
4154
5511
  cliCacheWriteTokens: void 0,
4155
- openAiToolCalls: []
5512
+ openAiToolCalls: [],
5513
+ messagesBlocks: /* @__PURE__ */ new Map(),
5514
+ replayBlocks: []
4156
5515
  };
4157
5516
  }
4158
5517
  function* closeText(asm) {
@@ -4213,9 +5572,10 @@ function* emitOpenAiToolCalls(asm) {
4213
5572
  }
4214
5573
  asm.openAiToolCalls.length = 0;
4215
5574
  }
4216
- var CommandCodeAdapter = class extends LlmAdapter {
5575
+ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
4217
5576
  deps;
4218
5577
  catalog = [];
5578
+ catalogState;
4219
5579
  fetchImpl;
4220
5580
  resolveAttachments;
4221
5581
  /**
@@ -4226,6 +5586,25 @@ var CommandCodeAdapter = class extends LlmAdapter {
4226
5586
  billingAccess = /* @__PURE__ */ new Map();
4227
5587
  billingAccessInflight = /* @__PURE__ */ new Map();
4228
5588
  protocolCache = /* @__PURE__ */ new Map();
5589
+ /** Namespace account-derived facts by gateway without exposing the key. */
5590
+ accountCacheKey(apiBase, apiKey) {
5591
+ return JSON.stringify([apiBase, apiKey]);
5592
+ }
5593
+ /**
5594
+ * Consecutive header timeouts for the last request shape, counted ACROSS
5595
+ * `stream()` calls: dsh-llm-retry re-invokes this same instance, so this is
5596
+ * the only place a streak can live. Keyed by `sessionId` (not by `agent`:
5597
+ * `GenerateOptions` carries no agent reference, only `sessionId`) so
5598
+ * concurrent sessions on this one shared adapter instance cannot clear or
5599
+ * inflate each other's count. A call with no `sessionId` (a one-shot,
5600
+ * session-less caller) falls back to {@link GLOBAL_HEADER_TIMEOUT_KEY},
5601
+ * matching the old single-slot behavior for exactly that case. Bounded by
5602
+ * {@link MAX_HEADER_TIMEOUT_STREAK_ENTRIES} with FIFO eviction (`Map`
5603
+ * preserves insertion order) since the key space is per-session rather than
5604
+ * per-account/model, so it is not naturally small like `protocolCache`.
5605
+ */
5606
+ headerTimeoutStreaks = /* @__PURE__ */ new Map();
5607
+ static MAX_HEADER_TIMEOUT_STREAK_ENTRIES = 200;
4229
5608
  constructor(deps) {
4230
5609
  super();
4231
5610
  this.deps = deps;
@@ -4234,6 +5613,38 @@ var CommandCodeAdapter = class extends LlmAdapter {
4234
5613
  this.surfaceOffload = deps.imageOffload ?? ENGINE_IMAGE_POLICY;
4235
5614
  }
4236
5615
  /**
5616
+ * Grade one failed attempt: a headers timeout that repeats on an unchanged
5617
+ * payload eventually leaves the retryable whitelist.
5618
+ *
5619
+ * `providerRetryPolicy()` cannot express this itself — dsh-llm's
5620
+ * `NormalRetryPolicyConfig` carries one `maxRetries` for every listed code.
5621
+ * Escalating here stops repeated identical header timeouts with a specific
5622
+ * diagnosis; the agent listener separately caps ordinary transient errors.
5623
+ *
5624
+ * @returns the error to surface: unchanged for any other failure, the
5625
+ * original timeout below the threshold, and a terminal one at it.
5626
+ */
5627
+ gradeHeaderTimeout(error, protocol, body, timeoutMs, sessionId) {
5628
+ if (!(error instanceof LlmError && error.code === "TIMEOUT")) return error;
5629
+ const key = sessionId ?? GLOBAL_HEADER_TIMEOUT_KEY;
5630
+ const fingerprint = `${protocol}:${fingerprintValue(body).sha256}`;
5631
+ const previous = this.headerTimeoutStreaks.get(key);
5632
+ const count = previous?.fingerprint === fingerprint ? previous.count + 1 : 1;
5633
+ if (previous === void 0) {
5634
+ if (this.headerTimeoutStreaks.size >= CommandCodeAdapter.MAX_HEADER_TIMEOUT_STREAK_ENTRIES) {
5635
+ const oldest = this.headerTimeoutStreaks.keys().next().value;
5636
+ if (oldest !== void 0) this.headerTimeoutStreaks.delete(oldest);
5637
+ }
5638
+ } else this.headerTimeoutStreaks.delete(key);
5639
+ this.headerTimeoutStreaks.set(key, {
5640
+ fingerprint,
5641
+ count
5642
+ });
5643
+ if (count < MAX_CONSECUTIVE_HEADER_TIMEOUTS) return error;
5644
+ this.headerTimeoutStreaks.delete(key);
5645
+ return bilingual("PROVIDER_HTTP_ERROR", `Command Code API stopped retrying: the gateway returned no response headers on ${count} consecutive attempts at the same request, each after the full ${timeoutMs}ms budget, so this is not a network blip — it is provider queueing, a Go-plan capacity limit, or a network/proxy path problem. Resending the identical payload again is unlikely to help; wait for the provider to recover, or switch model or account.`, `Command Code API 已停止重试:同一请求连续 ${count} 次在 ${timeoutMs} 毫秒内都没有收到任何响应头,这不是网络抖动,而是服务商排队、Go 套餐容量限制或网络/代理链路问题。继续原样重发同一个请求通常无济于事——可以等服务商恢复,或换模型/换账号。`);
5646
+ }
5647
+ /**
4237
5648
  * Display metadata for the picker's provider group header. The base class
4238
5649
  * returns the raw route id (`commandcode`, all lowercase) as the name, which
4239
5650
  * is what the model selector shows as this group's sticky title; return the
@@ -4247,12 +5658,11 @@ var CommandCodeAdapter = class extends LlmAdapter {
4247
5658
  };
4248
5659
  }
4249
5660
  /**
4250
- * Near-unbounded retry for transient failures only (`mode: 'normal'` with
4251
- * an explicit 1000-attempt cap — opencode-style persistence without the
4252
- * unbounded loop): `RATE_LIMIT`/`SERVER`/`TIMEOUT`/`TRANSPORT`/
4253
- * `EMPTY_RESPONSE` retry up to 1000 times with waits doubling from 500 ms
4254
- * and capping at 15 minutes (±10% jitter), so an exhausted 5-hour window
4255
- * recovers in-session instead of failing after two tries. Permanent failures
5661
+ * The official executor owns backoff and provider reset waits. Its high
5662
+ * route-wide cap lets a confirmed `RATE_LIMIT` window recover in-session;
5663
+ * our request-error listener limits `SERVER`/`TIMEOUT`/`EMPTY_RESPONSE`/
5664
+ * `THROTTLED` to three retries and `TRANSPORT` to its separate budget.
5665
+ * Waits double from 500 ms and cap at 15 minutes (±10% jitter). Permanent failures
4256
5666
  * (an invalid key's `INVALID_CREDENTIAL`, `UNSUPPORTED_CONTENT`, plan
4257
5667
  * rejections) are absent from the whitelist and surface immediately instead
4258
5668
  * of looping. Waits the pool/adapter attach as `providerRetryAfterMs` are
@@ -4270,6 +5680,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4270
5680
  retryableCodes: [
4271
5681
  "EMPTY_RESPONSE",
4272
5682
  "RATE_LIMIT",
5683
+ THROTTLED_CODE,
4273
5684
  "SERVER",
4274
5685
  "TIMEOUT",
4275
5686
  "TRANSPORT"
@@ -4296,26 +5707,61 @@ var CommandCodeAdapter = class extends LlmAdapter {
4296
5707
  const imageCapable = KNOWN_IMAGE_MODELS.has(model);
4297
5708
  return { priceImages: (images) => images.map((image) => priceRequestImage(image, model, imageCapable)) };
4298
5709
  }
4299
- /** Refresh the catalog (live fetch, cache fallback) and return it. */
5710
+ /** 地址或缓存文件变化时丢弃旧目录,防止旧网关的路由声明影响新请求。 */
5711
+ currentCatalogState(connection = this.deps.options()) {
5712
+ const { apiBase, modelsCachePath } = connection;
5713
+ const source = JSON.stringify([apiBase, modelsCachePath]);
5714
+ if (this.catalogState?.source !== source) {
5715
+ this.catalog = [];
5716
+ this.catalogState = {
5717
+ source,
5718
+ expiresAt: 0
5719
+ };
5720
+ }
5721
+ return this.catalogState;
5722
+ }
5723
+ /** 共享一次刷新;调用者取消只结束自己的等待,不取消其他调用者的请求。 */
4300
5724
  async loadCatalog(signal) {
4301
- const { apiBase, modelsCachePath } = this.deps.options();
4302
- try {
4303
- const response = await this.fetchImpl(`${apiBase}/provider/v1/models`, {
4304
- headers: {
4305
- accept: "application/json",
4306
- ...IDENTITY_ENCODING_HEADER,
4307
- ...attributionHeaders()
4308
- },
4309
- signal: signal ?? AbortSignal.timeout(1e4)
5725
+ signal?.throwIfAborted();
5726
+ const connection = this.deps.options();
5727
+ const state = this.currentCatalogState(connection);
5728
+ if (Date.now() < state.expiresAt) return this.catalog;
5729
+ const { apiBase, modelsCachePath } = connection;
5730
+ if (!state.inflight) {
5731
+ const fallback = this.catalog;
5732
+ state.inflight = (async () => {
5733
+ let models;
5734
+ let ttl = MODEL_CATALOG_TTL_MS;
5735
+ try {
5736
+ const { status, record } = await this.fetchJson(`${apiBase}/provider/v1/models`, {
5737
+ accept: "application/json",
5738
+ ...IDENTITY_ENCODING_HEADER,
5739
+ ...attributionHeaders()
5740
+ }, MODELS_TIMEOUT_MS);
5741
+ if (status < 200 || status >= 300) throw new Error(`models endpoint returned ${status}`);
5742
+ models = parseCatalogResponse(record, apiBase);
5743
+ if (this.catalogState === state) await writeModelsCache(modelsCachePath, apiBase, models, () => this.catalogState === state).catch(() => void 0);
5744
+ } catch {
5745
+ ttl = MODEL_CATALOG_RETRY_MS;
5746
+ models = await readModelsCache(modelsCachePath, apiBase).catch(() => fallback);
5747
+ }
5748
+ if (this.catalogState === state) {
5749
+ this.catalog = models;
5750
+ state.expiresAt = Date.now() + ttl;
5751
+ }
5752
+ return models;
5753
+ })().finally(() => {
5754
+ delete state.inflight;
4310
5755
  });
4311
- if (!response.ok) throw new Error(`models endpoint returned ${response.status}`);
4312
- this.catalog = parseCatalogResponse(await response.json());
4313
- await writeModelsCache(modelsCachePath, this.catalog).catch(() => void 0);
4314
- } catch (error) {
4315
- if (signal?.aborted) throw error;
4316
- this.catalog = await readModelsCache(modelsCachePath).catch(() => this.catalog);
4317
5756
  }
4318
- return this.catalog;
5757
+ if (!signal) return state.inflight;
5758
+ const inflight = state.inflight;
5759
+ return new Promise((resolve, reject) => {
5760
+ const abort = () => reject(signal.reason);
5761
+ signal.addEventListener("abort", abort, { once: true });
5762
+ inflight.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
5763
+ if (signal.aborted) abort();
5764
+ });
4319
5765
  }
4320
5766
  async listModels(provider, opts) {
4321
5767
  const catalog = await this.loadCatalog();
@@ -4340,7 +5786,33 @@ var CommandCodeAdapter = class extends LlmAdapter {
4340
5786
  return allow === void 0 || allow.has(model.id);
4341
5787
  }).map(toInfo).sort(compareByPlan);
4342
5788
  }
5789
+ /**
5790
+ * The per-model allowance bracket this account POOL falls into, or undefined
5791
+ * when there is nothing honest to show.
5792
+ *
5793
+ * The pricing page publishes a per-model monthly allowance for GOAT and Pro
5794
+ * only — its own words are "the boost is a per-model allowance, so it lives on
5795
+ * the plans that have them" — so a pool whose highest plan is Go, Provider,
5796
+ * Max or Ultra gets undefined rather than a neighbouring tier's figure. Taking
5797
+ * the HIGHEST tier matches the question the picker already answers for a pool
5798
+ * (see `modelVisibleForAnyAccount`): what the user can reach, not what the one
5799
+ * account serving this request happens to be. The fail-open shape is the same
5800
+ * too — an unreachable billing endpoint yields undefined, which hides the
5801
+ * allowance instead of inventing one.
5802
+ */
5803
+ async allowanceTier() {
5804
+ const accesses = await this.loadPoolBillingAccess();
5805
+ if (accesses === void 0) return void 0;
5806
+ let highest;
5807
+ for (const access of accesses) {
5808
+ const weight = access?.tierWeight;
5809
+ if (weight === void 0) continue;
5810
+ if (highest === void 0 || weight > highest) highest = weight;
5811
+ }
5812
+ return allowanceTierForWeight(highest);
5813
+ }
4343
5814
  async resolveModel(provider, model, signal) {
5815
+ this.currentCatalogState();
4344
5816
  const entry = this.catalog.find((m) => m.id === model) ?? (await this.loadCatalog(signal)).find((m) => m.id === model);
4345
5817
  const efforts = KNOWN_EFFORTS[model];
4346
5818
  const vision = KNOWN_IMAGE_MODELS.has(model);
@@ -4375,21 +5847,19 @@ var CommandCodeAdapter = class extends LlmAdapter {
4375
5847
  };
4376
5848
  }
4377
5849
  /**
4378
- * Fetch one account endpoint and parse its JSON body. Returns the HTTP
4379
- * status alongside the parsed record so each caller applies its own failure
4380
- * accounting: the billing probe fails open silently, the usage report books
4381
- * failures per endpoint. Non-2xx and invalid JSON bodies come back without a
4382
- * record; only a fetch failure propagates to the caller.
5850
+ * Fetch one JSON GET and return its HTTP status with a parsed record. The
5851
+ * catalog, billing probe, and usage report each apply their own failure
5852
+ * policy. Non-2xx and invalid JSON bodies come back without a record; only
5853
+ * a fetch failure propagates to the caller.
4383
5854
  *
4384
- * `timeoutMs` is the caller's budget for this one request. The default is the
4385
- * CATALOG probe's short cap, which fits the picker's fail-open reads; the
4386
- * usage report deliberately passes the connection's own request budget so an
4387
- * account query is not held to a stricter (and invisible) limit than chat.
5855
+ * `timeoutMs` is the caller's budget, unless it provides its own signal.
5856
+ * The usage report passes the connection's request budget rather than the
5857
+ * catalog/picker's shorter default.
4388
5858
  */
4389
- async fetchEndpointJson(url, headers, timeoutMs = MODELS_TIMEOUT_MS) {
5859
+ async fetchJson(url, headers, timeoutMs = MODELS_TIMEOUT_MS, signal) {
4390
5860
  const response = await this.fetchImpl(url, {
4391
5861
  headers,
4392
- signal: AbortSignal.timeout(timeoutMs)
5862
+ signal: signal ?? AbortSignal.timeout(timeoutMs)
4393
5863
  });
4394
5864
  if (!response.ok) return { status: response.status };
4395
5865
  let parsed;
@@ -4420,7 +5890,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
4420
5890
  async loadPoolBillingAccess() {
4421
5891
  const keys = await this.poolAccountKeys();
4422
5892
  if (keys.length === 0) return void 0;
4423
- return Promise.all(keys.map((key) => this.loadBillingAccessForKey(key)));
5893
+ const apiBase = this.deps.options().apiBase;
5894
+ return Promise.all(keys.map((key) => this.loadBillingAccessForKey(apiBase, key)));
4424
5895
  }
4425
5896
  /**
4426
5897
  * The API keys the picker's plan filter must consult: every account the host
@@ -4444,21 +5915,22 @@ var CommandCodeAdapter = class extends LlmAdapter {
4444
5915
  * shared across concurrent callers. `undefined` means "unknown — show
4445
5916
  * everything" (fail-open).
4446
5917
  */
4447
- async loadBillingAccessForKey(apiKey) {
4448
- const cached = this.billingAccess.get(apiKey);
5918
+ async loadBillingAccessForKey(apiBase, apiKey) {
5919
+ const cacheKey = this.accountCacheKey(apiBase, apiKey);
5920
+ const cached = this.billingAccess.get(cacheKey);
4449
5921
  if (cached !== void 0 && Date.now() - cached.at < 3e5) return cached.value;
4450
- const existing = this.billingAccessInflight.get(apiKey);
5922
+ const existing = this.billingAccessInflight.get(cacheKey);
4451
5923
  if (existing !== void 0) return existing;
4452
- const inflight = this.fetchBillingAccess(apiKey).then((value) => {
4453
- this.billingAccess.set(apiKey, {
5924
+ const inflight = this.fetchBillingAccess(apiBase, apiKey).then((value) => {
5925
+ this.billingAccess.set(cacheKey, {
4454
5926
  value,
4455
5927
  at: Date.now()
4456
5928
  });
4457
5929
  return value;
4458
5930
  }).finally(() => {
4459
- this.billingAccessInflight.delete(apiKey);
5931
+ this.billingAccessInflight.delete(cacheKey);
4460
5932
  });
4461
- this.billingAccessInflight.set(apiKey, inflight);
5933
+ this.billingAccessInflight.set(cacheKey, inflight);
4462
5934
  return inflight;
4463
5935
  }
4464
5936
  /**
@@ -4470,12 +5942,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
4470
5942
  * fallback (the CLI stamps plan identity from it too). Any failure resolves
4471
5943
  * to `undefined` (fail-open) rather than breaking the picker.
4472
5944
  */
4473
- async fetchBillingAccess(apiKey) {
5945
+ async fetchBillingAccess(apiBase, apiKey) {
4474
5946
  try {
4475
- const connection = this.deps.options();
4476
5947
  const headers = await this.accountHeaders(apiKey);
4477
- const base = connection.apiBase;
4478
- const getJson = async (path) => (await this.fetchEndpointJson(`${base}${path}`, headers)).record;
5948
+ const getJson = async (path) => (await this.fetchJson(`${apiBase}${path}`, headers)).record;
4479
5949
  const whoami = await getJson("/alpha/whoami");
4480
5950
  const orgData = whoami && isRecord(whoami.org) ? whoami.org : void 0;
4481
5951
  const orgId = orgData === void 0 ? void 0 : stringValue(orgData.id);
@@ -4497,50 +5967,65 @@ var CommandCodeAdapter = class extends LlmAdapter {
4497
5967
  }
4498
5968
  }
4499
5969
  /** Fresh cached billing tier weight for a key, or undefined when not known. */
4500
- cachedBillingTierWeight(apiKey) {
4501
- const hit = this.billingAccess.get(apiKey);
5970
+ cachedBillingTierWeight(apiBase, apiKey) {
5971
+ const hit = this.billingAccess.get(this.accountCacheKey(apiBase, apiKey));
4502
5972
  if (hit === void 0 || Date.now() - hit.at >= 3e5) return void 0;
4503
5973
  return hit.value?.tierWeight;
4504
5974
  }
4505
5975
  /** Cached protocol decision for a key, or undefined when expired/unknown. */
4506
- cachedProtocolUseCli(apiKey) {
4507
- const hit = this.protocolCache.get(apiKey);
5976
+ cachedProtocol(apiBase, apiKey) {
5977
+ const hit = this.protocolCache.get(this.accountCacheKey(apiBase, apiKey));
4508
5978
  if (hit === void 0 || Date.now() - hit.at >= 9e5) return void 0;
4509
- return hit.useCli;
5979
+ return hit.protocol;
4510
5980
  }
4511
- rememberProtocol(apiKey, useCli) {
4512
- this.protocolCache.set(apiKey, {
4513
- useCli,
5981
+ rememberProtocol(apiBase, apiKey, protocol) {
5982
+ this.protocolCache.set(this.accountCacheKey(apiBase, apiKey), {
5983
+ protocol,
4514
5984
  at: Date.now()
4515
5985
  });
4516
5986
  }
4517
5987
  /**
4518
- * Choose the initial protocol for one request. A fresh protocol cache entry
4519
- * wins; otherwise a cached (not network-fetched) billing tier of Go is
4520
- * treated as CLI-only. Unknown accounts default to Provider API and fall
4521
- * back only after an `upgrade_required` rejection.
5988
+ * True when the catalog routes `model` to `/provider/v1/messages`.
5989
+ *
5990
+ * The catalog's own `supported_endpoints` is authoritative (it is what
5991
+ * upstream publishes per model); `requiresMessagesEndpoint()` — the
5992
+ * `claude-*` prefix rule — is the fallback for a catalog entry that carries
5993
+ * no route list, which is the shape every pre-1.55 cache file has.
5994
+ */
5995
+ routesToMessages(model) {
5996
+ const entry = this.catalog.find((candidate) => candidate.id === model);
5997
+ if (entry !== void 0 && entry.supportedEndpoints.length > 0) return entry.supportedEndpoints.includes("/messages");
5998
+ return requiresMessagesEndpoint(model);
5999
+ }
6000
+ /**
6001
+ * Choose the initial protocol for one request.
4522
6002
  *
4523
- * Models the Provider API serves ONLY through `/provider/v1/messages` (the
4524
- * Claude family — `requiresMessagesEndpoint()`) always take the CLI
4525
- * transport, on any tier and even under a forced `'openai'` preference:
4526
- * that option is documented as "prefer the Provider API, still fall back",
4527
- * and the 400 these models answer with is not a preference to be honoured
4528
- * but a hard refusal. `/alpha/generate` routes every one of them, so this
4529
- * costs nothing.
6003
+ * Models the Provider API serves only through `/provider/v1/messages` (the
6004
+ * Claude family) take the Messages transport on any tier and even under a
6005
+ * forced `'openai'` preference: that option is documented as "prefer the
6006
+ * Provider API, still fall back", and the 400 these models answer with on
6007
+ * Chat Completions (`Model "<id>" must be called via /provider/v1/messages`)
6008
+ * is a hard refusal, not a preference to honour.
4530
6009
  *
4531
6010
  * That decision is deliberately NOT written to `protocolCache`, which is
4532
- * keyed by API key alone: remembering it would pin the whole ACCOUNT to the
4533
- * CLI transport and drag every other model off it until the entry expired.
6011
+ * keyed by gateway and API key: it depends on the model, not just the account, so
6012
+ * remembering it would pin the whole ACCOUNT to one transport and drag
6013
+ * every other model along (issue #46).
6014
+ *
6015
+ * Otherwise a fresh protocol cache entry wins, then a cached (not
6016
+ * network-fetched) billing tier of Go — the one plan without Provider API
6017
+ * access. Unknown accounts default to Chat Completions and fall back to the
6018
+ * CLI transport only after an `upgrade_required` rejection.
4534
6019
  */
4535
- resolveProtocol(apiKey, model) {
6020
+ resolveProtocol(apiBase, apiKey, model) {
4536
6021
  const forced = this.deps.options().protocol;
4537
6022
  if (forced === "cli") return forced;
4538
- if (requiresMessagesEndpoint(model)) return "cli";
6023
+ if (this.routesToMessages(model)) return "messages";
4539
6024
  if (forced === "openai") return forced;
4540
- const cached = this.cachedProtocolUseCli(apiKey);
4541
- if (cached !== void 0) return cached ? "cli" : "openai";
4542
- if (this.cachedBillingTierWeight(apiKey) === GO_TIER_WEIGHT) {
4543
- this.rememberProtocol(apiKey, true);
6025
+ const cached = this.cachedProtocol(apiBase, apiKey);
6026
+ if (cached !== void 0) return cached;
6027
+ if (this.cachedBillingTierWeight(apiBase, apiKey) === GO_TIER_WEIGHT) {
6028
+ this.rememberProtocol(apiBase, apiKey, "cli");
4544
6029
  return "cli";
4545
6030
  }
4546
6031
  return "openai";
@@ -4582,7 +6067,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4582
6067
  const failed = [];
4583
6068
  const getJson = async (path) => {
4584
6069
  try {
4585
- const { status, record, invalidResponse } = await this.fetchEndpointJson(`${base}${path}`, headers, connection.requestTimeoutMs);
6070
+ const { status, record, invalidResponse } = await this.fetchJson(`${base}${path}`, headers, connection.requestTimeoutMs);
4586
6071
  if (record === void 0) {
4587
6072
  failures.push(`${path}: HTTP ${status}${invalidResponse ? " returned an unreadable or invalid JSON body" : ""}`);
4588
6073
  failed.push(invalidResponse ? {
@@ -4655,13 +6140,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
4655
6140
  async probeWindowLimits(apiKey) {
4656
6141
  try {
4657
6142
  const connection = this.deps.options();
4658
- const response = await this.fetchImpl(`${connection.apiBase}/alpha/billing/credits`, {
4659
- headers: await this.accountHeaders(apiKey),
4660
- signal: AbortSignal.timeout(MODELS_TIMEOUT_MS)
4661
- });
4662
- if (!response.ok) return void 0;
4663
- const parsed = await response.json();
4664
- if (!isRecord(parsed)) return void 0;
6143
+ const { record: parsed } = await this.fetchJson(`${connection.apiBase}/alpha/billing/credits`, await this.accountHeaders(apiKey));
6144
+ if (parsed === void 0) return void 0;
4665
6145
  const windowLimits = isRecord(parsed.windowLimits) ? parsed.windowLimits : void 0;
4666
6146
  if (windowLimits === void 0) return void 0;
4667
6147
  let reported = false;
@@ -4685,6 +6165,59 @@ var CommandCodeAdapter = class extends LlmAdapter {
4685
6165
  }
4686
6166
  }
4687
6167
  async *stream(options) {
6168
+ const timing = openRequestTiming(options.model, this.deps.onRequestTiming);
6169
+ let outcome = "cancelled";
6170
+ let errorCode;
6171
+ try {
6172
+ for await (const chunk of this.streamRequest(options, timing)) {
6173
+ if (chunk.type === "text-delta" || chunk.type === "reasoning-delta" || chunk.type === "tool-call-delta") timing.first("firstContentMs");
6174
+ if (chunk.type === "finish") outcome = "finished";
6175
+ yield chunk;
6176
+ }
6177
+ } catch (error) {
6178
+ outcome = options.signal?.aborted ? "aborted" : "error";
6179
+ errorCode = error instanceof LlmError ? error.code : "unknown";
6180
+ throw error;
6181
+ } finally {
6182
+ await timing.close(outcome, errorCode);
6183
+ }
6184
+ }
6185
+ /**
6186
+ * One turn, with at most one output-ceiling downshift.
6187
+ *
6188
+ * The ceiling a model accepts is not published anywhere the request can be
6189
+ * built from (see `modelOutputTokenLimit`), and the endpoint states it only
6190
+ * by refusing. That refusal is worth one retry rather than a failed turn: the
6191
+ * harness's own recovery for it — compact the context — cannot help, because
6192
+ * nothing about the context is wrong, and resending the identical request
6193
+ * would be refused identically (issue #71). Reading the stated ceiling off
6194
+ * the rejection turns one wasted turn into one, for good: it is recorded
6195
+ * process-wide, so the next request for this model is built correctly
6196
+ * without spending the round trip again.
6197
+ *
6198
+ * Only BEFORE the first chunk reaches the host. Once a delta has been
6199
+ * delivered the answer is already partly on screen, and a second request
6200
+ * would duplicate it — so from that point the error stands and says what is
6201
+ * actually wrong. One downshift per turn, and a second refusal surfaces.
6202
+ */
6203
+ async *streamRequest(options, timing) {
6204
+ for (let attempt = 0;; attempt++) {
6205
+ const connection = this.deps.options();
6206
+ let delivered = false;
6207
+ try {
6208
+ for await (const chunk of this.streamAttempt(options, timing, connection)) {
6209
+ delivered = true;
6210
+ yield chunk;
6211
+ }
6212
+ return;
6213
+ } catch (error) {
6214
+ const ceiling = delivered ? void 0 : outputCeilingRefusal(connection.apiBase, options.model, error);
6215
+ if (attempt > 0 || ceiling === void 0) throw error;
6216
+ }
6217
+ }
6218
+ }
6219
+ async *streamAttempt(options, timing, connection) {
6220
+ this.currentCatalogState(connection);
4688
6221
  if (options.stop?.length) throw new LlmError("Command Code adapter does not support stop sequences", "UNSUPPORTED_OPTION");
4689
6222
  const hasImages = options.messages.some(hasImageContent);
4690
6223
  let readImage;
@@ -4692,30 +6225,51 @@ var CommandCodeAdapter = class extends LlmAdapter {
4692
6225
  if (!KNOWN_IMAGE_MODELS.has(options.model)) throw new LlmError(`Command Code model "${options.model}" does not support image input; switch to a Vision-capable model (see the model registry)`, "UNSUPPORTED_CONTENT");
4693
6226
  const attachments = this.resolveAttachments?.();
4694
6227
  if (attachments === void 0) throw new LlmError("Command Code image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
4695
- readImage = (ref) => readRequestImage(attachments, ref);
6228
+ readImage = requestImages(attachments, options.signal, timing);
4696
6229
  }
4697
- const connection = this.deps.options();
4698
- let apiKey = await this.deps.resolveApiKey(connection, options.model);
4699
- const modelMax = this.catalog.find((m) => m.id === options.model)?.maxTokens ?? 65536;
4700
- const maxTokens = Math.min(options.maxTokens ?? modelMax, modelMax, DEFAULT_GENERATE_MAX_TOKENS);
6230
+ const finishCredentials = timing.phase("credentials");
6231
+ let apiKey;
6232
+ try {
6233
+ apiKey = await this.deps.resolveApiKey(connection, options.model);
6234
+ } finally {
6235
+ finishCredentials();
6236
+ }
6237
+ const modelEntry = this.catalog.find((m) => m.id === options.model);
6238
+ const modelMax = Math.min(modelEntry?.maxTokens ?? Infinity, modelOutputTokenLimit(connection.apiBase, options.model));
6239
+ const requestedMaxTokens = Math.min(options.maxTokens ?? modelMax, modelMax, DEFAULT_GENERATE_MAX_TOKENS);
6240
+ let maxTokens = requestedMaxTokens;
6241
+ let contextBudget = {
6242
+ maxTokens,
6243
+ sizePressure: false
6244
+ };
4701
6245
  const effort = options.reasoningEffort;
4702
6246
  const supported = KNOWN_EFFORTS[options.model];
4703
6247
  const reasoningEffort = effort && effort !== "off" && supported?.includes(effort) ? effort : void 0;
4704
6248
  const systemText = [options.system ?? "", ...options.messages.filter((m) => m.role === "system").map((m) => m.content.map(blockText).filter(Boolean).join("\n"))].filter(Boolean).join("\n\n");
4705
- let protocol = this.resolveProtocol(apiKey, options.model);
4706
- let requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0]);
4707
- const buildBody = async (target) => target === "cli" ? buildCliBody(requestOptions, connection, {
4708
- maxTokens,
4709
- reasoningEffort,
4710
- systemText,
4711
- readImage
4712
- }) : buildOpenAIBody(requestOptions, {
4713
- maxTokens,
4714
- reasoningEffort,
4715
- systemText,
4716
- readImage
4717
- });
6249
+ let protocol = this.resolveProtocol(connection.apiBase, apiKey, options.model);
6250
+ let requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true && protocol === "cli" ? options.model : void 0);
6251
+ const buildBody = async (target) => {
6252
+ const finishBody = timing.phase("body");
6253
+ try {
6254
+ const facts = {
6255
+ maxTokens: requestedMaxTokens,
6256
+ reasoningEffort,
6257
+ selectableEffort: supported !== void 0 && supported.length > 0,
6258
+ systemText,
6259
+ readImage
6260
+ };
6261
+ const built = target === "cli" ? await buildCliBody(requestOptions, connection, facts) : target === "messages" ? await buildMessagesBody(requestOptions, facts) : await buildOpenAIBody(requestOptions, facts);
6262
+ contextBudget = requestContextBudget(target, built, requestOptions.messages, options.model, modelEntry?.contextWindow, requestedMaxTokens);
6263
+ maxTokens = contextBudget.maxTokens;
6264
+ if (target === "cli") recordOrEmpty(built.params).max_tokens = maxTokens;
6265
+ else built.max_tokens = maxTokens;
6266
+ return built;
6267
+ } finally {
6268
+ finishBody();
6269
+ }
6270
+ };
4718
6271
  let body = await buildBody(protocol);
6272
+ const serializedBodies = /* @__PURE__ */ new WeakMap();
4719
6273
  const trace = openStreamTrace(`${options.provider}/${options.model}`);
4720
6274
  const tried = /* @__PURE__ */ new Set();
4721
6275
  let attemptNumber = 0;
@@ -4727,14 +6281,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
4727
6281
  options,
4728
6282
  connection,
4729
6283
  fetchImpl: this.fetchImpl,
6284
+ timing,
4730
6285
  onResponse: (response) => {
6286
+ this.headerTimeoutStreaks.delete(options.sessionId ?? GLOBAL_HEADER_TIMEOUT_KEY);
4731
6287
  responseMetadata = {
4732
6288
  protocol,
4733
6289
  headers: responseIdentifiers(response.headers)
4734
6290
  };
4735
6291
  trace.record("response", {
4736
6292
  protocol,
4737
- endpoint: protocol === "cli" ? `${connection.apiBase}/alpha/generate` : `${connection.apiBase}/provider/v1/chat/completions`,
6293
+ endpoint: protocolEndpoint(protocol, connection.apiBase),
4738
6294
  status: response.status,
4739
6295
  attempt: attemptNumber,
4740
6296
  accountsTried: tried.size,
@@ -4758,14 +6314,27 @@ var CommandCodeAdapter = class extends LlmAdapter {
4758
6314
  sessionId: options.sessionId,
4759
6315
  ...requestFingerprint(protocol, body)
4760
6316
  });
4761
- const attempt = await connectGenerate(connectDeps, apiKey, protocol, body);
6317
+ let serialized = serializedBodies.get(body);
6318
+ if (serialized === void 0) {
6319
+ const finishSerialization = timing.phase("serialization");
6320
+ try {
6321
+ serialized = JSON.stringify(body, (_key, value) => sanitizeSurrogates(value));
6322
+ } finally {
6323
+ finishSerialization();
6324
+ }
6325
+ serializedBodies.set(body, serialized);
6326
+ }
6327
+ const attempt = await connectGenerate(connectDeps, apiKey, protocol, serialized);
4762
6328
  if ("response" in attempt) {
4763
6329
  connected = attempt;
4764
6330
  break;
4765
6331
  }
4766
- if (protocol === "openai" && isUpgradeRequiredError(attempt.status, attempt.errText)) {
6332
+ const providerError = parseProviderError(attempt.errText);
6333
+ if (protocol !== "cli" && isUpgradeRequiredError(attempt.status, attempt.errText, providerError)) {
6334
+ const wasMessages = protocol === "messages";
4767
6335
  protocol = "cli";
4768
- this.rememberProtocol(apiKey, true);
6336
+ if (!wasMessages) this.rememberProtocol(connection.apiBase, apiKey, "cli");
6337
+ requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true ? options.model : void 0);
4769
6338
  body = await buildBody("cli");
4770
6339
  continue;
4771
6340
  }
@@ -4774,7 +6343,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4774
6343
  if (missing > 0) throw imageOffloadRequired(missing);
4775
6344
  }
4776
6345
  const rotate = this.deps.rotateApiKey;
4777
- const rejection = classifyAccountRejection(attempt.status, attempt.errText);
6346
+ const rejection = classifyAccountRejection(attempt.status, attempt.errText, providerError);
4778
6347
  if (rejection !== void 0 && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
4779
6348
  const next = await rotate(apiKey, rejection.reason, connection, options.model, {
4780
6349
  tried: [...tried],
@@ -4782,17 +6351,26 @@ var CommandCodeAdapter = class extends LlmAdapter {
4782
6351
  });
4783
6352
  if (next !== void 0 && !tried.has(next)) {
4784
6353
  apiKey = next;
6354
+ const nextProtocol = this.resolveProtocol(connection.apiBase, apiKey, options.model);
6355
+ if (nextProtocol !== protocol) {
6356
+ protocol = nextProtocol;
6357
+ requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true && protocol === "cli" ? options.model : void 0);
6358
+ body = await buildBody(protocol);
6359
+ }
4785
6360
  continue;
4786
6361
  }
4787
6362
  }
4788
- if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled") && (rejection.resetAtMs !== void 0 || attempt.status !== 429)) {
6363
+ if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled")) {
4789
6364
  const reset = rejection.resetAtMs;
4790
- const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
6365
+ const wait = reset === void 0 ? attempt.retryAfterMs ?? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
4791
6366
  const when = reset === void 0 ? void 0 : new Date(reset).toISOString();
4792
6367
  const window = rejection.reason === "rate-limit";
4793
- throw new LlmError((window ? "llm-commandcode: the Command Code account is rate limited" : "llm-commandcode: the Command Code account is rate limited (429) — the provider did not report an exhausted usage window") + (when === void 0 ? "" : ` — the provider reports it resets at ${when}`) + (window ? ";当前 Command Code 账户已被限流" : ";当前 Command Code 账户被限流(429),服务商未报告用量窗口用尽") + (when === void 0 ? "" : `,服务商给出的重置时间为 ${when}`), "RATE_LIMIT", wait > 0 ? { providerRetryAfterMs: wait } : void 0);
6368
+ throw bilingual(window ? "RATE_LIMIT" : THROTTLED_CODE, (window ? "llm-commandcode: the Command Code account is rate limited" : "llm-commandcode: the Command Code account is rate limited (429) — the provider did not report an exhausted usage window") + (when === void 0 ? "" : ` — the provider reports it resets at ${when}`), (window ? "当前 Command Code 账户已被限流" : "当前 Command Code 账户被限流(429),服务商未报告用量窗口用尽") + (when === void 0 ? "" : `,服务商给出的重置时间为 ${when}`), {
6369
+ status: attempt.status,
6370
+ ...wait > 0 && wait <= 9e5 ? { providerRetryAfterMs: wait } : {}
6371
+ });
4794
6372
  }
4795
- throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs);
6373
+ throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs, providerError, contextBudget.sizePressure);
4796
6374
  }
4797
6375
  } catch (error) {
4798
6376
  trace.record("connect-error", {
@@ -4800,7 +6378,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
4800
6378
  responseMetadata
4801
6379
  });
4802
6380
  trace.close();
4803
- throw options.signal?.aborted ? error : failureWithRequestId(error, responseMetadata);
6381
+ if (options.signal?.aborted) throw error;
6382
+ throw failureWithRequestId(this.gradeHeaderTimeout(error, protocol, body, connection.requestTimeoutMs, options.sessionId), responseMetadata);
4804
6383
  }
4805
6384
  if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
4806
6385
  const { response, cleanup } = connected;
@@ -4860,7 +6439,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4860
6439
  silentMs: Date.now() - lastChunkAt,
4861
6440
  message: errorChain(error)
4862
6441
  });
4863
- throw new LlmError(`Command Code API stream from ${connection.apiBase} failed while reading: ${errorChain(error)};Command Code API 流式响应中途断开——网络波动所致,重试通常可恢复`, "TRANSPORT", { cause: error });
6442
+ throw bilingual("TRANSPORT", `Command Code API stream from ${connection.apiBase} failed while reading: ${errorChain(error)}`, "Command Code API 流式响应中途断开——网络波动所致,重试通常可恢复", { cause: error });
4864
6443
  } finally {
4865
6444
  clearIdle();
4866
6445
  }
@@ -4875,11 +6454,12 @@ var CommandCodeAdapter = class extends LlmAdapter {
4875
6454
  idleFired,
4876
6455
  buffered: buffer.length
4877
6456
  });
4878
- if (idleFired) throw new LlmError(`Command Code API stream from ${connection.apiBase} was idle for ${connection.streamIdleTimeoutMs}ms (no events) and was treated as a dead connection;Command Code API 流式响应已 ${connection.streamIdleTimeoutMs} 毫秒无任何事件,被判定为死连接——长思考模型可在设置中调大流空闲超时`, "TIMEOUT");
6457
+ if (idleFired) throw bilingual("TIMEOUT", `Command Code API stream from ${connection.apiBase} was idle for ${connection.streamIdleTimeoutMs}ms (no events) and was treated as a dead connection`, `Command Code API 流式响应已 ${connection.streamIdleTimeoutMs} 毫秒无任何事件,被判定为死连接——长思考模型可在设置中调大流空闲超时`);
4879
6458
  if (buffer.trim()) yield* handle(parseStreamEventLine(buffer));
4880
6459
  break;
4881
6460
  }
4882
6461
  const text = decoder.decode(value, { stream: true });
6462
+ if (value.byteLength > 0) timing.first("firstByteMs");
4883
6463
  buffer += text;
4884
6464
  chunkCount += 1;
4885
6465
  totalBytes += value.byteLength;
@@ -4917,9 +6497,13 @@ var CommandCodeAdapter = class extends LlmAdapter {
4917
6497
  });
4918
6498
  endRecorded = true;
4919
6499
  if (failure !== void 0) throw failure;
4920
- yield hasResponseIdentifiers(responseMetadata) ? {
6500
+ const replayBlocks = asm.replayBlocks;
6501
+ yield hasResponseIdentifiers(responseMetadata) || replayBlocks.length > 0 ? {
4921
6502
  ...finish,
4922
- replayState: { response: responseMetadata }
6503
+ replayState: {
6504
+ response: responseMetadata,
6505
+ ...replayBlocks.length > 0 ? { blocks: replayBlocks } : {}
6506
+ }
4923
6507
  } : finish;
4924
6508
  } else {
4925
6509
  const elapsed = Date.now() - streamStartedAt;
@@ -4933,8 +6517,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
4933
6517
  responseMetadata
4934
6518
  });
4935
6519
  endRecorded = true;
4936
- if (!asm.sawContent) throw new LlmError("Command Code returned an empty response;Command Code 返回了空响应,重试通常可恢复", "EMPTY_RESPONSE");
4937
- throw new LlmError(`Command Code API stream from ${connection.apiBase} ended before its finish event (${detail}) — the response was closed mid-generation, so this is not the model stopping by itself;Command Code API 流式响应在结束事件之前被关闭(生成中途断流),并非模型主动结束——请重试;若频繁出现,可用 ${STREAM_TRACE_ENV}=<文件路径> 抓取原始流以定位断流原因`, "STREAM_CLOSED");
6520
+ if (!asm.sawContent) throw bilingual("EMPTY_RESPONSE", "Command Code returned an empty response", "Command Code 返回了空响应,重试通常可恢复");
6521
+ throw bilingual("STREAM_CLOSED", `Command Code API stream from ${connection.apiBase} ended before its finish event (${detail}) — the response was closed mid-generation, so this is not the model stopping by itself`, `Command Code API 流式响应在结束事件之前被关闭(生成中途断流),并非模型主动结束——请重试;若频繁出现,可用 ${STREAM_TRACE_ENV}=<文件路径> 抓取原始流以定位断流原因`);
4938
6522
  }
4939
6523
  } catch (error) {
4940
6524
  if (!usageEmitted && usage !== void 0) yield {
@@ -4964,43 +6548,66 @@ var CommandCodeAdapter = class extends LlmAdapter {
4964
6548
  }
4965
6549
  };
4966
6550
  /**
4967
- * Map a pre-stream generate HTTP failure onto a stable LlmError. Command
4968
- * Code folds several business rejections into 403 (plan limits, CLI version,
4969
- * model access): prefer the machine-readable `error.code` when present; the
4970
- * status alone cannot distinguish them. The status also decides the failure
4971
- * CODE via {@link httpErrorCode}, which is what the retry policy routes on —
4972
- * a transient 5xx must not be reported as a permanent provider rejection. A
4973
- * 429's `Retry-After` header rides along as `providerRetryAfterMs` so
4974
- * dsh-llm-retry can wait exactly that long instead of guessing at the backoff
4975
- * cadence — capped at RETRY_MAX_DELAY_MS, because in normal mode a longer
4976
- * attached wait makes the executor abandon the retry outright.
4977
- *
4978
- * One body is read, not just its status: a client-side rejection whose
4979
- * `error.code`/`type`/`message` names the model context window is reported as
4980
- * `CONTEXT_WINDOW_EXCEEDED`, because that failure has a recovery the harness
4981
- * performs and a generic provider error does not. Every message built here is
4982
- * bilingual — the harness renders it verbatim in its retry chrome.
4983
- */
4984
- function generateHttpError(status, errText, retryAfterMs) {
4985
- let providerCode;
4986
- let providerDetail = "";
6551
+ * Unwrap one level of a double-encoded provider message.
6552
+ *
6553
+ * The Messages endpoint sometimes reports a rejection with `error.message`
6554
+ * holding a whole JSON document rather than the sentence
6555
+ * (`{"type":"error","error":{"type":"invalid_request_error","message":"{\"type\":
6556
+ * \"error\",…}"},"request_id":"req_…"}`, measured 2026-09-29). Showing that blob
6557
+ * to the user hides the actual refusal, so one nesting level is peeled; the
6558
+ * outer text is returned unchanged when it is not JSON or carries no message.
6559
+ */
6560
+ function unwrapProviderMessage(raw) {
6561
+ if (raw === void 0) return void 0;
6562
+ const trimmed = raw.trim();
6563
+ if (!trimmed.startsWith("{")) return raw;
6564
+ try {
6565
+ const inner = JSON.parse(trimmed);
6566
+ if (!isRecord(inner)) return raw;
6567
+ return stringValue(((isRecord(inner.error) ? inner.error : void 0) ?? inner).message) ?? raw;
6568
+ } catch {
6569
+ return raw;
6570
+ }
6571
+ }
6572
+ /** Parse a rejected body once, retaining the nested/root distinction. */
6573
+ function parseProviderError(errText) {
4987
6574
  try {
4988
6575
  const parsed = JSON.parse(errText);
4989
- if (isRecord(parsed) && isRecord(parsed.error)) {
4990
- providerCode = stringValue(parsed.error.code);
4991
- providerDetail = [
4992
- stringValue(parsed.error.code),
4993
- stringValue(parsed.error.type),
4994
- stringValue(parsed.error.message)
4995
- ].filter((part) => part !== void 0 && part !== "").join(" ");
4996
- }
4997
- } catch {}
6576
+ if (!isRecord(parsed)) return { message: errText };
6577
+ const nested = isRecord(parsed.error) ? parsed.error : void 0;
6578
+ const body = nested ?? parsed;
6579
+ const rawMessage = stringValue(body.message);
6580
+ return {
6581
+ root: parsed,
6582
+ nested,
6583
+ code: stringValue(body.code),
6584
+ type: stringValue(body.type),
6585
+ rawMessage,
6586
+ message: unwrapProviderMessage(rawMessage) ?? errText,
6587
+ rateLimit: isRecord(body.rateLimit) ? body.rateLimit : void 0
6588
+ };
6589
+ } catch {
6590
+ return { message: errText };
6591
+ }
6592
+ }
6593
+ function generateHttpError(status, errText, retryAfterMs, parsed = parseProviderError(errText), sizePressure = false) {
6594
+ const providerCode = parsed.nested === void 0 ? void 0 : parsed.code;
6595
+ const providerDetail = parsed.nested === void 0 ? "" : [
6596
+ parsed.code,
6597
+ parsed.type,
6598
+ parsed.rawMessage ?? parsed.message
6599
+ ].filter((part) => part !== void 0 && part !== "").join(" ");
4998
6600
  const detail = providerCode ?? `HTTP ${status}`;
4999
6601
  const overflowDetail = providerDetail !== "" ? providerDetail : errText.slice(0, 500);
5000
- if (status < 500 && isContextOverflowDetail(overflowDetail)) return new LlmError(`Command Code API error ${status}: the request exceeds the model's context window — this session is being compacted and the request retried;Command Code API 返回 ${status}:请求内容超出模型上下文窗口——正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, CONTEXT_WINDOW_EXCEEDED_CODE, { status });
5001
- if (status === 422 && isZdrNoProviders(providerCode, providerDetail, errText)) return new LlmError(`Command Code API error 422 (${detail}): zero data retention is enforced but no ZDR-capable upstream is available for this model — turn the ZDR setting off (or use another model);Command Code API 返回 422:已开启零数据保留(ZDR),但该模型当前没有可用的 ZDR 上游——请关闭 ZDR 开关,或改用其他模型`, "PROVIDER_HTTP_ERROR", { status: 422 });
5002
- if (status === 401) return new LlmError(`Command Code API error 401 (${detail}): the API key is missing or invalid — check the key stored for COMMANDCODE_API_KEY (Models page) or the auth file;Command Code API 返回 401:API 密钥缺失或无效——请在设置页检查 COMMANDCODE_API_KEY 存储的密钥,或检查 auth 文件`, "INVALID_CREDENTIAL", { status: 401 });
5003
- if (status === 413) return new LlmError("Command Code API error 413: the request body exceeds the provider's size limit (the cap is undocumented, measured at about 50 MB) — the session history is too large to send, usually because of accumulated image attachments. Start a new session, or drop the image-heavy part of this one, and retry;Command Code API 返回 413:请求体超过服务端的体积上限(官方未公开,实测约 50 MB)——会话历史过大,通常是历史图片累积所致。请新建会话,或移除本会话中图片较多的部分后重试", "PROVIDER_HTTP_ERROR", { status: 413 });
6602
+ if (status < 500 && OUTPUT_LIMIT_REJECTION.test(overflowDetail)) {
6603
+ const stated = /max_tokens:\s*\d+\s*>\s*\d+/.exec(overflowDetail)?.[0] ?? (parsed.message !== void 0 && parsed.message.length <= 200 ? parsed.message : void 0);
6604
+ return bilingual("PROVIDER_HTTP_ERROR", `Command Code API error ${status}: this request's max_tokens is not valid for this model` + (stated ? ` (${stated})` : "") + " — the session is not oversized, so compacting it will not help; lower \"Max output tokens\" or pick a model with a larger output ceiling", `Command Code API 返回 ${status}:本次请求的 max_tokens 对该模型不合法` + (stated ? `(${stated})` : "") + "——会话内容并未超长,压缩上下文无效;请调低「最大输出 token」或改用输出上限更大的模型", { status });
6605
+ }
6606
+ if (status < 500 && isContextOverflowDetail(overflowDetail)) return bilingual(CONTEXT_WINDOW_EXCEEDED_CODE, `Command Code API error ${status}: the request exceeds the model's context window — this session is being compacted and the request retried`, `Command Code API 返回 ${status}:请求内容超出模型上下文窗口——正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, { status });
6607
+ if (status === 400 && sizePressure && AMBIGUOUS_SIZE_REJECTION.test(providerDetail) && /\bprovider_error\b/i.test(providerDetail)) return bilingual(CONTEXT_WINDOW_EXCEEDED_CODE, "Command Code API error 400: the provider could not complete a request near the model context limit — compacting the session and retrying", "Command Code API 返回 400:请求接近模型上下文上限,服务商无法完成——正在压缩上下文后重试", { status });
6608
+ if (status === 422 && isZdrNoProviders(providerCode, providerDetail, errText)) return bilingual("PROVIDER_HTTP_ERROR", `Command Code API error 422 (${detail}): zero data retention is enforced but no ZDR-capable upstream is available for this model — turn the ZDR setting off (or use another model)`, "Command Code API 返回 422:已开启零数据保留(ZDR),但该模型当前没有可用的 ZDR 上游——请关闭 ZDR 开关,或改用其他模型", { status: 422 });
6609
+ if (status === 401) return bilingual("INVALID_CREDENTIAL", `Command Code API error 401 (${detail}): the API key is missing or invalid — check the key stored for COMMANDCODE_API_KEY (Models page) or the auth file`, "Command Code API 返回 401:API 密钥缺失或无效——请在设置页检查 COMMANDCODE_API_KEY 存储的密钥,或检查 auth 文件", { status: 401 });
6610
+ if (status === 413) return bilingual("PROVIDER_HTTP_ERROR", "Command Code API error 413: the request body exceeds the provider's size limit (the cap is undocumented, measured at about 50 MB) — the session history is too large to send, usually because of accumulated image attachments. Start a new session, or drop the image-heavy part of this one, and retry", "Command Code API 返回 413:请求体超过服务端的体积上限(官方未公开,实测约 50 MB)——会话历史过大,通常是历史图片累积所致。请新建会话,或移除本会话中图片较多的部分后重试", { status: 413 });
5004
6611
  return new LlmError(`Command Code API error ${status}${detail === `HTTP ${status}` ? "" : ` (${detail})`}: ${errText.slice(0, 500)}`, httpErrorCode(status), {
5005
6612
  status,
5006
6613
  ...retryAfterMs !== void 0 && retryAfterMs > 0 && retryAfterMs <= 9e5 ? { providerRetryAfterMs: retryAfterMs } : {}
@@ -5019,7 +6626,7 @@ function generateHttpError(status, errText, retryAfterMs) {
5019
6626
  * rejection repeats identically on every attempt.
5020
6627
  */
5021
6628
  function httpErrorCode(status) {
5022
- if (status === 429) return "RATE_LIMIT";
6629
+ if (status === 429) return THROTTLED_CODE;
5023
6630
  if (status === 408) return "TIMEOUT";
5024
6631
  if (status >= 500) return "SERVER";
5025
6632
  return "PROVIDER_HTTP_ERROR";
@@ -5239,8 +6846,195 @@ function handleOpenAIEvent(asm, event) {
5239
6846
  return chunks;
5240
6847
  }
5241
6848
  /** Dispatch one parsed stream event to the active transport's handler. */
6849
+ /**
6850
+ * Messages `usage` → the harness meter.
6851
+ *
6852
+ * The field names are the Anthropic ones, and unlike Chat Completions
6853
+ * `input_tokens` already excludes cache reads and writes, so it is mapped
6854
+ * as-is rather than reduced (the shape `dsh-llm-deepseek` implements).
6855
+ * Measured 2026-09-29: `{ input_tokens, cache_creation_input_tokens,
6856
+ * cache_read_input_tokens, output_tokens, output_tokens_details:
6857
+ * { thinking_tokens } }`, plus gateway additions the meter ignores.
6858
+ */
6859
+ function mapMessagesUsage(raw) {
6860
+ if (!isRecord(raw)) return void 0;
6861
+ const inputTokens = numberValue(raw.input_tokens) ?? 0;
6862
+ const outputTokens = numberValue(raw.output_tokens) ?? 0;
6863
+ const cacheRead = numberValue(raw.cache_read_input_tokens) ?? 0;
6864
+ const cacheWrite = numberValue(raw.cache_creation_input_tokens) ?? 0;
6865
+ const reasoningTokens = numberValue((isRecord(raw.output_tokens_details) ? raw.output_tokens_details : void 0)?.thinking_tokens);
6866
+ const usage = {
6867
+ inputTokens: Math.max(0, inputTokens),
6868
+ outputTokens,
6869
+ cacheReadTokens: cacheRead
6870
+ };
6871
+ if (cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
6872
+ if (reasoningTokens !== void 0) usage.reasoningTokens = reasoningTokens;
6873
+ return usage;
6874
+ }
6875
+ /** The harness block type one Messages content block maps to. */
6876
+ function messagesBlockType(native) {
6877
+ if (native.type === "text") return "text";
6878
+ if (native.type === "thinking") return "reasoning";
6879
+ if (native.type === "tool_use") return "tool-call";
6880
+ }
6881
+ /**
6882
+ * Handle one Messages-transport stream event, appending harness StreamChunks.
6883
+ *
6884
+ * The event sequence is the Anthropic one measured on 2026-09-29:
6885
+ * `message_start`, then per content block `content_block_start` /
6886
+ * `content_block_delta`* / `content_block_stop`, then `message_delta`
6887
+ * (carrying `stop_reason` and the final usage) and `message_stop`. A `ping`
6888
+ * keepalive interleaves freely and carries no content. The terminal chunks are
6889
+ * NOT emitted here: `stream()` holds the success finish until it has validated
6890
+ * the assembled answer, and it attaches the replay envelope.
6891
+ */
6892
+ function handleMessagesEvent(asm, event) {
6893
+ const chunks = [];
6894
+ if (!isRecord(event)) return chunks;
6895
+ switch (event.type) {
6896
+ case "message_start": {
6897
+ const usage = mapMessagesUsage((isRecord(event.message) ? event.message : void 0)?.usage);
6898
+ if (usage) chunks.push({
6899
+ type: "usage",
6900
+ usage
6901
+ });
6902
+ break;
6903
+ }
6904
+ case "content_block_start": {
6905
+ const wireIndex = numberValue(event.index);
6906
+ const native = isRecord(event.content_block) ? event.content_block : void 0;
6907
+ const type = native ? messagesBlockType(native) : void 0;
6908
+ if (wireIndex === void 0 || type === void 0) break;
6909
+ const chunkIndex = asm.nextIndex++;
6910
+ const state = {
6911
+ type,
6912
+ text: stringValue(native?.text) ?? "",
6913
+ signature: stringValue(native?.signature) ?? "",
6914
+ json: "",
6915
+ id: stringValue(native?.id) ?? "",
6916
+ name: stringValue(native?.name) ?? "",
6917
+ chunkIndex
6918
+ };
6919
+ asm.messagesBlocks.set(wireIndex, state);
6920
+ asm.replayBlocks[chunkIndex] = { type };
6921
+ chunks.push({
6922
+ type: "block-start",
6923
+ index: chunkIndex,
6924
+ blockType: type
6925
+ });
6926
+ if (type === "tool-call" && state.id !== "") chunks.push({
6927
+ type: "tool-call-delta",
6928
+ index: chunkIndex,
6929
+ id: ToolCallId(state.id),
6930
+ name: state.name,
6931
+ argumentsDelta: ""
6932
+ });
6933
+ break;
6934
+ }
6935
+ case "content_block_delta": {
6936
+ const wireIndex = numberValue(event.index);
6937
+ const state = wireIndex === void 0 ? void 0 : asm.messagesBlocks.get(wireIndex);
6938
+ const delta = isRecord(event.delta) ? event.delta : void 0;
6939
+ if (!state || !delta) break;
6940
+ const index = state.chunkIndex;
6941
+ if (delta.type === "text_delta" && state.type === "text") {
6942
+ const text = stringValue(delta.text) ?? "";
6943
+ state.text += text;
6944
+ if (text.trim() !== "") asm.sawContent = true;
6945
+ chunks.push({
6946
+ type: "text-delta",
6947
+ index,
6948
+ text
6949
+ });
6950
+ } else if (delta.type === "thinking_delta" && state.type === "reasoning") {
6951
+ const text = stringValue(delta.thinking) ?? "";
6952
+ state.text += text;
6953
+ chunks.push({
6954
+ type: "reasoning-delta",
6955
+ index,
6956
+ text
6957
+ });
6958
+ } else if (delta.type === "signature_delta" && state.type === "reasoning") {
6959
+ state.signature += stringValue(delta.signature) ?? "";
6960
+ const entry = asm.replayBlocks[index];
6961
+ if (entry) entry.signature = state.signature;
6962
+ } else if (delta.type === "input_json_delta" && state.type === "tool-call") {
6963
+ const fragment = stringValue(delta.partial_json) ?? "";
6964
+ state.json += fragment;
6965
+ chunks.push({
6966
+ type: "tool-call-delta",
6967
+ index,
6968
+ id: ToolCallId(state.id),
6969
+ name: state.name,
6970
+ argumentsDelta: fragment
6971
+ });
6972
+ }
6973
+ break;
6974
+ }
6975
+ case "content_block_stop": {
6976
+ const wireIndex = numberValue(event.index);
6977
+ if (wireIndex === void 0) break;
6978
+ const state = asm.messagesBlocks.get(wireIndex);
6979
+ if (!state) break;
6980
+ asm.messagesBlocks.delete(wireIndex);
6981
+ const index = state.chunkIndex;
6982
+ if (state.type === "text") {
6983
+ if (state.text.trim() !== "") asm.sawContent = true;
6984
+ chunks.push({
6985
+ type: "block-end",
6986
+ index,
6987
+ block: {
6988
+ type: "text",
6989
+ text: state.text
6990
+ }
6991
+ });
6992
+ } else if (state.type === "reasoning") chunks.push({
6993
+ type: "block-end",
6994
+ index,
6995
+ block: {
6996
+ type: "reasoning",
6997
+ text: state.text
6998
+ }
6999
+ });
7000
+ else {
7001
+ if (state.id !== "") asm.sawContent = true;
7002
+ chunks.push({
7003
+ type: "block-end",
7004
+ index,
7005
+ block: {
7006
+ type: "tool-call",
7007
+ id: ToolCallId(state.id),
7008
+ name: state.name,
7009
+ arguments: state.json || "{}"
7010
+ }
7011
+ });
7012
+ }
7013
+ break;
7014
+ }
7015
+ case "message_delta": {
7016
+ const reason = stringValue((isRecord(event.delta) ? event.delta : void 0)?.stop_reason);
7017
+ if (reason !== void 0) asm.finishReason = reason;
7018
+ const usage = mapMessagesUsage(event.usage);
7019
+ if (usage) chunks.push({
7020
+ type: "usage",
7021
+ usage
7022
+ });
7023
+ chunks.push({
7024
+ type: "finish",
7025
+ reason: mapFinishReason(reason)
7026
+ });
7027
+ break;
7028
+ }
7029
+ case "message_stop":
7030
+ case "ping": break;
7031
+ case "error": throw streamErrorToLlmError(event.error ?? event, stringValue(event.message));
7032
+ }
7033
+ return chunks;
7034
+ }
5242
7035
  function handleEvent(asm, protocol, event) {
5243
- return protocol === "openai" ? handleOpenAIEvent(asm, event) : handleCliEvent(asm, event);
7036
+ if (protocol === "cli") return handleCliEvent(asm, event);
7037
+ return protocol === "messages" ? handleMessagesEvent(asm, event) : handleOpenAIEvent(asm, event);
5244
7038
  }
5245
7039
  /**
5246
7040
  * Parse an HTTP `Retry-After` value (delay-seconds or an HTTP-date) into
@@ -5269,16 +7063,16 @@ function parseRetryAfterMs(value, now = Date.now()) {
5269
7063
  * one function serves both — a new reason cannot drift between them.
5270
7064
  */
5271
7065
  function mapFinishReason(reason) {
5272
- if (reason === "tool-calls" || reason === "tool_calls") return { kind: "tool-calls" };
7066
+ if (reason === "tool-calls" || reason === "tool_calls" || reason === "tool_use") return { kind: "tool-calls" };
5273
7067
  if (reason === "length" || reason === "max_tokens" || reason === "max-tokens" || reason === "max_output_tokens") return { kind: "max-tokens" };
5274
7068
  return { kind: "stop" };
5275
7069
  }
5276
7070
  /** A terminal marker without text or tools is a failed attempt, not a completed turn. */
5277
7071
  function emptyCompletionError(reason, maxTokens, usage) {
5278
7072
  const detail = `finish_reason=${reason ?? "unknown"}, max_tokens=${maxTokens}, outputTokens=${usage?.outputTokens ?? "unknown"}, reasoningTokens=${usage?.reasoningTokens ?? "unknown"}`;
5279
- if (mapFinishReason(reason).kind === "max-tokens") return new LlmError(`Command Code reached the output token limit without producing answer text or a tool call (${detail});Command Code 已达到输出 token 上限,但没有生成正文或工具调用;请调整思考强度,或在模型和接口上限内增加输出预算`, "OUTPUT_TOKEN_LIMIT");
5280
- if (reason === "content_filter" || reason === "content-filter") return new LlmError(`Command Code filtered the response (${detail});Command Code 拦截了响应,未生成正文或工具调用`, "CONTENT_FILTER");
5281
- return new LlmError(`Command Code finished without producing answer text or a tool call (${detail});Command Code 返回了空响应(可能只有思考内容),重试通常可恢复`, "EMPTY_RESPONSE");
7073
+ if (mapFinishReason(reason).kind === "max-tokens") return bilingual("OUTPUT_TOKEN_LIMIT", `Command Code reached the output token limit without producing answer text or a tool call (${detail})`, "Command Code 已达到输出 token 上限,但没有生成正文或工具调用;请调整思考强度,或在模型和接口上限内增加输出预算");
7074
+ if (reason === "content_filter" || reason === "content-filter") return bilingual("CONTENT_FILTER", `Command Code filtered the response (${detail})`, "Command Code 拦截了响应,未生成正文或工具调用");
7075
+ return bilingual("EMPTY_RESPONSE", `Command Code finished without producing answer text or a tool call (${detail})`, "Command Code 返回了空响应(可能只有思考内容),重试通常可恢复");
5282
7076
  }
5283
7077
  /**
5284
7078
  * Classify a pre-stream rejection that is about the ACCOUNT rather than about
@@ -5322,9 +7116,10 @@ const ACCOUNT_UNAVAILABLE_CODES = /* @__PURE__ */ new Set([
5322
7116
  "PREMIUM_CREDITS_EXHAUSTED",
5323
7117
  "MODEL_NOT_IN_PLAN"
5324
7118
  ]);
5325
- function classifyAccountRejection(status, errText) {
5326
- if (status === 429 || readProviderErrorCode(errText)?.toUpperCase() === "RATE_LIMITED") {
5327
- const evidence = readWindowLimitEvidence(errText);
7119
+ function classifyAccountRejection(status, errText, parsed = parseProviderError(errText)) {
7120
+ const code = (parsed.code ?? parsed.type)?.toUpperCase();
7121
+ if (status === 429 || code === "RATE_LIMITED") {
7122
+ const evidence = readWindowLimitEvidence(errText, parsed);
5328
7123
  if (evidence.window === void 0) return { reason: "throttled" };
5329
7124
  return evidence.resetAtMs === void 0 ? { reason: "rate-limit" } : {
5330
7125
  reason: "rate-limit",
@@ -5332,23 +7127,11 @@ function classifyAccountRejection(status, errText) {
5332
7127
  };
5333
7128
  }
5334
7129
  if (status === 401) return { reason: "invalid-credential" };
5335
- const code = readProviderErrorCode(errText)?.toUpperCase();
5336
7130
  if (code !== void 0 && ACCOUNT_UNAVAILABLE_CODES.has(code)) return { reason: "unavailable" };
5337
7131
  if (status < 400 || status >= 500) return void 0;
5338
7132
  const lower = errText.toLowerCase().replaceAll("_", " ");
5339
7133
  if (lower.includes("insufficient credits") || lower.includes("premium credits exhausted") || lower.includes("model not in plan") || /insufficient (credit|balance)/.test(lower) || /out of credit/.test(lower) || /not (available|included)[^.]{0,40}plan/.test(lower) || lower.includes("upgrade your plan")) return { reason: "unavailable" };
5340
7134
  }
5341
- /** The machine-readable `error.code`/`error.type` of a pre-stream failure body. */
5342
- function readProviderErrorCode(errText) {
5343
- try {
5344
- const parsed = JSON.parse(errText);
5345
- if (!isRecord(parsed)) return void 0;
5346
- const error = isRecord(parsed.error) ? parsed.error : parsed;
5347
- return stringValue(error.code) ?? stringValue(error.type);
5348
- } catch {
5349
- return;
5350
- }
5351
- }
5352
7135
  /**
5353
7136
  * How far ahead a provider-published reset may be and still be trusted.
5354
7137
  *
@@ -5376,20 +7159,10 @@ const MAX_TRUSTED_RESET_MS = 2592e6;
5376
7159
  * message is honored as the fallback. Either can be present without the other;
5377
7160
  * the window label is what decides the classification.
5378
7161
  */
5379
- function readWindowLimitEvidence(errText) {
5380
- let named;
5381
- let message = "";
5382
- let seconds;
5383
- try {
5384
- const parsed = JSON.parse(errText);
5385
- if (isRecord(parsed)) {
5386
- const error = isRecord(parsed.error) ? parsed.error : parsed;
5387
- const rateLimit = isRecord(error.rateLimit) ? error.rateLimit : void 0;
5388
- named = stringValue(rateLimit?.window);
5389
- message = stringValue(error.message) ?? stringValue(parsed.message) ?? "";
5390
- seconds = numberValue(rateLimit?.reset);
5391
- }
5392
- } catch {}
7162
+ function readWindowLimitEvidence(errText, parsed) {
7163
+ const named = stringValue(parsed.rateLimit?.window);
7164
+ const message = parsed.message ?? stringValue(parsed.root?.message) ?? "";
7165
+ const seconds = numberValue(parsed.rateLimit?.reset);
5393
7166
  const window = named === "fiveHour" || named === "weekly" || named === "daily" ? named : /usage limit for your plan/i.test(message) || /usage limit for your plan/i.test(errText) ? /weekly/i.test(message) ? "weekly" : "fiveHour" : void 0;
5394
7167
  const stamp = /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(message)?.[1] ?? /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(errText)?.[1];
5395
7168
  const stamped = stamp === void 0 ? void 0 : Date.parse(stamp);
@@ -5407,24 +7180,15 @@ function readWindowLimitEvidence(errText) {
5407
7180
  * other 4xx/5xx must surface as ordinary errors so real account/model
5408
7181
  * problems are not masked.
5409
7182
  */
5410
- function isUpgradeRequiredError(status, errText) {
7183
+ function isUpgradeRequiredError(status, errText, parsed = parseProviderError(errText)) {
5411
7184
  if (status !== 403) return false;
5412
7185
  const lower = errText.toLowerCase();
5413
7186
  if (lower.includes("upgrade_required")) return true;
5414
7187
  if (lower.includes("go plan") && lower.includes("api access")) return true;
5415
7188
  if (lower.includes("only plan without api access")) return true;
5416
7189
  if (lower.includes("upgrade to goat or higher")) return true;
5417
- try {
5418
- const parsed = JSON.parse(errText);
5419
- if (isRecord(parsed)) {
5420
- const error = isRecord(parsed.error) ? parsed.error : parsed;
5421
- if ((stringValue(error.code) ?? stringValue(error.type))?.toLowerCase() === "upgrade_required") return true;
5422
- const message = stringValue(error.message) ?? "";
5423
- if (message.toLowerCase().includes("go plan") && message.toLowerCase().includes("api access")) return true;
5424
- }
5425
- } catch {
5426
- return false;
5427
- }
7190
+ if ((parsed.code ?? parsed.type)?.toLowerCase() === "upgrade_required") return true;
7191
+ if (parsed.message?.toLowerCase().includes("go plan") && parsed.message.toLowerCase().includes("api access")) return true;
5428
7192
  return false;
5429
7193
  }
5430
7194
  /** Map an OpenAI Chat Completions usage payload to harness disjoint counts. */
@@ -5532,33 +7296,42 @@ function commandCopy(locale, key) {
5532
7296
  return commandcodeCommand[locale][key] ?? commandcodeCommand.en[key] ?? key;
5533
7297
  }
5534
7298
  //#endregion
5535
- //#region src/commands.ts
5536
- function money(value) {
5537
- return `$${value.toFixed(4)}`;
5538
- }
5539
- /** Compact dollar amount (2 decimals), for the summary and window rows. */
5540
- function moneyShort(value) {
7299
+ //#region src/display-format.ts
7300
+ /** Shared, dependency-free formatting for Host commands and browser surfaces. */
7301
+ function formatMoney(value) {
5541
7302
  return `$${value.toFixed(2)}`;
5542
7303
  }
5543
- /** Format a large token count compactly (1.9M style). */
5544
- function tokensCompact(value) {
7304
+ function formatMoneyExact(value) {
7305
+ return `$${value.toFixed(4)}`;
7306
+ }
7307
+ function formatTokensCompact(value) {
5545
7308
  if (value >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
5546
7309
  if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
5547
7310
  if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
5548
7311
  return String(value);
5549
7312
  }
5550
- /**
5551
- * Success-rate percentage (already in percent units): at most two decimals,
5552
- * trailing zeros trimmed — mirrors `formatSuccessRate` in
5553
- * `./client/usage.ts`, which the settings card uses.
5554
- */
5555
- function successRateText(value) {
7313
+ /** The input is already in percent units; callers add the percent sign. */
7314
+ function formatSuccessRate(value) {
5556
7315
  return String(Number(value.toFixed(2)));
5557
7316
  }
7317
+ /** One window's fill ratio in [0, 1]; zero when uncapped. */
7318
+ function windowRatio(used, cap) {
7319
+ if (cap <= 0) return 0;
7320
+ return Math.max(0, Math.min(1, used / cap));
7321
+ }
7322
+ /** Local date-time, with the empty state chosen by the caller. */
7323
+ function formatResetAt(ms) {
7324
+ return ms <= 0 ? "" : new Date(ms).toLocaleString();
7325
+ }
7326
+ //#endregion
7327
+ //#region src/commands.ts
7328
+ const money = formatMoneyExact;
7329
+ const moneyShort = formatMoney;
7330
+ const tokensCompact = formatTokensCompact;
7331
+ const successRateText = formatSuccessRate;
5558
7332
  /** A local date, or `n/a` when the provider published no reset time. */
5559
7333
  function resetLabel(ms) {
5560
- if (ms <= 0) return "n/a";
5561
- return new Date(ms).toLocaleString();
7334
+ return formatResetAt(ms) || "n/a";
5562
7335
  }
5563
7336
  /**
5564
7337
  * A 10-cell horizontal bar: `██████████` for 100%, `███░░░░░░░` for ~33%.
@@ -5566,7 +7339,7 @@ function resetLabel(ms) {
5566
7339
  */
5567
7340
  function bar(used, cap) {
5568
7341
  if (cap <= 0) return "—";
5569
- const ratio = Math.max(0, Math.min(1, used / cap));
7342
+ const ratio = windowRatio(used, cap);
5570
7343
  const filled = Math.round(ratio * 10);
5571
7344
  return "█".repeat(filled) + "░".repeat(10 - filled);
5572
7345
  }
@@ -5849,7 +7622,7 @@ const MODELS_ENDPOINT = "commandcode/models";
5849
7622
  * separate instance so catalog boundary errors name `commandcode/models`,
5850
7623
  * not the report endpoint.
5851
7624
  */
5852
- const { record: catalogRecord, stringField: catalogString } = makeBoundaryValidator("commandcode/models result:");
7625
+ const { record: catalogRecord, stringField: catalogString, numberField: catalogNumber } = makeBoundaryValidator("commandcode/models result:");
5853
7626
  /** Parse one untrusted boundary value into a {@link CommandCodeCatalogModel}. */
5854
7627
  function parseCatalogModel(value) {
5855
7628
  const source = catalogRecord(value, "model");
@@ -5858,6 +7631,7 @@ function parseCatalogModel(value) {
5858
7631
  name: catalogString(source, "name", "model.name")
5859
7632
  };
5860
7633
  if (source.tier !== void 0) model.tier = catalogString(source, "tier", "model.tier");
7634
+ if (source.allowance !== void 0) model.allowance = catalogNumber(source, "allowance", "model.allowance");
5861
7635
  return model;
5862
7636
  }
5863
7637
  /** Parse the wire result into a {@link CommandCodeCatalog}. */
@@ -5999,12 +7773,17 @@ function loginDescriptor(endpoint, method) {
5999
7773
  }]
6000
7774
  } : descriptor;
6001
7775
  }
6002
- /** The three login descriptors, shared verbatim by Host registration and Client mount. */
6003
- const LOGIN_DESCRIPTORS = [
6004
- loginDescriptor(LOGIN_BEGIN_ENDPOINT, "loginBegin"),
6005
- loginDescriptor(LOGIN_STATUS_ENDPOINT, "loginStatus"),
6006
- loginDescriptor(LOGIN_CANCEL_ENDPOINT, "loginCancel")
6007
- ];
7776
+ /** The Host-face contribution fragment registered on `ctx.typert`. */
7777
+ const LOGIN_HOST_CONTRIBUTION = {
7778
+ package: REMOTE_PACKAGE,
7779
+ face: "host",
7780
+ schemas: [],
7781
+ invocations: [
7782
+ loginDescriptor(LOGIN_BEGIN_ENDPOINT, "loginBegin"),
7783
+ loginDescriptor(LOGIN_STATUS_ENDPOINT, "loginStatus"),
7784
+ loginDescriptor(LOGIN_CANCEL_ENDPOINT, "loginCancel")
7785
+ ]
7786
+ };
6008
7787
  //#endregion
6009
7788
  //#region src/usage-remote.ts
6010
7789
  /**
@@ -6093,7 +7872,7 @@ function applyUsageRemote(ctx, deps) {
6093
7872
  ...USAGE_HOST_CONTRIBUTION.invocations,
6094
7873
  MODELS_DESCRIPTOR,
6095
7874
  PRICES_DESCRIPTOR,
6096
- ...LOGIN_DESCRIPTORS
7875
+ ...LOGIN_HOST_CONTRIBUTION.invocations
6097
7876
  ]
6098
7877
  });
6099
7878
  remoteCtx.effect(() => () => void unregister(), "dsh-commandcode-provider: usage remote");
@@ -6810,22 +8589,8 @@ var CommandCodeSearchProvider = class {
6810
8589
  const ACTIVE_ACCOUNT_AUTO = "auto";
6811
8590
  /** The selector value meaning "no language override — follow the shell locale". */
6812
8591
  const LANG_AUTO = "auto";
6813
- /** Tier display names, mirroring the web dropdown's headings (`model-select.ts`). */
6814
- const TIER_TITLES = {
6815
- go: "Go",
6816
- goat: "GOAT",
6817
- pro: "Pro",
6818
- provider: "Provider",
6819
- max: "Max"
6820
- };
6821
8592
  /** Tiers in picker order; a model outside this set joins the "Other" group. */
6822
- const TIER_ORDER = [
6823
- "go",
6824
- "goat",
6825
- "pro",
6826
- "provider",
6827
- "max"
6828
- ];
8593
+ const TIER_ORDER = PLAN_TIER_ORDER;
6829
8594
  /** The group holding models this build's catalog does not know. */
6830
8595
  const OTHER_GROUP_ID = "models-other";
6831
8596
  /**
@@ -6883,8 +8648,8 @@ function buildCommandCodeTuiSection(deps) {
6883
8648
  const extras = [.../* @__PURE__ */ new Set([...stored, ...Object.keys(overrides)])].filter((id) => !known.has(id));
6884
8649
  const tierGroups = TIER_ORDER.filter((tier) => choices.some((choice) => choice.tier === tier)).map((tier) => ({
6885
8650
  id: `models-${tier}`,
6886
- title: `${TIER_TITLES[tier] ?? tier} models`,
6887
- descriptions: { zh: `${TIER_TITLES[tier] ?? tier} 模型` }
8651
+ title: `${PLAN_LABELS[tier] ?? tier} models`,
8652
+ descriptions: { zh: `${PLAN_LABELS[tier] ?? tier} 模型` }
6888
8653
  }));
6889
8654
  const unranked = choices.filter((choice) => tierRank(choice.tier) === TIER_ORDER.length);
6890
8655
  const otherGroup = unranked.length > 0 || extras.length > 0 ? [{
@@ -6989,6 +8754,20 @@ function buildCommandCodeTuiSection(deps) {
6989
8754
  value: text.trim() === "true"
6990
8755
  })
6991
8756
  },
8757
+ {
8758
+ path: ["offloadSeenImagesForCache"],
8759
+ group: "advanced",
8760
+ kind: "boolean",
8761
+ label: "Stop replaying images already seen",
8762
+ descriptions: { zh: "已看过的图片不再重复发送" },
8763
+ hint: "CLI transport only. After this model has answered, durably replace older images with text to keep later prompt text cacheable. Old pixels need a fresh file read or attachment to inspect again. Off by default.",
8764
+ hintDescriptions: { zh: "仅 CLI 传输。模型看过图片并回复后,永久用文字替代旧图,以保留后续文本缓存;重看旧图需要重新读取文件或附加。默认关闭。" },
8765
+ format: (value) => value === true ? "true" : "false",
8766
+ parse: (text) => ({
8767
+ kind: "set",
8768
+ value: text.trim() === "true"
8769
+ })
8770
+ },
6992
8771
  {
6993
8772
  path: ["filterModelsByPlan"],
6994
8773
  group: "models",
@@ -7149,27 +8928,18 @@ function applyCommandCodeTuiSettings(ctx, deps) {
7149
8928
  * A bounded retry budget for `TRANSPORT` failures (issue #39, second report).
7150
8929
  *
7151
8930
  * The route policy (`providerRetryPolicy` in `./adapter.ts`) is near-unbounded
7152
- * on purpose — 1000 attempts doubling to a 15-minute cap — because it exists
7153
- * for failures the provider ASKS to have retried, where the wait is the point.
7154
- * A connection that cannot be established is not that: either a blip the first
7155
- * few attempts absorb, or an outage no in-request waiting fixes (and its waits
7156
- * count BEFORE each attempt, so attempt 11 alone already waits 512 s). So
7157
- * transport failures get their own budget; waiting out a real outage is the
7158
- * user's call, and the next send starts a fresh budget.
7159
- *
7160
- * Not counted here: the pre-stream recovery the ADAPTER performs inside one
7161
- * `stream()` call (account rotation, the 413 image-budget step-down, the
7162
- * Provider-API → CLI switch) — invisible to the caller and bounded on its own.
7163
- * This budget caps only the harness's retry loop.
7164
- *
7165
- * Reset rule: the budget clears at each new STEP (one step = one model request).
7166
- * `agent/status` → `idle` is NOT it, though it looks natural: the loop's
7167
- * `setPhase` emits it only on a status CHANGE, and a turn's steps all run inside
7168
- * one `running` phase, so a reset there would fail every later step of an outage
7169
- * turn at once instead of retrying each. `assistant/attempt` is wrong the other
7170
- * way — the loop appends it for the FAILED attempt itself, before dispatching
7171
- * `agent/request-error`, so resetting on it would clear the budget on every
7172
- * failure and restore the very loop this module exists to stop.
8931
+ * on purpose — 1000 attempts doubling to a 15-minute cap — for failures the
8932
+ * provider ASKS to have retried. A connection that cannot be established is
8933
+ * not that: a blip the first few attempts absorb, or an outage no in-request
8934
+ * waiting fixes (attempt 11 alone already waits 512 s). So transport failures
8935
+ * get their own budget, cleared at each new STEP (one step = one model
8936
+ * request). `agent/status` → `idle` is NOT the reset (one `running` phase spans
8937
+ * a turn's steps), and neither is `assistant/attempt` (appended for the FAILED
8938
+ * attempt itself, which would clear the budget on every failure).
8939
+ *
8940
+ * Not counted here: the adapter's in-`stream()` pre-stream recovery (account
8941
+ * rotation, the 413 step-down, the Provider-API → CLI switch) — invisible to
8942
+ * the caller and bounded on its own.
7173
8943
  *
7174
8944
  * @module
7175
8945
  */
@@ -7251,7 +9021,7 @@ function transportBudgetMessage(failureMessage, maxRetries) {
7251
9021
  * Mark one schema field volatile.
7252
9022
  *
7253
9023
  * `.volatile()` is unconditional: it is a schemastery 3.18.3 feature and the
7254
- * only supported engine (`dsh 0.1.7-rc.2`) pins `~3.18.4`, so the method is
9024
+ * only supported engine (`dsh 0.2.0-rc.2`) pins `~3.18.4`, so the method is
7255
9025
  * always present. It is reached through a structural member so the caller's
7256
9026
  * declared field type survives — the mark changes what the schema PARSES to (a
7257
9027
  * live reference), not the plugin-facing `Config` shape.
@@ -7348,6 +9118,7 @@ const Config = z.object(markVolatileFields({
7348
9118
  modelsCachePath: z.string(),
7349
9119
  requestTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
7350
9120
  streamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
9121
+ offloadSeenImagesForCache: z.boolean().default(false),
7351
9122
  transportMaxRetries: z.number().min(0).max(50),
7352
9123
  filterModelsByPlan: z.boolean(),
7353
9124
  visibleModels: z.array(z.string()),
@@ -7380,8 +9151,9 @@ function resolveAdapterOptions(config) {
7380
9151
  apiBase: config.apiBase ?? "https://api.commandcode.ai",
7381
9152
  workingDir: config.workingDir ?? process.cwd(),
7382
9153
  modelsCachePath: config.modelsCachePath ?? DEFAULT_MODELS_CACHE_PATH,
7383
- requestTimeoutMs: config.requestTimeoutMs ?? 6e4,
9154
+ requestTimeoutMs: config.requestTimeoutMs ?? 3e5,
7384
9155
  streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
9156
+ offloadSeenImagesForCache: config.offloadSeenImagesForCache === true,
7385
9157
  filterModelsByPlan: config.filterModelsByPlan ?? true,
7386
9158
  visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : void 0,
7387
9159
  modelVisibility: readModelVisibility(config.modelVisibility),
@@ -7483,10 +9255,13 @@ function apply(ctx, config) {
7483
9255
  return Math.min(Math.trunc(value), 50);
7484
9256
  };
7485
9257
  const sessionAgents = /* @__PURE__ */ new WeakMap();
7486
- ctx.on("agent/request-error", async ({ agent, failure, signal }, next) => {
7487
- if (signal.aborted) return next();
9258
+ ctx.on("agent/request-error", async ({ agent, provider, failure, signal }, next) => {
9259
+ if (signal.aborted || provider !== "commandcode") return next();
7488
9260
  const session = Reflect.get(agent, "session");
7489
9261
  if (typeof session === "object" && session !== null) sessionAgents.set(session, agent);
9262
+ const transient = absorbTransientFailure(agent, failure.code);
9263
+ if (transient === "exhausted") throw new Error(transientBudgetMessage(failure.code, failure.message));
9264
+ if (transient === "retry") return next();
7490
9265
  const decision = absorbTransportFailure(agent, failure.code, transportMaxRetries());
7491
9266
  if (decision === "ignored") return next();
7492
9267
  if (decision === "retry") return { kind: "retry" };
@@ -7502,10 +9277,16 @@ function apply(ctx, config) {
7502
9277
  }
7503
9278
  if (action !== "reset") return;
7504
9279
  const agent = sessionAgents.get(session);
7505
- if (agent !== void 0) resetTransportFailures(agent);
9280
+ if (agent !== void 0) {
9281
+ resetTransportFailures(agent);
9282
+ resetTransientFailures(agent);
9283
+ }
7506
9284
  });
7507
9285
  ctx.on("agent/status", ({ agent, status }) => {
7508
- if (status === "idle") resetTransportFailures(agent);
9286
+ if (status === "idle") {
9287
+ resetTransportFailures(agent);
9288
+ resetTransientFailures(agent);
9289
+ }
7509
9290
  });
7510
9291
  const usageReports = async () => {
7511
9292
  const described = await pool.describeAccounts();
@@ -7555,12 +9336,16 @@ function apply(ctx, config) {
7555
9336
  });
7556
9337
  ctx.effect(() => () => loginFlow.dispose(), "dsh-commandcode-provider: login flow");
7557
9338
  const catalogForEditors = async () => {
7558
- return { models: (await adapter.listModels(PROVIDER, { unfiltered: true })).map((model) => {
9339
+ const models = await adapter.listModels(PROVIDER, { unfiltered: true });
9340
+ const bracket = await adapter.allowanceTier();
9341
+ return { models: models.map((model) => {
7559
9342
  const tier = KNOWN_PLANS[model.id];
9343
+ const allowance = bracket === void 0 ? void 0 : modelAllowanceFor(model.id)?.[bracket];
7560
9344
  return {
7561
9345
  id: model.id,
7562
9346
  name: model.name.replace(/\s*\(CC\)$/, ""),
7563
- ...tier === void 0 ? {} : { tier }
9347
+ ...tier === void 0 ? {} : { tier },
9348
+ ...allowance === void 0 ? {} : { allowance }
7564
9349
  };
7565
9350
  }) };
7566
9351
  };