@mars-sea/dsh-commandcode-provider 0.12.0 → 0.12.2

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,7 +6,7 @@ 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";
@@ -103,7 +103,7 @@ const PLAN_ORDER = Object.fromEntries(PLAN_TIER_ORDER.map((tier, index) => [tier
103
103
  * subscription-plan labels, deals, and hourly (peak/off-peak) pricing.
104
104
  *
105
105
  * Everything here is synced from official sources — the command-code CLI
106
- * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.68.0) and
106
+ * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.73.0) and
107
107
  * the official plan/pricing/model docs; see the dsh-commandcode-upstream skill
108
108
  * for the extraction procedures. Keeping the snapshot in its own module
109
109
  * confines those frequent sync diffs here: src/adapter.ts holds only the stable
@@ -408,6 +408,18 @@ const KNOWN_EFFORTS = {
408
408
  "low",
409
409
  "medium",
410
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"
411
423
  ]
412
424
  };
413
425
  /**
@@ -464,6 +476,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
464
476
  "gpt-6-astra",
465
477
  "gpt-6-luna",
466
478
  "gpt-6-sol",
479
+ "gpt-6.1-sol",
467
480
  "meta/muse-spark-1.1",
468
481
  "meta/muse-spark-1.2",
469
482
  "meta/muse-spark-1.2-contributor",
@@ -493,7 +506,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
493
506
  ]);
494
507
  /**
495
508
  * Models WITHOUT a zero-data-retention upstream, per the official CLI's own
496
- * registry (`command-code@1.68.0` `dist/cli.mjs`): `modelSupportsZdr(id)` is
509
+ * registry (`command-code@1.73.0` `dist/cli.mjs`): `modelSupportsZdr(id)` is
497
510
  * exactly `!nonZdrSet.has(canonicalize(id))`, and `knownModelSupportsZdr`
498
511
  * carries the same membership in the sibling route table — the UNION of both
499
512
  * is this set. Reading only the sibling route table would drop `meituan/
@@ -594,16 +607,22 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
594
607
  /**
595
608
  * Catalog models the Provider API serves ONLY through `/provider/v1/messages`
596
609
  * (Anthropic Messages shape). Every other model in the catalog answers on
597
- * `/provider/v1/chat/completions`; these reject it outright with HTTP 400
598
- * `Model "<id>" must be called via /provider/v1/messages (Anthropic Messages
599
- * shape)`.
610
+ * `/provider/v1/chat/completions`.
611
+ *
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`.
600
617
  *
601
- * Measured 2026-09-16 against the live catalog (command-code@1.54.0): all 69
602
- * models were posted to `/provider/v1/chat/completions`; exactly these eight
603
- * — the whole Claude family then — refused, and every one of them is routed
604
- * normally by `/alpha/generate` (a lower-plan key gets the ordinary
605
- * `MODEL_NOT_IN_PLAN` 403 there, never a routing error). So the CLI transport
606
- * is a complete fallback and the adapter does not need a Messages transport.
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()`.
607
626
  *
608
627
  * Note what this list is NOT: it is not a plan gate. `claude-sonnet-5` is
609
628
  * Pro-tier and `claude-opus-4-8` Provider-tier, so the accounts entitled to
@@ -612,20 +631,21 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
612
631
  * request before this snapshot existed.
613
632
  *
614
633
  * `requiresMessagesEndpoint()` additionally treats any `claude-*` id as
615
- * Messages-only, so a Claude model added upstream (`claude-opus-5-5` in
616
- * command-code@1.64.0 was the first this snapshot missed) is still routed
617
- * correctly by an un-updated plugin instead of hard-failing with the 400
618
- * above. The worst case of that rule going stale the other way (upstream
619
- * teaching `/provider/v1/chat/completions` to serve Claude) is one model
620
- * 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.
621
639
  *
622
640
  * Keep in sync when models ship (see the dsh-commandcode-upstream skill).
623
641
  */
624
642
  const MESSAGES_ONLY_MODELS = /* @__PURE__ */ new Set([
643
+ "claude-sonnet-5-5",
625
644
  "claude-sonnet-5",
626
645
  "claude-sonnet-4-6",
627
646
  "claude-fable-5-1",
628
647
  "claude-fable-5",
648
+ "claude-opus-5-5",
629
649
  "claude-opus-5",
630
650
  "claude-opus-4-8",
631
651
  "claude-opus-4-7",
@@ -636,12 +656,139 @@ function requiresMessagesEndpoint(modelId) {
636
656
  return MESSAGES_ONLY_MODELS.has(modelId) || modelId.startsWith("claude-");
637
657
  }
638
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
+ /**
639
786
  * The minimum subscription plan a model is included in, per the official plan
640
787
  * pages (`/docs/plans/go`, `/docs/plans/goat`, `/docs/plans/pro`,
641
788
  * `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
642
789
  * list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
643
790
  * Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
644
- * Provider-tier. Re-verified at command-code@1.68.0 (2026-09-29): 84 catalog
791
+ * Provider-tier. Re-verified at command-code@1.73.0 (2026-10-01): 86 catalog
645
792
  * ids at 53/62/76/84 cumulative, a strict superset chain — every release since
646
793
  * 1.49.0 has been additive with no tier move, and per-entry tags below name the
647
794
  * release that added each row.
@@ -678,6 +825,7 @@ const KNOWN_PLANS = {
678
825
  "gpt-6-luna": "go",
679
826
  "meituan/LongCat-2.0": "go",
680
827
  "inclusionai/ling-3.0-flash-sante:free": "go",
828
+ "inclusionai/ling-3.1-flash:free": "go",
681
829
  "meta/muse-spark-1.2-contributor": "go",
682
830
  "meta/muse-spark-1.3-contributor": "go",
683
831
  "moonshotai/Kimi-K2.5": "go",
@@ -738,7 +886,8 @@ const KNOWN_PLANS = {
738
886
  "claude-opus-5-5": "provider",
739
887
  "claude-opus-5": "provider",
740
888
  "gpt-6-astra": "provider",
741
- "sakana/fugu-ultra": "provider"
889
+ "sakana/fugu-ultra": "provider",
890
+ "gpt-6.1-sol": "max"
742
891
  };
743
892
  /**
744
893
  * Whether a model is free (requests cost no credits), per the pricing page's
@@ -767,7 +916,7 @@ function compareByPlan(a, b) {
767
916
  /**
768
917
  * Subscription plan table, synced from the official CLI bundle's plan maps
769
918
  * (located by the `"individual-go"` key in `dist/cli.mjs`, re-verified unchanged
770
- * through command-code@1.68.0): subscription `planId` prefix → display name and
919
+ * through command-code@1.73.0): subscription `planId` prefix → display name and
771
920
  * the plan's monthly credit total. This is the account's own subscription
772
921
  * (from `/alpha/billing/subscriptions`) — distinct from {@link KNOWN_PLANS},
773
922
  * which maps catalog models to their minimum tier.
@@ -782,6 +931,11 @@ const KNOWN_SUBSCRIPTION_PLANS = {
782
931
  monthlyCredits: 10,
783
932
  tierWeight: 0
784
933
  },
934
+ "individual-go-v1": {
935
+ name: "Go",
936
+ monthlyCredits: 10,
937
+ tierWeight: 0
938
+ },
785
939
  "individual-goat": {
786
940
  name: "GOAT",
787
941
  monthlyCredits: 70,
@@ -870,10 +1024,6 @@ const KNOWN_DEALS = {
870
1024
  "MiniMaxAI/MiniMax-M3": { label: "50% off" },
871
1025
  "xiaomi/mimo-v2.5-pro": { label: "99% off" },
872
1026
  "xiaomi/mimo-v2.5": { label: "98% off" },
873
- "xai/grok-4.7": {
874
- label: "40% off",
875
- expiresAt: "2026-09-27T23:59:59.999Z"
876
- },
877
1027
  "poolside/laguna-s-2.1-free": {
878
1028
  label: "FREE",
879
1029
  free: true
@@ -882,6 +1032,10 @@ const KNOWN_DEALS = {
882
1032
  label: "FREE",
883
1033
  free: true
884
1034
  },
1035
+ "inclusionai/ling-3.1-flash:free": {
1036
+ label: "FREE",
1037
+ free: true
1038
+ },
885
1039
  "stealth/space-bunny-alpha": {
886
1040
  label: "FREE",
887
1041
  free: true
@@ -1032,16 +1186,19 @@ function capabilityDescription(modelId, contextWindow, now = Date.now()) {
1032
1186
  * rather than against hard-coded numbers, so this table and the subscription
1033
1187
  * table cannot drift apart silently.
1034
1188
  */
1035
- const ALLOWANCE_TIERS = ["goat", "pro"];
1189
+ const ALLOWANCE_TIERS = [
1190
+ "go",
1191
+ "goat",
1192
+ "pro"
1193
+ ];
1036
1194
  /**
1037
1195
  * Which allowance bracket a plan-tier weight belongs to, or undefined when that
1038
1196
  * plan has no per-model allowance.
1039
1197
  *
1040
1198
  * Weights come from `KNOWN_SUBSCRIPTION_PLANS` (go 0 · goat 1 · pro 2 ·
1041
1199
  * provider 3 · max/ultra 4) and {@link PLAN_ORDER} is the same scale, which is
1042
- * what makes the lookup exact: Go, Provider, Max and Ultra have no published
1043
- * allowance, and answering with a neighbouring tier's figure would be a
1044
- * fabricated number rather than a missing one.
1200
+ * what makes the lookup exact: Provider、Max、Ultra
1201
+ * 没有本地额度;缺项时不能用相邻套餐的数字代替。
1045
1202
  */
1046
1203
  function allowanceTierForWeight(tierWeight) {
1047
1204
  if (tierWeight === void 0) return void 0;
@@ -1069,6 +1226,7 @@ const MODEL_PRICE_ROWS = [
1069
1226
  12.5
1070
1227
  ],
1071
1228
  allowance: {
1229
+ go: 6,
1072
1230
  goat: 20,
1073
1231
  pro: 20
1074
1232
  }
@@ -1082,6 +1240,7 @@ const MODEL_PRICE_ROWS = [
1082
1240
  12.5
1083
1241
  ],
1084
1242
  allowance: {
1243
+ go: 6,
1085
1244
  goat: 20,
1086
1245
  pro: 20
1087
1246
  }
@@ -1095,6 +1254,7 @@ const MODEL_PRICE_ROWS = [
1095
1254
  1.25
1096
1255
  ],
1097
1256
  allowance: {
1257
+ go: 6,
1098
1258
  goat: 20,
1099
1259
  pro: 20
1100
1260
  }
@@ -1108,6 +1268,7 @@ const MODEL_PRICE_ROWS = [
1108
1268
  6.25
1109
1269
  ],
1110
1270
  allowance: {
1271
+ go: 6,
1111
1272
  goat: 20,
1112
1273
  pro: 20
1113
1274
  }
@@ -1121,6 +1282,7 @@ const MODEL_PRICE_ROWS = [
1121
1282
  6.25
1122
1283
  ],
1123
1284
  allowance: {
1285
+ go: 6,
1124
1286
  goat: 20,
1125
1287
  pro: 20
1126
1288
  }
@@ -1134,6 +1296,7 @@ const MODEL_PRICE_ROWS = [
1134
1296
  6.25
1135
1297
  ],
1136
1298
  allowance: {
1299
+ go: 6,
1137
1300
  goat: 20,
1138
1301
  pro: 20
1139
1302
  }
@@ -1147,6 +1310,7 @@ const MODEL_PRICE_ROWS = [
1147
1310
  6.25
1148
1311
  ],
1149
1312
  allowance: {
1313
+ go: 6,
1150
1314
  goat: 20,
1151
1315
  pro: 20
1152
1316
  }
@@ -1160,6 +1324,7 @@ const MODEL_PRICE_ROWS = [
1160
1324
  5
1161
1325
  ],
1162
1326
  allowance: {
1327
+ go: 6,
1163
1328
  goat: 20,
1164
1329
  pro: 20
1165
1330
  }
@@ -1173,6 +1338,7 @@ const MODEL_PRICE_ROWS = [
1173
1338
  3.75
1174
1339
  ],
1175
1340
  allowance: {
1341
+ go: 6,
1176
1342
  goat: 20,
1177
1343
  pro: 20
1178
1344
  }
@@ -1186,6 +1352,7 @@ const MODEL_PRICE_ROWS = [
1186
1352
  2.5
1187
1353
  ],
1188
1354
  allowance: {
1355
+ go: 6,
1189
1356
  goat: 20,
1190
1357
  pro: 20
1191
1358
  }
@@ -1199,6 +1366,7 @@ const MODEL_PRICE_ROWS = [
1199
1366
  2.5
1200
1367
  ],
1201
1368
  allowance: {
1369
+ go: 6,
1202
1370
  goat: 10,
1203
1371
  pro: 20
1204
1372
  }
@@ -1216,6 +1384,7 @@ const MODEL_PRICE_ROWS = [
1216
1384
  .006
1217
1385
  ],
1218
1386
  allowance: {
1387
+ go: 6,
1219
1388
  goat: 60,
1220
1389
  pro: 70
1221
1390
  }
@@ -1228,6 +1397,7 @@ const MODEL_PRICE_ROWS = [
1228
1397
  .07
1229
1398
  ],
1230
1399
  allowance: {
1400
+ go: 8,
1231
1401
  goat: 20,
1232
1402
  pro: 30
1233
1403
  }
@@ -1245,6 +1415,7 @@ const MODEL_PRICE_ROWS = [
1245
1415
  .006
1246
1416
  ],
1247
1417
  allowance: {
1418
+ go: 6,
1248
1419
  goat: 20,
1249
1420
  pro: 30
1250
1421
  }
@@ -1262,6 +1433,7 @@ const MODEL_PRICE_ROWS = [
1262
1433
  .044
1263
1434
  ],
1264
1435
  allowance: {
1436
+ go: 6,
1265
1437
  goat: 20,
1266
1438
  pro: 30
1267
1439
  }
@@ -1279,6 +1451,7 @@ const MODEL_PRICE_ROWS = [
1279
1451
  .006
1280
1452
  ],
1281
1453
  allowance: {
1454
+ go: 10,
1282
1455
  goat: 60,
1283
1456
  pro: 70
1284
1457
  }
@@ -1296,6 +1469,7 @@ const MODEL_PRICE_ROWS = [
1296
1469
  .032
1297
1470
  ],
1298
1471
  allowance: {
1472
+ go: 6,
1299
1473
  goat: 60,
1300
1474
  pro: 70
1301
1475
  }
@@ -1308,6 +1482,7 @@ const MODEL_PRICE_ROWS = [
1308
1482
  .5
1309
1483
  ],
1310
1484
  allowance: {
1485
+ go: 6,
1311
1486
  goat: 20,
1312
1487
  pro: 20
1313
1488
  }
@@ -1320,6 +1495,7 @@ const MODEL_PRICE_ROWS = [
1320
1495
  .03
1321
1496
  ],
1322
1497
  allowance: {
1498
+ go: 6,
1323
1499
  goat: 20,
1324
1500
  pro: 20
1325
1501
  }
@@ -1332,6 +1508,7 @@ const MODEL_PRICE_ROWS = [
1332
1508
  .15
1333
1509
  ],
1334
1510
  allowance: {
1511
+ go: 6,
1335
1512
  goat: 20,
1336
1513
  pro: 20
1337
1514
  }
@@ -1344,6 +1521,7 @@ const MODEL_PRICE_ROWS = [
1344
1521
  .03
1345
1522
  ],
1346
1523
  allowance: {
1524
+ go: 6,
1347
1525
  goat: 20,
1348
1526
  pro: 20
1349
1527
  }
@@ -1356,6 +1534,7 @@ const MODEL_PRICE_ROWS = [
1356
1534
  .15
1357
1535
  ],
1358
1536
  allowance: {
1537
+ go: 6,
1359
1538
  goat: 20,
1360
1539
  pro: 20
1361
1540
  }
@@ -1369,6 +1548,7 @@ const MODEL_PRICE_ROWS = [
1369
1548
  .08334
1370
1549
  ],
1371
1550
  allowance: {
1551
+ go: 6,
1372
1552
  goat: 40,
1373
1553
  pro: 50
1374
1554
  }
@@ -1381,6 +1561,7 @@ const MODEL_PRICE_ROWS = [
1381
1561
  .15
1382
1562
  ],
1383
1563
  allowance: {
1564
+ go: 6,
1384
1565
  goat: 40,
1385
1566
  pro: 50
1386
1567
  }
@@ -1393,6 +1574,7 @@ const MODEL_PRICE_ROWS = [
1393
1574
  .2
1394
1575
  ],
1395
1576
  allowance: {
1577
+ go: 6,
1396
1578
  goat: 20,
1397
1579
  pro: 30
1398
1580
  }
@@ -1405,6 +1587,7 @@ const MODEL_PRICE_ROWS = [
1405
1587
  .26
1406
1588
  ],
1407
1589
  allowance: {
1590
+ go: 6,
1408
1591
  goat: 20,
1409
1592
  pro: 30
1410
1593
  }
@@ -1417,6 +1600,7 @@ const MODEL_PRICE_ROWS = [
1417
1600
  .26
1418
1601
  ],
1419
1602
  allowance: {
1603
+ go: 10,
1420
1604
  goat: 70,
1421
1605
  pro: 80
1422
1606
  }
@@ -1429,6 +1613,7 @@ const MODEL_PRICE_ROWS = [
1429
1613
  .5
1430
1614
  ],
1431
1615
  allowance: {
1616
+ go: 9,
1432
1617
  goat: 20,
1433
1618
  pro: 30
1434
1619
  }
@@ -1441,6 +1626,7 @@ const MODEL_PRICE_ROWS = [
1441
1626
  .26
1442
1627
  ],
1443
1628
  allowance: {
1629
+ go: 10,
1444
1630
  goat: 20,
1445
1631
  pro: 30
1446
1632
  }
@@ -1453,6 +1639,7 @@ const MODEL_PRICE_ROWS = [
1453
1639
  .03
1454
1640
  ],
1455
1641
  allowance: {
1642
+ go: 10,
1456
1643
  goat: 40,
1457
1644
  pro: 50
1458
1645
  }
@@ -1465,6 +1652,7 @@ const MODEL_PRICE_ROWS = [
1465
1652
  .075
1466
1653
  ],
1467
1654
  allowance: {
1655
+ go: 6,
1468
1656
  goat: 20,
1469
1657
  pro: 30
1470
1658
  }
@@ -1478,6 +1666,7 @@ const MODEL_PRICE_ROWS = [
1478
1666
  0
1479
1667
  ],
1480
1668
  allowance: {
1669
+ go: 6,
1481
1670
  goat: 20,
1482
1671
  pro: 20
1483
1672
  }
@@ -1491,6 +1680,7 @@ const MODEL_PRICE_ROWS = [
1491
1680
  0
1492
1681
  ],
1493
1682
  allowance: {
1683
+ go: 6,
1494
1684
  goat: 20,
1495
1685
  pro: 20
1496
1686
  }
@@ -1504,6 +1694,7 @@ const MODEL_PRICE_ROWS = [
1504
1694
  0
1505
1695
  ],
1506
1696
  allowance: {
1697
+ go: 6,
1507
1698
  goat: 20,
1508
1699
  pro: 20
1509
1700
  }
@@ -1517,6 +1708,7 @@ const MODEL_PRICE_ROWS = [
1517
1708
  0
1518
1709
  ],
1519
1710
  allowance: {
1711
+ go: 6,
1520
1712
  goat: 20,
1521
1713
  pro: 20
1522
1714
  }
@@ -1530,6 +1722,7 @@ const MODEL_PRICE_ROWS = [
1530
1722
  .25
1531
1723
  ],
1532
1724
  allowance: {
1725
+ go: 6,
1533
1726
  goat: 20,
1534
1727
  pro: 30
1535
1728
  },
@@ -1557,6 +1750,7 @@ const MODEL_PRICE_ROWS = [
1557
1750
  6.25
1558
1751
  ],
1559
1752
  allowance: {
1753
+ go: 6,
1560
1754
  goat: 70,
1561
1755
  pro: 80
1562
1756
  },
@@ -1584,6 +1778,7 @@ const MODEL_PRICE_ROWS = [
1584
1778
  2.5
1585
1779
  ],
1586
1780
  allowance: {
1781
+ go: 6,
1587
1782
  goat: 20,
1588
1783
  pro: 20
1589
1784
  },
@@ -1611,6 +1806,7 @@ const MODEL_PRICE_ROWS = [
1611
1806
  12.5
1612
1807
  ],
1613
1808
  allowance: {
1809
+ go: 6,
1614
1810
  goat: 20,
1615
1811
  pro: 20
1616
1812
  },
@@ -1638,6 +1834,7 @@ const MODEL_PRICE_ROWS = [
1638
1834
  .125
1639
1835
  ],
1640
1836
  allowance: {
1837
+ go: 6,
1641
1838
  goat: 20,
1642
1839
  pro: 30
1643
1840
  },
@@ -1665,6 +1862,7 @@ const MODEL_PRICE_ROWS = [
1665
1862
  2.5
1666
1863
  ],
1667
1864
  allowance: {
1865
+ go: 6,
1668
1866
  goat: 20,
1669
1867
  pro: 20
1670
1868
  },
@@ -1683,6 +1881,34 @@ const MODEL_PRICE_ROWS = [
1683
1881
  5
1684
1882
  ] }]
1685
1883
  },
1884
+ {
1885
+ id: "gpt-6.1-sol",
1886
+ rates: [
1887
+ 2,
1888
+ 10,
1889
+ .1,
1890
+ 2.5
1891
+ ],
1892
+ allowance: {
1893
+ go: 6,
1894
+ goat: 20,
1895
+ pro: 20
1896
+ },
1897
+ contextTiers: [{
1898
+ "maxContext": 272e3,
1899
+ "rates": [
1900
+ 2,
1901
+ 10,
1902
+ .1,
1903
+ 2.5
1904
+ ]
1905
+ }, { "rates": [
1906
+ 4,
1907
+ 15,
1908
+ .2,
1909
+ 5
1910
+ ] }]
1911
+ },
1686
1912
  {
1687
1913
  id: "grok-4.5",
1688
1914
  rates: [
@@ -1691,6 +1917,7 @@ const MODEL_PRICE_ROWS = [
1691
1917
  .5
1692
1918
  ],
1693
1919
  allowance: {
1920
+ go: 6,
1694
1921
  goat: 20,
1695
1922
  pro: 30
1696
1923
  }
@@ -1703,6 +1930,7 @@ const MODEL_PRICE_ROWS = [
1703
1930
  .5
1704
1931
  ],
1705
1932
  allowance: {
1933
+ go: 6,
1706
1934
  goat: 20,
1707
1935
  pro: 30
1708
1936
  },
@@ -1722,43 +1950,27 @@ const MODEL_PRICE_ROWS = [
1722
1950
  {
1723
1951
  id: "grok-4.7",
1724
1952
  rates: [
1725
- 1.2,
1726
- 3.6,
1727
- .3
1728
- ],
1729
- listRates: [
1730
1953
  2,
1731
1954
  6,
1732
1955
  .5
1733
1956
  ],
1734
1957
  allowance: {
1958
+ go: 6,
1735
1959
  goat: 20,
1736
1960
  pro: 30
1737
1961
  },
1738
1962
  contextTiers: [{
1739
1963
  "maxContext": 2e5,
1740
1964
  "rates": [
1741
- 1.2,
1742
- 3.6,
1743
- .3
1744
- ],
1745
- "listRates": [
1746
1965
  2,
1747
1966
  6,
1748
1967
  .5
1749
1968
  ]
1750
- }, {
1751
- "rates": [
1752
- 2.4,
1753
- 7.2,
1754
- .6
1755
- ],
1756
- "listRates": [
1757
- 4,
1758
- 12,
1759
- 1
1760
- ]
1761
- }]
1969
+ }, { "rates": [
1970
+ 4,
1971
+ 12,
1972
+ 1
1973
+ ] }]
1762
1974
  },
1763
1975
  {
1764
1976
  id: "inkling",
@@ -1768,6 +1980,7 @@ const MODEL_PRICE_ROWS = [
1768
1980
  .17
1769
1981
  ],
1770
1982
  allowance: {
1983
+ go: 6,
1771
1984
  goat: 20,
1772
1985
  pro: 30
1773
1986
  }
@@ -1780,6 +1993,7 @@ const MODEL_PRICE_ROWS = [
1780
1993
  .1
1781
1994
  ],
1782
1995
  allowance: {
1996
+ go: 6,
1783
1997
  goat: 20,
1784
1998
  pro: 30
1785
1999
  }
@@ -1792,6 +2006,7 @@ const MODEL_PRICE_ROWS = [
1792
2006
  .1
1793
2007
  ],
1794
2008
  allowance: {
2009
+ go: 6,
1795
2010
  goat: 20,
1796
2011
  pro: 30
1797
2012
  }
@@ -1804,6 +2019,7 @@ const MODEL_PRICE_ROWS = [
1804
2019
  .16
1805
2020
  ],
1806
2021
  allowance: {
2022
+ go: 6,
1807
2023
  goat: 20,
1808
2024
  pro: 30
1809
2025
  }
@@ -1816,6 +2032,7 @@ const MODEL_PRICE_ROWS = [
1816
2032
  .19
1817
2033
  ],
1818
2034
  allowance: {
2035
+ go: 9,
1819
2036
  goat: 60,
1820
2037
  pro: 70
1821
2038
  }
@@ -1828,6 +2045,7 @@ const MODEL_PRICE_ROWS = [
1828
2045
  .38
1829
2046
  ],
1830
2047
  allowance: {
2048
+ go: 6,
1831
2049
  goat: 20,
1832
2050
  pro: 30
1833
2051
  }
@@ -1840,6 +2058,7 @@ const MODEL_PRICE_ROWS = [
1840
2058
  .3
1841
2059
  ],
1842
2060
  allowance: {
2061
+ go: 8,
1843
2062
  goat: 20,
1844
2063
  pro: 30
1845
2064
  }
@@ -1852,6 +2071,7 @@ const MODEL_PRICE_ROWS = [
1852
2071
  .006
1853
2072
  ],
1854
2073
  allowance: {
2074
+ go: 10,
1855
2075
  goat: 50,
1856
2076
  pro: 60
1857
2077
  }
@@ -1869,6 +2089,7 @@ const MODEL_PRICE_ROWS = [
1869
2089
  .16
1870
2090
  ],
1871
2091
  allowance: {
2092
+ go: 6,
1872
2093
  goat: 30,
1873
2094
  pro: 40
1874
2095
  }
@@ -1886,6 +2107,7 @@ const MODEL_PRICE_ROWS = [
1886
2107
  .4
1887
2108
  ],
1888
2109
  allowance: {
2110
+ go: 6,
1889
2111
  goat: 20,
1890
2112
  pro: 30
1891
2113
  }
@@ -1898,6 +2120,7 @@ const MODEL_PRICE_ROWS = [
1898
2120
  .0028
1899
2121
  ],
1900
2122
  allowance: {
2123
+ go: 6,
1901
2124
  goat: 20,
1902
2125
  pro: 30
1903
2126
  }
@@ -1910,6 +2133,7 @@ const MODEL_PRICE_ROWS = [
1910
2133
  .0036
1911
2134
  ],
1912
2135
  allowance: {
2136
+ go: 6,
1913
2137
  goat: 20,
1914
2138
  pro: 30
1915
2139
  }
@@ -1922,6 +2146,7 @@ const MODEL_PRICE_ROWS = [
1922
2146
  .036
1923
2147
  ],
1924
2148
  allowance: {
2149
+ go: 6,
1925
2150
  goat: 10,
1926
2151
  pro: 20
1927
2152
  }
@@ -1934,6 +2159,7 @@ const MODEL_PRICE_ROWS = [
1934
2159
  .03
1935
2160
  ],
1936
2161
  allowance: {
2162
+ go: 6,
1937
2163
  goat: 20,
1938
2164
  pro: 30
1939
2165
  }
@@ -1946,6 +2172,7 @@ const MODEL_PRICE_ROWS = [
1946
2172
  .06
1947
2173
  ],
1948
2174
  allowance: {
2175
+ go: 6,
1949
2176
  goat: 20,
1950
2177
  pro: 30
1951
2178
  }
@@ -1963,6 +2190,7 @@ const MODEL_PRICE_ROWS = [
1963
2190
  .12
1964
2191
  ],
1965
2192
  allowance: {
2193
+ go: 8,
1966
2194
  goat: 47,
1967
2195
  pro: 57
1968
2196
  }
@@ -1975,6 +2203,7 @@ const MODEL_PRICE_ROWS = [
1975
2203
  .15
1976
2204
  ],
1977
2205
  allowance: {
2206
+ go: 6,
1978
2207
  goat: 20,
1979
2208
  pro: 20
1980
2209
  }
@@ -1987,6 +2216,7 @@ const MODEL_PRICE_ROWS = [
1987
2216
  .15
1988
2217
  ],
1989
2218
  allowance: {
2219
+ go: 6,
1990
2220
  goat: 20,
1991
2221
  pro: 30
1992
2222
  }
@@ -1999,6 +2229,7 @@ const MODEL_PRICE_ROWS = [
1999
2229
  .002
2000
2230
  ],
2001
2231
  allowance: {
2232
+ go: 6,
2002
2233
  goat: 20,
2003
2234
  pro: 30
2004
2235
  }
@@ -2011,6 +2242,7 @@ const MODEL_PRICE_ROWS = [
2011
2242
  .15
2012
2243
  ],
2013
2244
  allowance: {
2245
+ go: 6,
2014
2246
  goat: 20,
2015
2247
  pro: 30
2016
2248
  }
@@ -2023,6 +2255,7 @@ const MODEL_PRICE_ROWS = [
2023
2255
  .002
2024
2256
  ],
2025
2257
  allowance: {
2258
+ go: 6,
2026
2259
  goat: 20,
2027
2260
  pro: 30
2028
2261
  }
@@ -2035,6 +2268,7 @@ const MODEL_PRICE_ROWS = [
2035
2268
  .12
2036
2269
  ],
2037
2270
  allowance: {
2271
+ go: 6,
2038
2272
  goat: 20,
2039
2273
  pro: 30
2040
2274
  }
@@ -2048,6 +2282,7 @@ const MODEL_PRICE_ROWS = [
2048
2282
  1.63
2049
2283
  ],
2050
2284
  allowance: {
2285
+ go: 6,
2051
2286
  goat: 20,
2052
2287
  pro: 30
2053
2288
  }
@@ -2060,6 +2295,7 @@ const MODEL_PRICE_ROWS = [
2060
2295
  .1
2061
2296
  ],
2062
2297
  allowance: {
2298
+ go: 10,
2063
2299
  goat: 33,
2064
2300
  pro: 43
2065
2301
  },
@@ -2085,6 +2321,7 @@ const MODEL_PRICE_ROWS = [
2085
2321
  .038
2086
2322
  ],
2087
2323
  allowance: {
2324
+ go: 6,
2088
2325
  goat: 20,
2089
2326
  pro: 30
2090
2327
  },
@@ -2124,6 +2361,7 @@ const MODEL_PRICE_ROWS = [
2124
2361
  3.13
2125
2362
  ],
2126
2363
  allowance: {
2364
+ go: 10,
2127
2365
  goat: 33,
2128
2366
  pro: 43
2129
2367
  }
@@ -2137,6 +2375,7 @@ const MODEL_PRICE_ROWS = [
2137
2375
  .5
2138
2376
  ],
2139
2377
  allowance: {
2378
+ go: 10,
2140
2379
  goat: 33,
2141
2380
  pro: 43
2142
2381
  },
@@ -2163,6 +2402,7 @@ const MODEL_PRICE_ROWS = [
2163
2402
  .04
2164
2403
  ],
2165
2404
  allowance: {
2405
+ go: 10,
2166
2406
  goat: 70,
2167
2407
  pro: 80
2168
2408
  }
@@ -2175,6 +2415,7 @@ const MODEL_PRICE_ROWS = [
2175
2415
  .016
2176
2416
  ],
2177
2417
  allowance: {
2418
+ go: 6,
2178
2419
  goat: 20,
2179
2420
  pro: 30
2180
2421
  }
@@ -2188,6 +2429,7 @@ const MODEL_PRICE_ROWS = [
2188
2429
  2.5
2189
2430
  ],
2190
2431
  allowance: {
2432
+ go: 7,
2191
2433
  goat: 20,
2192
2434
  pro: 30
2193
2435
  }
@@ -2200,6 +2442,7 @@ const MODEL_PRICE_ROWS = [
2200
2442
  .25
2201
2443
  ],
2202
2444
  allowance: {
2445
+ go: 7,
2203
2446
  goat: 20,
2204
2447
  pro: 30
2205
2448
  }
@@ -2212,6 +2455,7 @@ const MODEL_PRICE_ROWS = [
2212
2455
  .016
2213
2456
  ],
2214
2457
  allowance: {
2458
+ go: 6,
2215
2459
  goat: 20,
2216
2460
  pro: 30
2217
2461
  }
@@ -2224,6 +2468,7 @@ const MODEL_PRICE_ROWS = [
2224
2468
  .02
2225
2469
  ],
2226
2470
  allowance: {
2471
+ go: 6,
2227
2472
  goat: 20,
2228
2473
  pro: 30
2229
2474
  }
@@ -2236,6 +2481,7 @@ const MODEL_PRICE_ROWS = [
2236
2481
  .04
2237
2482
  ],
2238
2483
  allowance: {
2484
+ go: 6,
2239
2485
  goat: 20,
2240
2486
  pro: 30
2241
2487
  }
@@ -2248,6 +2494,7 @@ const MODEL_PRICE_ROWS = [
2248
2494
  .05
2249
2495
  ],
2250
2496
  allowance: {
2497
+ go: 6,
2251
2498
  goat: 20,
2252
2499
  pro: 30
2253
2500
  }
@@ -2260,6 +2507,7 @@ const MODEL_PRICE_ROWS = [
2260
2507
  .035
2261
2508
  ],
2262
2509
  allowance: {
2510
+ go: 10,
2263
2511
  goat: 70,
2264
2512
  pro: 80
2265
2513
  }
@@ -2272,6 +2520,7 @@ const MODEL_PRICE_ROWS = [
2272
2520
  .042
2273
2521
  ],
2274
2522
  allowance: {
2523
+ go: 6,
2275
2524
  goat: 20,
2276
2525
  pro: 30
2277
2526
  }
@@ -2284,6 +2533,7 @@ const MODEL_PRICE_ROWS = [
2284
2533
  0
2285
2534
  ],
2286
2535
  allowance: {
2536
+ go: 6,
2287
2537
  goat: 20,
2288
2538
  pro: 30
2289
2539
  }
@@ -2597,6 +2847,37 @@ function installCostProjection(ctx) {
2597
2847
  */
2598
2848
  const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
2599
2849
  //#endregion
2850
+ //#region src/timeout-limits.ts
2851
+ const MAX_TIMEOUT_SECONDS = 3600;
2852
+ //#endregion
2853
+ //#region src/model-visibility.ts
2854
+ /** 网页、终端与模型目录共用的选择规则;显式开关优先于白名单。 */
2855
+ function modelIsVisible(id, allowlist, overrides) {
2856
+ const flag = overrides !== null && typeof overrides === "object" && !Array.isArray(overrides) ? overrides[id] : void 0;
2857
+ return typeof flag === "boolean" ? flag : allowlist.length === 0 || allowlist.includes(id);
2858
+ }
2859
+ const THROTTLED_CODE = "THROTTLED";
2860
+ const boundedCodes = /* @__PURE__ */ new Set([
2861
+ "EMPTY_RESPONSE",
2862
+ "SERVER",
2863
+ "TIMEOUT",
2864
+ THROTTLED_CODE
2865
+ ]);
2866
+ const counts$1 = /* @__PURE__ */ new WeakMap();
2867
+ function absorbTransientFailure(agent, code) {
2868
+ if (!boundedCodes.has(code)) return "ignored";
2869
+ const used = counts$1.get(agent) ?? 0;
2870
+ if (used >= 3) return "exhausted";
2871
+ counts$1.set(agent, used + 1);
2872
+ return "retry";
2873
+ }
2874
+ function resetTransientFailures(agent) {
2875
+ counts$1.delete(agent);
2876
+ }
2877
+ function transientBudgetMessage(code, message) {
2878
+ return `Command Code 请求在 3 次自动重试后仍失败,已停止重试(${code})。请稍后重新发送;若反复出现,请检查服务状态或请求耗时诊断。 原因:${message}`;
2879
+ }
2880
+ //#endregion
2600
2881
  //#region src/accounts.ts
2601
2882
  /**
2602
2883
  * Multi-account pool for the Command Code provider (host side).
@@ -2760,28 +3041,10 @@ var CommandCodeAccountPool = class {
2760
3041
  if (routed !== void 0) return this.pick(routed);
2761
3042
  const chosen = selectActiveAccount(available, preferred);
2762
3043
  if (chosen !== void 0) {
2763
- const explicit = this.explicitAccount(available, options?.model ?? "", preferred);
2764
- if (explicit !== void 0 && explicit.key !== chosen.key && this.canProbeExplicit(explicit)) {
2765
- const revived = await this.probeExplicit(explicit);
2766
- if (revived !== void 0) return this.pick(revived);
2767
- }
2768
- return this.pick(chosen);
3044
+ const revived = await this.reviveExplicit(available, options?.model ?? "", preferred, chosen);
3045
+ return this.pick(revived ?? chosen);
2769
3046
  }
2770
- await Promise.all(available.map(async (account) => {
2771
- if (account.state?.kind === "disabled") return;
2772
- let probe;
2773
- try {
2774
- probe = await this.deps.probeWindow(account.key);
2775
- } catch {
2776
- return;
2777
- }
2778
- if (probe === void 0) return;
2779
- if (!probe.exceeded) {
2780
- this.states.delete(account.key);
2781
- return;
2782
- }
2783
- this.stampWindowMark(account, probe);
2784
- }));
3047
+ await this.probeAllMarked(available);
2785
3048
  const latestAccounts = await this.resolvedAccounts();
2786
3049
  const revived = selectActiveAccount(tried === void 0 || tried.length === 0 ? latestAccounts : latestAccounts.filter((account) => !tried.includes(account.key)), preferred);
2787
3050
  if (revived !== void 0) return this.pick(revived);
@@ -2789,6 +3052,34 @@ var CommandCodeAccountPool = class {
2789
3052
  throw allAccountsUnusable(latestAccounts.length > 0 ? latestAccounts : accounts);
2790
3053
  }
2791
3054
  /**
3055
+ * Which account is serving right now, for display (the settings page's
3056
+ * account card, the sidebar usage stats) — without consuming a request's
3057
+ * `tried` budget and without throwing when nothing can serve.
3058
+ *
3059
+ * Reads and self-heals exactly like {@link resolveKey} would: an `unknown`
3060
+ * mark on the pinned account is re-probed (throttled the same way, sharing
3061
+ * the same per-key timestamp), and a pool where nothing currently resolves
3062
+ * gets one batch probe pass too. Without this, the badge read a possibly
3063
+ * stale mark forever — nothing but an actual generate request ever cleared
3064
+ * an `unknown` mark, so a page left open with no chat activity kept
3065
+ * showing "switched to the fallback account" long after a real request
3066
+ * would have quietly recovered (issue #51's follow-up report: the account
3067
+ * card and the sidebar stats both stuck on the wrong account).
3068
+ *
3069
+ * No `model` parameter: the badge describes the pool as a whole, not one
3070
+ * request, so model-routing rules are deliberately not consulted here —
3071
+ * matching what the direct `selectActiveAccount` call this replaces did.
3072
+ */
3073
+ async activeAccount() {
3074
+ const accounts = await this.resolvedAccounts();
3075
+ if (accounts.length === 0) return void 0;
3076
+ const preferred = this.deps.preferredId?.();
3077
+ const chosen = selectActiveAccount(accounts, preferred);
3078
+ if (chosen !== void 0) return await this.reviveExplicit(accounts, "", preferred, chosen) ?? chosen;
3079
+ await this.probeAllMarked(accounts);
3080
+ return selectActiveAccount(await this.resolvedAccounts(), preferred);
3081
+ }
3082
+ /**
2792
3083
  * Record a rejection against one key. A `throttled` rejection (the plain 429
2793
3084
  * that named no window) marks the key with the `throttle` cause, leaving
2794
3085
  * rotation exactly like a window mark but keeping the pool's own diagnosis at
@@ -2938,6 +3229,46 @@ var CommandCodeAccountPool = class {
2938
3229
  until: 0
2939
3230
  });
2940
3231
  }
3232
+ /**
3233
+ * The explicit-account fallback re-check: when `chosen` (what rotation
3234
+ * would currently serve) is not the account the user actually asked for,
3235
+ * re-probe that explicit account's mark before accepting the fallback —
3236
+ * so a rate-limit mark never demotes the user's own selection for good
3237
+ * (issue #51). Returns the revived account, or `undefined` when there is
3238
+ * nothing to revive (explicit account already serving, not due for a
3239
+ * probe, or the probe found it still exceeded). Shared by {@link resolveKey}
3240
+ * (the request path) and {@link activeAccount} (the display path), so both
3241
+ * agree on what "currently usable" means instead of drifting apart.
3242
+ */
3243
+ async reviveExplicit(accounts, model, preferred, chosen) {
3244
+ const explicit = this.explicitAccount(accounts, model, preferred);
3245
+ if (explicit === void 0 || explicit.key === chosen.key || !this.canProbeExplicit(explicit)) return;
3246
+ return this.probeExplicit(explicit);
3247
+ }
3248
+ /**
3249
+ * Probe every marked account's real usage window when nothing currently
3250
+ * resolves. Disabled (401) keys are not probed — an invalid key stays
3251
+ * invalid. A throwing probe counts as "unknown" (like a failed one): it
3252
+ * must not turn the all-exhausted path into a raw rejection instead of
3253
+ * RATE_LIMIT. Shared by {@link resolveKey} and {@link activeAccount}.
3254
+ */
3255
+ async probeAllMarked(accounts) {
3256
+ await Promise.all(accounts.map(async (account) => {
3257
+ if (account.state?.kind === "disabled") return;
3258
+ let probe;
3259
+ try {
3260
+ probe = await this.deps.probeWindow(account.key);
3261
+ } catch {
3262
+ return;
3263
+ }
3264
+ if (probe === void 0) return;
3265
+ if (!probe.exceeded) {
3266
+ this.states.delete(account.key);
3267
+ return;
3268
+ }
3269
+ this.stampWindowMark(account, probe);
3270
+ }));
3271
+ }
2941
3272
  /** Hand out the chosen account's key. */
2942
3273
  pick(account) {
2943
3274
  return {
@@ -2973,7 +3304,7 @@ var CommandCodeAccountPool = class {
2973
3304
  function allAccountsUnusable(accounts) {
2974
3305
  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");
2975
3306
  const windowMarked = accounts.map((account) => account.state).filter((state) => state !== void 0).filter((state) => state.cause === "window");
2976
- 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");
3307
+ 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);
2977
3308
  const resets = windowMarked.filter((state) => state.kind === "cooldown" && state.until > 0).map((state) => state.until);
2978
3309
  const earliest = resets.length > 0 ? Math.min(...resets) : 0;
2979
3310
  const wait = earliest > 0 ? Math.max(1e3, earliest - Date.now()) : 0;
@@ -3394,6 +3725,95 @@ function openStreamTrace(scope, options = {}) {
3394
3725
  };
3395
3726
  }
3396
3727
  //#endregion
3728
+ //#region src/request-timing.ts
3729
+ /** 请求耗时只记录阶段、计数和状态;不记录消息、凭据、请求头或错误正文。 */
3730
+ const REQUEST_TIMING_ENV = "DSH_COMMANDCODE_TIMING";
3731
+ /** 每次生成独立计时;可注入时钟验证顺序,不依赖真实等待。 */
3732
+ var RequestTiming = class {
3733
+ sink;
3734
+ now;
3735
+ started;
3736
+ summary;
3737
+ constructor(model, sink, now = () => performance.now()) {
3738
+ this.sink = sink;
3739
+ this.now = now;
3740
+ this.started = now();
3741
+ this.summary = {
3742
+ version: 1,
3743
+ startedAt: Date.now(),
3744
+ model,
3745
+ outcome: "cancelled",
3746
+ totalMs: 0,
3747
+ phasesMs: {
3748
+ credentials: 0,
3749
+ images: 0,
3750
+ body: 0,
3751
+ serialization: 0,
3752
+ headers: 0
3753
+ },
3754
+ attempts: []
3755
+ };
3756
+ }
3757
+ /** 返回结束函数,确保成功和异常都计入同一阶段。阶段可嵌套,不能直接相加。 */
3758
+ phase(name) {
3759
+ if (!this.sink) return () => {};
3760
+ const start = this.now();
3761
+ return () => {
3762
+ this.summary.phasesMs[name] += this.now() - start;
3763
+ };
3764
+ }
3765
+ attempt(protocol, bodyBytes) {
3766
+ if (!this.sink) return () => {};
3767
+ const start = this.now();
3768
+ const attempt = {
3769
+ protocol,
3770
+ bodyBytes,
3771
+ headersMs: 0
3772
+ };
3773
+ this.summary.attempts.push(attempt);
3774
+ return (status) => {
3775
+ Object.assign(attempt, {
3776
+ headersMs: this.now() - start,
3777
+ ...status === void 0 ? {} : { status }
3778
+ });
3779
+ this.summary.phasesMs.headers += attempt.headersMs;
3780
+ };
3781
+ }
3782
+ first(kind) {
3783
+ if (this.sink && this.summary[kind] === void 0) this.summary[kind] = this.now() - this.started;
3784
+ }
3785
+ async close(outcome, errorCode) {
3786
+ if (!this.sink) return;
3787
+ this.summary.outcome = outcome;
3788
+ this.summary.totalMs = this.now() - this.started;
3789
+ if (errorCode !== void 0) this.summary.errorCode = errorCode;
3790
+ try {
3791
+ await this.sink(this.summary);
3792
+ } catch {}
3793
+ }
3794
+ };
3795
+ /** 与原始流诊断独立开关,便于仅收集数值耗时。 */
3796
+ function openRequestTiming(model, sink) {
3797
+ if (sink) return new RequestTiming(model, sink);
3798
+ const raw = process.env[REQUEST_TIMING_ENV]?.trim();
3799
+ if (!raw || [
3800
+ "0",
3801
+ "false",
3802
+ "no",
3803
+ "off"
3804
+ ].includes(raw.toLowerCase())) return new RequestTiming(model);
3805
+ const path = [
3806
+ "1",
3807
+ "true",
3808
+ "yes",
3809
+ "on"
3810
+ ].includes(raw.toLowerCase()) ? join(tmpdir(), "dsh-commandcode-timing.jsonl") : raw;
3811
+ return new RequestTiming(model, async (summary) => {
3812
+ await mkdir(dirname(path), { recursive: true });
3813
+ await appendFile(path, `${JSON.stringify(summary)}\n`, { mode: 384 });
3814
+ });
3815
+ }
3816
+ //#endregion
3397
3817
  //#region src/adapter.ts
3398
3818
  /**
3399
3819
  * DeepSeek Harness LLM adapter for the Command Code Provider API.
@@ -3402,21 +3822,113 @@ function openStreamTrace(scope, options = {}) {
3402
3822
  * community-maintained integration; you need your own Command Code account
3403
3823
  * and API key or subscription, and Command Code's terms apply.
3404
3824
  *
3405
- * Owns the two chat transports (`/alpha/generate`, `/provider/v1/chat/completions`),
3825
+ * Owns the three chat transports (`/alpha/generate`, `/provider/v1/chat/completions`,
3826
+ * `/provider/v1/messages`),
3406
3827
  * message conversion, SSE/JSONL parsing, the catalog + its on-disk cache, and
3407
3828
  * the pre-stream account rotation loop. The wire protocol is
3408
- * reverse-engineered (command-code@1.28.4, re-verified through 1.68.0);
3409
- * AGENTS.md holds the full protocol record.
3829
+ * reverse-engineered (command-code@1.28.4, re-verified through 1.73.0);
3830
+ * docs/agent-reference/adapter-protocol.md holds the full protocol record.
3410
3831
  *
3411
3832
  * The adapter is deliberately free of cordis/schemastery: it receives a
3412
3833
  * per-request options thunk and an API-key resolver from the plugin entry
3413
3834
  * (src/index.ts), so a settings change reaches the very next request.
3414
3835
  */
3836
+ /**
3837
+ * Output ceilings the endpoint has named in a live rejection, per model.
3838
+ *
3839
+ * The snapshot in ./capabilities.ts carries the vendor-published ceiling for
3840
+ * 74 of the 84 catalog models (see scripts/sync-output-limits.mjs). The other
3841
+ * 10 are models no vendor publishes yet, and a gateway is free to sit below
3842
+ * the vendor figure even where one exists — so this map is the correction
3843
+ * channel, not a fallback of last resort: the endpoint states the model's own
3844
+ * ceiling in its rejection, so one wasted round trip is enough to know it for
3845
+ * the rest of the process.
3846
+ *
3847
+ * Shared by adapters for the same gateway, but not across apiBase values: a
3848
+ * proxy can enforce a smaller ceiling for the same model id. Only ever tightens
3849
+ * within that gateway, so a stale catalog row cannot undo a live refusal.
3850
+ */
3851
+ const learnedOutputLimits = /* @__PURE__ */ new Map();
3852
+ /**
3853
+ * Output ceilings the CATALOG published, per model.
3854
+ *
3855
+ * `/provider/v1/models` carries `max_output_tokens` in its schema and the
3856
+ * official pi provider already prefers it, but the endpoint does not send it
3857
+ * yet — so this is empty today and fills in on its own the day it does. It sits
3858
+ * above the models.dev snapshot and below what a live rejection proved, because
3859
+ * that ordering is also the order of trust: the gateway knows its own ceiling,
3860
+ * a rejection proves it, and models.dev is a third party describing the same
3861
+ * model. Restored from the on-disk catalog cache so a published ceiling
3862
+ * survives a restart without waiting for the next catalog fetch.
3863
+ */
3864
+ const publishedOutputLimits = /* @__PURE__ */ new Map();
3865
+ /** Keep model facts from one gateway from constraining another gateway. */
3866
+ function outputLimitsFor(store, apiBase) {
3867
+ let limits = store.get(apiBase);
3868
+ if (limits === void 0) {
3869
+ limits = /* @__PURE__ */ new Map();
3870
+ store.set(apiBase, limits);
3871
+ }
3872
+ return limits;
3873
+ }
3874
+ /**
3875
+ * The output ceiling this gateway route sends for `modelId`.
3876
+ *
3877
+ * `/provider/v1/models` publishes `context_length` and `supported_endpoints`
3878
+ * but no output cap today, while each endpoint refuses a larger `max_tokens` per
3879
+ * model — the Messages endpoint names its own in the rejection (`max_tokens:
3880
+ * 200000 > 128000, which is the maximum allowed number of output tokens for
3881
+ * claude-sonnet-5-5`). What the endpoint has already named wins, then the
3882
+ * catalog's own figure once it publishes one, then the snapshot, then — for
3883
+ * the Messages-route models only — the conservative value, because the ceilings
3884
+ * actually observed are below the global default and assuming otherwise would
3885
+ * fail every request.
3886
+ */
3887
+ function modelOutputTokenLimit(apiBase, modelId) {
3888
+ const learned = outputLimitsFor(learnedOutputLimits, apiBase).get(modelId);
3889
+ const known = outputLimitsFor(publishedOutputLimits, apiBase).get(modelId) ?? MODEL_OUTPUT_TOKEN_LIMITS.get(modelId);
3890
+ if (learned !== void 0 && (known === void 0 || learned < known)) return learned;
3891
+ if (known !== void 0) return known;
3892
+ return requiresMessagesEndpoint(modelId) ? DEFAULT_MESSAGES_MAX_TOKENS : DEFAULT_MAX_OUTPUT_TOKENS;
3893
+ }
3894
+ /**
3895
+ * Record a ceiling the endpoint just stated, if it is one this route can act
3896
+ * on: a real number, and one below what the next request would otherwise send.
3897
+ * A ceiling at or above the current budget describes a request that was not
3898
+ * refused for being too large, and acting on it would be a guess.
3899
+ */
3900
+ function noteOutputCeiling(apiBase, modelId, ceiling) {
3901
+ if (!Number.isSafeInteger(ceiling) || ceiling <= 0) return false;
3902
+ if (ceiling >= modelOutputTokenLimit(apiBase, modelId)) return false;
3903
+ outputLimitsFor(learnedOutputLimits, apiBase).set(modelId, ceiling);
3904
+ return true;
3905
+ }
3906
+ /**
3907
+ * The ceiling to retry this turn with, when `error` is a request-side output
3908
+ * refusal the route can act on — else undefined, meaning surface it.
3909
+ *
3910
+ * Read from the message, so it covers both shapes the same refusal arrives in:
3911
+ * an HTTP 400 (`PROVIDER_HTTP_ERROR`) and an in-stream error frame
3912
+ * (`PROVIDER_STREAM_ERROR`). Both carry the endpoint's wording, and both are
3913
+ * already classified as output refusals rather than context overflows by the
3914
+ * time they reach here.
3915
+ *
3916
+ * Recording is what makes the retry pay off: the ceiling is kept for this
3917
+ * gateway and model, so a refused turn leaves its next request correct.
3918
+ */
3919
+ function outputCeilingRefusal(apiBase, modelId, error) {
3920
+ if (!(error instanceof LlmError)) return void 0;
3921
+ const detail = error.message;
3922
+ if (!OUTPUT_LIMIT_REJECTION.test(detail)) return void 0;
3923
+ const stated = parseOutputCeiling(detail);
3924
+ if (stated === void 0) return void 0;
3925
+ return noteOutputCeiling(apiBase, modelId, stated) ? stated : void 0;
3926
+ }
3415
3927
  /** Keep the two user-facing languages and error metadata together. */
3416
3928
  function bilingual(code, en, zh, options) {
3417
3929
  return new LlmError(`${en};${zh}`, code, options);
3418
3930
  }
3419
- const COMMAND_CODE_CLI_VERSION = "1.68.0";
3931
+ const COMMAND_CODE_CLI_VERSION = "1.73.0";
3420
3932
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
3421
3933
  /**
3422
3934
  * The output budget this bundle asks for, and the ceiling it will never exceed.
@@ -3434,9 +3946,16 @@ const DEFAULT_API_BASE = "https://api.commandcode.ai";
3434
3946
  * (a 64 000 request answered headers in 1.28 s against 1.57 s for 131 072) but
3435
3947
  * it did cap what a model could ever say in one turn.
3436
3948
  *
3437
- * This is only an UPPER bound. `requestContextBudget` lowers it per request
3438
- * from the prompt actually being sent, so raising the ceiling cannot push a
3439
- * large session over its window.
3949
+ * No client-side clamp is applied on top of this: an earlier version pre-shrank
3950
+ * `max_tokens` from an estimate of the prompt already sent (`requestContextBudget()`,
3951
+ * issue #67), but that estimate over-corrected on ordinary long conversations —
3952
+ * exactly the "output gets cut short as the conversation grows" symptom issue #74
3953
+ * reported — while a live A/B probe (2026-09-30, `stealth/space-bunny-alpha`,
3954
+ * ~19.6% over its 1,000,000-token window with no client-side shrink) showed the
3955
+ * endpoint answers a genuine overflow with an unambiguous `400 … exceeds the
3956
+ * model's context window`, which `isContextOverflowDetail()` below already
3957
+ * classifies as `CONTEXT_WINDOW_EXCEEDED` independently of this constant. See
3958
+ * [决策记录](../docs/决策记录.md).
3440
3959
  */
3441
3960
  const DEFAULT_GENERATE_MAX_TOKENS = 131072;
3442
3961
  /**
@@ -3449,9 +3968,10 @@ const DEFAULT_GENERATE_MAX_TOKENS = 131072;
3449
3968
  * `Math.min(contextLength, this)` before the ceiling above is ever consulted.
3450
3969
  */
3451
3970
  const DEFAULT_MAX_OUTPUT_TOKENS = 131072;
3452
- /** Preserve a small usable answer budget when the heuristic overshoots. */
3453
- const MIN_CONTEXT_OUTPUT_TOKENS = 1024;
3454
3971
  const MODELS_TIMEOUT_MS = 1e4;
3972
+ /** 目录短期复用;刷新失败只短暂退避,避免反复阻塞模型选择器。 */
3973
+ const MODEL_CATALOG_TTL_MS = 3e5;
3974
+ const MODEL_CATALOG_RETRY_MS = 1e4;
3455
3975
  /** How long the picker's plan-filter billing facts stay cached before refetching. */
3456
3976
  const BILLING_ACCESS_TTL_MS = 3e5;
3457
3977
  /** The entry-plan tier weight (`individual-go` in KNOWN_SUBSCRIPTION_PLANS). */
@@ -3489,7 +4009,49 @@ const ACTIVE_SUBSCRIPTION_STATUSES = /* @__PURE__ */ new Set([
3489
4009
  const DEFAULT_REQUEST_TIMEOUT_MS = 3e5;
3490
4010
  /** Stream idle timeout: a generation that stalls this long is a dead connection. */
3491
4011
  const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
3492
- const MODEL_CACHE_VERSION = 1;
4012
+ /** Version 3 binds cached catalog facts to their gateway; older files lack a safe source. */
4013
+ const MODEL_CACHE_VERSION = 3;
4014
+ /**
4015
+ * A lone UTF-16 surrogate half: a high surrogate not followed by a low one, or
4016
+ * a low surrogate not preceded by a high one. Such a half is not a character —
4017
+ * it only arises from a truncated slice, a lossy decode, or a lone `\uD83D`
4018
+ * escape — and it survives `JSON.stringify` as an escape (`"\ud83d"`) rather
4019
+ * than failing locally, so the request leaves the process carrying a string no
4020
+ * UTF-8 consumer can round-trip. Providers running strict decoders reject it.
4021
+ *
4022
+ * This is the same expression `@earendil-works/pi-ai` applies to every outgoing
4023
+ * string on this endpoint. Valid astral characters (emoji and anything else
4024
+ * outside the BMP) are PAIRED surrogates and pass through untouched.
4025
+ */
4026
+ const LONE_SURROGATE_PATTERN = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
4027
+ /**
4028
+ * A cheap pre-test for "this string contains any surrogate code unit at all",
4029
+ * paired or not. The exact pattern above needs lookaround, which V8 cannot
4030
+ * optimize into a fast scan; this one is a plain range test. Real prompts are
4031
+ * overwhelmingly ASCII-plus-CJK with no surrogates, so the common case pays one
4032
+ * linear scan instead of the lookaround pass. Measured on a 412 KB request
4033
+ * body: 1.40 ms with the exact pattern alone against 0.77 ms with this gate,
4034
+ * where a bare `JSON.stringify` is 0.59 ms.
4035
+ */
4036
+ const ANY_SURROGATE_PATTERN = /[\uD800-\uDFFF]/;
4037
+ /**
4038
+ * Drop lone surrogate halves from one outgoing string.
4039
+ *
4040
+ * Applied at the single point a request body is serialized, so every field of
4041
+ * every transport is covered — system text, message content, tool schemas and
4042
+ * tool results alike — instead of enumerating the fields that can carry text
4043
+ * and missing one. The value is returned unchanged whenever it holds no
4044
+ * surrogate at all, which is the ordinary case.
4045
+ *
4046
+ * The `test()` call deliberately uses the flagless {@link ANY_SURROGATE_PATTERN}:
4047
+ * a global regex is stateful, so testing with one would advance `lastIndex` and
4048
+ * make consecutive calls on the same string alternate between true and false.
4049
+ * `replace()` resets `lastIndex` itself, so the global pattern is safe there.
4050
+ */
4051
+ function sanitizeSurrogates(value) {
4052
+ if (typeof value !== "string") return value;
4053
+ return ANY_SURROGATE_PATTERN.test(value) ? value.replace(LONE_SURROGATE_PATTERN, "") : value;
4054
+ }
3493
4055
  /** Hash one request value without retaining or logging its contents. */
3494
4056
  function fingerprintValue(value) {
3495
4057
  let serialized;
@@ -3662,15 +4224,39 @@ const CLI_CONTEXT_OVERFLOW_PATTERN = /prompt is too long|context.*(length|window
3662
4224
  function isContextOverflowDetail(detail) {
3663
4225
  return isContextWindowExceededError(detail) || CLI_CONTEXT_OVERFLOW_PATTERN.test(detail);
3664
4226
  }
3665
- /** A gateway refusal observed for an over-reserved request, but not unique to that cause. */
3666
- const AMBIGUOUS_SIZE_REJECTION = /\bmodel could not complete the request\b/i;
4227
+ /**
4228
+ * A request-side `max_tokens` refusal, told apart from a context overflow that
4229
+ * mentions the same field. Kept narrow on purpose: the overflow pattern also
4230
+ * matches a bare `max_tokens`, so this must name the specific shapes the
4231
+ * endpoint uses — a ceiling comparison, its prose about output tokens, or a
4232
+ * request-validation error on the field.
4233
+ */
4234
+ const OUTPUT_LIMIT_REJECTION = new RegExp([
4235
+ "max_tokens:\\s*\\d+\\s*>\\s*\\d+",
4236
+ "maximum allowed number of output tokens",
4237
+ "invalid input[^\\n]*max_tokens",
4238
+ "expected number[^\\n]*at max_tokens"
4239
+ ].join("|"), "i");
4240
+ /**
4241
+ * The output ceiling the endpoint states in a `max_tokens` refusal, read off
4242
+ * its own comparison: `max_tokens: 131072 > 128000` yields 128000.
4243
+ *
4244
+ * Only the comparison form is parsed. The other shapes
4245
+ * {@link OUTPUT_LIMIT_REJECTION} matches — a bare validation error, or the
4246
+ * prose without the numbers — name no ceiling, and guessing one would be worse
4247
+ * than failing: a fabricated value is indistinguishable from a real one at the
4248
+ * point where it decides the next request's budget.
4249
+ */
4250
+ function parseOutputCeiling(detail) {
4251
+ const stated = /max_tokens:\s*\d+\s*>\s*(\d+)/i.exec(detail)?.[1];
4252
+ if (stated === void 0) return void 0;
4253
+ const ceiling = Number(stated);
4254
+ return Number.isSafeInteger(ceiling) && ceiling > 0 ? ceiling : void 0;
4255
+ }
3667
4256
  /**
3668
4257
  * The advice floor for a Provider-API timeout: doubling a 60 s budget is still
3669
4258
  * short for a slow upstream first token, so the suggestion never goes below 5
3670
- * minutes. Capped by `RETRY_MAX_DELAY_MS` rather than the schema's timer
3671
- * ceiling on purpose — a single header wait that outlasts the whole retry
3672
- * backoff is not a budget, and clamping to the engine constant would also pull
3673
- * a runtime dependency into the bundle.
4259
+ * minutes. 提示上限与页面可编辑范围相同,重试退避上限不约束单次请求。
3674
4260
  */
3675
4261
  const MIN_SUGGESTED_TIMEOUT_MS = 3e5;
3676
4262
  /**
@@ -3694,10 +4280,13 @@ const MIN_SUGGESTED_TIMEOUT_MS = 3e5;
3694
4280
  * @param timeoutMs - the header budget that just expired.
3695
4281
  */
3696
4282
  function headersTimeoutAdvice(protocol, timeoutMs) {
3697
- if (protocol === "openai") return {
3698
- cause: "upstream-first-token",
3699
- suggestionMs: Math.min(RETRY_MAX_DELAY_MS, Math.max(timeoutMs * 2, MIN_SUGGESTED_TIMEOUT_MS))
3700
- };
4283
+ if (protocol === "openai") {
4284
+ const suggestionMs = Math.min(MAX_TIMEOUT_SECONDS * 1e3, Math.max(timeoutMs * 2, MIN_SUGGESTED_TIMEOUT_MS));
4285
+ return {
4286
+ cause: "upstream-first-token",
4287
+ ...suggestionMs > timeoutMs ? { suggestionMs } : {}
4288
+ };
4289
+ }
3701
4290
  return { cause: "gateway" };
3702
4291
  }
3703
4292
  /**
@@ -3711,6 +4300,8 @@ function headersTimeoutAdvice(protocol, timeoutMs) {
3711
4300
  * answer either way.
3712
4301
  */
3713
4302
  const MAX_CONSECUTIVE_HEADER_TIMEOUTS = 3;
4303
+ /** Streak key for a session-less call, so those keep the old single-slot semantics instead of never escalating. */
4304
+ const GLOBAL_HEADER_TIMEOUT_KEY = "\0global";
3714
4305
  /** UTF-8 byte length, computed only on the timeout path so a large body is encoded once, not per attempt. */
3715
4306
  function utf8ByteLength(text) {
3716
4307
  return new TextEncoder().encode(text).length;
@@ -3777,9 +4368,21 @@ function streamErrorToLlmError(value, fallbackMessage) {
3777
4368
  message
3778
4369
  ].filter((part) => part !== void 0 && part !== "").join(" ");
3779
4370
  const statusOption = statusCode !== void 0 ? { status: statusCode } : void 0;
4371
+ if (OUTPUT_LIMIT_REJECTION.test(detail)) {
4372
+ const stated = /max_tokens:\s*\d+\s*>\s*\d+/i.exec(detail)?.[0];
4373
+ 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);
4374
+ }
3780
4375
  if (isContextOverflowDetail(detail)) return bilingual(CONTEXT_WINDOW_EXCEEDED_CODE, `Command Code stream error: ${message}`, "Command Code API 拒绝了这次请求:内容超出模型上下文窗口。正在压缩上下文后重试;如仍失败,请新建会话或减少上下文", statusOption);
3781
4376
  const terminal = hasTerminalStreamMarker(detail);
3782
4377
  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);
4378
+ const rejection = classifyAccountRejection(statusCode ?? 0, JSON.stringify(value) ?? message);
4379
+ if (rejection?.reason === "rate-limit" || rejection?.reason === "throttled") {
4380
+ const wait = rejection.resetAtMs === void 0 ? 0 : Math.min(RETRY_MAX_DELAY_MS, Math.max(1e3, rejection.resetAtMs - Date.now()));
4381
+ return bilingual(rejection.reason === "rate-limit" ? "RATE_LIMIT" : THROTTLED_CODE, `Command Code stream error: ${message}`, rejection.reason === "rate-limit" ? "Command Code 用量窗口已限流,等待恢复" : "Command Code 请求暂时被限流,正在重试", {
4382
+ ...statusOption,
4383
+ ...wait > 0 ? { providerRetryAfterMs: wait } : {}
4384
+ });
4385
+ }
3783
4386
  return new LlmError(`Command Code stream error: ${message}`, "SERVER", statusOption);
3784
4387
  }
3785
4388
  function recordOrEmpty(value) {
@@ -3944,8 +4547,9 @@ function resolveAuthFileApiKey() {
3944
4547
  return apiKeyFromCredentialRecord(parsed.commandcode) ?? apiKeyFromCredentialRecord(parsed["command-code"]);
3945
4548
  } catch {}
3946
4549
  }
3947
- function parseCatalogResponse(value) {
4550
+ function parseCatalogResponse(value, apiBase) {
3948
4551
  if (!isRecord(value) || value.object !== "list" || !Array.isArray(value.data)) throw new LlmError("Unexpected Command Code models response shape", "PROVIDER_PROTOCOL_ERROR");
4552
+ publishedOutputLimits.set(apiBase, /* @__PURE__ */ new Map());
3949
4553
  const models = [];
3950
4554
  for (const entry of value.data) {
3951
4555
  if (!isRecord(entry)) continue;
@@ -3953,33 +4557,52 @@ function parseCatalogResponse(value) {
3953
4557
  const name = stringValue(entry.name);
3954
4558
  const contextLength = numberValue(entry.context_length);
3955
4559
  if (!id || !name || !contextLength || contextLength <= 0) continue;
4560
+ const endpoints = Array.isArray(entry.supported_endpoints) ? entry.supported_endpoints.filter((item) => typeof item === "string") : [];
4561
+ const published = numberValue(entry.max_output_tokens);
4562
+ if (published !== void 0 && published > 0) outputLimitsFor(publishedOutputLimits, apiBase).set(id, published);
3956
4563
  models.push({
3957
4564
  id,
3958
4565
  name,
3959
4566
  contextWindow: contextLength,
3960
- maxTokens: Math.min(contextLength, DEFAULT_MAX_OUTPUT_TOKENS)
4567
+ maxTokens: Math.min(contextLength, modelOutputTokenLimit(apiBase, id)),
4568
+ ...published !== void 0 && published > 0 ? { publishedMaxTokens: published } : {},
4569
+ supportedEndpoints: endpoints
3961
4570
  });
3962
4571
  }
3963
4572
  if (models.length === 0) throw new LlmError("Command Code returned an empty model catalog", "PROVIDER_PROTOCOL_ERROR");
3964
4573
  return models;
3965
4574
  }
3966
- async function readModelsCache(cachePath) {
4575
+ async function readModelsCache(cachePath, apiBase, restorePublished = true) {
3967
4576
  const parsed = JSON.parse(await readFile(cachePath, "utf-8"));
3968
- if (!isRecord(parsed) || parsed.version !== MODEL_CACHE_VERSION || !Array.isArray(parsed.models)) throw new Error(`Invalid model cache at ${cachePath}`);
3969
- return parsed.models;
4577
+ if (!isRecord(parsed) || parsed.version !== MODEL_CACHE_VERSION || parsed.apiBase !== apiBase || !Array.isArray(parsed.models)) throw new Error(`Invalid model cache at ${cachePath}`);
4578
+ if (restorePublished) publishedOutputLimits.set(apiBase, /* @__PURE__ */ new Map());
4579
+ return parsed.models.map((entry) => {
4580
+ const model = isRecord(entry) ? entry : {};
4581
+ const endpoints = Array.isArray(model.supportedEndpoints) ? model.supportedEndpoints.filter((item) => typeof item === "string") : [];
4582
+ const id = stringValue(model.id);
4583
+ const published = numberValue(model.publishedMaxTokens);
4584
+ if (restorePublished && id !== void 0 && published !== void 0 && published > 0) outputLimitsFor(publishedOutputLimits, apiBase).set(id, published);
4585
+ const contextWindow = numberValue(model.contextWindow);
4586
+ return {
4587
+ ...model,
4588
+ supportedEndpoints: endpoints,
4589
+ maxTokens: id === void 0 || contextWindow === void 0 ? numberValue(model.maxTokens) ?? 131072 : Math.min(contextWindow, modelOutputTokenLimit(apiBase, id), published !== void 0 && published > 0 ? published : Infinity)
4590
+ };
4591
+ });
3970
4592
  }
3971
- async function writeModelsCache(cachePath, models) {
4593
+ async function writeModelsCache(cachePath, apiBase, models, mayCommit) {
3972
4594
  await mkdir(dirname(cachePath), { recursive: true });
3973
- const tmp = `${cachePath}.${process.pid}.tmp`;
4595
+ const tmp = `${cachePath}.${process.pid}.${randomUUID()}.tmp`;
3974
4596
  try {
3975
4597
  await writeFile(tmp, `${JSON.stringify({
3976
4598
  version: MODEL_CACHE_VERSION,
4599
+ apiBase,
3977
4600
  models
3978
4601
  }, null, 2)}\n`, {
3979
4602
  encoding: "utf-8",
3980
4603
  mode: 384
3981
4604
  });
3982
- await rename(tmp, cachePath);
4605
+ if (mayCommit()) await rename(tmp, cachePath);
3983
4606
  } finally {
3984
4607
  await rm(tmp, { force: true }).catch(() => void 0);
3985
4608
  }
@@ -4229,6 +4852,38 @@ function seenImageOffloadCount(messages, model) {
4229
4852
  async function readRequestImage(attachments, ref) {
4230
4853
  return attachments.readImageRequest(ref, requestImageTarget(ref));
4231
4854
  }
4855
+ /** 同一次生成及其协议切换共享图片读取,最多同时准备四张,避免内存峰值失控。 */
4856
+ function requestImages(attachments, signal, timing) {
4857
+ const cache = /* @__PURE__ */ new Map();
4858
+ const read = (ref) => {
4859
+ signal?.throwIfAborted();
4860
+ const key = JSON.stringify([ref.attachmentId, requestImageTarget(ref)]);
4861
+ let pending = cache.get(key);
4862
+ if (!pending) {
4863
+ pending = readRequestImage(attachments, ref);
4864
+ cache.set(key, pending);
4865
+ }
4866
+ return pending;
4867
+ };
4868
+ read.prepare = async (refs) => {
4869
+ if (refs.length === 0) return;
4870
+ const finishImages = timing.phase("images");
4871
+ let cursor = 0;
4872
+ try {
4873
+ const workers = Array.from({ length: Math.min(4, refs.length) }, async () => {
4874
+ while (cursor < refs.length) {
4875
+ signal?.throwIfAborted();
4876
+ await read(refs[cursor++]);
4877
+ }
4878
+ });
4879
+ const failure = (await Promise.allSettled(workers)).find((result) => result.status === "rejected");
4880
+ if (failure?.status === "rejected") throw failure.reason;
4881
+ } finally {
4882
+ finishImages();
4883
+ }
4884
+ };
4885
+ return read;
4886
+ }
4232
4887
  /**
4233
4888
  * Price one request occurrence.
4234
4889
  *
@@ -4272,6 +4927,17 @@ async function imageToCommandCode(ref, readImage) {
4272
4927
  };
4273
4928
  }
4274
4929
  /**
4930
+ * The signature recorded for the reasoning block at `position` of a
4931
+ * historical assistant message, or undefined when the message was produced
4932
+ * by another route or an older plugin version.
4933
+ */
4934
+ function messagesSignatureAt(message, position) {
4935
+ const replay = message.source?.replayState;
4936
+ if (!isRecord(replay) || !Array.isArray(replay.blocks)) return void 0;
4937
+ const entry = replay.blocks[position];
4938
+ return isRecord(entry) ? stringValue(entry.signature) : void 0;
4939
+ }
4940
+ /**
4275
4941
  * The common pairing and image-carrier ordering for both transports. Each
4276
4942
  * codec owns its wire shape: CLI content blocks and Chat Completions
4277
4943
  * reasoning/tool calls differ beyond the image envelope.
@@ -4279,6 +4945,16 @@ async function imageToCommandCode(ref, readImage) {
4279
4945
  async function convertMessages(messages, readImage, codec) {
4280
4946
  const out = [];
4281
4947
  const { ids: paired, names: toolNames } = pairedToolCalls(messages);
4948
+ if (readImage?.prepare) {
4949
+ const refs = [];
4950
+ for (const message of messages) {
4951
+ const result = toolResultOf(message);
4952
+ if (message.role === "user" && !result) {
4953
+ for (const block of message.content) if (block.type === "image") refs.push(block.attachment);
4954
+ } else if (result && paired.has(result.toolCallId)) refs.push(...toolResultMedia(result).images);
4955
+ }
4956
+ await readImage.prepare(refs);
4957
+ }
4282
4958
  const wireIds = wireToolCallIds(paired);
4283
4959
  const pendingImages = [];
4284
4960
  const flushPendingImages = () => {
@@ -4314,8 +4990,8 @@ async function convertMessages(messages, readImage, codec) {
4314
4990
  if (!result || !paired.has(result.toolCallId)) continue;
4315
4991
  const media = toolResultMedia(result);
4316
4992
  const wireId = wireIds.get(result.toolCallId) ?? result.toolCallId;
4317
- out.push(codec.encodeTool(result, media, wireId, toolNames.get(result.toolCallId) || "unknown"));
4318
- if (media.images.length > 0) {
4993
+ out.push(await codec.encodeTool(result, media, wireId, toolNames.get(result.toolCallId) || "unknown", readImage));
4994
+ if (!codec.carriesToolResultMedia && media.images.length > 0) {
4319
4995
  const carried = [{
4320
4996
  type: "text",
4321
4997
  text: toolResultImageNote(media, wireId)
@@ -4328,6 +5004,7 @@ async function convertMessages(messages, readImage, codec) {
4328
5004
  return out;
4329
5005
  }
4330
5006
  const ccMessageCodec = {
5007
+ carriesToolResultMedia: false,
4331
5008
  encodeImage: imageToCommandCode,
4332
5009
  encodeUser: (parts) => ({
4333
5010
  role: "user",
@@ -4395,6 +5072,7 @@ async function imageToOpenAI(ref, readImage) {
4395
5072
  * replayed (same policy as the CLI path).
4396
5073
  */
4397
5074
  const openAiMessageCodec = {
5075
+ carriesToolResultMedia: false,
4398
5076
  encodeImage: imageToOpenAI,
4399
5077
  encodeUser: (parts) => {
4400
5078
  return !parts.some((part) => part.type === "image_url") && parts.length === 1 ? {
@@ -4434,6 +5112,119 @@ const openAiMessageCodec = {
4434
5112
  async function messagesToOpenAI(messages, readImage) {
4435
5113
  return convertMessages(messages, readImage, openAiMessageCodec);
4436
5114
  }
5115
+ /**
5116
+ * Convert one image reference to the Messages wire shape. Unlike the other two
5117
+ * transports this one keeps the native media type rather than a data URL, and
5118
+ * it may be used INSIDE a tool result — `tool_result.content` accepts image
5119
+ * blocks, which is why this codec sets `carriesToolResultMedia`.
5120
+ */
5121
+ async function imageToMessages(ref, readImage) {
5122
+ const version = await readImage(ref);
5123
+ return {
5124
+ type: "image",
5125
+ source: {
5126
+ type: "base64",
5127
+ media_type: version.mediaType,
5128
+ data: Buffer.from(version.data).toString("base64")
5129
+ }
5130
+ };
5131
+ }
5132
+ /** A tool call's `arguments` is a raw JSON string in the harness, an object here. */
5133
+ function parseToolArguments(raw) {
5134
+ try {
5135
+ const parsed = JSON.parse(raw);
5136
+ return isRecord(parsed) ? parsed : {};
5137
+ } catch {
5138
+ return {};
5139
+ }
5140
+ }
5141
+ /**
5142
+ * Convert harness messages to the Anthropic Messages request shape.
5143
+ *
5144
+ * Differences from the other two transports, all measured against the live
5145
+ * endpoint on 2026-09-29:
5146
+ *
5147
+ * - Content is always a block array; there is no string shorthand, so a
5148
+ * single-text user turn is still `{ role: 'user', content: [{type:'text'…}] }`.
5149
+ * - Tool results live in a `user` turn as `tool_result` blocks (not a
5150
+ * `role: 'tool'` message) and may carry images natively, so no image is
5151
+ * split into a separate user message here.
5152
+ * - A replayed `thinking` block is only valid WITH its `signature`: the
5153
+ * endpoint answers `thinking.signature: Field required` without it, and
5154
+ * `each thinking block must contain thinking` when the text is empty and
5155
+ * the signature is blank. A reasoning block with no recorded signature is
5156
+ * therefore dropped rather than sent — replaying it would fail the request,
5157
+ * while omitting it is accepted (verified 200 both ways).
5158
+ * - The conversation must end with a user turn; the endpoint refuses an
5159
+ * assistant prefill (`This model does not support assistant message
5160
+ * prefill`), which `convertMessages` already guarantees for a tool loop.
5161
+ */
5162
+ const messagesMessageCodec = {
5163
+ carriesToolResultMedia: true,
5164
+ encodeImage: imageToMessages,
5165
+ encodeUser: (parts) => ({
5166
+ role: "user",
5167
+ content: parts
5168
+ }),
5169
+ encodeAssistant: (message, paired, wireIds) => {
5170
+ const parts = [];
5171
+ let position = 0;
5172
+ for (const block of message.content) {
5173
+ if (block.type === "text") {
5174
+ parts.push({
5175
+ type: "text",
5176
+ text: block.text
5177
+ });
5178
+ position++;
5179
+ continue;
5180
+ }
5181
+ if (block.type === "reasoning") {
5182
+ const signature = messagesSignatureAt(message, position);
5183
+ if (signature !== void 0) parts.push({
5184
+ type: "thinking",
5185
+ thinking: block.text,
5186
+ signature
5187
+ });
5188
+ position++;
5189
+ continue;
5190
+ }
5191
+ if (block.type === "tool-call" && paired.has(block.id)) {
5192
+ parts.push({
5193
+ type: "tool_use",
5194
+ id: wireIds.get(block.id) ?? block.id,
5195
+ name: block.name,
5196
+ input: parseToolArguments(block.arguments)
5197
+ });
5198
+ position++;
5199
+ }
5200
+ }
5201
+ return parts.length > 0 ? {
5202
+ role: "assistant",
5203
+ content: parts
5204
+ } : void 0;
5205
+ },
5206
+ encodeTool: async (result, media, wireId, _toolName, readImage) => {
5207
+ const content = [];
5208
+ if (readImage) for (const attachment of media.images) content.push(await imageToMessages(attachment, readImage));
5209
+ content.push({
5210
+ type: "text",
5211
+ text: toolResultTextForWire(media)
5212
+ });
5213
+ const toolResult = {
5214
+ type: "tool_result",
5215
+ tool_use_id: wireId,
5216
+ content
5217
+ };
5218
+ if (result.isError) toolResult.is_error = true;
5219
+ return {
5220
+ role: "user",
5221
+ content: [toolResult]
5222
+ };
5223
+ }
5224
+ };
5225
+ async function messagesToMessages(messages, readImage) {
5226
+ return convertMessages(messages, readImage, messagesMessageCodec);
5227
+ }
4437
5228
  /** Account endpoints fetched by one `getUsage()` run (see the classification there). */
4438
5229
  const USAGE_ENDPOINT_COUNT = 4;
4439
5230
  /**
@@ -4561,6 +5352,50 @@ function cliSystem(systemText) {
4561
5352
  cache_control: { type: "ephemeral" }
4562
5353
  }];
4563
5354
  }
5355
+ /**
5356
+ * The Anthropic ephemeral cache marker, shared by every breakpoint this route
5357
+ * sets. One object is reused across positions because it is only ever
5358
+ * serialized, never mutated.
5359
+ *
5360
+ * No `ttl` is sent: the default 5-minute entry covers a continuously advancing
5361
+ * session, and `ttl: "1h"` is a separate billable tier whose read/write prices
5362
+ * are identical — it would only lengthen survival, not reduce cost.
5363
+ */
5364
+ const EPHEMERAL_CACHE_CONTROL = { type: "ephemeral" };
5365
+ /**
5366
+ * Content-block types that may carry the rolling breakpoint. Matches what the
5367
+ * official Command Code provider for pi marks through
5368
+ * `@earendil-works/pi-ai`'s `anthropic-messages` transport (its own list also
5369
+ * holds pi-internal `tool_addition` / `tool_removal`, which this route never
5370
+ * emits). Assistant-side blocks are deliberately excluded: a breakpoint on a
5371
+ * `thinking` or `tool_use` block caches a prefix the next turn invalidates.
5372
+ */
5373
+ const CACHEABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
5374
+ "text",
5375
+ "image",
5376
+ "tool_result"
5377
+ ]);
5378
+ /**
5379
+ * Mark the last user turn as the rolling cache breakpoint.
5380
+ *
5381
+ * Anthropic caching is explicit — a prefix is stored only when a
5382
+ * `cache_control` marker names it — so an unmarked route re-prefills the whole
5383
+ * replayed history at the full input price on every turn. Measured cost of that
5384
+ * omission on a 10-turn session: $0.5648, all of it at the undiscounted
5385
+ * $2/M input rate, against $0.1869 with the three breakpoints this route sets.
5386
+ *
5387
+ * The final user message is the rolling boundary: DSH resends the whole
5388
+ * history each turn, so a marker here grows with the conversation, whereas a
5389
+ * fixed `system`-only marker would leave every later turn's history
5390
+ * uncached. {@link messagesMessageCodec} renders both user input and tool
5391
+ * results as `role: 'user'`, so this covers a tool loop's trailing turn too.
5392
+ */
5393
+ function markLastUserCacheControl(messages) {
5394
+ const last = messages[messages.length - 1];
5395
+ if (!isRecord(last) || last.role !== "user" || !Array.isArray(last.content)) return;
5396
+ const lastBlock = last.content[last.content.length - 1];
5397
+ if (isRecord(lastBlock) && typeof lastBlock.type === "string" && CACHEABLE_BLOCK_TYPES.has(lastBlock.type)) lastBlock.cache_control = EPHEMERAL_CACHE_CONTROL;
5398
+ }
4564
5399
  /** Build the legacy CLI (`/alpha/generate`) request body for one call. */
4565
5400
  async function buildCliBody(options, connection, facts) {
4566
5401
  return {
@@ -4578,6 +5413,7 @@ async function buildCliBody(options, connection, facts) {
4578
5413
  memory: null,
4579
5414
  taste: null,
4580
5415
  skills: null,
5416
+ permissionMode: "standard",
4581
5417
  params: {
4582
5418
  model: options.model,
4583
5419
  messages: await messagesToCC(options.messages, facts.readImage),
@@ -4621,39 +5457,107 @@ async function buildOpenAIBody(options, facts) {
4621
5457
  };
4622
5458
  }
4623
5459
  /**
4624
- * Estimate the text the selected transport actually sends. JSON framing and
4625
- * punctuation each get a token; ordinary ASCII words get one, with an extra
4626
- * allowance for long identifiers. UTF-8 bytes / 4 tracks the measured CJK
4627
- * case better than the engine's UTF-16 characters / 4. This is a heuristic,
4628
- * never proof that a prompt cannot fit: a valid long English prompt can still
4629
- * be larger under this estimate than under the provider's tokenizer.
4630
- */
4631
- function estimateWireInputTokens(protocol, body, messages, model) {
4632
- const params = protocol === "cli" ? recordOrEmpty(body.params) : body;
4633
- const wire = JSON.stringify(params, (_key, value) => typeof value === "string" && /^data:[^,]*;base64,/.test(value) ? "[image]" : value);
4634
- let tokens = 0;
4635
- for (const match of wire.matchAll(/[A-Za-z0-9_]+|[^\x00-\x7f]+|[^\s]/gu)) {
4636
- const part = match[0];
4637
- if (/^[A-Za-z0-9_]+$/.test(part)) tokens += 1 + Math.ceil(Math.max(0, part.length - 12) / 4);
4638
- else if (part.charCodeAt(0) > 127) tokens += Math.ceil(Buffer.byteLength(part) / 4);
4639
- else tokens += 1;
4640
- }
4641
- for (const message of messages) for (const block of message.content) if (block.type === "image") tokens += priceRequestImage(block, model, true).visualTokens;
4642
- return tokens;
4643
- }
4644
- function requestContextBudget(protocol, body, messages, model, contextWindow, requestedMaxTokens) {
4645
- if (contextWindow === void 0) return {
4646
- maxTokens: requestedMaxTokens,
4647
- sizePressure: false
4648
- };
4649
- const estimatedInput = estimateWireInputTokens(protocol, body, messages, model);
4650
- const headroom = Math.min(16384, Math.max(2048, Math.ceil(contextWindow * .02)));
4651
- const available = Math.floor(contextWindow - estimatedInput - headroom);
4652
- const floor = Math.min(requestedMaxTokens, MIN_CONTEXT_OUTPUT_TOKENS, Math.max(1, Math.floor(contextWindow / 8)));
4653
- return {
4654
- maxTokens: Math.min(requestedMaxTokens, Math.max(floor, available)),
4655
- sizePressure: estimatedInput >= contextWindow / 4 && estimatedInput + requestedMaxTokens + headroom >= contextWindow
5460
+ * Build the Anthropic Messages request body for one call.
5461
+ *
5462
+ * Measured deviations from DeepSeek's own Messages API (the shape
5463
+ * `dsh-llm-deepseek` implements), all confirmed on 2026-09-29:
5464
+ *
5465
+ * - `thinking` accepts ONLY `{ type: 'adaptive' }` here. `enabled` and
5466
+ * `disabled` are refused with `"thinking.type.enabled" is not supported for
5467
+ * this model. Use "thinking.type.adaptive" and "output_config.effort" to
5468
+ * control thinking behavior.`, and a `between_tools` value fails validation
5469
+ * outright. The model reasons on its own; this switch only admits it.
5470
+ * - Reasoning strength travels in `output_config.effort`, whose accepted
5471
+ * values are `low | medium | high | xhigh | max` — there is no `none`, and
5472
+ * `xhigh` is absent from DeepSeek's own type.
5473
+ * - `stop_sequences` is a real field here, but this adapter still rejects
5474
+ * `GenerateOptions.stop` in `stream()` because Command Code documents no
5475
+ * equivalent for the other two transports.
5476
+ * - **`temperature` is never sent.** Adaptive thinking constrains it to 1:
5477
+ * `` `temperature` may only be set to 1 when thinking is enabled or in
5478
+ * adaptive mode `` (measured 2026-09-29 — every request carrying the other
5479
+ * transports' 0.3 default was refused with HTTP 400). Omitting the field
5480
+ * is the only way to honour both the thinking switch and a caller that
5481
+ * asked for a temperature, so Claude loses sampling control on this route
5482
+ * because the endpoint, not this adapter, removed it.
5483
+ * - `system` travels as a one-block ARRAY carrying `cache_control`, not as the
5484
+ * bare string the other two transports send. Anthropic's caching is opt-in
5485
+ * per block, and this route is the only Claude path, so the marker is what
5486
+ * makes a session's replayed history bill at the cache-read rate instead of
5487
+ * the full input rate. See {@link EPHEMERAL_CACHE_CONTROL}.
5488
+ */
5489
+ async function buildMessagesBody(options, facts) {
5490
+ const body = {
5491
+ model: options.model,
5492
+ max_tokens: facts.maxTokens,
5493
+ stream: true,
5494
+ messages: await messagesToMessages(options.messages, facts.readImage)
4656
5495
  };
5496
+ if (facts.selectableEffort) body.thinking = { type: "adaptive" };
5497
+ if (facts.systemText) body.system = [{
5498
+ type: "text",
5499
+ text: facts.systemText,
5500
+ cache_control: EPHEMERAL_CACHE_CONTROL
5501
+ }];
5502
+ if (facts.reasoningEffort) body.output_config = { effort: facts.reasoningEffort };
5503
+ const tools = (options.tools ?? []).map((tool) => ({
5504
+ name: tool.name,
5505
+ description: tool.description,
5506
+ input_schema: toolParametersSchema(tool.parameters)
5507
+ }));
5508
+ if (tools.length > 0) {
5509
+ tools[tools.length - 1].cache_control = EPHEMERAL_CACHE_CONTROL;
5510
+ body.tools = tools;
5511
+ }
5512
+ markLastUserCacheControl(body.messages);
5513
+ return body;
5514
+ }
5515
+ /**
5516
+ * One connect attempt's inputs: everything `connectGenerate` needs beyond
5517
+ * the per-attempt key and protocol. Passed explicitly (not closed over) so
5518
+ * the rotation loop's mutation of `protocol`/`body` stays visible at the
5519
+ * call site.
5520
+ */
5521
+ /** The generate endpoint one protocol posts to. */
5522
+ function protocolEndpoint(protocol, apiBase) {
5523
+ if (protocol === "cli") return `${apiBase}/alpha/generate`;
5524
+ return protocol === "messages" ? `${apiBase}/provider/v1/messages` : `${apiBase}/provider/v1/chat/completions`;
5525
+ }
5526
+ /** 错误正文只供诊断与分类使用,不能让坏网关占满内存或无限拖住重试。 */
5527
+ async function readGenerateErrorBody(response, abort, timeoutMs) {
5528
+ if (response.body === null) return "";
5529
+ const reader = response.body.getReader();
5530
+ const decoder = new TextDecoder();
5531
+ let text = "";
5532
+ let bytes = 0;
5533
+ const limit = 65536;
5534
+ let removeAbort = () => void 0;
5535
+ const stopped = new Promise((resolve) => {
5536
+ if (abort.signal.aborted) resolve(void 0);
5537
+ else abort.signal.addEventListener("abort", onAbort, { once: true });
5538
+ function onAbort() {
5539
+ resolve(void 0);
5540
+ }
5541
+ removeAbort = () => abort.signal.removeEventListener("abort", onAbort);
5542
+ });
5543
+ const timer = setTimeout(() => abort.abort(), Math.min(timeoutMs, 3e4));
5544
+ try {
5545
+ while (bytes < limit) {
5546
+ const read = await Promise.race([reader.read(), stopped]);
5547
+ if (read === void 0 || read.done) break;
5548
+ const remaining = limit - bytes;
5549
+ text += decoder.decode(read.value.subarray(0, remaining), { stream: true });
5550
+ bytes += read.value.byteLength;
5551
+ }
5552
+ return text + decoder.decode();
5553
+ } catch {
5554
+ return text + decoder.decode();
5555
+ } finally {
5556
+ clearTimeout(timer);
5557
+ removeAbort();
5558
+ reader.cancel().catch(() => void 0);
5559
+ reader.releaseLock();
5560
+ }
4657
5561
  }
4658
5562
  /**
4659
5563
  * One pre-stream connect attempt: POST the body and wait for response
@@ -4663,11 +5567,11 @@ function requestContextBudget(protocol, body, messages, model, contextWindow, re
4663
5567
  * on success the caller-abort listener outlives the connect phase (it aborts a
4664
5568
  * stalled body read), so the streaming tail calls cleanup.
4665
5569
  */
4666
- async function connectGenerate(deps, key, protocol, body) {
5570
+ async function connectGenerate(deps, key, protocol, serializedBody) {
4667
5571
  const { options, connection, fetchImpl } = deps;
4668
5572
  const connectAbort = new AbortController();
4669
5573
  let connectTimedOut = false;
4670
- const endpoint = protocol === "cli" ? `${connection.apiBase}/alpha/generate` : `${connection.apiBase}/provider/v1/chat/completions`;
5574
+ const endpoint = protocolEndpoint(protocol, connection.apiBase);
4671
5575
  const connectTimer = setTimeout(() => {
4672
5576
  connectTimedOut = true;
4673
5577
  connectAbort.abort(new DOMException(`Command Code API request to ${endpoint} did not respond within ${connection.requestTimeoutMs}ms`, "TimeoutError"));
@@ -4685,26 +5589,25 @@ async function connectGenerate(deps, key, protocol, body) {
4685
5589
  };
4686
5590
  let response;
4687
5591
  const zdr = connection.zdr === true;
4688
- const headers = protocol === "cli" ? {
5592
+ const sharedHeaders = {
4689
5593
  "Content-Type": "application/json",
4690
5594
  ...IDENTITY_ENCODING_HEADER,
4691
5595
  Authorization: `Bearer ${key}`,
5596
+ ...zdr ? { "x-cmd-zdr": "1" } : {},
5597
+ ...attributionHeaders()
5598
+ };
5599
+ const headers = protocol === "cli" ? {
5600
+ ...sharedHeaders,
4692
5601
  "x-command-code-version": COMMAND_CODE_CLI_VERSION,
4693
5602
  "x-cli-environment": "production",
4694
5603
  "x-project-slug": projectSlugFromPath(connection.workingDir),
4695
- "x-taste-learning": "true",
4696
- "x-co-flag": "false",
4697
- ...zdr ? { "x-cmd-zdr": "1" } : {},
4698
- ...attributionHeaders()
5604
+ "x-taste-learning": "false",
5605
+ "x-co-flag": "false"
4699
5606
  } : {
4700
- "Content-Type": "application/json",
4701
- ...IDENTITY_ENCODING_HEADER,
4702
- Authorization: `Bearer ${key}`,
4703
- Accept: "text/event-stream",
4704
- ...zdr ? { "x-cmd-zdr": "1" } : {},
4705
- ...attributionHeaders()
5607
+ ...sharedHeaders,
5608
+ Accept: "text/event-stream"
4706
5609
  };
4707
- const serializedBody = JSON.stringify(body);
5610
+ const finishHeaders = deps.timing.attempt(protocol, Buffer.byteLength(serializedBody));
4708
5611
  try {
4709
5612
  response = await fetchImpl(endpoint, {
4710
5613
  method: "POST",
@@ -4712,8 +5615,10 @@ async function connectGenerate(deps, key, protocol, body) {
4712
5615
  body: serializedBody,
4713
5616
  signal: connectAbort.signal
4714
5617
  });
5618
+ finishHeaders(response.status);
4715
5619
  clearTimeout(connectTimer);
4716
5620
  } catch (error) {
5621
+ finishHeaders();
4717
5622
  cleanup();
4718
5623
  if (options.signal?.aborted) throw error;
4719
5624
  if (connectTimedOut || error instanceof DOMException && error.name === "TimeoutError") throw headersTimeoutError({
@@ -4727,8 +5632,13 @@ async function connectGenerate(deps, key, protocol, body) {
4727
5632
  }
4728
5633
  deps.onResponse(response);
4729
5634
  if (!response.ok) {
4730
- const errText = await response.text().catch(() => "");
4731
- cleanup();
5635
+ let errText;
5636
+ try {
5637
+ errText = await readGenerateErrorBody(response, connectAbort, connection.requestTimeoutMs);
5638
+ if (options.signal?.aborted) throw options.signal.reason ?? new DOMException("已取消请求", "AbortError");
5639
+ } finally {
5640
+ cleanup();
5641
+ }
4732
5642
  const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
4733
5643
  return retryAfterMs === void 0 ? {
4734
5644
  status: response.status,
@@ -4755,7 +5665,10 @@ function createBlockAssembler() {
4755
5665
  sawContent: false,
4756
5666
  finishReason: void 0,
4757
5667
  cliCacheWriteTokens: void 0,
4758
- openAiToolCalls: []
5668
+ openAiToolCalls: [],
5669
+ messagesBlocks: /* @__PURE__ */ new Map(),
5670
+ messagesUsage: void 0,
5671
+ replayBlocks: []
4759
5672
  };
4760
5673
  }
4761
5674
  function* closeText(asm) {
@@ -4816,9 +5729,10 @@ function* emitOpenAiToolCalls(asm) {
4816
5729
  }
4817
5730
  asm.openAiToolCalls.length = 0;
4818
5731
  }
4819
- var CommandCodeAdapter = class extends LlmAdapter {
5732
+ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
4820
5733
  deps;
4821
5734
  catalog = [];
5735
+ catalogState;
4822
5736
  fetchImpl;
4823
5737
  resolveAttachments;
4824
5738
  /**
@@ -4829,13 +5743,25 @@ var CommandCodeAdapter = class extends LlmAdapter {
4829
5743
  billingAccess = /* @__PURE__ */ new Map();
4830
5744
  billingAccessInflight = /* @__PURE__ */ new Map();
4831
5745
  protocolCache = /* @__PURE__ */ new Map();
5746
+ /** Namespace account-derived facts by gateway without exposing the key. */
5747
+ accountCacheKey(apiBase, apiKey) {
5748
+ return JSON.stringify([apiBase, apiKey]);
5749
+ }
4832
5750
  /**
4833
5751
  * Consecutive header timeouts for the last request shape, counted ACROSS
4834
5752
  * `stream()` calls: dsh-llm-retry re-invokes this same instance, so this is
4835
- * the only place a streak can live. One slot rather than a map is the point —
4836
- * the claim is "consecutive", and a single record cannot grow.
5753
+ * the only place a streak can live. Keyed by `sessionId` (not by `agent`:
5754
+ * `GenerateOptions` carries no agent reference, only `sessionId`) so
5755
+ * concurrent sessions on this one shared adapter instance cannot clear or
5756
+ * inflate each other's count. A call with no `sessionId` (a one-shot,
5757
+ * session-less caller) falls back to {@link GLOBAL_HEADER_TIMEOUT_KEY},
5758
+ * matching the old single-slot behavior for exactly that case. Bounded by
5759
+ * {@link MAX_HEADER_TIMEOUT_STREAK_ENTRIES} with FIFO eviction (`Map`
5760
+ * preserves insertion order) since the key space is per-session rather than
5761
+ * per-account/model, so it is not naturally small like `protocolCache`.
4837
5762
  */
4838
- headerTimeoutStreak;
5763
+ headerTimeoutStreaks = /* @__PURE__ */ new Map();
5764
+ static MAX_HEADER_TIMEOUT_STREAK_ENTRIES = 200;
4839
5765
  constructor(deps) {
4840
5766
  super();
4841
5767
  this.deps = deps;
@@ -4848,25 +5774,31 @@ var CommandCodeAdapter = class extends LlmAdapter {
4848
5774
  * payload eventually leaves the retryable whitelist.
4849
5775
  *
4850
5776
  * `providerRetryPolicy()` cannot express this itself — dsh-llm's
4851
- * `NormalRetryPolicyConfig` carries a single scalar `maxRetries` next to the
4852
- * code list, so there is no per-code limit to lower. Escalating from here is
4853
- * the only way to keep `RATE_LIMIT` / `SERVER` / `TRANSPORT` retryable (each
4854
- * carries a real "try again later" signal) while stopping the one code that,
4855
- * repeated unchanged, only burns the user's session.
5777
+ * `NormalRetryPolicyConfig` carries one `maxRetries` for every listed code.
5778
+ * Escalating here stops repeated identical header timeouts with a specific
5779
+ * diagnosis; the agent listener separately caps ordinary transient errors.
4856
5780
  *
4857
5781
  * @returns the error to surface: unchanged for any other failure, the
4858
5782
  * original timeout below the threshold, and a terminal one at it.
4859
5783
  */
4860
- gradeHeaderTimeout(error, protocol, body, timeoutMs) {
5784
+ gradeHeaderTimeout(error, protocol, body, timeoutMs, sessionId) {
4861
5785
  if (!(error instanceof LlmError && error.code === "TIMEOUT")) return error;
5786
+ const key = sessionId ?? GLOBAL_HEADER_TIMEOUT_KEY;
4862
5787
  const fingerprint = `${protocol}:${fingerprintValue(body).sha256}`;
4863
- const count = this.headerTimeoutStreak?.fingerprint === fingerprint ? this.headerTimeoutStreak.count + 1 : 1;
4864
- this.headerTimeoutStreak = {
5788
+ const previous = this.headerTimeoutStreaks.get(key);
5789
+ const count = previous?.fingerprint === fingerprint ? previous.count + 1 : 1;
5790
+ if (previous === void 0) {
5791
+ if (this.headerTimeoutStreaks.size >= CommandCodeAdapter.MAX_HEADER_TIMEOUT_STREAK_ENTRIES) {
5792
+ const oldest = this.headerTimeoutStreaks.keys().next().value;
5793
+ if (oldest !== void 0) this.headerTimeoutStreaks.delete(oldest);
5794
+ }
5795
+ } else this.headerTimeoutStreaks.delete(key);
5796
+ this.headerTimeoutStreaks.set(key, {
4865
5797
  fingerprint,
4866
5798
  count
4867
- };
5799
+ });
4868
5800
  if (count < MAX_CONSECUTIVE_HEADER_TIMEOUTS) return error;
4869
- this.headerTimeoutStreak = void 0;
5801
+ this.headerTimeoutStreaks.delete(key);
4870
5802
  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 套餐容量限制或网络/代理链路问题。继续原样重发同一个请求通常无济于事——可以等服务商恢复,或换模型/换账号。`);
4871
5803
  }
4872
5804
  /**
@@ -4883,12 +5815,11 @@ var CommandCodeAdapter = class extends LlmAdapter {
4883
5815
  };
4884
5816
  }
4885
5817
  /**
4886
- * Near-unbounded retry for transient failures only (`mode: 'normal'` with
4887
- * an explicit 1000-attempt cap — opencode-style persistence without the
4888
- * unbounded loop): `RATE_LIMIT`/`SERVER`/`TIMEOUT`/`TRANSPORT`/
4889
- * `EMPTY_RESPONSE` retry up to 1000 times with waits doubling from 500 ms
4890
- * and capping at 15 minutes (±10% jitter), so an exhausted 5-hour window
4891
- * recovers in-session instead of failing after two tries. Permanent failures
5818
+ * The official executor owns backoff and provider reset waits. Its high
5819
+ * route-wide cap lets a confirmed `RATE_LIMIT` window recover in-session;
5820
+ * our request-error listener limits `SERVER`/`TIMEOUT`/`EMPTY_RESPONSE`/
5821
+ * `THROTTLED` to three retries and `TRANSPORT` to its separate budget.
5822
+ * Waits double from 500 ms and cap at 15 minutes (±10% jitter). Permanent failures
4892
5823
  * (an invalid key's `INVALID_CREDENTIAL`, `UNSUPPORTED_CONTENT`, plan
4893
5824
  * rejections) are absent from the whitelist and surface immediately instead
4894
5825
  * of looping. Waits the pool/adapter attach as `providerRetryAfterMs` are
@@ -4906,6 +5837,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4906
5837
  retryableCodes: [
4907
5838
  "EMPTY_RESPONSE",
4908
5839
  "RATE_LIMIT",
5840
+ THROTTLED_CODE,
4909
5841
  "SERVER",
4910
5842
  "TIMEOUT",
4911
5843
  "TRANSPORT"
@@ -4932,23 +5864,72 @@ var CommandCodeAdapter = class extends LlmAdapter {
4932
5864
  const imageCapable = KNOWN_IMAGE_MODELS.has(model);
4933
5865
  return { priceImages: (images) => images.map((image) => priceRequestImage(image, model, imageCapable)) };
4934
5866
  }
4935
- /** Refresh the catalog (live fetch, cache fallback) and return it. */
5867
+ /** 地址或缓存文件变化时丢弃旧目录,防止旧网关的路由声明影响新请求。 */
5868
+ currentCatalogState(connection = this.deps.options()) {
5869
+ const { apiBase, modelsCachePath } = connection;
5870
+ const source = JSON.stringify([apiBase, modelsCachePath]);
5871
+ if (this.catalogState?.source !== source) {
5872
+ this.catalog = [];
5873
+ this.catalogState = {
5874
+ source,
5875
+ expiresAt: 0
5876
+ };
5877
+ }
5878
+ return this.catalogState;
5879
+ }
5880
+ /** 直接生成也使用本地可信目录,不额外联网;并发冷请求共用一次文件读取。 */
5881
+ async catalogForGenerate(connection) {
5882
+ const state = this.currentCatalogState(connection);
5883
+ if (this.catalog.length > 0 || state.expiresAt > 0) return this.catalog;
5884
+ state.disk ??= readModelsCache(connection.modelsCachePath, connection.apiBase, false).catch(() => []);
5885
+ const cached = await state.disk;
5886
+ if (this.catalogState !== state) return cached;
5887
+ if (this.catalog.length > 0 || state.expiresAt > 0) return this.catalog;
5888
+ if (state.inflight === void 0) this.catalog = cached;
5889
+ return cached;
5890
+ }
5891
+ /** 共享一次刷新;调用者取消只结束自己的等待,不取消其他调用者的请求。 */
4936
5892
  async loadCatalog(signal) {
4937
- const { apiBase, modelsCachePath } = this.deps.options();
4938
- try {
4939
- const { status, record } = await this.fetchJson(`${apiBase}/provider/v1/models`, {
4940
- accept: "application/json",
4941
- ...IDENTITY_ENCODING_HEADER,
4942
- ...attributionHeaders()
4943
- }, MODELS_TIMEOUT_MS, signal);
4944
- if (status < 200 || status >= 300) throw new Error(`models endpoint returned ${status}`);
4945
- this.catalog = parseCatalogResponse(record);
4946
- await writeModelsCache(modelsCachePath, this.catalog).catch(() => void 0);
4947
- } catch (error) {
4948
- if (signal?.aborted) throw error;
4949
- this.catalog = await readModelsCache(modelsCachePath).catch(() => this.catalog);
5893
+ signal?.throwIfAborted();
5894
+ const connection = this.deps.options();
5895
+ const state = this.currentCatalogState(connection);
5896
+ if (Date.now() < state.expiresAt) return this.catalog;
5897
+ const { apiBase, modelsCachePath } = connection;
5898
+ if (!state.inflight) {
5899
+ const fallback = this.catalog;
5900
+ state.inflight = (async () => {
5901
+ let models;
5902
+ let ttl = MODEL_CATALOG_TTL_MS;
5903
+ try {
5904
+ const { status, record } = await this.fetchJson(`${apiBase}/provider/v1/models`, {
5905
+ accept: "application/json",
5906
+ ...IDENTITY_ENCODING_HEADER,
5907
+ ...attributionHeaders()
5908
+ }, MODELS_TIMEOUT_MS);
5909
+ if (status < 200 || status >= 300) throw new Error(`models endpoint returned ${status}`);
5910
+ models = parseCatalogResponse(record, apiBase);
5911
+ if (this.catalogState === state) await writeModelsCache(modelsCachePath, apiBase, models, () => this.catalogState === state).catch(() => void 0);
5912
+ } catch {
5913
+ ttl = MODEL_CATALOG_RETRY_MS;
5914
+ models = await readModelsCache(modelsCachePath, apiBase).catch(() => fallback);
5915
+ }
5916
+ if (this.catalogState === state) {
5917
+ this.catalog = models;
5918
+ state.expiresAt = Date.now() + ttl;
5919
+ }
5920
+ return models;
5921
+ })().finally(() => {
5922
+ delete state.inflight;
5923
+ });
4950
5924
  }
4951
- return this.catalog;
5925
+ if (!signal) return state.inflight;
5926
+ const inflight = state.inflight;
5927
+ return new Promise((resolve, reject) => {
5928
+ const abort = () => reject(signal.reason);
5929
+ signal.addEventListener("abort", abort, { once: true });
5930
+ inflight.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
5931
+ if (signal.aborted) abort();
5932
+ });
4952
5933
  }
4953
5934
  async listModels(provider, opts) {
4954
5935
  const catalog = await this.loadCatalog();
@@ -4965,21 +5946,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
4965
5946
  if (opts?.unfiltered === true) return catalog.map(toInfo).sort(compareByPlan);
4966
5947
  const accesses = this.deps.options().filterModelsByPlan === false ? void 0 : await this.loadPoolBillingAccess();
4967
5948
  const visible = this.deps.options().visibleModels;
4968
- const allow = Array.isArray(visible) && visible.length > 0 ? new Set(visible.filter((id) => typeof id === "string" && id !== "")) : void 0;
5949
+ const allow = Array.isArray(visible) ? visible.filter((id) => typeof id === "string" && id !== "") : [];
4969
5950
  const overrides = this.deps.options().modelVisibility;
4970
- return catalog.filter((model) => modelVisibleForAnyAccount(model.id, accesses)).filter((model) => {
4971
- const override = overrides?.[model.id];
4972
- if (typeof override === "boolean") return override;
4973
- return allow === void 0 || allow.has(model.id);
4974
- }).map(toInfo).sort(compareByPlan);
5951
+ return catalog.filter((model) => modelVisibleForAnyAccount(model.id, accesses)).filter((model) => modelIsVisible(model.id, allow, overrides)).map(toInfo).sort(compareByPlan);
4975
5952
  }
4976
5953
  /**
4977
5954
  * The per-model allowance bracket this account POOL falls into, or undefined
4978
5955
  * when there is nothing honest to show.
4979
5956
  *
4980
- * The pricing page publishes a per-model monthly allowance for GOAT and Pro
4981
- * only — its own words are "the boost is a per-model allowance, so it lives on
4982
- * the plans that have them" — so a pool whose highest plan is Go, Provider,
5957
+ * The pricing page publishes per-model monthly allowances for Go, GOAT and
5958
+ * Pro. A pool whose highest plan is Provider,
4983
5959
  * Max or Ultra gets undefined rather than a neighbouring tier's figure. Taking
4984
5960
  * the HIGHEST tier matches the question the picker already answers for a pool
4985
5961
  * (see `modelVisibleForAnyAccount`): what the user can reach, not what the one
@@ -4999,6 +5975,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4999
5975
  return allowanceTierForWeight(highest);
5000
5976
  }
5001
5977
  async resolveModel(provider, model, signal) {
5978
+ this.currentCatalogState();
5002
5979
  const entry = this.catalog.find((m) => m.id === model) ?? (await this.loadCatalog(signal)).find((m) => m.id === model);
5003
5980
  const efforts = KNOWN_EFFORTS[model];
5004
5981
  const vision = KNOWN_IMAGE_MODELS.has(model);
@@ -5076,7 +6053,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
5076
6053
  async loadPoolBillingAccess() {
5077
6054
  const keys = await this.poolAccountKeys();
5078
6055
  if (keys.length === 0) return void 0;
5079
- return Promise.all(keys.map((key) => this.loadBillingAccessForKey(key)));
6056
+ const apiBase = this.deps.options().apiBase;
6057
+ return Promise.all(keys.map((key) => this.loadBillingAccessForKey(apiBase, key)));
5080
6058
  }
5081
6059
  /**
5082
6060
  * The API keys the picker's plan filter must consult: every account the host
@@ -5100,21 +6078,22 @@ var CommandCodeAdapter = class extends LlmAdapter {
5100
6078
  * shared across concurrent callers. `undefined` means "unknown — show
5101
6079
  * everything" (fail-open).
5102
6080
  */
5103
- async loadBillingAccessForKey(apiKey) {
5104
- const cached = this.billingAccess.get(apiKey);
6081
+ async loadBillingAccessForKey(apiBase, apiKey) {
6082
+ const cacheKey = this.accountCacheKey(apiBase, apiKey);
6083
+ const cached = this.billingAccess.get(cacheKey);
5105
6084
  if (cached !== void 0 && Date.now() - cached.at < 3e5) return cached.value;
5106
- const existing = this.billingAccessInflight.get(apiKey);
6085
+ const existing = this.billingAccessInflight.get(cacheKey);
5107
6086
  if (existing !== void 0) return existing;
5108
- const inflight = this.fetchBillingAccess(apiKey).then((value) => {
5109
- this.billingAccess.set(apiKey, {
6087
+ const inflight = this.fetchBillingAccess(apiBase, apiKey).then((value) => {
6088
+ this.billingAccess.set(cacheKey, {
5110
6089
  value,
5111
6090
  at: Date.now()
5112
6091
  });
5113
6092
  return value;
5114
6093
  }).finally(() => {
5115
- this.billingAccessInflight.delete(apiKey);
6094
+ this.billingAccessInflight.delete(cacheKey);
5116
6095
  });
5117
- this.billingAccessInflight.set(apiKey, inflight);
6096
+ this.billingAccessInflight.set(cacheKey, inflight);
5118
6097
  return inflight;
5119
6098
  }
5120
6099
  /**
@@ -5126,12 +6105,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
5126
6105
  * fallback (the CLI stamps plan identity from it too). Any failure resolves
5127
6106
  * to `undefined` (fail-open) rather than breaking the picker.
5128
6107
  */
5129
- async fetchBillingAccess(apiKey) {
6108
+ async fetchBillingAccess(apiBase, apiKey) {
5130
6109
  try {
5131
- const connection = this.deps.options();
5132
6110
  const headers = await this.accountHeaders(apiKey);
5133
- const base = connection.apiBase;
5134
- const getJson = async (path) => (await this.fetchJson(`${base}${path}`, headers)).record;
6111
+ const getJson = async (path) => (await this.fetchJson(`${apiBase}${path}`, headers)).record;
5135
6112
  const whoami = await getJson("/alpha/whoami");
5136
6113
  const orgData = whoami && isRecord(whoami.org) ? whoami.org : void 0;
5137
6114
  const orgId = orgData === void 0 ? void 0 : stringValue(orgData.id);
@@ -5153,50 +6130,65 @@ var CommandCodeAdapter = class extends LlmAdapter {
5153
6130
  }
5154
6131
  }
5155
6132
  /** Fresh cached billing tier weight for a key, or undefined when not known. */
5156
- cachedBillingTierWeight(apiKey) {
5157
- const hit = this.billingAccess.get(apiKey);
6133
+ cachedBillingTierWeight(apiBase, apiKey) {
6134
+ const hit = this.billingAccess.get(this.accountCacheKey(apiBase, apiKey));
5158
6135
  if (hit === void 0 || Date.now() - hit.at >= 3e5) return void 0;
5159
6136
  return hit.value?.tierWeight;
5160
6137
  }
5161
6138
  /** Cached protocol decision for a key, or undefined when expired/unknown. */
5162
- cachedProtocolUseCli(apiKey) {
5163
- const hit = this.protocolCache.get(apiKey);
6139
+ cachedProtocol(apiBase, apiKey) {
6140
+ const hit = this.protocolCache.get(this.accountCacheKey(apiBase, apiKey));
5164
6141
  if (hit === void 0 || Date.now() - hit.at >= 9e5) return void 0;
5165
- return hit.useCli;
6142
+ return hit.protocol;
5166
6143
  }
5167
- rememberProtocol(apiKey, useCli) {
5168
- this.protocolCache.set(apiKey, {
5169
- useCli,
6144
+ rememberProtocol(apiBase, apiKey, protocol) {
6145
+ this.protocolCache.set(this.accountCacheKey(apiBase, apiKey), {
6146
+ protocol,
5170
6147
  at: Date.now()
5171
6148
  });
5172
6149
  }
5173
6150
  /**
5174
- * Choose the initial protocol for one request. A fresh protocol cache entry
5175
- * wins; otherwise a cached (not network-fetched) billing tier of Go is
5176
- * treated as CLI-only. Unknown accounts default to Provider API and fall
5177
- * back only after an `upgrade_required` rejection.
6151
+ * True when the catalog routes `model` to `/provider/v1/messages`.
6152
+ *
6153
+ * The catalog's own `supported_endpoints` is authoritative (it is what
6154
+ * upstream publishes per model); `requiresMessagesEndpoint()` — the
6155
+ * `claude-*` prefix rule — is the fallback for a catalog entry that carries
6156
+ * no route list, which is the shape every pre-1.55 cache file has.
6157
+ */
6158
+ routesToMessages(model) {
6159
+ const entry = this.catalog.find((candidate) => candidate.id === model);
6160
+ if (entry !== void 0 && entry.supportedEndpoints.length > 0) return entry.supportedEndpoints.includes("/messages");
6161
+ return requiresMessagesEndpoint(model);
6162
+ }
6163
+ /**
6164
+ * Choose the initial protocol for one request.
5178
6165
  *
5179
- * Models the Provider API serves ONLY through `/provider/v1/messages` (the
5180
- * Claude family — `requiresMessagesEndpoint()`) always take the CLI
5181
- * transport, on any tier and even under a forced `'openai'` preference:
5182
- * that option is documented as "prefer the Provider API, still fall back",
5183
- * and the 400 these models answer with is not a preference to be honoured
5184
- * but a hard refusal. `/alpha/generate` routes every one of them, so this
5185
- * costs nothing.
6166
+ * Models the Provider API serves only through `/provider/v1/messages` (the
6167
+ * Claude family) take the Messages transport on any tier and even under a
6168
+ * forced `'openai'` preference: that option is documented as "prefer the
6169
+ * Provider API, still fall back", and the 400 these models answer with on
6170
+ * Chat Completions (`Model "<id>" must be called via /provider/v1/messages`)
6171
+ * is a hard refusal, not a preference to honour.
5186
6172
  *
5187
6173
  * That decision is deliberately NOT written to `protocolCache`, which is
5188
- * keyed by API key alone: remembering it would pin the whole ACCOUNT to the
5189
- * CLI transport and drag every other model off it until the entry expired.
6174
+ * keyed by gateway and API key: it depends on the model, not just the account, so
6175
+ * remembering it would pin the whole ACCOUNT to one transport and drag
6176
+ * every other model along (issue #46).
6177
+ *
6178
+ * Otherwise a fresh protocol cache entry wins, then a cached (not
6179
+ * network-fetched) billing tier of Go — the one plan without Provider API
6180
+ * access. Unknown accounts default to Chat Completions and fall back to the
6181
+ * CLI transport only after an `upgrade_required` rejection.
5190
6182
  */
5191
- resolveProtocol(apiKey, model) {
6183
+ resolveProtocol(apiBase, apiKey, model) {
5192
6184
  const forced = this.deps.options().protocol;
5193
6185
  if (forced === "cli") return forced;
5194
- if (requiresMessagesEndpoint(model)) return "cli";
6186
+ if (this.routesToMessages(model)) return "messages";
5195
6187
  if (forced === "openai") return forced;
5196
- const cached = this.cachedProtocolUseCli(apiKey);
5197
- if (cached !== void 0) return cached ? "cli" : "openai";
5198
- if (this.cachedBillingTierWeight(apiKey) === GO_TIER_WEIGHT) {
5199
- this.rememberProtocol(apiKey, true);
6188
+ const cached = this.cachedProtocol(apiBase, apiKey);
6189
+ if (cached !== void 0) return cached;
6190
+ if (this.cachedBillingTierWeight(apiBase, apiKey) === GO_TIER_WEIGHT) {
6191
+ this.rememberProtocol(apiBase, apiKey, "cli");
5200
6192
  return "cli";
5201
6193
  }
5202
6194
  return "openai";
@@ -5336,6 +6328,59 @@ var CommandCodeAdapter = class extends LlmAdapter {
5336
6328
  }
5337
6329
  }
5338
6330
  async *stream(options) {
6331
+ const timing = openRequestTiming(options.model, this.deps.onRequestTiming);
6332
+ let outcome = "cancelled";
6333
+ let errorCode;
6334
+ try {
6335
+ for await (const chunk of this.streamRequest(options, timing)) {
6336
+ if (chunk.type === "text-delta" || chunk.type === "reasoning-delta" || chunk.type === "tool-call-delta") timing.first("firstContentMs");
6337
+ if (chunk.type === "finish") outcome = "finished";
6338
+ yield chunk;
6339
+ }
6340
+ } catch (error) {
6341
+ outcome = options.signal?.aborted ? "aborted" : "error";
6342
+ errorCode = error instanceof LlmError ? error.code : "unknown";
6343
+ throw error;
6344
+ } finally {
6345
+ await timing.close(outcome, errorCode);
6346
+ }
6347
+ }
6348
+ /**
6349
+ * One turn, with at most one output-ceiling downshift.
6350
+ *
6351
+ * The ceiling a model accepts is not published anywhere the request can be
6352
+ * built from (see `modelOutputTokenLimit`), and the endpoint states it only
6353
+ * by refusing. That refusal is worth one retry rather than a failed turn: the
6354
+ * harness's own recovery for it — compact the context — cannot help, because
6355
+ * nothing about the context is wrong, and resending the identical request
6356
+ * would be refused identically (issue #71). Reading the stated ceiling off
6357
+ * the rejection turns one wasted turn into one, for good: it is recorded
6358
+ * process-wide, so the next request for this model is built correctly
6359
+ * without spending the round trip again.
6360
+ *
6361
+ * Only BEFORE the first chunk reaches the host. Once a delta has been
6362
+ * delivered the answer is already partly on screen, and a second request
6363
+ * would duplicate it — so from that point the error stands and says what is
6364
+ * actually wrong. One downshift per turn, and a second refusal surfaces.
6365
+ */
6366
+ async *streamRequest(options, timing) {
6367
+ for (let attempt = 0;; attempt++) {
6368
+ const connection = this.deps.options();
6369
+ let delivered = false;
6370
+ try {
6371
+ for await (const chunk of this.streamAttempt(options, timing, connection)) {
6372
+ delivered = true;
6373
+ yield chunk;
6374
+ }
6375
+ return;
6376
+ } catch (error) {
6377
+ const ceiling = delivered ? void 0 : outputCeilingRefusal(connection.apiBase, options.model, error);
6378
+ if (attempt > 0 || ceiling === void 0) throw error;
6379
+ }
6380
+ }
6381
+ }
6382
+ async *streamAttempt(options, timing, connection) {
6383
+ this.currentCatalogState(connection);
5339
6384
  if (options.stop?.length) throw new LlmError("Command Code adapter does not support stop sequences", "UNSUPPORTED_OPTION");
5340
6385
  const hasImages = options.messages.some(hasImageContent);
5341
6386
  let readImage;
@@ -5343,39 +6388,47 @@ var CommandCodeAdapter = class extends LlmAdapter {
5343
6388
  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");
5344
6389
  const attachments = this.resolveAttachments?.();
5345
6390
  if (attachments === void 0) throw new LlmError("Command Code image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
5346
- readImage = (ref) => readRequestImage(attachments, ref);
6391
+ readImage = requestImages(attachments, options.signal, timing);
5347
6392
  }
5348
- const connection = this.deps.options();
5349
- let apiKey = await this.deps.resolveApiKey(connection, options.model);
5350
- const modelEntry = this.catalog.find((m) => m.id === options.model);
5351
- const modelMax = modelEntry?.maxTokens ?? 131072;
6393
+ const finishCredentials = timing.phase("credentials");
6394
+ let apiKey;
6395
+ try {
6396
+ apiKey = await this.deps.resolveApiKey(connection, options.model);
6397
+ } finally {
6398
+ finishCredentials();
6399
+ }
6400
+ const catalog = await this.catalogForGenerate(connection);
6401
+ options.signal?.throwIfAborted();
6402
+ const modelEntry = catalog.find((m) => m.id === options.model);
6403
+ const modelMax = Math.min(modelEntry?.maxTokens ?? Infinity, modelOutputTokenLimit(connection.apiBase, options.model));
5352
6404
  const requestedMaxTokens = Math.min(options.maxTokens ?? modelMax, modelMax, DEFAULT_GENERATE_MAX_TOKENS);
5353
- let maxTokens = requestedMaxTokens;
5354
- let contextBudget = {
5355
- maxTokens,
5356
- sizePressure: false
5357
- };
6405
+ const maxTokens = requestedMaxTokens;
5358
6406
  const effort = options.reasoningEffort;
5359
6407
  const supported = KNOWN_EFFORTS[options.model];
5360
6408
  const reasoningEffort = effort && effort !== "off" && supported?.includes(effort) ? effort : void 0;
5361
6409
  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");
5362
- let protocol = this.resolveProtocol(apiKey, options.model);
6410
+ let protocol = this.resolveProtocol(connection.apiBase, apiKey, options.model);
5363
6411
  let requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true && protocol === "cli" ? options.model : void 0);
5364
6412
  const buildBody = async (target) => {
5365
- const facts = {
5366
- maxTokens: requestedMaxTokens,
5367
- reasoningEffort,
5368
- systemText,
5369
- readImage
5370
- };
5371
- const built = target === "cli" ? await buildCliBody(requestOptions, connection, facts) : await buildOpenAIBody(requestOptions, facts);
5372
- contextBudget = requestContextBudget(target, built, requestOptions.messages, options.model, modelEntry?.contextWindow, requestedMaxTokens);
5373
- maxTokens = contextBudget.maxTokens;
5374
- if (target === "cli") recordOrEmpty(built.params).max_tokens = maxTokens;
5375
- else built.max_tokens = maxTokens;
5376
- return built;
6413
+ const finishBody = timing.phase("body");
6414
+ try {
6415
+ const facts = {
6416
+ maxTokens: requestedMaxTokens,
6417
+ reasoningEffort,
6418
+ selectableEffort: supported !== void 0 && supported.length > 0,
6419
+ systemText,
6420
+ readImage
6421
+ };
6422
+ const built = target === "cli" ? await buildCliBody(requestOptions, connection, facts) : target === "messages" ? await buildMessagesBody(requestOptions, facts) : await buildOpenAIBody(requestOptions, facts);
6423
+ if (target === "cli") recordOrEmpty(built.params).max_tokens = maxTokens;
6424
+ else built.max_tokens = maxTokens;
6425
+ return built;
6426
+ } finally {
6427
+ finishBody();
6428
+ }
5377
6429
  };
5378
6430
  let body = await buildBody(protocol);
6431
+ const serializedBodies = /* @__PURE__ */ new WeakMap();
5379
6432
  const trace = openStreamTrace(`${options.provider}/${options.model}`);
5380
6433
  const tried = /* @__PURE__ */ new Set();
5381
6434
  let attemptNumber = 0;
@@ -5387,15 +6440,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
5387
6440
  options,
5388
6441
  connection,
5389
6442
  fetchImpl: this.fetchImpl,
6443
+ timing,
5390
6444
  onResponse: (response) => {
5391
- this.headerTimeoutStreak = void 0;
6445
+ this.headerTimeoutStreaks.delete(options.sessionId ?? GLOBAL_HEADER_TIMEOUT_KEY);
5392
6446
  responseMetadata = {
5393
6447
  protocol,
5394
6448
  headers: responseIdentifiers(response.headers)
5395
6449
  };
5396
6450
  trace.record("response", {
5397
6451
  protocol,
5398
- endpoint: protocol === "cli" ? `${connection.apiBase}/alpha/generate` : `${connection.apiBase}/provider/v1/chat/completions`,
6452
+ endpoint: protocolEndpoint(protocol, connection.apiBase),
5399
6453
  status: response.status,
5400
6454
  attempt: attemptNumber,
5401
6455
  accountsTried: tried.size,
@@ -5419,15 +6473,26 @@ var CommandCodeAdapter = class extends LlmAdapter {
5419
6473
  sessionId: options.sessionId,
5420
6474
  ...requestFingerprint(protocol, body)
5421
6475
  });
5422
- const attempt = await connectGenerate(connectDeps, apiKey, protocol, body);
6476
+ let serialized = serializedBodies.get(body);
6477
+ if (serialized === void 0) {
6478
+ const finishSerialization = timing.phase("serialization");
6479
+ try {
6480
+ serialized = JSON.stringify(body, (_key, value) => sanitizeSurrogates(value));
6481
+ } finally {
6482
+ finishSerialization();
6483
+ }
6484
+ serializedBodies.set(body, serialized);
6485
+ }
6486
+ const attempt = await connectGenerate(connectDeps, apiKey, protocol, serialized);
5423
6487
  if ("response" in attempt) {
5424
6488
  connected = attempt;
5425
6489
  break;
5426
6490
  }
5427
6491
  const providerError = parseProviderError(attempt.errText);
5428
- if (protocol === "openai" && isUpgradeRequiredError(attempt.status, attempt.errText, providerError)) {
6492
+ if (protocol !== "cli" && isUpgradeRequiredError(attempt.status, attempt.errText, providerError)) {
6493
+ const wasMessages = protocol === "messages";
5429
6494
  protocol = "cli";
5430
- this.rememberProtocol(apiKey, true);
6495
+ if (!wasMessages) this.rememberProtocol(connection.apiBase, apiKey, "cli");
5431
6496
  requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true ? options.model : void 0);
5432
6497
  body = await buildBody("cli");
5433
6498
  continue;
@@ -5445,17 +6510,26 @@ var CommandCodeAdapter = class extends LlmAdapter {
5445
6510
  });
5446
6511
  if (next !== void 0 && !tried.has(next)) {
5447
6512
  apiKey = next;
6513
+ const nextProtocol = this.resolveProtocol(connection.apiBase, apiKey, options.model);
6514
+ if (nextProtocol !== protocol) {
6515
+ protocol = nextProtocol;
6516
+ requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true && protocol === "cli" ? options.model : void 0);
6517
+ body = await buildBody(protocol);
6518
+ }
5448
6519
  continue;
5449
6520
  }
5450
6521
  }
5451
- if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled") && (rejection.resetAtMs !== void 0 || attempt.status !== 429)) {
6522
+ if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled")) {
5452
6523
  const reset = rejection.resetAtMs;
5453
- const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
6524
+ const wait = reset === void 0 ? attempt.retryAfterMs ?? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
5454
6525
  const when = reset === void 0 ? void 0 : new Date(reset).toISOString();
5455
6526
  const window = rejection.reason === "rate-limit";
5456
- throw bilingual("RATE_LIMIT", (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}`), wait > 0 ? { providerRetryAfterMs: wait } : void 0);
6527
+ 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}`), {
6528
+ status: attempt.status,
6529
+ ...wait > 0 && wait <= 9e5 ? { providerRetryAfterMs: wait } : {}
6530
+ });
5457
6531
  }
5458
- throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs, providerError, contextBudget.sizePressure);
6532
+ throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs, providerError);
5459
6533
  }
5460
6534
  } catch (error) {
5461
6535
  trace.record("connect-error", {
@@ -5464,7 +6538,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
5464
6538
  });
5465
6539
  trace.close();
5466
6540
  if (options.signal?.aborted) throw error;
5467
- throw failureWithRequestId(this.gradeHeaderTimeout(error, protocol, body, connection.requestTimeoutMs), responseMetadata);
6541
+ throw failureWithRequestId(this.gradeHeaderTimeout(error, protocol, body, connection.requestTimeoutMs, options.sessionId), responseMetadata);
5468
6542
  }
5469
6543
  if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
5470
6544
  const { response, cleanup } = connected;
@@ -5504,12 +6578,21 @@ var CommandCodeAdapter = class extends LlmAdapter {
5504
6578
  let usage;
5505
6579
  let usageEmitted = false;
5506
6580
  let endRecorded = false;
6581
+ let protocolStopped = false;
5507
6582
  function* handle(event) {
5508
6583
  captureResponseIdentifiers(responseMetadata, event);
6584
+ if (protocol === "messages" && isRecord(event) && event.type === "message_stop") protocolStopped = true;
5509
6585
  for (const chunk of handleEvent(asm, protocol, event)) if (chunk.type === "finish") finish = chunk;
5510
6586
  else if (chunk.type === "usage") usage = chunk.usage;
5511
6587
  else yield chunk;
5512
6588
  }
6589
+ function* handleLine(line) {
6590
+ if (protocol === "openai" && line.trim().replace(/^data:\s*/, "") === "[DONE]") {
6591
+ protocolStopped = true;
6592
+ return;
6593
+ }
6594
+ yield* handle(parseStreamEventLine(line));
6595
+ }
5513
6596
  try {
5514
6597
  for (;;) {
5515
6598
  let read;
@@ -5540,10 +6623,11 @@ var CommandCodeAdapter = class extends LlmAdapter {
5540
6623
  buffered: buffer.length
5541
6624
  });
5542
6625
  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} 毫秒无任何事件,被判定为死连接——长思考模型可在设置中调大流空闲超时`);
5543
- if (buffer.trim()) yield* handle(parseStreamEventLine(buffer));
6626
+ if (buffer.trim()) yield* handleLine(buffer);
5544
6627
  break;
5545
6628
  }
5546
6629
  const text = decoder.decode(value, { stream: true });
6630
+ if (value.byteLength > 0) timing.first("firstByteMs");
5547
6631
  buffer += text;
5548
6632
  chunkCount += 1;
5549
6633
  totalBytes += value.byteLength;
@@ -5555,8 +6639,11 @@ var CommandCodeAdapter = class extends LlmAdapter {
5555
6639
  });
5556
6640
  const lines = buffer.split("\n");
5557
6641
  buffer = lines.pop() ?? "";
5558
- for (const line of lines) yield* handle(parseStreamEventLine(line));
5559
- if (finish !== void 0) break;
6642
+ for (const line of lines) {
6643
+ yield* handleLine(line);
6644
+ if (protocolStopped) break;
6645
+ }
6646
+ if (protocolStopped || protocol === "cli" && finish !== void 0) break;
5560
6647
  }
5561
6648
  if (usage !== void 0) {
5562
6649
  usageEmitted = true;
@@ -5581,9 +6668,13 @@ var CommandCodeAdapter = class extends LlmAdapter {
5581
6668
  });
5582
6669
  endRecorded = true;
5583
6670
  if (failure !== void 0) throw failure;
5584
- yield hasResponseIdentifiers(responseMetadata) ? {
6671
+ const replayBlocks = asm.replayBlocks;
6672
+ yield hasResponseIdentifiers(responseMetadata) || replayBlocks.length > 0 ? {
5585
6673
  ...finish,
5586
- replayState: { response: responseMetadata }
6674
+ replayState: {
6675
+ response: responseMetadata,
6676
+ ...replayBlocks.length > 0 ? { blocks: replayBlocks } : {}
6677
+ }
5587
6678
  } : finish;
5588
6679
  } else {
5589
6680
  const elapsed = Date.now() - streamStartedAt;
@@ -5627,40 +6718,69 @@ var CommandCodeAdapter = class extends LlmAdapter {
5627
6718
  }
5628
6719
  }
5629
6720
  };
6721
+ /**
6722
+ * Unwrap one level of a double-encoded provider message.
6723
+ *
6724
+ * The Messages endpoint sometimes reports a rejection with `error.message`
6725
+ * holding a whole JSON document rather than the sentence
6726
+ * (`{"type":"error","error":{"type":"invalid_request_error","message":"{\"type\":
6727
+ * \"error\",…}"},"request_id":"req_…"}`, measured 2026-09-29). Showing that blob
6728
+ * to the user hides the actual refusal, so one nesting level is peeled; the
6729
+ * outer text is returned unchanged when it is not JSON or carries no message.
6730
+ */
6731
+ function unwrapProviderMessage(raw) {
6732
+ if (raw === void 0) return void 0;
6733
+ const trimmed = raw.trim();
6734
+ if (!trimmed.startsWith("{")) return raw;
6735
+ try {
6736
+ const inner = JSON.parse(trimmed);
6737
+ if (!isRecord(inner)) return raw;
6738
+ return stringValue(((isRecord(inner.error) ? inner.error : void 0) ?? inner).message) ?? raw;
6739
+ } catch {
6740
+ return raw;
6741
+ }
6742
+ }
5630
6743
  /** Parse a rejected body once, retaining the nested/root distinction. */
5631
6744
  function parseProviderError(errText) {
5632
6745
  try {
5633
6746
  const parsed = JSON.parse(errText);
5634
- if (!isRecord(parsed)) return {};
6747
+ if (!isRecord(parsed)) return { message: errText };
5635
6748
  const nested = isRecord(parsed.error) ? parsed.error : void 0;
5636
6749
  const body = nested ?? parsed;
6750
+ const rawMessage = stringValue(body.message);
5637
6751
  return {
5638
6752
  root: parsed,
5639
6753
  nested,
5640
6754
  code: stringValue(body.code),
5641
6755
  type: stringValue(body.type),
5642
- message: stringValue(body.message),
6756
+ rawMessage,
6757
+ message: unwrapProviderMessage(rawMessage) ?? errText,
5643
6758
  rateLimit: isRecord(body.rateLimit) ? body.rateLimit : void 0
5644
6759
  };
5645
6760
  } catch {
5646
- return {};
6761
+ return { message: errText };
5647
6762
  }
5648
6763
  }
5649
- function generateHttpError(status, errText, retryAfterMs, parsed = parseProviderError(errText), sizePressure = false) {
6764
+ function generateHttpError(status, errText, retryAfterMs, parsed = parseProviderError(errText)) {
5650
6765
  const providerCode = parsed.nested === void 0 ? void 0 : parsed.code;
5651
6766
  const providerDetail = parsed.nested === void 0 ? "" : [
5652
6767
  parsed.code,
5653
6768
  parsed.type,
5654
- parsed.message
6769
+ parsed.rawMessage ?? parsed.message
5655
6770
  ].filter((part) => part !== void 0 && part !== "").join(" ");
5656
6771
  const detail = providerCode ?? `HTTP ${status}`;
5657
6772
  const overflowDetail = providerDetail !== "" ? providerDetail : errText.slice(0, 500);
6773
+ if (status < 500 && OUTPUT_LIMIT_REJECTION.test(overflowDetail)) {
6774
+ const stated = /max_tokens:\s*\d+\s*>\s*\d+/.exec(overflowDetail)?.[0] ?? (parsed.message !== void 0 && parsed.message.length <= 200 ? parsed.message : void 0);
6775
+ 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 });
6776
+ }
5658
6777
  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 });
5659
- 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 });
5660
6778
  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 });
5661
6779
  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 });
5662
6780
  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 });
5663
- return new LlmError(`Command Code API error ${status}${detail === `HTTP ${status}` ? "" : ` (${detail})`}: ${errText.slice(0, 500)}`, httpErrorCode(status), {
6781
+ const diagnosticCode = parsed.code ?? /^\s*(MODEL_NOT_IN_PLAN|INSUFFICIENT_CREDITS|USAGE_EXCEEDED|PREMIUM_CREDITS_EXHAUSTED)\b/i.exec(parsed.message ?? "")?.[1];
6782
+ const diagnostic = diagnosticCode !== void 0 && /^[A-Za-z0-9_.:-]{1,80}$/.test(diagnosticCode) ? `(${diagnosticCode})` : "";
6783
+ return new LlmError(`Command Code 请求失败(HTTP ${status})${diagnostic}:` + (status === 429 ? "请求受到用量或速率限制,请稍后重试并检查账户额度。" : status === 408 ? "服务端处理超时,请稍后重试。" : status >= 500 ? "上游服务暂时无法完成请求,请稍后重试。" : "服务端拒绝了本次请求,请检查模型、套餐和请求设置。"), httpErrorCode(status), {
5664
6784
  status,
5665
6785
  ...retryAfterMs !== void 0 && retryAfterMs > 0 && retryAfterMs <= 9e5 ? { providerRetryAfterMs: retryAfterMs } : {}
5666
6786
  });
@@ -5678,7 +6798,7 @@ function generateHttpError(status, errText, retryAfterMs, parsed = parseProvider
5678
6798
  * rejection repeats identically on every attempt.
5679
6799
  */
5680
6800
  function httpErrorCode(status) {
5681
- if (status === 429) return "RATE_LIMIT";
6801
+ if (status === 429) return THROTTLED_CODE;
5682
6802
  if (status === 408) return "TIMEOUT";
5683
6803
  if (status >= 500) return "SERVER";
5684
6804
  return "PROVIDER_HTTP_ERROR";
@@ -5898,8 +7018,202 @@ function handleOpenAIEvent(asm, event) {
5898
7018
  return chunks;
5899
7019
  }
5900
7020
  /** Dispatch one parsed stream event to the active transport's handler. */
7021
+ /**
7022
+ * Messages `usage` → the harness meter.
7023
+ *
7024
+ * The field names are the Anthropic ones, and unlike Chat Completions
7025
+ * `input_tokens` already excludes cache reads and writes, so it is mapped
7026
+ * as-is rather than reduced (the shape `dsh-llm-deepseek` implements).
7027
+ * Measured 2026-09-29: `{ input_tokens, cache_creation_input_tokens,
7028
+ * cache_read_input_tokens, output_tokens, output_tokens_details:
7029
+ * { thinking_tokens } }`, plus gateway additions the meter ignores.
7030
+ */
7031
+ function mapMessagesUsage(raw, previous) {
7032
+ if (!isRecord(raw)) return void 0;
7033
+ const inputTokens = numberValue(raw.input_tokens) ?? previous?.inputTokens ?? 0;
7034
+ const outputTokens = numberValue(raw.output_tokens) ?? previous?.outputTokens ?? 0;
7035
+ const cacheRead = numberValue(raw.cache_read_input_tokens) ?? previous?.cacheReadTokens ?? 0;
7036
+ const cacheWrite = numberValue(raw.cache_creation_input_tokens) ?? previous?.cacheWriteTokens ?? 0;
7037
+ const reasoningTokens = numberValue((isRecord(raw.output_tokens_details) ? raw.output_tokens_details : void 0)?.thinking_tokens) ?? previous?.reasoningTokens;
7038
+ const usage = {
7039
+ inputTokens: Math.max(0, inputTokens),
7040
+ outputTokens,
7041
+ cacheReadTokens: cacheRead
7042
+ };
7043
+ if (cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
7044
+ if (reasoningTokens !== void 0) usage.reasoningTokens = reasoningTokens;
7045
+ return usage;
7046
+ }
7047
+ /** The harness block type one Messages content block maps to. */
7048
+ function messagesBlockType(native) {
7049
+ if (native.type === "text") return "text";
7050
+ if (native.type === "thinking") return "reasoning";
7051
+ if (native.type === "tool_use") return "tool-call";
7052
+ }
7053
+ /**
7054
+ * Handle one Messages-transport stream event, appending harness StreamChunks.
7055
+ *
7056
+ * The event sequence is the Anthropic one measured on 2026-09-29:
7057
+ * `message_start`, then per content block `content_block_start` /
7058
+ * `content_block_delta`* / `content_block_stop`, then `message_delta`
7059
+ * (carrying `stop_reason` and the final usage) and `message_stop`. A `ping`
7060
+ * keepalive interleaves freely and carries no content. The terminal chunks are
7061
+ * NOT emitted here: `stream()` holds the success finish until it has validated
7062
+ * the assembled answer, and it attaches the replay envelope.
7063
+ */
7064
+ function handleMessagesEvent(asm, event) {
7065
+ const chunks = [];
7066
+ if (!isRecord(event)) return chunks;
7067
+ switch (event.type) {
7068
+ case "message_start": {
7069
+ const usage = mapMessagesUsage((isRecord(event.message) ? event.message : void 0)?.usage);
7070
+ if (usage) {
7071
+ asm.messagesUsage = usage;
7072
+ chunks.push({
7073
+ type: "usage",
7074
+ usage
7075
+ });
7076
+ }
7077
+ break;
7078
+ }
7079
+ case "content_block_start": {
7080
+ const wireIndex = numberValue(event.index);
7081
+ const native = isRecord(event.content_block) ? event.content_block : void 0;
7082
+ const type = native ? messagesBlockType(native) : void 0;
7083
+ if (wireIndex === void 0 || type === void 0) break;
7084
+ const chunkIndex = asm.nextIndex++;
7085
+ const state = {
7086
+ type,
7087
+ text: stringValue(native?.text) ?? "",
7088
+ signature: stringValue(native?.signature) ?? "",
7089
+ json: "",
7090
+ id: stringValue(native?.id) ?? "",
7091
+ name: stringValue(native?.name) ?? "",
7092
+ chunkIndex
7093
+ };
7094
+ asm.messagesBlocks.set(wireIndex, state);
7095
+ asm.replayBlocks[chunkIndex] = { type };
7096
+ chunks.push({
7097
+ type: "block-start",
7098
+ index: chunkIndex,
7099
+ blockType: type
7100
+ });
7101
+ if (type === "tool-call" && state.id !== "") chunks.push({
7102
+ type: "tool-call-delta",
7103
+ index: chunkIndex,
7104
+ id: ToolCallId(state.id),
7105
+ name: state.name,
7106
+ argumentsDelta: ""
7107
+ });
7108
+ break;
7109
+ }
7110
+ case "content_block_delta": {
7111
+ const wireIndex = numberValue(event.index);
7112
+ const state = wireIndex === void 0 ? void 0 : asm.messagesBlocks.get(wireIndex);
7113
+ const delta = isRecord(event.delta) ? event.delta : void 0;
7114
+ if (!state || !delta) break;
7115
+ const index = state.chunkIndex;
7116
+ if (delta.type === "text_delta" && state.type === "text") {
7117
+ const text = stringValue(delta.text) ?? "";
7118
+ state.text += text;
7119
+ if (text.trim() !== "") asm.sawContent = true;
7120
+ chunks.push({
7121
+ type: "text-delta",
7122
+ index,
7123
+ text
7124
+ });
7125
+ } else if (delta.type === "thinking_delta" && state.type === "reasoning") {
7126
+ const text = stringValue(delta.thinking) ?? "";
7127
+ state.text += text;
7128
+ chunks.push({
7129
+ type: "reasoning-delta",
7130
+ index,
7131
+ text
7132
+ });
7133
+ } else if (delta.type === "signature_delta" && state.type === "reasoning") {
7134
+ state.signature += stringValue(delta.signature) ?? "";
7135
+ const entry = asm.replayBlocks[index];
7136
+ if (entry) entry.signature = state.signature;
7137
+ } else if (delta.type === "input_json_delta" && state.type === "tool-call") {
7138
+ const fragment = stringValue(delta.partial_json) ?? "";
7139
+ state.json += fragment;
7140
+ chunks.push({
7141
+ type: "tool-call-delta",
7142
+ index,
7143
+ id: ToolCallId(state.id),
7144
+ name: state.name,
7145
+ argumentsDelta: fragment
7146
+ });
7147
+ }
7148
+ break;
7149
+ }
7150
+ case "content_block_stop": {
7151
+ const wireIndex = numberValue(event.index);
7152
+ if (wireIndex === void 0) break;
7153
+ const state = asm.messagesBlocks.get(wireIndex);
7154
+ if (!state) break;
7155
+ asm.messagesBlocks.delete(wireIndex);
7156
+ const index = state.chunkIndex;
7157
+ if (state.type === "text") {
7158
+ if (state.text.trim() !== "") asm.sawContent = true;
7159
+ chunks.push({
7160
+ type: "block-end",
7161
+ index,
7162
+ block: {
7163
+ type: "text",
7164
+ text: state.text
7165
+ }
7166
+ });
7167
+ } else if (state.type === "reasoning") chunks.push({
7168
+ type: "block-end",
7169
+ index,
7170
+ block: {
7171
+ type: "reasoning",
7172
+ text: state.text
7173
+ }
7174
+ });
7175
+ else {
7176
+ if (state.id !== "") asm.sawContent = true;
7177
+ chunks.push({
7178
+ type: "block-end",
7179
+ index,
7180
+ block: {
7181
+ type: "tool-call",
7182
+ id: ToolCallId(state.id),
7183
+ name: state.name,
7184
+ arguments: state.json || "{}"
7185
+ }
7186
+ });
7187
+ }
7188
+ break;
7189
+ }
7190
+ case "message_delta": {
7191
+ const reason = stringValue((isRecord(event.delta) ? event.delta : void 0)?.stop_reason);
7192
+ if (reason !== void 0) asm.finishReason = reason;
7193
+ const usage = mapMessagesUsage(event.usage, asm.messagesUsage);
7194
+ if (usage) {
7195
+ asm.messagesUsage = usage;
7196
+ chunks.push({
7197
+ type: "usage",
7198
+ usage
7199
+ });
7200
+ }
7201
+ break;
7202
+ }
7203
+ case "message_stop":
7204
+ chunks.push({
7205
+ type: "finish",
7206
+ reason: mapFinishReason(asm.finishReason)
7207
+ });
7208
+ break;
7209
+ case "ping": break;
7210
+ case "error": throw streamErrorToLlmError(event.error ?? event, stringValue(event.message));
7211
+ }
7212
+ return chunks;
7213
+ }
5901
7214
  function handleEvent(asm, protocol, event) {
5902
- return protocol === "openai" ? handleOpenAIEvent(asm, event) : handleCliEvent(asm, event);
7215
+ if (protocol === "cli") return handleCliEvent(asm, event);
7216
+ return protocol === "messages" ? handleMessagesEvent(asm, event) : handleOpenAIEvent(asm, event);
5903
7217
  }
5904
7218
  /**
5905
7219
  * Parse an HTTP `Retry-After` value (delay-seconds or an HTTP-date) into
@@ -5928,7 +7242,7 @@ function parseRetryAfterMs(value, now = Date.now()) {
5928
7242
  * one function serves both — a new reason cannot drift between them.
5929
7243
  */
5930
7244
  function mapFinishReason(reason) {
5931
- if (reason === "tool-calls" || reason === "tool_calls") return { kind: "tool-calls" };
7245
+ if (reason === "tool-calls" || reason === "tool_calls" || reason === "tool_use") return { kind: "tool-calls" };
5932
7246
  if (reason === "length" || reason === "max_tokens" || reason === "max-tokens" || reason === "max_output_tokens") return { kind: "max-tokens" };
5933
7247
  return { kind: "stop" };
5934
7248
  }
@@ -6774,7 +8088,7 @@ function applyUsageRemote(ctx, deps) {
6774
8088
  *
6775
8089
  * @module dsh-commandcode-provider/login
6776
8090
  */
6777
- /** Give up on the browser after this long without a callback (mirrors the CLI). */
8091
+ /** 登录总期限包含浏览器回调、密钥验证与存储,不能在收到回调后撤掉保护。 */
6778
8092
  const LOGIN_TIMEOUT_MS = 12e4;
6779
8093
  /** First local port the flow tries (mirrors the CLI). */
6780
8094
  const LOGIN_START_PORT = 5959;
@@ -6812,7 +8126,7 @@ function studioBaseForApiBase(apiBase) {
6812
8126
  * tests). Mirrors the CLI's verdicts: 401 → invalid_key, other non-OK →
6813
8127
  * server_error, transport failure → network_error.
6814
8128
  */
6815
- async function validateCommandApiKey(fetchImpl, apiBase, apiKey) {
8129
+ async function validateCommandApiKey(fetchImpl, apiBase, apiKey, signal) {
6816
8130
  try {
6817
8131
  const response = await fetchImpl(`${apiBase}/alpha/whoami`, {
6818
8132
  method: "GET",
@@ -6820,8 +8134,10 @@ async function validateCommandApiKey(fetchImpl, apiBase, apiKey) {
6820
8134
  "Content-Type": "application/json",
6821
8135
  ...IDENTITY_ENCODING_HEADER,
6822
8136
  Authorization: `Bearer ${apiKey}`
6823
- }
8137
+ },
8138
+ ...signal === void 0 ? {} : { signal }
6824
8139
  });
8140
+ response.body?.cancel().catch(() => void 0);
6825
8141
  if (response.status === 401) return {
6826
8142
  valid: false,
6827
8143
  error: "invalid_key"
@@ -6867,6 +8183,7 @@ var CommandCodeLoginFlow = class {
6867
8183
  statusValue = { state: "idle" };
6868
8184
  server;
6869
8185
  timer;
8186
+ attemptAbort;
6870
8187
  /** Settle hooks of the live attempt's callback promise. */
6871
8188
  settle;
6872
8189
  /**
@@ -6936,6 +8253,7 @@ var CommandCodeLoginFlow = class {
6936
8253
  async startAttempt(targetRef) {
6937
8254
  this.teardown();
6938
8255
  const attempt = ++this.attemptSeq;
8256
+ this.attemptAbort = new AbortController();
6939
8257
  const port = await this.findPort();
6940
8258
  const expectedState = this.deps.randomToken?.(32) ?? randomBytes(32).toString("base64url");
6941
8259
  const settled = new Promise((resolve, reject) => {
@@ -6965,11 +8283,12 @@ var CommandCodeLoginFlow = class {
6965
8283
  })
6966
8284
  });
6967
8285
  this.timer = setTimeout(() => {
8286
+ if (!this.ownsAttempt(attempt)) return;
6968
8287
  this.teardown();
6969
8288
  this.setStatus({
6970
8289
  state: "failed",
6971
8290
  reason: "timeout",
6972
- message: "No browser callback arrived within the login window."
8291
+ message: "登录未在规定时间内完成,请重试。"
6973
8292
  });
6974
8293
  }, this.deps.timeoutMs ?? 12e4);
6975
8294
  this.timer.unref?.();
@@ -7137,7 +8456,8 @@ var CommandCodeLoginFlow = class {
7137
8456
  settleAttempt(json, code, body, failure, credentials) {
7138
8457
  json(code, body);
7139
8458
  const settle = this.settle;
7140
- this.teardown();
8459
+ if (failure !== void 0) this.teardown();
8460
+ else this.closeCallback();
7141
8461
  if (settle === void 0) return;
7142
8462
  if (failure !== void 0) settle.reject(failure);
7143
8463
  else if (credentials !== void 0) settle.resolve(credentials);
@@ -7154,10 +8474,11 @@ var CommandCodeLoginFlow = class {
7154
8474
  */
7155
8475
  async complete(attempt, credentials, targetRef) {
7156
8476
  if (!this.ownsAttempt(attempt)) return;
7157
- const validation = await validateCommandApiKey(this.deps.fetchImpl ?? fetch, this.readApiBase(), credentials.apiKey);
8477
+ const validation = await validateCommandApiKey(this.deps.fetchImpl ?? fetch, this.readApiBase(), credentials.apiKey, this.attemptAbort?.signal);
7158
8478
  if (!this.ownsAttempt(attempt)) return;
7159
8479
  if (!validation.valid) {
7160
8480
  const reason = validation.error === "invalid_key" ? "invalid-key" : validation.error === "network_error" ? "network" : "error";
8481
+ this.teardown();
7161
8482
  this.setStatus({
7162
8483
  state: "failed",
7163
8484
  reason,
@@ -7171,6 +8492,7 @@ var CommandCodeLoginFlow = class {
7171
8492
  await this.deps.storeKey(credentials, targetRef);
7172
8493
  } catch (error) {
7173
8494
  if (!this.ownsAttempt(attempt)) return;
8495
+ this.teardown();
7174
8496
  this.setStatus({
7175
8497
  state: "failed",
7176
8498
  reason: "unavailable",
@@ -7179,7 +8501,7 @@ var CommandCodeLoginFlow = class {
7179
8501
  return;
7180
8502
  }
7181
8503
  if (!this.ownsAttempt(attempt)) return;
7182
- this.clearTimer();
8504
+ this.teardown();
7183
8505
  this.setStatus({
7184
8506
  state: "success",
7185
8507
  userName: credentials.userName,
@@ -7194,6 +8516,7 @@ var CommandCodeLoginFlow = class {
7194
8516
  failFrom(attempt, failure) {
7195
8517
  if (!(failure instanceof LoginSettleError)) return;
7196
8518
  if (!this.ownsAttempt(attempt)) return;
8519
+ this.teardown();
7197
8520
  this.setStatus({
7198
8521
  state: "failed",
7199
8522
  reason: failure.reason,
@@ -7206,13 +8529,19 @@ var CommandCodeLoginFlow = class {
7206
8529
  this.timer = void 0;
7207
8530
  }
7208
8531
  }
7209
- /** Close the server and watchdog without touching the published status. */
7210
- teardown() {
7211
- this.clearTimer();
8532
+ /** 消费一次回调后关闭监听;保留总期限以约束后续验证。 */
8533
+ closeCallback() {
7212
8534
  closeServer(this.server);
7213
8535
  this.server = void 0;
7214
8536
  this.settle = void 0;
7215
8537
  }
8538
+ /** 结束整个尝试,同时中止验证请求,迟到结果由所有权检查拦截。 */
8539
+ teardown() {
8540
+ this.clearTimer();
8541
+ this.attemptAbort?.abort();
8542
+ this.attemptAbort = void 0;
8543
+ this.closeCallback();
8544
+ }
7216
8545
  };
7217
8546
  /** Close one loopback server without letting "not running"/"already closed" escape. */
7218
8547
  function closeServer(server) {
@@ -7530,9 +8859,8 @@ function buildCommandCodeTuiSection(deps) {
7530
8859
  * ONE draft: every one of them then parses that same draft on save, all N
7531
8860
  * write ops address the same path, and only the LAST field's op survives —
7532
8861
  * which silently rewrote the allowlist from the last catalog model instead
7533
- * of the one that was toggled. An override equal to what the array already
7534
- * says is written as a CLEAR, so toggling a model back to its inherited
7535
- * state leaves no residue.
8862
+ * of the one that was toggled. 保存显式布尔值,不能因为与白名单相同就
8863
+ * 清除用户层:清除后可能重新露出基础配置中的相反开关。
7536
8864
  */
7537
8865
  const modelField = (id, hint, group) => ({
7538
8866
  path: ["modelVisibility", id],
@@ -7546,11 +8874,9 @@ function buildCommandCodeTuiSection(deps) {
7546
8874
  return String(listed.length === 0 || listed.includes(id));
7547
8875
  },
7548
8876
  parse: (text) => {
7549
- const on = text.trim() === "true";
7550
- const listed = storedIds(deps.visibleModels());
7551
- return on === (listed.length === 0 || listed.includes(id)) ? { kind: "clear" } : {
8877
+ return {
7552
8878
  kind: "set",
7553
- value: on
8879
+ value: text.trim() === "true"
7554
8880
  };
7555
8881
  }
7556
8882
  });
@@ -7886,7 +9212,7 @@ function transportBudgetMessage(failureMessage, maxRetries) {
7886
9212
  * Mark one schema field volatile.
7887
9213
  *
7888
9214
  * `.volatile()` is unconditional: it is a schemastery 3.18.3 feature and the
7889
- * only supported engine (`dsh 0.2.0-rc.1`) pins `~3.18.4`, so the method is
9215
+ * only supported engine (`dsh 0.2.0-rc.2`) pins `~3.18.4`, so the method is
7890
9216
  * always present. It is reached through a structural member so the caller's
7891
9217
  * declared field type survives — the mark changes what the schema PARSES to (a
7892
9218
  * live reference), not the plugin-facing `Config` shape.
@@ -7997,6 +9323,7 @@ const Config = z.object(markVolatileFields({
7997
9323
  apiKey: z.string().role("secret")
7998
9324
  })),
7999
9325
  activeAccount: z.string(),
9326
+ credentialCleanupRefs: z.array(z.string().role("credential-ref")),
8000
9327
  modelAccountRules: z.array(z.object({
8001
9328
  models: z.array(z.string()),
8002
9329
  account: z.string()
@@ -8120,10 +9447,13 @@ function apply(ctx, config) {
8120
9447
  return Math.min(Math.trunc(value), 50);
8121
9448
  };
8122
9449
  const sessionAgents = /* @__PURE__ */ new WeakMap();
8123
- ctx.on("agent/request-error", async ({ agent, failure, signal }, next) => {
8124
- if (signal.aborted) return next();
9450
+ ctx.on("agent/request-error", async ({ agent, provider, failure, signal }, next) => {
9451
+ if (signal.aborted || provider !== "commandcode") return next();
8125
9452
  const session = Reflect.get(agent, "session");
8126
9453
  if (typeof session === "object" && session !== null) sessionAgents.set(session, agent);
9454
+ const transient = absorbTransientFailure(agent, failure.code);
9455
+ if (transient === "exhausted") throw new Error(transientBudgetMessage(failure.code, failure.message));
9456
+ if (transient === "retry") return next();
8127
9457
  const decision = absorbTransportFailure(agent, failure.code, transportMaxRetries());
8128
9458
  if (decision === "ignored") return next();
8129
9459
  if (decision === "retry") return { kind: "retry" };
@@ -8139,15 +9469,21 @@ function apply(ctx, config) {
8139
9469
  }
8140
9470
  if (action !== "reset") return;
8141
9471
  const agent = sessionAgents.get(session);
8142
- if (agent !== void 0) resetTransportFailures(agent);
9472
+ if (agent !== void 0) {
9473
+ resetTransportFailures(agent);
9474
+ resetTransientFailures(agent);
9475
+ }
8143
9476
  });
8144
9477
  ctx.on("agent/status", ({ agent, status }) => {
8145
- if (status === "idle") resetTransportFailures(agent);
9478
+ if (status === "idle") {
9479
+ resetTransportFailures(agent);
9480
+ resetTransientFailures(agent);
9481
+ }
8146
9482
  });
8147
9483
  const usageReports = async () => {
8148
9484
  const described = await pool.describeAccounts();
8149
9485
  const byId = new Map(described.map((account) => [account.slot.id, account]));
8150
- const active = selectActiveAccount(await pool.resolvedAccounts(), preferredId());
9486
+ const active = await pool.activeAccount();
8151
9487
  return { accounts: await Promise.all(slots().map(async (slot) => {
8152
9488
  const account = byId.get(slot.id);
8153
9489
  let report;