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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -12,6 +12,90 @@ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
12
12
  import { createServer } from "node:http";
13
13
  import { createServer as createServer$1 } from "node:net";
14
14
  import { WebError } from "@deepseek-ai/dsh-web";
15
+ //#region src/cost-facts.ts
16
+ const zeroCostTokens = () => ({
17
+ uncachedInputTokens: 0,
18
+ outputTokens: 0,
19
+ cacheReadTokens: 0,
20
+ cacheWriteTokens: 0
21
+ });
22
+ const COST_TOKEN_KEYS = [
23
+ "uncachedInputTokens",
24
+ "outputTokens",
25
+ "cacheReadTokens",
26
+ "cacheWriteTokens"
27
+ ];
28
+ function peakHour(at, windows) {
29
+ const time = new Date(at);
30
+ return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
31
+ }
32
+ /**
33
+ * All prompt billing buckets determine a request's band, never cumulative
34
+ * session input.
35
+ *
36
+ * A tiered model returns the matching band and does NOT then apply `peak`: the
37
+ * page publishes those two dimensions independently and no row carries both
38
+ * today, so the combination becoming real is visible before it reaches a user.
39
+ * This ordering is a latent choice, not a verified upstream rule.
40
+ */
41
+ function requestRates(price, at, contextTokens, table) {
42
+ const tier = price.contextTiers?.find((tier) => tier.maxContext === void 0 || contextTokens <= tier.maxContext);
43
+ if (tier !== void 0) return tier;
44
+ return price.peak !== void 0 && at !== null && peakHour(at, table.peakHours) ? price.peak : price;
45
+ }
46
+ /** Cache version and wire guard: changing bands/rates must refold historical groups. */
47
+ function pricingKey(table) {
48
+ const rates = (r) => [
49
+ r.inputCost,
50
+ r.outputCost,
51
+ r.cacheReadCost,
52
+ r.cacheWriteCost ?? null
53
+ ];
54
+ const canonical = JSON.stringify([
55
+ "commandCodeCost-v1",
56
+ table.peakHours,
57
+ table.models.map((p) => [
58
+ p.id,
59
+ p.slug,
60
+ p.free === true,
61
+ rates(p),
62
+ p.peak ? rates(p.peak) : null,
63
+ p.contextTiers?.map((t) => [t.maxContext ?? null, rates(t)]) ?? null
64
+ ])
65
+ ]);
66
+ let hash = 2166136261;
67
+ for (const c of canonical) hash = Math.imul(hash ^ c.charCodeAt(0), 16777619) >>> 0;
68
+ return hash;
69
+ }
70
+ /** Only safe to merge requests whose billing classification is identical. */
71
+ function costGroupKey(group, table) {
72
+ const price = table.models.find((p) => p.id === group.model || p.slug === group.model);
73
+ return JSON.stringify([
74
+ group.provider,
75
+ group.model,
76
+ price === void 0 ? null : requestRates(price, group.at, group.contextTokens, table),
77
+ group.at === null
78
+ ]);
79
+ }
80
+ //#endregion
81
+ //#region src/plan-tiers.ts
82
+ /** Shared plan-tier presentation facts; safe to inline into the browser bundle. */
83
+ const PLAN_TIER_ORDER = [
84
+ "go",
85
+ "goat",
86
+ "pro",
87
+ "provider",
88
+ "max"
89
+ ];
90
+ const PLAN_LABELS = {
91
+ go: "Go",
92
+ goat: "GOAT",
93
+ pro: "Pro",
94
+ provider: "Provider",
95
+ max: "Max"
96
+ };
97
+ const PLAN_ORDER = Object.fromEntries(PLAN_TIER_ORDER.map((tier, index) => [tier, index]));
98
+ //#endregion
15
99
  //#region src/capabilities.ts
16
100
  /**
17
101
  * Static capability snapshot for the Command Code provider: model →
@@ -19,7 +103,7 @@ import { WebError } from "@deepseek-ai/dsh-web";
19
103
  * subscription-plan labels, deals, and hourly (peak/off-peak) pricing.
20
104
  *
21
105
  * Everything here is synced from official sources — the command-code CLI
22
- * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.66.0) and
106
+ * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.68.0) and
23
107
  * the official plan/pricing/model docs; see the dsh-commandcode-upstream skill
24
108
  * for the extraction procedures. Keeping the snapshot in its own module
25
109
  * confines those frequent sync diffs here: src/adapter.ts holds only the stable
@@ -103,6 +187,13 @@ const KNOWN_EFFORTS = {
103
187
  "xhigh",
104
188
  "max"
105
189
  ],
190
+ "claude-sonnet-5-5": [
191
+ "low",
192
+ "medium",
193
+ "high",
194
+ "xhigh",
195
+ "max"
196
+ ],
106
197
  "deepseek/deepseek-v4-flash-fast": [
107
198
  "low",
108
199
  "high",
@@ -113,6 +204,11 @@ const KNOWN_EFFORTS = {
113
204
  "high",
114
205
  "max"
115
206
  ],
207
+ "deepseek/deepseek-v4.1-flash-fast": [
208
+ "low",
209
+ "high",
210
+ "max"
211
+ ],
116
212
  "deepseek/deepseek-v4-flash": ["high", "max"],
117
213
  "deepseek/deepseek-v4-flash-vision-exp": ["high", "max"],
118
214
  "deepseek/deepseek-v4-pro": ["high", "max"],
@@ -348,8 +444,10 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
348
444
  "claude-opus-5",
349
445
  "claude-sonnet-4-6",
350
446
  "claude-sonnet-5",
447
+ "claude-sonnet-5-5",
351
448
  "deepseek/deepseek-v4-flash-vision-exp",
352
449
  "deepseek/deepseek-v4.1-flash",
450
+ "deepseek/deepseek-v4.1-flash-fast",
353
451
  "google/gemini-3.1-flash-lite",
354
452
  "google/gemini-3.5-flash",
355
453
  "google/gemini-3.5-flash-lite",
@@ -395,12 +493,13 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
395
493
  ]);
396
494
  /**
397
495
  * Models WITHOUT a zero-data-retention upstream, per the official CLI's own
398
- * registry (`command-code@1.66.0` `dist/cli.mjs`): `modelSupportsZdr(id)` is
496
+ * registry (`command-code@1.68.0` `dist/cli.mjs`): `modelSupportsZdr(id)` is
399
497
  * exactly `!nonZdrSet.has(canonicalize(id))`, and `knownModelSupportsZdr`
400
498
  * carries the same membership in the sibling route table — the UNION of both
401
499
  * is this set. Reading only the sibling route table would drop `meituan/
402
- * LongCat-2.0` and `stealth/pixel-canary`, which each appear in
403
- * `modelSupportsZdr` alone. The official docs (commandcode.ai/docs/resources/
500
+ * LongCat-2.0`, which appears in `modelSupportsZdr` alone (re-read from the
501
+ * 1.66.0 / 1.67.0 / 1.68.0 artifacts on 2026-09-29: the two stealth-preview
502
+ * rows sit in BOTH sets, so LongCat is the only divergence). The official docs (commandcode.ai/docs/resources/
404
503
  * zdr) put it in prose — "99% of our models have ZDR-capable upstreams … only
405
504
  * a small handful of models are affected" — so the CLI's exclusion list is
406
505
  * the only per-model evidence there is; a ZDR request naming one of these
@@ -422,12 +521,15 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
422
521
  * (its `zdr:{only:[…]}` provider routes and the per-provider `zdr`/`noTraining`
423
522
  * flags) is upstream-internal routing, not a per-model contract, so this table
424
523
  * is the snapshot of the exclusion set and nothing more. It is a rare change:
425
- * 20 members held across 1.62.0 → 1.64.0, and 1.65.0 and 1.66.0 each added
426
- * exactly one (the two stealth-preview models below).
524
+ * 20 members held across 1.62.0 → 1.64.0, 1.65.0 and 1.66.0 each added exactly
525
+ * one (the two stealth-preview models below), 1.67.0 added one
526
+ * (`deepseek/deepseek-v4.1-flash-fast`), and 1.68.0 changed nothing — 23
527
+ * members as of 2026-09-29.
427
528
  */
428
529
  const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
429
530
  "MiniMaxAI/MiniMax-M3",
430
531
  "Qwen/Qwen3.8-Max-0902",
532
+ "deepseek/deepseek-v4.1-flash-fast",
431
533
  "meituan/LongCat-2.0",
432
534
  "meta/muse-spark-1.1",
433
535
  "meta/muse-spark-1.2",
@@ -539,8 +641,8 @@ function requiresMessagesEndpoint(modelId) {
539
641
  * `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
540
642
  * list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
541
643
  * Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
542
- * Provider-tier. Re-verified at command-code@1.66.0 (2026-09-27): 82 catalog
543
- * ids at 52/60/74/82 cumulative, a strict superset chain — every release since
644
+ * Provider-tier. Re-verified at command-code@1.68.0 (2026-09-29): 84 catalog
645
+ * ids at 53/62/76/84 cumulative, a strict superset chain — every release since
544
646
  * 1.49.0 has been additive with no tier move, and per-entry tags below name the
545
647
  * release that added each row.
546
648
  *
@@ -568,6 +670,7 @@ const KNOWN_PLANS = {
568
670
  "Qwen/Qwen3.8-Omni-Flash": "go",
569
671
  "deepseek/deepseek-v4-flash-fast": "go",
570
672
  "deepseek/deepseek-v4.1-flash": "go",
673
+ "deepseek/deepseek-v4.1-flash-fast": "go",
571
674
  "deepseek/deepseek-v4-flash": "go",
572
675
  "deepseek/deepseek-v4-flash-vision-exp": "go",
573
676
  "deepseek/deepseek-v4-pro": "go",
@@ -605,6 +708,7 @@ const KNOWN_PLANS = {
605
708
  "zai-org/GLM-5.2": "go",
606
709
  "zai-org/GLM-5.2-Fast": "go",
607
710
  "zai-org/GLM-5.3": "go",
711
+ "claude-sonnet-5-5": "goat",
608
712
  "google/gemini-3.7-flash": "goat",
609
713
  "google/gemini-3.8-flash": "goat",
610
714
  "gpt-5.6-sol": "goat",
@@ -636,25 +740,6 @@ const KNOWN_PLANS = {
636
740
  "gpt-6-astra": "provider",
637
741
  "sakana/fugu-ultra": "provider"
638
742
  };
639
- /** Official display labels for each plan tier. */
640
- const PLAN_LABELS = {
641
- go: "Go",
642
- goat: "GOAT",
643
- pro: "Pro",
644
- provider: "Provider",
645
- max: "Max"
646
- };
647
- /**
648
- * Plan-tier sort weights, low to high. Models outside the snapshot (unknown
649
- * plans) sort after every known tier, keeping known models predictable.
650
- */
651
- const PLAN_ORDER = {
652
- go: 0,
653
- goat: 1,
654
- pro: 2,
655
- provider: 3,
656
- max: 4
657
- };
658
743
  /**
659
744
  * Whether a model is free (requests cost no credits), per the pricing page's
660
745
  * deals (`KNOWN_DEALS` `free: true`). Free models lead the picker regardless
@@ -682,7 +767,7 @@ function compareByPlan(a, b) {
682
767
  /**
683
768
  * Subscription plan table, synced from the official CLI bundle's plan maps
684
769
  * (located by the `"individual-go"` key in `dist/cli.mjs`, re-verified unchanged
685
- * through command-code@1.66.0): subscription `planId` prefix → display name and
770
+ * through command-code@1.68.0): subscription `planId` prefix → display name and
686
771
  * the plan's monthly credit total. This is the account's own subscription
687
772
  * (from `/alpha/billing/subscriptions`) — distinct from {@link KNOWN_PLANS},
688
773
  * which maps catalog models to their minimum tier.
@@ -812,7 +897,7 @@ const KNOWN_DEALS = {
812
897
  * charges by the hour: peak hours are 01:00–04:00 and 06:00–10:00 UTC (7h per
813
898
  * weekday, full price) **Monday to Friday only**; the other 17 hours of a
814
899
  * weekday and every hour of Saturday/Sunday (UTC) are off-peak at half price.
815
- * Exactly four models carry the page's `timeOfDay` block (the four rows below).
900
+ * Exactly five models carry the page's `timeOfDay` block (the five rows below).
816
901
  * The picker shows the *current* state as a compact label (`Peak`/`Half`)
817
902
  * matching the English noun style of the other markers (`Image`, `FREE`), so a
818
903
  * developer can tell at a glance whether calling the model right now is cheap
@@ -837,7 +922,8 @@ const KNOWN_PEAK_PRICING = /* @__PURE__ */ new Set([
837
922
  "deepseek/deepseek-v4-pro",
838
923
  "deepseek/deepseek-v4-flash",
839
924
  "deepseek/deepseek-v4-flash-vision-exp",
840
- "deepseek/deepseek-v4.1-flash"
925
+ "deepseek/deepseek-v4.1-flash",
926
+ "deepseek/deepseek-v4.1-flash-fast"
841
927
  ]);
842
928
  /**
843
929
  * Peak hours (UTC, hour-of-day range end-exclusive): 01–03 and 06–09.
@@ -860,11 +946,7 @@ const PEAK_HOUR_RANGES = [[1, 4], [6, 10]];
860
946
  * the one in {@link KNOWN_PEAK_PRICING} still gets the right half of the day.
861
947
  */
862
948
  function isPeakPricingHour(now = Date.now()) {
863
- const at = new Date(now);
864
- const day = at.getUTCDay();
865
- if (day === 0 || day === 6) return false;
866
- const hour = at.getUTCHours();
867
- return PEAK_HOUR_RANGES.some(([start, end]) => hour >= start && hour < end);
949
+ return peakHour(now, PEAK_HOUR_RANGES);
868
950
  }
869
951
  /**
870
952
  * As {@link isPeakPricingHour}, plus the {@link KNOWN_PEAK_PRICING} membership
@@ -945,6 +1027,27 @@ function capabilityDescription(modelId, contextWindow, now = Date.now()) {
945
1027
  //#endregion
946
1028
  //#region src/model-prices.ts
947
1029
  /**
1030
+ * The plan tiers a per-model allowance exists for, cheapest first. Ordering
1031
+ * matters: {@link allowanceTierForWeight} matches a tier weight against these
1032
+ * rather than against hard-coded numbers, so this table and the subscription
1033
+ * table cannot drift apart silently.
1034
+ */
1035
+ const ALLOWANCE_TIERS = ["goat", "pro"];
1036
+ /**
1037
+ * Which allowance bracket a plan-tier weight belongs to, or undefined when that
1038
+ * plan has no per-model allowance.
1039
+ *
1040
+ * Weights come from `KNOWN_SUBSCRIPTION_PLANS` (go 0 · goat 1 · pro 2 ·
1041
+ * 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.
1045
+ */
1046
+ function allowanceTierForWeight(tierWeight) {
1047
+ if (tierWeight === void 0) return void 0;
1048
+ return ALLOWANCE_TIERS.find((tier) => PLAN_ORDER[tier] === tierWeight);
1049
+ }
1050
+ /**
948
1051
  * Every price row the page publishes, ordered by its own slug.
949
1052
  *
950
1053
  * Note on the GPT rows: `gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini` and
@@ -964,7 +1067,11 @@ const MODEL_PRICE_ROWS = [
964
1067
  50,
965
1068
  1,
966
1069
  12.5
967
- ]
1070
+ ],
1071
+ allowance: {
1072
+ goat: 20,
1073
+ pro: 20
1074
+ }
968
1075
  },
969
1076
  {
970
1077
  id: "claude-fable-5-1",
@@ -973,7 +1080,11 @@ const MODEL_PRICE_ROWS = [
973
1080
  50,
974
1081
  .25,
975
1082
  12.5
976
- ]
1083
+ ],
1084
+ allowance: {
1085
+ goat: 20,
1086
+ pro: 20
1087
+ }
977
1088
  },
978
1089
  {
979
1090
  id: "claude-haiku-4-5",
@@ -982,7 +1093,11 @@ const MODEL_PRICE_ROWS = [
982
1093
  5,
983
1094
  .1,
984
1095
  1.25
985
- ]
1096
+ ],
1097
+ allowance: {
1098
+ goat: 20,
1099
+ pro: 20
1100
+ }
986
1101
  },
987
1102
  {
988
1103
  id: "claude-opus-4-6",
@@ -991,7 +1106,11 @@ const MODEL_PRICE_ROWS = [
991
1106
  25,
992
1107
  .5,
993
1108
  6.25
994
- ]
1109
+ ],
1110
+ allowance: {
1111
+ goat: 20,
1112
+ pro: 20
1113
+ }
995
1114
  },
996
1115
  {
997
1116
  id: "claude-opus-4-7",
@@ -1000,7 +1119,11 @@ const MODEL_PRICE_ROWS = [
1000
1119
  25,
1001
1120
  .5,
1002
1121
  6.25
1003
- ]
1122
+ ],
1123
+ allowance: {
1124
+ goat: 20,
1125
+ pro: 20
1126
+ }
1004
1127
  },
1005
1128
  {
1006
1129
  id: "claude-opus-4-8",
@@ -1009,7 +1132,11 @@ const MODEL_PRICE_ROWS = [
1009
1132
  25,
1010
1133
  .5,
1011
1134
  6.25
1012
- ]
1135
+ ],
1136
+ allowance: {
1137
+ goat: 20,
1138
+ pro: 20
1139
+ }
1013
1140
  },
1014
1141
  {
1015
1142
  id: "claude-opus-5",
@@ -1018,7 +1145,11 @@ const MODEL_PRICE_ROWS = [
1018
1145
  25,
1019
1146
  .5,
1020
1147
  6.25
1021
- ]
1148
+ ],
1149
+ allowance: {
1150
+ goat: 20,
1151
+ pro: 20
1152
+ }
1022
1153
  },
1023
1154
  {
1024
1155
  id: "claude-opus-5-5",
@@ -1027,7 +1158,11 @@ const MODEL_PRICE_ROWS = [
1027
1158
  20,
1028
1159
  .2,
1029
1160
  5
1030
- ]
1161
+ ],
1162
+ allowance: {
1163
+ goat: 20,
1164
+ pro: 20
1165
+ }
1031
1166
  },
1032
1167
  {
1033
1168
  id: "claude-sonnet-4-6",
@@ -1036,7 +1171,11 @@ const MODEL_PRICE_ROWS = [
1036
1171
  15,
1037
1172
  .3,
1038
1173
  3.75
1039
- ]
1174
+ ],
1175
+ allowance: {
1176
+ goat: 20,
1177
+ pro: 20
1178
+ }
1040
1179
  },
1041
1180
  {
1042
1181
  id: "claude-sonnet-5",
@@ -1045,7 +1184,24 @@ const MODEL_PRICE_ROWS = [
1045
1184
  10,
1046
1185
  .2,
1047
1186
  2.5
1048
- ]
1187
+ ],
1188
+ allowance: {
1189
+ goat: 20,
1190
+ pro: 20
1191
+ }
1192
+ },
1193
+ {
1194
+ id: "claude-sonnet-5-5",
1195
+ rates: [
1196
+ 2,
1197
+ 10,
1198
+ .2,
1199
+ 2.5
1200
+ ],
1201
+ allowance: {
1202
+ goat: 10,
1203
+ pro: 20
1204
+ }
1049
1205
  },
1050
1206
  {
1051
1207
  id: "deepseek-v4-flash",
@@ -1058,7 +1214,11 @@ const MODEL_PRICE_ROWS = [
1058
1214
  .3,
1059
1215
  1.2,
1060
1216
  .006
1061
- ]
1217
+ ],
1218
+ allowance: {
1219
+ goat: 60,
1220
+ pro: 70
1221
+ }
1062
1222
  },
1063
1223
  {
1064
1224
  id: "deepseek-v4-flash-fast",
@@ -1066,7 +1226,11 @@ const MODEL_PRICE_ROWS = [
1066
1226
  .28,
1067
1227
  .56,
1068
1228
  .07
1069
- ]
1229
+ ],
1230
+ allowance: {
1231
+ goat: 20,
1232
+ pro: 30
1233
+ }
1070
1234
  },
1071
1235
  {
1072
1236
  id: "deepseek-v4-flash-vision-exp",
@@ -1079,7 +1243,11 @@ const MODEL_PRICE_ROWS = [
1079
1243
  .3,
1080
1244
  1.2,
1081
1245
  .006
1082
- ]
1246
+ ],
1247
+ allowance: {
1248
+ goat: 20,
1249
+ pro: 30
1250
+ }
1083
1251
  },
1084
1252
  {
1085
1253
  id: "deepseek-v4-pro",
@@ -1092,7 +1260,11 @@ const MODEL_PRICE_ROWS = [
1092
1260
  1.32,
1093
1261
  3.96,
1094
1262
  .044
1095
- ]
1263
+ ],
1264
+ allowance: {
1265
+ goat: 20,
1266
+ pro: 30
1267
+ }
1096
1268
  },
1097
1269
  {
1098
1270
  id: "deepseek-v4.1-flash",
@@ -1105,7 +1277,28 @@ const MODEL_PRICE_ROWS = [
1105
1277
  .3,
1106
1278
  1.2,
1107
1279
  .006
1108
- ]
1280
+ ],
1281
+ allowance: {
1282
+ goat: 60,
1283
+ pro: 70
1284
+ }
1285
+ },
1286
+ {
1287
+ id: "deepseek-v4.1-flash-fast",
1288
+ rates: [
1289
+ .16,
1290
+ .58,
1291
+ .016
1292
+ ],
1293
+ peak: [
1294
+ .32,
1295
+ 1.16,
1296
+ .032
1297
+ ],
1298
+ allowance: {
1299
+ goat: 60,
1300
+ pro: 70
1301
+ }
1109
1302
  },
1110
1303
  {
1111
1304
  id: "fugu-ultra",
@@ -1113,7 +1306,11 @@ const MODEL_PRICE_ROWS = [
1113
1306
  5,
1114
1307
  30,
1115
1308
  .5
1116
- ]
1309
+ ],
1310
+ allowance: {
1311
+ goat: 20,
1312
+ pro: 20
1313
+ }
1117
1314
  },
1118
1315
  {
1119
1316
  id: "gemini-3.1-flash-lite",
@@ -1121,7 +1318,11 @@ const MODEL_PRICE_ROWS = [
1121
1318
  .25,
1122
1319
  1.5,
1123
1320
  .03
1124
- ]
1321
+ ],
1322
+ allowance: {
1323
+ goat: 20,
1324
+ pro: 20
1325
+ }
1125
1326
  },
1126
1327
  {
1127
1328
  id: "gemini-3.5-flash",
@@ -1129,7 +1330,11 @@ const MODEL_PRICE_ROWS = [
1129
1330
  1.5,
1130
1331
  9,
1131
1332
  .15
1132
- ]
1333
+ ],
1334
+ allowance: {
1335
+ goat: 20,
1336
+ pro: 20
1337
+ }
1133
1338
  },
1134
1339
  {
1135
1340
  id: "gemini-3.5-flash-lite",
@@ -1137,7 +1342,11 @@ const MODEL_PRICE_ROWS = [
1137
1342
  .3,
1138
1343
  2.5,
1139
1344
  .03
1140
- ]
1345
+ ],
1346
+ allowance: {
1347
+ goat: 20,
1348
+ pro: 20
1349
+ }
1141
1350
  },
1142
1351
  {
1143
1352
  id: "gemini-3.6-flash",
@@ -1145,7 +1354,11 @@ const MODEL_PRICE_ROWS = [
1145
1354
  1.5,
1146
1355
  7.5,
1147
1356
  .15
1148
- ]
1357
+ ],
1358
+ allowance: {
1359
+ goat: 20,
1360
+ pro: 20
1361
+ }
1149
1362
  },
1150
1363
  {
1151
1364
  id: "gemini-3.7-flash",
@@ -1154,7 +1367,11 @@ const MODEL_PRICE_ROWS = [
1154
1367
  7.5,
1155
1368
  .15,
1156
1369
  .08334
1157
- ]
1370
+ ],
1371
+ allowance: {
1372
+ goat: 40,
1373
+ pro: 50
1374
+ }
1158
1375
  },
1159
1376
  {
1160
1377
  id: "gemini-3.8-flash",
@@ -1162,7 +1379,11 @@ const MODEL_PRICE_ROWS = [
1162
1379
  1.5,
1163
1380
  7.5,
1164
1381
  .15
1165
- ]
1382
+ ],
1383
+ allowance: {
1384
+ goat: 40,
1385
+ pro: 50
1386
+ }
1166
1387
  },
1167
1388
  {
1168
1389
  id: "glm-5",
@@ -1170,7 +1391,11 @@ const MODEL_PRICE_ROWS = [
1170
1391
  1,
1171
1392
  3.2,
1172
1393
  .2
1173
- ]
1394
+ ],
1395
+ allowance: {
1396
+ goat: 20,
1397
+ pro: 30
1398
+ }
1174
1399
  },
1175
1400
  {
1176
1401
  id: "glm-5.1",
@@ -1178,7 +1403,11 @@ const MODEL_PRICE_ROWS = [
1178
1403
  1.4,
1179
1404
  4.4,
1180
1405
  .26
1181
- ]
1406
+ ],
1407
+ allowance: {
1408
+ goat: 20,
1409
+ pro: 30
1410
+ }
1182
1411
  },
1183
1412
  {
1184
1413
  id: "glm-5.2",
@@ -1186,7 +1415,11 @@ const MODEL_PRICE_ROWS = [
1186
1415
  1.4,
1187
1416
  4.4,
1188
1417
  .26
1189
- ]
1418
+ ],
1419
+ allowance: {
1420
+ goat: 70,
1421
+ pro: 80
1422
+ }
1190
1423
  },
1191
1424
  {
1192
1425
  id: "glm-5.2-fast",
@@ -1194,7 +1427,11 @@ const MODEL_PRICE_ROWS = [
1194
1427
  3,
1195
1428
  10.25,
1196
1429
  .5
1197
- ]
1430
+ ],
1431
+ allowance: {
1432
+ goat: 20,
1433
+ pro: 30
1434
+ }
1198
1435
  },
1199
1436
  {
1200
1437
  id: "glm-5.3",
@@ -1202,7 +1439,11 @@ const MODEL_PRICE_ROWS = [
1202
1439
  1.4,
1203
1440
  4.4,
1204
1441
  .26
1205
- ]
1442
+ ],
1443
+ allowance: {
1444
+ goat: 20,
1445
+ pro: 30
1446
+ }
1206
1447
  },
1207
1448
  {
1208
1449
  id: "glm-5.3-flash",
@@ -1210,7 +1451,11 @@ const MODEL_PRICE_ROWS = [
1210
1451
  .15,
1211
1452
  .5,
1212
1453
  .03
1213
- ]
1454
+ ],
1455
+ allowance: {
1456
+ goat: 40,
1457
+ pro: 50
1458
+ }
1214
1459
  },
1215
1460
  {
1216
1461
  id: "glm-5.3-flashx",
@@ -1218,7 +1463,11 @@ const MODEL_PRICE_ROWS = [
1218
1463
  .37,
1219
1464
  1.25,
1220
1465
  .075
1221
- ]
1466
+ ],
1467
+ allowance: {
1468
+ goat: 20,
1469
+ pro: 30
1470
+ }
1222
1471
  },
1223
1472
  {
1224
1473
  id: "gpt-5.3-codex",
@@ -1227,7 +1476,11 @@ const MODEL_PRICE_ROWS = [
1227
1476
  8,
1228
1477
  .5,
1229
1478
  0
1230
- ]
1479
+ ],
1480
+ allowance: {
1481
+ goat: 20,
1482
+ pro: 20
1483
+ }
1231
1484
  },
1232
1485
  {
1233
1486
  id: "gpt-5.4",
@@ -1236,7 +1489,11 @@ const MODEL_PRICE_ROWS = [
1236
1489
  15,
1237
1490
  .25,
1238
1491
  0
1239
- ]
1492
+ ],
1493
+ allowance: {
1494
+ goat: 20,
1495
+ pro: 20
1496
+ }
1240
1497
  },
1241
1498
  {
1242
1499
  id: "gpt-5.4-mini",
@@ -1245,7 +1502,11 @@ const MODEL_PRICE_ROWS = [
1245
1502
  4.5,
1246
1503
  .075,
1247
1504
  0
1248
- ]
1505
+ ],
1506
+ allowance: {
1507
+ goat: 20,
1508
+ pro: 20
1509
+ }
1249
1510
  },
1250
1511
  {
1251
1512
  id: "gpt-5.5",
@@ -1254,7 +1515,11 @@ const MODEL_PRICE_ROWS = [
1254
1515
  30,
1255
1516
  .5,
1256
1517
  0
1257
- ]
1518
+ ],
1519
+ allowance: {
1520
+ goat: 20,
1521
+ pro: 20
1522
+ }
1258
1523
  },
1259
1524
  {
1260
1525
  id: "gpt-5.6-luna",
@@ -1264,6 +1529,10 @@ const MODEL_PRICE_ROWS = [
1264
1529
  .02,
1265
1530
  .25
1266
1531
  ],
1532
+ allowance: {
1533
+ goat: 20,
1534
+ pro: 30
1535
+ },
1267
1536
  contextTiers: [{
1268
1537
  "maxContext": 272e3,
1269
1538
  "rates": [
@@ -1287,6 +1556,10 @@ const MODEL_PRICE_ROWS = [
1287
1556
  .5,
1288
1557
  6.25
1289
1558
  ],
1559
+ allowance: {
1560
+ goat: 70,
1561
+ pro: 80
1562
+ },
1290
1563
  contextTiers: [{
1291
1564
  "maxContext": 272e3,
1292
1565
  "rates": [
@@ -1310,6 +1583,10 @@ const MODEL_PRICE_ROWS = [
1310
1583
  .2,
1311
1584
  2.5
1312
1585
  ],
1586
+ allowance: {
1587
+ goat: 20,
1588
+ pro: 20
1589
+ },
1313
1590
  contextTiers: [{
1314
1591
  "maxContext": 272e3,
1315
1592
  "rates": [
@@ -1333,6 +1610,10 @@ const MODEL_PRICE_ROWS = [
1333
1610
  1,
1334
1611
  12.5
1335
1612
  ],
1613
+ allowance: {
1614
+ goat: 20,
1615
+ pro: 20
1616
+ },
1336
1617
  contextTiers: [{
1337
1618
  "maxContext": 272e3,
1338
1619
  "rates": [
@@ -1356,6 +1637,10 @@ const MODEL_PRICE_ROWS = [
1356
1637
  .01,
1357
1638
  .125
1358
1639
  ],
1640
+ allowance: {
1641
+ goat: 20,
1642
+ pro: 30
1643
+ },
1359
1644
  contextTiers: [{
1360
1645
  "maxContext": 272e3,
1361
1646
  "rates": [
@@ -1379,6 +1664,10 @@ const MODEL_PRICE_ROWS = [
1379
1664
  .2,
1380
1665
  2.5
1381
1666
  ],
1667
+ allowance: {
1668
+ goat: 20,
1669
+ pro: 20
1670
+ },
1382
1671
  contextTiers: [{
1383
1672
  "maxContext": 272e3,
1384
1673
  "rates": [
@@ -1400,7 +1689,11 @@ const MODEL_PRICE_ROWS = [
1400
1689
  2,
1401
1690
  6,
1402
1691
  .5
1403
- ]
1692
+ ],
1693
+ allowance: {
1694
+ goat: 20,
1695
+ pro: 30
1696
+ }
1404
1697
  },
1405
1698
  {
1406
1699
  id: "grok-4.6",
@@ -1409,6 +1702,10 @@ const MODEL_PRICE_ROWS = [
1409
1702
  6,
1410
1703
  .5
1411
1704
  ],
1705
+ allowance: {
1706
+ goat: 20,
1707
+ pro: 30
1708
+ },
1412
1709
  contextTiers: [{
1413
1710
  "maxContext": 2e5,
1414
1711
  "rates": [
@@ -1429,18 +1726,39 @@ const MODEL_PRICE_ROWS = [
1429
1726
  3.6,
1430
1727
  .3
1431
1728
  ],
1729
+ listRates: [
1730
+ 2,
1731
+ 6,
1732
+ .5
1733
+ ],
1734
+ allowance: {
1735
+ goat: 20,
1736
+ pro: 30
1737
+ },
1432
1738
  contextTiers: [{
1433
1739
  "maxContext": 2e5,
1434
1740
  "rates": [
1435
1741
  1.2,
1436
1742
  3.6,
1437
1743
  .3
1744
+ ],
1745
+ "listRates": [
1746
+ 2,
1747
+ 6,
1748
+ .5
1438
1749
  ]
1439
- }, { "rates": [
1440
- 2.4,
1441
- 7.2,
1442
- .6
1443
- ] }]
1750
+ }, {
1751
+ "rates": [
1752
+ 2.4,
1753
+ 7.2,
1754
+ .6
1755
+ ],
1756
+ "listRates": [
1757
+ 4,
1758
+ 12,
1759
+ 1
1760
+ ]
1761
+ }]
1444
1762
  },
1445
1763
  {
1446
1764
  id: "inkling",
@@ -1448,7 +1766,11 @@ const MODEL_PRICE_ROWS = [
1448
1766
  1,
1449
1767
  4.05,
1450
1768
  .17
1451
- ]
1769
+ ],
1770
+ allowance: {
1771
+ goat: 20,
1772
+ pro: 30
1773
+ }
1452
1774
  },
1453
1775
  {
1454
1776
  id: "inkling-small",
@@ -1456,7 +1778,11 @@ const MODEL_PRICE_ROWS = [
1456
1778
  .5,
1457
1779
  1.2,
1458
1780
  .1
1459
- ]
1781
+ ],
1782
+ allowance: {
1783
+ goat: 20,
1784
+ pro: 30
1785
+ }
1460
1786
  },
1461
1787
  {
1462
1788
  id: "kimi-k2.5",
@@ -1464,7 +1790,11 @@ const MODEL_PRICE_ROWS = [
1464
1790
  .6,
1465
1791
  3,
1466
1792
  .1
1467
- ]
1793
+ ],
1794
+ allowance: {
1795
+ goat: 20,
1796
+ pro: 30
1797
+ }
1468
1798
  },
1469
1799
  {
1470
1800
  id: "kimi-k2.6",
@@ -1472,7 +1802,11 @@ const MODEL_PRICE_ROWS = [
1472
1802
  .95,
1473
1803
  4,
1474
1804
  .16
1475
- ]
1805
+ ],
1806
+ allowance: {
1807
+ goat: 20,
1808
+ pro: 30
1809
+ }
1476
1810
  },
1477
1811
  {
1478
1812
  id: "kimi-k2.7-code",
@@ -1480,7 +1814,11 @@ const MODEL_PRICE_ROWS = [
1480
1814
  .95,
1481
1815
  4,
1482
1816
  .19
1483
- ]
1817
+ ],
1818
+ allowance: {
1819
+ goat: 60,
1820
+ pro: 70
1821
+ }
1484
1822
  },
1485
1823
  {
1486
1824
  id: "kimi-k2.7-code-highspeed",
@@ -1488,7 +1826,11 @@ const MODEL_PRICE_ROWS = [
1488
1826
  1.9,
1489
1827
  8,
1490
1828
  .38
1491
- ]
1829
+ ],
1830
+ allowance: {
1831
+ goat: 20,
1832
+ pro: 30
1833
+ }
1492
1834
  },
1493
1835
  {
1494
1836
  id: "kimi-k3",
@@ -1496,7 +1838,11 @@ const MODEL_PRICE_ROWS = [
1496
1838
  3,
1497
1839
  15,
1498
1840
  .3
1499
- ]
1841
+ ],
1842
+ allowance: {
1843
+ goat: 20,
1844
+ pro: 30
1845
+ }
1500
1846
  },
1501
1847
  {
1502
1848
  id: "longcat-2.0",
@@ -1504,7 +1850,11 @@ const MODEL_PRICE_ROWS = [
1504
1850
  .3,
1505
1851
  1.2,
1506
1852
  .006
1507
- ]
1853
+ ],
1854
+ allowance: {
1855
+ goat: 50,
1856
+ pro: 60
1857
+ }
1508
1858
  },
1509
1859
  {
1510
1860
  id: "mimo-v2.5",
@@ -1512,7 +1862,16 @@ const MODEL_PRICE_ROWS = [
1512
1862
  .14,
1513
1863
  .28,
1514
1864
  .0028
1515
- ]
1865
+ ],
1866
+ listRates: [
1867
+ .8,
1868
+ 4,
1869
+ .16
1870
+ ],
1871
+ allowance: {
1872
+ goat: 30,
1873
+ pro: 40
1874
+ }
1516
1875
  },
1517
1876
  {
1518
1877
  id: "mimo-v2.5-pro",
@@ -1520,7 +1879,16 @@ const MODEL_PRICE_ROWS = [
1520
1879
  .435,
1521
1880
  .87,
1522
1881
  .0036
1523
- ]
1882
+ ],
1883
+ listRates: [
1884
+ 2,
1885
+ 6,
1886
+ .4
1887
+ ],
1888
+ allowance: {
1889
+ goat: 20,
1890
+ pro: 30
1891
+ }
1524
1892
  },
1525
1893
  {
1526
1894
  id: "mimo-v2.6-flash",
@@ -1528,7 +1896,11 @@ const MODEL_PRICE_ROWS = [
1528
1896
  .14,
1529
1897
  .28,
1530
1898
  .0028
1531
- ]
1899
+ ],
1900
+ allowance: {
1901
+ goat: 20,
1902
+ pro: 30
1903
+ }
1532
1904
  },
1533
1905
  {
1534
1906
  id: "mimo-v2.6-pro",
@@ -1536,7 +1908,11 @@ const MODEL_PRICE_ROWS = [
1536
1908
  .435,
1537
1909
  .87,
1538
1910
  .0036
1539
- ]
1911
+ ],
1912
+ allowance: {
1913
+ goat: 20,
1914
+ pro: 30
1915
+ }
1540
1916
  },
1541
1917
  {
1542
1918
  id: "mimo-v2.6-pro-ultraspeed",
@@ -1544,7 +1920,11 @@ const MODEL_PRICE_ROWS = [
1544
1920
  4.35,
1545
1921
  8.7,
1546
1922
  .036
1547
- ]
1923
+ ],
1924
+ allowance: {
1925
+ goat: 10,
1926
+ pro: 20
1927
+ }
1548
1928
  },
1549
1929
  {
1550
1930
  id: "minimax-m2.5",
@@ -1552,7 +1932,11 @@ const MODEL_PRICE_ROWS = [
1552
1932
  .3,
1553
1933
  1.2,
1554
1934
  .03
1555
- ]
1935
+ ],
1936
+ allowance: {
1937
+ goat: 20,
1938
+ pro: 30
1939
+ }
1556
1940
  },
1557
1941
  {
1558
1942
  id: "minimax-m2.7",
@@ -1560,7 +1944,11 @@ const MODEL_PRICE_ROWS = [
1560
1944
  .3,
1561
1945
  1.2,
1562
1946
  .06
1563
- ]
1947
+ ],
1948
+ allowance: {
1949
+ goat: 20,
1950
+ pro: 30
1951
+ }
1564
1952
  },
1565
1953
  {
1566
1954
  id: "minimax-m3",
@@ -1568,7 +1956,16 @@ const MODEL_PRICE_ROWS = [
1568
1956
  .3,
1569
1957
  1.2,
1570
1958
  .06
1571
- ]
1959
+ ],
1960
+ listRates: [
1961
+ .6,
1962
+ 2.4,
1963
+ .12
1964
+ ],
1965
+ allowance: {
1966
+ goat: 47,
1967
+ pro: 57
1968
+ }
1572
1969
  },
1573
1970
  {
1574
1971
  id: "muse-spark-1.1",
@@ -1576,7 +1973,11 @@ const MODEL_PRICE_ROWS = [
1576
1973
  1.25,
1577
1974
  4.25,
1578
1975
  .15
1579
- ]
1976
+ ],
1977
+ allowance: {
1978
+ goat: 20,
1979
+ pro: 20
1980
+ }
1580
1981
  },
1581
1982
  {
1582
1983
  id: "muse-spark-1.2",
@@ -1584,7 +1985,11 @@ const MODEL_PRICE_ROWS = [
1584
1985
  1.25,
1585
1986
  4.25,
1586
1987
  .15
1587
- ]
1988
+ ],
1989
+ allowance: {
1990
+ goat: 20,
1991
+ pro: 30
1992
+ }
1588
1993
  },
1589
1994
  {
1590
1995
  id: "muse-spark-1.2-contributor",
@@ -1592,7 +1997,11 @@ const MODEL_PRICE_ROWS = [
1592
1997
  .1,
1593
1998
  .2,
1594
1999
  .002
1595
- ]
2000
+ ],
2001
+ allowance: {
2002
+ goat: 20,
2003
+ pro: 30
2004
+ }
1596
2005
  },
1597
2006
  {
1598
2007
  id: "muse-spark-1.3",
@@ -1600,7 +2009,11 @@ const MODEL_PRICE_ROWS = [
1600
2009
  1.25,
1601
2010
  4.25,
1602
2011
  .15
1603
- ]
2012
+ ],
2013
+ allowance: {
2014
+ goat: 20,
2015
+ pro: 30
2016
+ }
1604
2017
  },
1605
2018
  {
1606
2019
  id: "muse-spark-1.3-contributor",
@@ -1608,7 +2021,11 @@ const MODEL_PRICE_ROWS = [
1608
2021
  .1,
1609
2022
  .2,
1610
2023
  .002
1611
- ]
2024
+ ],
2025
+ allowance: {
2026
+ goat: 20,
2027
+ pro: 30
2028
+ }
1612
2029
  },
1613
2030
  {
1614
2031
  id: "nemotron-3-ultra",
@@ -1616,7 +2033,11 @@ const MODEL_PRICE_ROWS = [
1616
2033
  .6,
1617
2034
  2.4,
1618
2035
  .12
1619
- ]
2036
+ ],
2037
+ allowance: {
2038
+ goat: 20,
2039
+ pro: 30
2040
+ }
1620
2041
  },
1621
2042
  {
1622
2043
  id: "qwen-3.6-max",
@@ -1625,7 +2046,11 @@ const MODEL_PRICE_ROWS = [
1625
2046
  7.8,
1626
2047
  .26,
1627
2048
  1.63
1628
- ]
2049
+ ],
2050
+ allowance: {
2051
+ goat: 20,
2052
+ pro: 30
2053
+ }
1629
2054
  },
1630
2055
  {
1631
2056
  id: "qwen-3.6-plus",
@@ -1634,6 +2059,10 @@ const MODEL_PRICE_ROWS = [
1634
2059
  3,
1635
2060
  .1
1636
2061
  ],
2062
+ allowance: {
2063
+ goat: 33,
2064
+ pro: 43
2065
+ },
1637
2066
  contextTiers: [{
1638
2067
  "maxContext": 256e3,
1639
2068
  "rates": [
@@ -1655,6 +2084,10 @@ const MODEL_PRICE_ROWS = [
1655
2084
  .006,
1656
2085
  .038
1657
2086
  ],
2087
+ allowance: {
2088
+ goat: 20,
2089
+ pro: 30
2090
+ },
1658
2091
  contextTiers: [
1659
2092
  {
1660
2093
  "maxContext": 32e3,
@@ -1689,7 +2122,11 @@ const MODEL_PRICE_ROWS = [
1689
2122
  7.5,
1690
2123
  .5,
1691
2124
  3.13
1692
- ]
2125
+ ],
2126
+ allowance: {
2127
+ goat: 33,
2128
+ pro: 43
2129
+ }
1693
2130
  },
1694
2131
  {
1695
2132
  id: "qwen-3.7-plus",
@@ -1699,6 +2136,10 @@ const MODEL_PRICE_ROWS = [
1699
2136
  .08,
1700
2137
  .5
1701
2138
  ],
2139
+ allowance: {
2140
+ goat: 33,
2141
+ pro: 43
2142
+ },
1702
2143
  contextTiers: [{
1703
2144
  "maxContext": 256e3,
1704
2145
  "rates": [
@@ -1720,7 +2161,11 @@ const MODEL_PRICE_ROWS = [
1720
2161
  .4,
1721
2162
  3,
1722
2163
  .04
1723
- ]
2164
+ ],
2165
+ allowance: {
2166
+ goat: 70,
2167
+ pro: 80
2168
+ }
1724
2169
  },
1725
2170
  {
1726
2171
  id: "qwen-3.8-flash",
@@ -1728,7 +2173,11 @@ const MODEL_PRICE_ROWS = [
1728
2173
  .16,
1729
2174
  .47,
1730
2175
  .016
1731
- ]
2176
+ ],
2177
+ allowance: {
2178
+ goat: 20,
2179
+ pro: 30
2180
+ }
1732
2181
  },
1733
2182
  {
1734
2183
  id: "qwen-3.8-max",
@@ -1737,7 +2186,11 @@ const MODEL_PRICE_ROWS = [
1737
2186
  6,
1738
2187
  .25,
1739
2188
  2.5
1740
- ]
2189
+ ],
2190
+ allowance: {
2191
+ goat: 20,
2192
+ pro: 30
2193
+ }
1741
2194
  },
1742
2195
  {
1743
2196
  id: "qwen-3.8-max-0902",
@@ -1745,7 +2198,11 @@ const MODEL_PRICE_ROWS = [
1745
2198
  2,
1746
2199
  6,
1747
2200
  .25
1748
- ]
2201
+ ],
2202
+ allowance: {
2203
+ goat: 20,
2204
+ pro: 30
2205
+ }
1749
2206
  },
1750
2207
  {
1751
2208
  id: "qwen-3.8-omni-flash",
@@ -1753,7 +2210,11 @@ const MODEL_PRICE_ROWS = [
1753
2210
  .15,
1754
2211
  .47,
1755
2212
  .016
1756
- ]
2213
+ ],
2214
+ allowance: {
2215
+ goat: 20,
2216
+ pro: 30
2217
+ }
1757
2218
  },
1758
2219
  {
1759
2220
  id: "step-3.5-flash",
@@ -1761,7 +2222,11 @@ const MODEL_PRICE_ROWS = [
1761
2222
  .09,
1762
2223
  .3,
1763
2224
  .02
1764
- ]
2225
+ ],
2226
+ allowance: {
2227
+ goat: 20,
2228
+ pro: 30
2229
+ }
1765
2230
  },
1766
2231
  {
1767
2232
  id: "step-3.7-flash",
@@ -1769,7 +2234,11 @@ const MODEL_PRICE_ROWS = [
1769
2234
  .2,
1770
2235
  1.15,
1771
2236
  .04
1772
- ]
2237
+ ],
2238
+ allowance: {
2239
+ goat: 20,
2240
+ pro: 30
2241
+ }
1773
2242
  },
1774
2243
  {
1775
2244
  id: "step-5-preview",
@@ -1777,7 +2246,11 @@ const MODEL_PRICE_ROWS = [
1777
2246
  1,
1778
2247
  2.7,
1779
2248
  .05
1780
- ]
2249
+ ],
2250
+ allowance: {
2251
+ goat: 20,
2252
+ pro: 30
2253
+ }
1781
2254
  },
1782
2255
  {
1783
2256
  id: "tencent/hy3-paid",
@@ -1785,7 +2258,11 @@ const MODEL_PRICE_ROWS = [
1785
2258
  .14,
1786
2259
  .58,
1787
2260
  .035
1788
- ]
2261
+ ],
2262
+ allowance: {
2263
+ goat: 70,
2264
+ pro: 80
2265
+ }
1789
2266
  },
1790
2267
  {
1791
2268
  id: "tencent/hy4-preview",
@@ -1793,7 +2270,11 @@ const MODEL_PRICE_ROWS = [
1793
2270
  .834,
1794
2271
  2.501,
1795
2272
  .042
1796
- ]
2273
+ ],
2274
+ allowance: {
2275
+ goat: 20,
2276
+ pro: 30
2277
+ }
1797
2278
  },
1798
2279
  {
1799
2280
  id: "typesafe/jev",
@@ -1801,7 +2282,11 @@ const MODEL_PRICE_ROWS = [
1801
2282
  .042,
1802
2283
  0,
1803
2284
  0
1804
- ]
2285
+ ],
2286
+ allowance: {
2287
+ goat: 20,
2288
+ pro: 30
2289
+ }
1805
2290
  }
1806
2291
  ];
1807
2292
  /**
@@ -1854,16 +2339,23 @@ function ratesOf(values) {
1854
2339
  if (values[3] !== void 0) rates.cacheWriteCost = values[3];
1855
2340
  return rates;
1856
2341
  }
1857
- /** Build the full price row the browser prices a session with. */
1858
- function wireRow(id, slug, row) {
2342
+ /**
2343
+ * Build the full price row the browser prices a session with.
2344
+ *
2345
+ * Every figure switches together: once the deal stops being live, the base
2346
+ * rates AND each context band come from the pre-discount set, so a session
2347
+ * priced after a promotion ends can never mix the two.
2348
+ */
2349
+ function wireRow(id, slug, row, now) {
2350
+ const promotional = dealLabel(id, now) !== void 0;
1859
2351
  const price = {
1860
2352
  id,
1861
2353
  slug,
1862
- ...ratesOf(row.rates)
2354
+ ...ratesOf(promotional ? row.rates : row.listRates ?? row.rates)
1863
2355
  };
1864
2356
  if (row.peak !== void 0) price.peak = ratesOf(row.peak);
1865
2357
  if (row.contextTiers !== void 0) price.contextTiers = row.contextTiers.map((tier) => ({
1866
- ...ratesOf(tier.rates),
2358
+ ...ratesOf(promotional ? tier.rates : tier.listRates ?? tier.rates),
1867
2359
  ...tier.maxContext === void 0 ? {} : { maxContext: tier.maxContext }
1868
2360
  }));
1869
2361
  return price;
@@ -1878,8 +2370,13 @@ function wireRow(id, slug, row) {
1878
2370
  * explicitly at zero so the composer can say so instead of showing nothing; the
1879
2371
  * peak windows travel with the table so the browser applies the very schedule
1880
2372
  * this snapshot knows instead of restating it.
2373
+ *
2374
+ * `now` (defaults to `Date.now()`) resolves lapsed promotions: a discounted row
2375
+ * whose `expiresAt` has passed is served at its pre-discount `listRates`, the
2376
+ * same instant {@link dealLabel} stops showing its badge. The table is rebuilt
2377
+ * per request, so that switch needs no redeploy.
1881
2378
  */
1882
- function modelPriceTable() {
2379
+ function modelPriceTable(now = Date.now()) {
1883
2380
  const bySlug = new Map(MODEL_PRICE_ROWS.map((row) => [row.id, row]));
1884
2381
  const known = new Set(bySlug.keys());
1885
2382
  const models = [];
@@ -1901,86 +2398,56 @@ function modelPriceTable() {
1901
2398
  const row = bySlug.get(slug);
1902
2399
  if (row === void 0) continue;
1903
2400
  claimed.add(slug);
1904
- models.push(wireRow(catalogId, slug, row));
2401
+ models.push(wireRow(catalogId, slug, row, now));
1905
2402
  }
1906
2403
  for (const row of MODEL_PRICE_ROWS) {
1907
2404
  if (claimed.has(row.id)) continue;
1908
- models.push(wireRow(row.id, row.id, row));
2405
+ models.push(wireRow(row.id, row.id, row, now));
1909
2406
  }
1910
2407
  return {
1911
2408
  models,
1912
2409
  peakHours: PEAK_HOUR_RANGES.map(([start, end]) => [start, end])
1913
2410
  };
1914
2411
  }
1915
- //#endregion
1916
- //#region src/cost-facts.ts
1917
- const zeroCostTokens = () => ({
1918
- uncachedInputTokens: 0,
1919
- outputTokens: 0,
1920
- cacheReadTokens: 0,
1921
- cacheWriteTokens: 0
1922
- });
1923
- const COST_TOKEN_KEYS = [
1924
- "uncachedInputTokens",
1925
- "outputTokens",
1926
- "cacheReadTokens",
1927
- "cacheWriteTokens"
1928
- ];
1929
- function peakHour(at, windows) {
1930
- const time = new Date(at);
1931
- return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
1932
- }
2412
+ /** Slug-keyed view of the rows, built once: neither table nor allowance mutates. */
2413
+ const ROWS_BY_SLUG = new Map(MODEL_PRICE_ROWS.map((row) => [row.id, row]));
2414
+ const KNOWN_SLUGS = new Set(ROWS_BY_SLUG.keys());
1933
2415
  /**
1934
- * All prompt billing buckets determine a request's band, never cumulative
1935
- * session input.
2416
+ * The per-model monthly allowance for a CATALOG model id, or undefined when the
2417
+ * page carries none (a model without a price row, or the free stealth previews
2418
+ * the estimator array omits).
1936
2419
  *
1937
- * A tiered model returns the matching band and does NOT then apply `peak`: the
1938
- * page publishes those two dimensions independently and no row carries both
1939
- * today, so the combination becoming real is visible before it reaches a user.
1940
- * This ordering is a latent choice, not a verified upstream rule.
1941
- */
1942
- function requestRates(price, at, contextTokens, table) {
1943
- const tier = price.contextTiers?.find((tier) => tier.maxContext === void 0 || contextTokens <= tier.maxContext);
1944
- if (tier !== void 0) return tier;
1945
- return price.peak !== void 0 && at !== null && peakHour(at, table.peakHours) ? price.peak : price;
2420
+ * Resolution rides the same slug rules as the price table, so
2421
+ * `MiniMaxAI/MiniMax-M3` finds the row the page files under `minimax-m3` — the
2422
+ * settings page must not grow a second mapping that can drift from this one.
2423
+ * The settings page asks for a model only while rendering a list it already
2424
+ * has, so this stays a pure lookup with no clock: an allowance is not
2425
+ * time-dependent (a plan's figure does not lapse the way a promotional rate
2426
+ * does; when the page re-prices it, the next sync carries it).
2427
+ */
2428
+ function modelAllowanceFor(modelId) {
2429
+ const slug = priceSlugFor(modelId, KNOWN_SLUGS);
2430
+ if (slug === void 0) return void 0;
2431
+ return ROWS_BY_SLUG.get(slug)?.allowance;
1946
2432
  }
1947
- /** Cache version and wire guard: changing bands/rates must refold historical groups. */
1948
- function pricingKey(table) {
1949
- const rates = (r) => [
1950
- r.inputCost,
1951
- r.outputCost,
1952
- r.cacheReadCost,
1953
- r.cacheWriteCost ?? null
1954
- ];
1955
- const canonical = JSON.stringify([
1956
- "commandCodeCost-v1",
1957
- table.peakHours,
1958
- table.models.map((p) => [
1959
- p.id,
1960
- p.slug,
1961
- p.free === true,
1962
- rates(p),
1963
- p.peak ? rates(p.peak) : null,
1964
- p.contextTiers?.map((t) => [t.maxContext ?? null, rates(t)]) ?? null
1965
- ])
1966
- ]);
1967
- let hash = 2166136261;
1968
- for (const c of canonical) hash = Math.imul(hash ^ c.charCodeAt(0), 16777619) >>> 0;
1969
- return hash;
2433
+ //#endregion
2434
+ //#region src/wire-guards.ts
2435
+ /** Non-throwing value guards for untrusted provider payloads. */
2436
+ function isRecord(value) {
2437
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1970
2438
  }
1971
- /** Only safe to merge requests whose billing classification is identical. */
1972
- function costGroupKey(group, table) {
1973
- const price = table.models.find((p) => p.id === group.model || p.slug === group.model);
1974
- return JSON.stringify([
1975
- group.provider,
1976
- group.model,
1977
- price === void 0 ? null : requestRates(price, group.at, group.contextTokens, table),
1978
- group.at === null
1979
- ]);
2439
+ function stringValue(value) {
2440
+ return typeof value === "string" ? value : void 0;
2441
+ }
2442
+ function numberValue(value) {
2443
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2444
+ }
2445
+ function booleanValue(value) {
2446
+ return typeof value === "boolean" ? value : void 0;
1980
2447
  }
1981
2448
  //#endregion
1982
2449
  //#region src/cost-projection.ts
1983
- const record$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
2450
+ const record$2 = (v) => isRecord(v) ? v : void 0;
1984
2451
  const finite = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
1985
2452
  function tokensOf(value) {
1986
2453
  const u = record$2(value);
@@ -2069,11 +2536,18 @@ function createCostProjection(table = modelPriceTable()) {
2069
2536
  ...g,
2070
2537
  tokens: { ...g.tokens }
2071
2538
  }));
2539
+ const byKey = /* @__PURE__ */ new Map();
2540
+ groups.forEach((g, i) => {
2541
+ const key = costGroupKey(g, table);
2542
+ if (!byKey.has(key)) byKey.set(key, i);
2543
+ });
2544
+ const groupKey = costGroupKey(group, table);
2072
2545
  if (previous) {
2073
- const old = groups.find((g) => costGroupKey(g, table) === costGroupKey(previous, table));
2074
- if (old) for (const k of COST_TOKEN_KEYS) old.tokens[k] -= previous.tokens[k];
2546
+ const old = byKey.get(costGroupKey(previous, table));
2547
+ if (old !== void 0) for (const k of COST_TOKEN_KEYS) groups[old].tokens[k] -= previous.tokens[k];
2075
2548
  }
2076
- let target = groups.find((g) => costGroupKey(g, table) === costGroupKey(group, table));
2549
+ const targetIdx = byKey.get(groupKey);
2550
+ let target = targetIdx === void 0 ? void 0 : groups[targetIdx];
2077
2551
  if (!target) {
2078
2552
  target = {
2079
2553
  ...group,
@@ -2143,24 +2617,18 @@ const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
2143
2617
  * @module dsh-commandcode-provider/accounts
2144
2618
  */
2145
2619
  /**
2146
- * Upper bound on the retry wait this pool attaches to the all-exhausted
2147
- * `RATE_LIMIT` error. Must equal the `backoff.maxDelayMs` in the adapter's
2148
- * `providerRetryPolicy` (which imports it from here): dsh-llm-retry honors a
2149
- * provider-specified wait verbatim only at or below that cap — a LONGER one
2150
- * makes the executor abandon the retry instead of falling back to local backoff
2151
- * (in normal mode), turning "poll until the window opens" into "fail now".
2620
+ * Upper bound on the retry wait attached to the all-exhausted `RATE_LIMIT`.
2621
+ * Must equal the adapter's `backoff.maxDelayMs`: dsh-llm-retry honors a
2622
+ * provider wait verbatim only at or below that cap — a longer one abandons the
2623
+ * retry instead of falling back to local backoff, turning "poll until the
2624
+ * window opens" into "fail now".
2152
2625
  */
2153
2626
  const RETRY_MAX_DELAY_MS = 9e5;
2154
2627
  /**
2155
2628
  * How long an explicitly selected account's rate-limit mark may stand before
2156
- * its window is probed again.
2157
- *
2158
- * A 429 whose reset was never learned marks the key `unknown`, which
2159
- * {@link accountUsable} refuses — for a key nobody asked for that is correct
2160
- * (only the all-marked pass clears it, and it costs one billing call), but for
2161
- * the user's own pinned account a single 429 would demote it until the process
2162
- * restarts (issue #51). Once per interval per key bounds a probe endpoint that
2163
- * keeps failing; a successful probe resolves the mark for good.
2629
+ * its window is probed again. Without this, a single 429 on the user's pinned
2630
+ * account demotes it until the process restarts (issue #51); probing at most
2631
+ * once per interval per key bounds a probe endpoint that keeps failing.
2164
2632
  */
2165
2633
  const EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS = 6e4;
2166
2634
  /** A labeled, human-readable clock reading for error messages. */
@@ -2265,26 +2733,18 @@ var CommandCodeAccountPool = class {
2265
2733
  return out;
2266
2734
  }
2267
2735
  /**
2268
- * Hand out the key for a request: the model-routed account when the request's
2269
- * model matches a rule (and that account is usable), else the manually
2270
- * preferred account when usable, else the first usable account in rotation
2271
- * order. Returns `undefined` when no account resolves any key at all, or when
2272
- * an account this request already used is still UNMARKED — that rejection is
2273
- * the honest answer, so the caller's own rejection surfaces as itself. Throws
2274
- * `RATE_LIMIT` (naming the earliest window reset) or `INVALID_CREDENTIAL`
2275
- * when accounts exist but every one of them is marked.
2736
+ * Hand out the key for a request: the model-routed account when usable, else
2737
+ * the manually preferred account when usable, else the first usable account
2738
+ * in rotation order. Returns `undefined` when no account resolves any key,
2739
+ * or when an account this request already used is still UNMARKED — the
2740
+ * caller's own rejection is the honest answer then. Throws `RATE_LIMIT` /
2741
+ * `INVALID_CREDENTIAL` when every account is marked.
2276
2742
  *
2277
- * `options.model` is the request's model id; routing rules re-read per
2278
- * resolution, so a settings change applies live. `options.tried` lists the
2279
- * keys this request has already used — the just-rejected one included — and
2280
- * they are removed from the resolution entirely, which is what lets one
2281
- * request walk a four-account pool: a rejection that does not mark the key
2282
- * (no credits, a model outside the account's plan) would otherwise be offered
2283
- * again on every attempt and the accounts behind it never reached.
2284
- *
2285
- * An explicit selection (the pin or a model rule) that a rate-limit mark would
2286
- * demote is probed before the fallback serves, so "falls back while exhausted"
2287
- * never becomes "stays demoted until the process restarts" (issue #51).
2743
+ * `options.tried` lists the keys this request already used; they are removed
2744
+ * from the resolution entirely, which is what lets one request walk a
2745
+ * four-account pool. An explicit selection (pin or model rule) that a
2746
+ * rate-limit mark would demote is probed first, so a fallback never becomes
2747
+ * permanent (issue #51). Rules re-read per resolution, so settings apply live.
2288
2748
  */
2289
2749
  async resolveKey(options) {
2290
2750
  const accounts = await this.resolvedAccounts();
@@ -2320,18 +2780,7 @@ var CommandCodeAccountPool = class {
2320
2780
  this.states.delete(account.key);
2321
2781
  return;
2322
2782
  }
2323
- if (probe.resetAt > 0) this.states.set(account.key, {
2324
- kind: "cooldown",
2325
- cause: "window",
2326
- reason: account.state?.reason ?? "usage window exhausted (429)",
2327
- until: probe.resetAt
2328
- });
2329
- else if (account.state?.kind !== "cooldown") this.states.set(account.key, {
2330
- kind: "unknown",
2331
- cause: "window",
2332
- reason: account.state?.reason ?? "usage window exhausted (429)",
2333
- until: 0
2334
- });
2783
+ this.stampWindowMark(account, probe);
2335
2784
  }));
2336
2785
  const latestAccounts = await this.resolvedAccounts();
2337
2786
  const revived = selectActiveAccount(tried === void 0 || tried.length === 0 ? latestAccounts : latestAccounts.filter((account) => !tried.includes(account.key)), preferred);
@@ -2464,6 +2913,18 @@ var CommandCodeAccountPool = class {
2464
2913
  state: void 0
2465
2914
  };
2466
2915
  }
2916
+ this.stampWindowMark(account, probe);
2917
+ }
2918
+ /**
2919
+ * Stamp a probe-read window onto one key. A known reset becomes a cooldown
2920
+ * that expires by itself; without one the mark stays `unknown`, so a later
2921
+ * probe can still learn it — and a mark that already carries a cooldown
2922
+ * keeps it, never trading a known reset for an unknown one. The cause is
2923
+ * `window` either way: a probe that read a window as exceeded IS window
2924
+ * evidence, whatever the key was marked for. An `until: 0` cooldown would
2925
+ * read as "never usable again" to {@link accountUsable}, so `unknown` it is.
2926
+ */
2927
+ stampWindowMark(account, probe) {
2467
2928
  if (probe.resetAt > 0) this.states.set(account.key, {
2468
2929
  kind: "cooldown",
2469
2930
  cause: "window",
@@ -2647,12 +3108,12 @@ function commandCodeImageTokens(modelId, width, height) {
2647
3108
  return imageTokenCost(imageTokenFamily(modelId), width, height);
2648
3109
  }
2649
3110
  /**
2650
- * Anthropic: the image is first scaled so its long edge is at most 1568 px
2651
- * (never enlarged), then charged roughly one token per 750 pixels.
3111
+ * Anthropic: the image is first scaled to the request long edge (never
3112
+ * enlarged), then charged roughly one token per 750 pixels.
2652
3113
  * Source: Anthropic vision documentation, "Image size and token usage".
2653
3114
  */
2654
3115
  function anthropicImageTokens(width, height) {
2655
- const projected = longEdgeDimensions(width, height, 1568);
3116
+ const projected = longEdgeDimensions(width, height, REQUEST_IMAGE_MAX_LONG_EDGE);
2656
3117
  return Math.max(1, Math.ceil(projected.width * projected.height / 750));
2657
3118
  }
2658
3119
  /**
@@ -2944,17 +3405,52 @@ function openStreamTrace(scope, options = {}) {
2944
3405
  * Owns the two chat transports (`/alpha/generate`, `/provider/v1/chat/completions`),
2945
3406
  * message conversion, SSE/JSONL parsing, the catalog + its on-disk cache, and
2946
3407
  * the pre-stream account rotation loop. The wire protocol is
2947
- * reverse-engineered (command-code@1.28.4, re-verified through 1.66.0);
3408
+ * reverse-engineered (command-code@1.28.4, re-verified through 1.68.0);
2948
3409
  * AGENTS.md holds the full protocol record.
2949
3410
  *
2950
3411
  * The adapter is deliberately free of cordis/schemastery: it receives a
2951
3412
  * per-request options thunk and an API-key resolver from the plugin entry
2952
3413
  * (src/index.ts), so a settings change reaches the very next request.
2953
3414
  */
2954
- const COMMAND_CODE_CLI_VERSION = "1.66.0";
3415
+ /** Keep the two user-facing languages and error metadata together. */
3416
+ function bilingual(code, en, zh, options) {
3417
+ return new LlmError(`${en};${zh}`, code, options);
3418
+ }
3419
+ const COMMAND_CODE_CLI_VERSION = "1.68.0";
2955
3420
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
2956
- const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
2957
- const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
3421
+ /**
3422
+ * The output budget this bundle asks for, and the ceiling it will never exceed.
3423
+ *
3424
+ * 131 072, chosen against a measured provider limit rather than a round guess.
3425
+ * Probing `/alpha/generate` directly on 2026-09-28: 64 000 → 200, 131 072 → 200,
3426
+ * 196 608 → 200, **200 000 → 200, 200 704 → 400**, 1 000 000 → 400. The server
3427
+ * therefore caps `max_tokens` at 200 000, and it does so identically for a
3428
+ * 262 144-window model (`stealth/pixel-canary`) and a 1 000 000-window one
3429
+ * (`stealth/space-bunny-alpha`) — the cap is global, not per model. 131 072
3430
+ * keeps a 35 % margin under it while covering the real output ceiling of the
3431
+ * models that have one (Claude 64 K, Gemini 64 K, several at 128 K).
3432
+ *
3433
+ * The old 64 000 was never a provider requirement: it cost nothing in latency
3434
+ * (a 64 000 request answered headers in 1.28 s against 1.57 s for 131 072) but
3435
+ * it did cap what a model could ever say in one turn.
3436
+ *
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.
3440
+ */
3441
+ const DEFAULT_GENERATE_MAX_TOKENS = 131072;
3442
+ /**
3443
+ * The fallback when the catalog carries no output ceiling for a model.
3444
+ *
3445
+ * `/provider/v1/models` publishes only `context_length` — no output limit — so
3446
+ * the catalog cannot learn one per model and derives a stand-in from the
3447
+ * window instead. Kept equal to {@link DEFAULT_GENERATE_MAX_TOKENS} on purpose:
3448
+ * a lower value here would silently re-cap every request through
3449
+ * `Math.min(contextLength, this)` before the ceiling above is ever consulted.
3450
+ */
3451
+ const DEFAULT_MAX_OUTPUT_TOKENS = 131072;
3452
+ /** Preserve a small usable answer budget when the heuristic overshoots. */
3453
+ const MIN_CONTEXT_OUTPUT_TOKENS = 1024;
2958
3454
  const MODELS_TIMEOUT_MS = 1e4;
2959
3455
  /** How long the picker's plan-filter billing facts stay cached before refetching. */
2960
3456
  const BILLING_ACCESS_TTL_MS = 3e5;
@@ -2971,20 +3467,29 @@ const ACTIVE_SUBSCRIPTION_STATUSES = /* @__PURE__ */ new Set([
2971
3467
  "trialing",
2972
3468
  "past_due"
2973
3469
  ]);
2974
- /** Head-of-request timeout: how long to wait for the first response byte. */
2975
- const DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
3470
+ /**
3471
+ * Head-of-request timeout: how long to wait for the first response byte.
3472
+ *
3473
+ * 300 s, and that number is the official CLI's, not a round choice. Decompiled
3474
+ * from `command-code@1.66.0`: its `createNodeTransport` fetch passes only the
3475
+ * caller's `signal` and sets no timeout at all, so the model request inherits
3476
+ * Node/undici's own default — measured at 301.0 s against a local server that
3477
+ * accepts the connection and never sends headers. The previous 60 s default was
3478
+ * five times stricter than the CLI it emulates, and being stricter only ever
3479
+ * costs legitimate requests: a slow upstream first token on the Provider API
3480
+ * route can outlast 60 s, and the official CLI simply waits it out. Measured
3481
+ * headers (2026-09-28, 26 250-token prompt, `max_tokens: 16` to separate the
3482
+ * header from the generation): `/alpha/generate` 2.12 / 2.56 / 2.75 s,
3483
+ * `/provider/v1/chat/completions` 5.52 / 2.98 / 3.70 s — a ~1.1 s median gap
3484
+ * with the Provider API route varying over a 2.5 s span against the CLI route's
3485
+ * 0.6 s. A gateway that is genuinely dead now takes longer to name itself,
3486
+ * which is what the consecutive-timeout streak exists to bound (three
3487
+ * attempts, then a diagnosis instead of another re-send).
3488
+ */
3489
+ const DEFAULT_REQUEST_TIMEOUT_MS = 3e5;
2976
3490
  /** Stream idle timeout: a generation that stalls this long is a dead connection. */
2977
3491
  const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
2978
3492
  const MODEL_CACHE_VERSION = 1;
2979
- function isRecord(value) {
2980
- return typeof value === "object" && value !== null && !Array.isArray(value);
2981
- }
2982
- function stringValue(value) {
2983
- return typeof value === "string" ? value : void 0;
2984
- }
2985
- function numberValue(value) {
2986
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2987
- }
2988
3493
  /** Hash one request value without retaining or logging its contents. */
2989
3494
  function fingerprintValue(value) {
2990
3495
  let serialized;
@@ -3076,9 +3581,6 @@ function failureWithRequestId(error, metadata) {
3076
3581
  ...error.cause === void 0 ? {} : { cause: error.cause }
3077
3582
  });
3078
3583
  }
3079
- function booleanValue(value) {
3080
- return typeof value === "boolean" ? value : void 0;
3081
- }
3082
3584
  /**
3083
3585
  * Thinking text carried by an OpenRouter-shaped `reasoning_details` array,
3084
3586
  * concatenated in the order the gateway sent it.
@@ -3160,6 +3662,78 @@ const CLI_CONTEXT_OVERFLOW_PATTERN = /prompt is too long|context.*(length|window
3160
3662
  function isContextOverflowDetail(detail) {
3161
3663
  return isContextWindowExceededError(detail) || CLI_CONTEXT_OVERFLOW_PATTERN.test(detail);
3162
3664
  }
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;
3667
+ /**
3668
+ * The advice floor for a Provider-API timeout: doubling a 60 s budget is still
3669
+ * 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.
3674
+ */
3675
+ const MIN_SUGGESTED_TIMEOUT_MS = 3e5;
3676
+ /**
3677
+ * Why one attempt saw no response headers, and what the user can do about it.
3678
+ *
3679
+ * The TRANSPORT is the reliable signal; the request size is not — and the
3680
+ * mechanism is read straight out of the official CLI rather than inferred from
3681
+ * a timing. `createNodeTransport` passes only the caller's `signal` and sets no
3682
+ * timeout, so the two routes differ in WHO answers the headers:
3683
+ * `/alpha/generate` has the gateway answer them before the model starts, while
3684
+ * `/provider/v1/chat/completions` cannot answer until its upstream emits a
3685
+ * first token, so whatever that upstream is doing is charged to the header
3686
+ * wait. Measured 2026-09-28 (26 250-token prompt, `max_tokens: 16`):
3687
+ * `/alpha/generate` 2.12 / 2.56 / 2.75 s, `/provider/v1/chat/completions`
3688
+ * 5.52 / 2.98 / 3.70 s — a ~1.1 s median gap, with the Provider API route
3689
+ * swinging across 2.5 s where the CLI route moved 0.6 s. A larger prompt moves
3690
+ * neither, which is why an earlier draft that branched on a 1 MiB body
3691
+ * threshold was wrong: size is not the variable here. Only the route decides.
3692
+ *
3693
+ * @param protocol - the transport the attempt used.
3694
+ * @param timeoutMs - the header budget that just expired.
3695
+ */
3696
+ 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
+ };
3701
+ return { cause: "gateway" };
3702
+ }
3703
+ /**
3704
+ * How many consecutive header timeouts on ONE unchanged payload stop being a
3705
+ * blip. The retry policy whitelists `TIMEOUT` with `maxRetries: 1000` and a
3706
+ * 500 ms → 15 min backoff, which is right for a provider that says when to
3707
+ * come back but wrong for a gateway that simply never answers: the reporter
3708
+ * saw the identical multi-megabyte body re-sent for hours (issue #67) instead
3709
+ * of a diagnosis. Three is where a 60 s budget has already spent three
3710
+ * minutes waiting, so the cost of being wrong is bounded and the user gets an
3711
+ * answer either way.
3712
+ */
3713
+ const MAX_CONSECUTIVE_HEADER_TIMEOUTS = 3;
3714
+ /** UTF-8 byte length, computed only on the timeout path so a large body is encoded once, not per attempt. */
3715
+ function utf8ByteLength(text) {
3716
+ return new TextEncoder().encode(text).length;
3717
+ }
3718
+ /**
3719
+ * The thrown headers timeout, worded for the cause it actually has.
3720
+ *
3721
+ * Every earlier version of this message said "通常是网络或代理问题" for both
3722
+ * transports, which is actively wrong for a large `/provider/v1/chat/completions`
3723
+ * prompt: there the model simply needs longer than any budget, and a user sent
3724
+ * to check their proxy never finds anything. The code stays `TIMEOUT` (still
3725
+ * retryable) in both branches — only the diagnosis and the advice change.
3726
+ */
3727
+ function headersTimeoutError(input) {
3728
+ const { protocol, endpoint, timeoutMs, cause } = input;
3729
+ const megabytes = `${(utf8ByteLength(input.body) / 1048576).toFixed(1)} MB`;
3730
+ const advice = headersTimeoutAdvice(protocol, timeoutMs);
3731
+ if (advice.cause === "upstream-first-token") {
3732
+ const suggestion = advice.suggestionMs ?? timeoutMs;
3733
+ return bilingual("TIMEOUT", `Command Code API request to ${endpoint} did not respond within ${timeoutMs}ms: this route withholds response headers until the upstream model emits its first token, and it did not start within the budget. Raise "Request timeout" in the plugin's advanced settings to ${suggestion}ms, or compact the session or start a new one. The request body was ${megabytes}, but size is not the cause here — a slow upstream is. ${errorChain(cause)}`, `Command Code API 请求在 ${timeoutMs} 毫秒内未收到响应:该路由要等上游模型吐出第一个 token 才返回响应头,而上游在预算内没有开始作答。可在插件「高级」设置里把「请求超时」调到 ${suggestion} 毫秒,或压缩上下文 / 新开会话。本次请求体约 ${megabytes},但体积不是原因——慢的是上游。(${errorChain(cause)})`, { cause });
3734
+ }
3735
+ return bilingual("TIMEOUT", `Command Code API request to ${endpoint} did not respond within ${timeoutMs}ms: the gateway returned no response headers at all — it answers headers itself before the model replies. This is usually provider queueing, a Go-plan capacity limit, or a network/proxy path problem; a longer timeout will not help, but retrying usually recovers. The request body was ${megabytes}, and size is not the cause on this route. ${errorChain(cause)}`, `Command Code API 请求在 ${timeoutMs} 毫秒内未收到响应:网关完全没有返回响应头——它会在模型开始回答前先返回响应头。这通常是服务商排队、Go 套餐容量限制,或网络/代理链路问题;调大超时不会有帮助,重试通常可恢复。本次请求体约 ${megabytes},而在该路由上体积不是原因。(${errorChain(cause)})`, { cause });
3736
+ }
3163
3737
  /**
3164
3738
  * Whether one rejection says "ZDR requested, no ZDR-capable upstream": the
3165
3739
  * provider spells the code `cmd_zdr_no_providers` in JSON error bodies and
@@ -3203,7 +3777,7 @@ function streamErrorToLlmError(value, fallbackMessage) {
3203
3777
  message
3204
3778
  ].filter((part) => part !== void 0 && part !== "").join(" ");
3205
3779
  const statusOption = statusCode !== void 0 ? { status: statusCode } : void 0;
3206
- if (isContextOverflowDetail(detail)) return new LlmError(`Command Code stream error: ${message};Command Code API 拒绝了这次请求:内容超出模型上下文窗口。正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, CONTEXT_WINDOW_EXCEEDED_CODE, statusOption);
3780
+ if (isContextOverflowDetail(detail)) return bilingual(CONTEXT_WINDOW_EXCEEDED_CODE, `Command Code stream error: ${message}`, "Command Code API 拒绝了这次请求:内容超出模型上下文窗口。正在压缩上下文后重试;如仍失败,请新建会话或减少上下文", statusOption);
3207
3781
  const terminal = hasTerminalStreamMarker(detail);
3208
3782
  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);
3209
3783
  return new LlmError(`Command Code stream error: ${message}`, "SERVER", statusOption);
@@ -3581,9 +4155,9 @@ const ENGINE_IMAGE_POLICY = {
3581
4155
  * dsh-llm-retry. On a profile that does not mount that plugin the failure is
3582
4156
  * terminal — every shipped profile reaches it through dsh-base.
3583
4157
  */
3584
- function imageOffloadRequired(missing) {
4158
+ function imageOffloadRequired(missing, reason = "budget") {
3585
4159
  const options = { offloadImages: missing };
3586
- return new LlmError(`commandcode request images exceed the route budget; ${missing} more oldest occurrence(s) must be offloaded.`, IMAGE_OFFLOAD_REQUIRED_CODE, options);
4160
+ return new LlmError(reason === "cache" ? `commandcode cache-aware image history: ${missing} already-seen oldest image occurrence(s) must be offloaded before replay.` : `commandcode request images exceed the route budget; ${missing} more oldest occurrence(s) must be offloaded.`, IMAGE_OFFLOAD_REQUIRED_CODE, options);
3587
4161
  }
3588
4162
  /** The budget in the shape the core counter takes (representation + the rung). */
3589
4163
  function coreBudget(budget) {
@@ -3609,17 +4183,40 @@ const imageVersionBytes = (block) => block.attachment.bytes;
3609
4183
  * otherwise the 413 retry could reintroduce images the first rung had already
3610
4184
  * evicted.
3611
4185
  */
3612
- function withSurfaceOffload(options, policy, budget) {
4186
+ function withSurfaceOffload(options, policy, budget, cacheOffloadModel) {
3613
4187
  const marked = policy.projectOffloadedImages(options.messages, (ref) => offloadedImageText(ref));
3614
4188
  const messages = marked === options.messages ? options.messages : [...marked];
3615
- const missing = policy.requiredImageOffload(messages, coreBudget(budget), imageVersionBytes);
3616
- if (missing > 0) throw imageOffloadRequired(missing);
4189
+ const budgetMissing = policy.requiredImageOffload(messages, coreBudget(budget), imageVersionBytes);
4190
+ const cacheMissing = cacheOffloadModel === void 0 ? 0 : seenImageOffloadCount(messages, cacheOffloadModel);
4191
+ const missing = Math.max(budgetMissing, cacheMissing);
4192
+ if (missing > 0) throw imageOffloadRequired(missing, cacheMissing > budgetMissing ? "cache" : "budget");
3617
4193
  return messages === options.messages ? options : {
3618
4194
  ...options,
3619
4195
  messages
3620
4196
  };
3621
4197
  }
3622
4198
  /**
4199
+ * Count retained images already followed by an answer from this same model.
4200
+ * The current tool result's image has not yet been seen by the model and must
4201
+ * stay on the wire. The engine can only offload durable user/tool input nodes;
4202
+ * assistant output and request-only input have no selectable surface event.
4203
+ */
4204
+ function seenImageOffloadCount(messages, model) {
4205
+ let lastAnswer = -1;
4206
+ for (let index = 0; index < messages.length; index++) {
4207
+ const message = messages[index];
4208
+ if (message.role === "assistant" && message.source.kind === "model" && message.source.provider === "commandcode" && message.source.model === model) lastAnswer = index;
4209
+ }
4210
+ if (lastAnswer < 0) return 0;
4211
+ let count = 0;
4212
+ for (let index = 0; index < lastAnswer; index++) {
4213
+ const message = messages[index];
4214
+ if (message.role !== "user" && message.role !== "tool" || message.id === void 0) continue;
4215
+ for (const block of message.content) if (block.type === "image") count++;
4216
+ }
4217
+ return count;
4218
+ }
4219
+ /**
3623
4220
  * One image's bytes for the wire: the attachment service's REQUEST VERSION.
3624
4221
  *
3625
4222
  * `readImage()` answers the stored original, which is what admission accepted —
@@ -3630,7 +4227,7 @@ function withSurfaceOffload(options, policy, budget) {
3630
4227
  * the point here, the encoded bytes).
3631
4228
  */
3632
4229
  async function readRequestImage(attachments, ref) {
3633
- return (await attachments.readImageRequest(ref, requestImageTarget(ref))).data;
4230
+ return attachments.readImageRequest(ref, requestImageTarget(ref));
3634
4231
  }
3635
4232
  /**
3636
4233
  * Price one request occurrence.
@@ -3661,23 +4258,25 @@ function priceRequestImage(block, model, imageCapable) {
3661
4258
  };
3662
4259
  }
3663
4260
  /**
3664
- * Convert one image reference to the Command Code wire format, as the official
3665
- * CLI does: `{ type: 'image', source: { type: 'base64', media_type, data } }`.
3666
- * Bytes come from the durable attachment service; the media type is the one
3667
- * verified at save time.
4261
+ * Convert one image reference to the final `/alpha/generate` wire format.
4262
+ * The official CLI accepts a base64 source internally, then toWireMessages()
4263
+ * sends a data URL plus mimeType. Use the request version's media type because
4264
+ * readImageRequest() can transcode the stored image to JPEG or WebP.
3668
4265
  */
3669
4266
  async function imageToCommandCode(ref, readImage) {
3670
- const data = await readImage(ref);
4267
+ const version = await readImage(ref);
3671
4268
  return {
3672
4269
  type: "image",
3673
- source: {
3674
- type: "base64",
3675
- media_type: ref.mediaType,
3676
- data: Buffer.from(data).toString("base64")
3677
- }
4270
+ image: `data:${version.mediaType};base64,${Buffer.from(version.data).toString("base64")}`,
4271
+ mimeType: version.mediaType
3678
4272
  };
3679
4273
  }
3680
- async function messagesToCC(messages, readImage) {
4274
+ /**
4275
+ * The common pairing and image-carrier ordering for both transports. Each
4276
+ * codec owns its wire shape: CLI content blocks and Chat Completions
4277
+ * reasoning/tool calls differ beyond the image envelope.
4278
+ */
4279
+ async function convertMessages(messages, readImage, codec) {
3681
4280
  const out = [];
3682
4281
  const { ids: paired, names: toolNames } = pairedToolCalls(messages);
3683
4282
  const wireIds = wireToolCallIds(paired);
@@ -3686,6 +4285,10 @@ async function messagesToCC(messages, readImage) {
3686
4285
  for (const message of pendingImages) out.push(message);
3687
4286
  pendingImages.length = 0;
3688
4287
  };
4288
+ const encodeImage = (ref) => {
4289
+ if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
4290
+ return codec.encodeImage(ref, readImage);
4291
+ };
3689
4292
  for (const message of messages) {
3690
4293
  if (message.role === "system") continue;
3691
4294
  const result = toolResultOf(message);
@@ -3697,86 +4300,88 @@ async function messagesToCC(messages, readImage) {
3697
4300
  type: "text",
3698
4301
  text: block.text
3699
4302
  });
3700
- if (block.type === "image") {
3701
- if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
3702
- parts.push(await imageToCommandCode(block.attachment, readImage));
3703
- }
4303
+ if (block.type === "image") parts.push(await encodeImage(block.attachment));
3704
4304
  }
3705
- if (parts.length === 0) continue;
3706
- out.push({
3707
- role: "user",
3708
- content: parts
3709
- });
4305
+ if (parts.length > 0) out.push(codec.encodeUser(parts));
3710
4306
  continue;
3711
4307
  }
3712
4308
  if (message.role === "assistant") {
3713
4309
  flushPendingImages();
3714
- const parts = [];
3715
- for (const block of message.content) if (block.type === "text") parts.push({
3716
- type: "text",
3717
- text: block.text
3718
- });
3719
- else if (block.type === "reasoning") parts.push({
3720
- type: "reasoning",
3721
- text: block.text
3722
- });
3723
- else if (block.type === "tool-call" && paired.has(block.id)) parts.push({
3724
- type: "tool-call",
3725
- toolCallId: wireIds.get(block.id) ?? block.id,
3726
- toolName: block.name,
3727
- input: recordOrEmpty(block.arguments)
3728
- });
3729
- if (parts.length > 0) out.push({
3730
- role: "assistant",
3731
- content: parts
3732
- });
4310
+ const assistant = codec.encodeAssistant(message, paired, wireIds);
4311
+ if (assistant !== void 0) out.push(assistant);
3733
4312
  continue;
3734
4313
  }
3735
- if (result) {
3736
- if (!paired.has(result.toolCallId)) continue;
3737
- const media = toolResultMedia(result);
3738
- const wireToolCallId = wireIds.get(result.toolCallId) ?? result.toolCallId;
3739
- out.push({
3740
- role: "tool",
3741
- content: [{
3742
- type: "tool-result",
3743
- toolCallId: wireToolCallId,
3744
- toolName: toolNames.get(result.toolCallId) || "unknown",
3745
- output: result.isError ? {
3746
- type: "error-text",
3747
- value: toolResultTextForWire(media)
3748
- } : {
3749
- type: "text",
3750
- value: toolResultTextForWire(media)
3751
- }
3752
- }]
3753
- });
3754
- if (media.images.length > 0) {
3755
- if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
3756
- const carried = [{
3757
- type: "text",
3758
- text: toolResultImageNote(media, wireToolCallId)
3759
- }];
3760
- for (const attachment of media.images) carried.push(await imageToCommandCode(attachment, readImage));
3761
- pendingImages.push({
3762
- role: "user",
3763
- content: carried
3764
- });
3765
- }
4314
+ if (!result || !paired.has(result.toolCallId)) continue;
4315
+ const media = toolResultMedia(result);
4316
+ 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) {
4319
+ const carried = [{
4320
+ type: "text",
4321
+ text: toolResultImageNote(media, wireId)
4322
+ }];
4323
+ for (const attachment of media.images) carried.push(await encodeImage(attachment));
4324
+ pendingImages.push(codec.encodeUser(carried));
3766
4325
  }
3767
4326
  }
3768
4327
  flushPendingImages();
3769
4328
  return out;
3770
4329
  }
4330
+ const ccMessageCodec = {
4331
+ encodeImage: imageToCommandCode,
4332
+ encodeUser: (parts) => ({
4333
+ role: "user",
4334
+ content: parts
4335
+ }),
4336
+ encodeAssistant: (message, paired, wireIds) => {
4337
+ const parts = [];
4338
+ for (const block of message.content) if (block.type === "text") parts.push({
4339
+ type: "text",
4340
+ text: block.text
4341
+ });
4342
+ else if (block.type === "reasoning") parts.push({
4343
+ type: "reasoning",
4344
+ text: block.text
4345
+ });
4346
+ else if (block.type === "tool-call" && paired.has(block.id)) parts.push({
4347
+ type: "tool-call",
4348
+ toolCallId: wireIds.get(block.id) ?? block.id,
4349
+ toolName: block.name,
4350
+ input: recordOrEmpty(block.arguments)
4351
+ });
4352
+ return parts.length > 0 ? {
4353
+ role: "assistant",
4354
+ content: parts
4355
+ } : void 0;
4356
+ },
4357
+ encodeTool: (result, media, wireId, toolName) => ({
4358
+ role: "tool",
4359
+ content: [{
4360
+ type: "tool-result",
4361
+ toolCallId: wireId,
4362
+ toolName,
4363
+ output: result.isError ? {
4364
+ type: "error-text",
4365
+ value: toolResultTextForWire(media)
4366
+ } : {
4367
+ type: "text",
4368
+ value: toolResultTextForWire(media)
4369
+ }
4370
+ }]
4371
+ })
4372
+ };
4373
+ async function messagesToCC(messages, readImage) {
4374
+ return convertMessages(messages, readImage, ccMessageCodec);
4375
+ }
3771
4376
  /**
3772
4377
  * Convert one image reference to the OpenAI Chat Completions wire format:
3773
4378
  * `{ type: 'image_url', image_url: { url: 'data:...;base64,...' } }`.
3774
4379
  */
3775
4380
  async function imageToOpenAI(ref, readImage) {
3776
- const data = await readImage(ref);
4381
+ const version = await readImage(ref);
3777
4382
  return {
3778
4383
  type: "image_url",
3779
- image_url: { url: `data:${ref.mediaType};base64,${Buffer.from(data).toString("base64")}` }
4384
+ image_url: { url: `data:${version.mediaType};base64,${Buffer.from(version.data).toString("base64")}` }
3780
4385
  };
3781
4386
  }
3782
4387
  /**
@@ -3789,89 +4394,45 @@ async function imageToOpenAI(ref, readImage) {
3789
4394
  * on assistant messages. Only tool calls with a paired tool result are
3790
4395
  * replayed (same policy as the CLI path).
3791
4396
  */
3792
- async function messagesToOpenAI(messages, readImage) {
3793
- const out = [];
3794
- const { ids: paired } = pairedToolCalls(messages);
3795
- const wireIds = wireToolCallIds(paired);
3796
- const pendingImages = [];
3797
- const flushPendingImages = () => {
3798
- for (const message of pendingImages) out.push(message);
3799
- pendingImages.length = 0;
3800
- };
3801
- for (const message of messages) {
3802
- if (message.role === "system") continue;
3803
- const result = toolResultOf(message);
3804
- if (message.role === "user" && !result) {
3805
- flushPendingImages();
3806
- const parts = [];
3807
- for (const block of message.content) {
3808
- if (block.type === "text") parts.push({
3809
- type: "text",
3810
- text: block.text
3811
- });
3812
- if (block.type === "image") {
3813
- if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
3814
- parts.push(await imageToOpenAI(block.attachment, readImage));
3815
- }
3816
- }
3817
- if (parts.length === 0) continue;
3818
- if (!parts.some((part) => part.type === "image_url") && parts.length === 1) out.push({
3819
- role: "user",
3820
- content: parts[0].text
3821
- });
3822
- else out.push({
3823
- role: "user",
3824
- content: parts
3825
- });
3826
- continue;
3827
- }
3828
- if (message.role === "assistant") {
3829
- flushPendingImages();
3830
- const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
3831
- const reasoning = message.content.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
3832
- const toolCalls = message.content.filter((block) => block.type === "tool-call" && paired.has(block.id)).map((block) => ({
3833
- id: wireIds.get(block.id) ?? block.id,
3834
- type: "function",
3835
- function: {
3836
- name: block.name,
3837
- arguments: block.arguments
3838
- }
3839
- }));
3840
- if (text === "" && reasoning === "" && toolCalls.length === 0) continue;
3841
- const assistant = {
3842
- role: "assistant",
3843
- content: text === "" ? null : text
3844
- };
3845
- if (reasoning !== "") assistant.reasoning_content = reasoning;
3846
- if (toolCalls.length > 0) assistant.tool_calls = toolCalls;
3847
- out.push(assistant);
3848
- continue;
3849
- }
3850
- if (result) {
3851
- if (!paired.has(result.toolCallId)) continue;
3852
- const media = toolResultMedia(result);
3853
- const wireToolCallId = wireIds.get(result.toolCallId) ?? result.toolCallId;
3854
- out.push({
3855
- role: "tool",
3856
- tool_call_id: wireToolCallId,
3857
- content: toolResultTextForWire(media)
3858
- });
3859
- if (media.images.length > 0) {
3860
- if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
3861
- const carried = [{
3862
- type: "text",
3863
- text: toolResultImageNote(media, wireToolCallId)
3864
- }];
3865
- for (const attachment of media.images) carried.push(await imageToOpenAI(attachment, readImage));
3866
- pendingImages.push({
3867
- role: "user",
3868
- content: carried
3869
- });
4397
+ const openAiMessageCodec = {
4398
+ encodeImage: imageToOpenAI,
4399
+ encodeUser: (parts) => {
4400
+ return !parts.some((part) => part.type === "image_url") && parts.length === 1 ? {
4401
+ role: "user",
4402
+ content: parts[0].text
4403
+ } : {
4404
+ role: "user",
4405
+ content: parts
4406
+ };
4407
+ },
4408
+ encodeAssistant: (message, paired, wireIds) => {
4409
+ const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
4410
+ const reasoning = message.content.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
4411
+ const toolCalls = message.content.filter((block) => block.type === "tool-call" && paired.has(block.id)).map((block) => ({
4412
+ id: wireIds.get(block.id) ?? block.id,
4413
+ type: "function",
4414
+ function: {
4415
+ name: block.name,
4416
+ arguments: block.arguments
3870
4417
  }
3871
- }
3872
- }
3873
- flushPendingImages();
3874
- return out;
4418
+ }));
4419
+ if (text === "" && reasoning === "" && toolCalls.length === 0) return void 0;
4420
+ const assistant = {
4421
+ role: "assistant",
4422
+ content: text === "" ? null : text
4423
+ };
4424
+ if (reasoning !== "") assistant.reasoning_content = reasoning;
4425
+ if (toolCalls.length > 0) assistant.tool_calls = toolCalls;
4426
+ return assistant;
4427
+ },
4428
+ encodeTool: (_result, media, wireId) => ({
4429
+ role: "tool",
4430
+ tool_call_id: wireId,
4431
+ content: toolResultTextForWire(media)
4432
+ })
4433
+ };
4434
+ async function messagesToOpenAI(messages, readImage) {
4435
+ return convertMessages(messages, readImage, openAiMessageCodec);
3875
4436
  }
3876
4437
  /** Account endpoints fetched by one `getUsage()` run (see the classification there). */
3877
4438
  const USAGE_ENDPOINT_COUNT = 4;
@@ -4060,6 +4621,41 @@ async function buildOpenAIBody(options, facts) {
4060
4621
  };
4061
4622
  }
4062
4623
  /**
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
4656
+ };
4657
+ }
4658
+ /**
4063
4659
  * One pre-stream connect attempt: POST the body and wait for response
4064
4660
  * headers only (`requestTimeoutMs` must never bound the body stream). Returns
4065
4661
  * the live response plus its cleanup, or the rejection facts for the rotation
@@ -4108,19 +4704,26 @@ async function connectGenerate(deps, key, protocol, body) {
4108
4704
  ...zdr ? { "x-cmd-zdr": "1" } : {},
4109
4705
  ...attributionHeaders()
4110
4706
  };
4707
+ const serializedBody = JSON.stringify(body);
4111
4708
  try {
4112
4709
  response = await fetchImpl(endpoint, {
4113
4710
  method: "POST",
4114
4711
  headers,
4115
- body: JSON.stringify(body),
4712
+ body: serializedBody,
4116
4713
  signal: connectAbort.signal
4117
4714
  });
4118
4715
  clearTimeout(connectTimer);
4119
4716
  } catch (error) {
4120
4717
  cleanup();
4121
4718
  if (options.signal?.aborted) throw error;
4122
- if (connectTimedOut || error instanceof DOMException && error.name === "TimeoutError") throw new LlmError(`Command Code API request to ${endpoint} did not respond within ${connection.requestTimeoutMs}ms: ${errorChain(error)};Command Code API 请求在 ${connection.requestTimeoutMs} 毫秒内未收到响应——通常是网络或代理问题,请检查后重试`, "TIMEOUT", { cause: error });
4123
- throw new LlmError(`Command Code API request to ${endpoint} failed: ${errorChain(error)};Command Code API 请求连接失败——通常是网络或代理问题,请检查网络或代理设置后重试`, "TRANSPORT", { cause: error });
4719
+ if (connectTimedOut || error instanceof DOMException && error.name === "TimeoutError") throw headersTimeoutError({
4720
+ protocol,
4721
+ endpoint,
4722
+ body: serializedBody,
4723
+ timeoutMs: connection.requestTimeoutMs,
4724
+ cause: error
4725
+ });
4726
+ throw bilingual("TRANSPORT", `Command Code API request to ${endpoint} failed: ${errorChain(error)}`, "Command Code API 请求连接失败——通常是网络或代理问题,请检查网络或代理设置后重试", { cause: error });
4124
4727
  }
4125
4728
  deps.onResponse(response);
4126
4729
  if (!response.ok) {
@@ -4226,6 +4829,13 @@ var CommandCodeAdapter = class extends LlmAdapter {
4226
4829
  billingAccess = /* @__PURE__ */ new Map();
4227
4830
  billingAccessInflight = /* @__PURE__ */ new Map();
4228
4831
  protocolCache = /* @__PURE__ */ new Map();
4832
+ /**
4833
+ * Consecutive header timeouts for the last request shape, counted ACROSS
4834
+ * `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.
4837
+ */
4838
+ headerTimeoutStreak;
4229
4839
  constructor(deps) {
4230
4840
  super();
4231
4841
  this.deps = deps;
@@ -4234,6 +4844,32 @@ var CommandCodeAdapter = class extends LlmAdapter {
4234
4844
  this.surfaceOffload = deps.imageOffload ?? ENGINE_IMAGE_POLICY;
4235
4845
  }
4236
4846
  /**
4847
+ * Grade one failed attempt: a headers timeout that repeats on an unchanged
4848
+ * payload eventually leaves the retryable whitelist.
4849
+ *
4850
+ * `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.
4856
+ *
4857
+ * @returns the error to surface: unchanged for any other failure, the
4858
+ * original timeout below the threshold, and a terminal one at it.
4859
+ */
4860
+ gradeHeaderTimeout(error, protocol, body, timeoutMs) {
4861
+ if (!(error instanceof LlmError && error.code === "TIMEOUT")) return error;
4862
+ const fingerprint = `${protocol}:${fingerprintValue(body).sha256}`;
4863
+ const count = this.headerTimeoutStreak?.fingerprint === fingerprint ? this.headerTimeoutStreak.count + 1 : 1;
4864
+ this.headerTimeoutStreak = {
4865
+ fingerprint,
4866
+ count
4867
+ };
4868
+ if (count < MAX_CONSECUTIVE_HEADER_TIMEOUTS) return error;
4869
+ this.headerTimeoutStreak = void 0;
4870
+ 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
+ }
4872
+ /**
4237
4873
  * Display metadata for the picker's provider group header. The base class
4238
4874
  * returns the raw route id (`commandcode`, all lowercase) as the name, which
4239
4875
  * is what the model selector shows as this group's sticky title; return the
@@ -4300,16 +4936,13 @@ var CommandCodeAdapter = class extends LlmAdapter {
4300
4936
  async loadCatalog(signal) {
4301
4937
  const { apiBase, modelsCachePath } = this.deps.options();
4302
4938
  try {
4303
- const response = await this.fetchImpl(`${apiBase}/provider/v1/models`, {
4304
- headers: {
4305
- accept: "application/json",
4306
- ...IDENTITY_ENCODING_HEADER,
4307
- ...attributionHeaders()
4308
- },
4309
- signal: signal ?? AbortSignal.timeout(1e4)
4310
- });
4311
- if (!response.ok) throw new Error(`models endpoint returned ${response.status}`);
4312
- this.catalog = parseCatalogResponse(await response.json());
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);
4313
4946
  await writeModelsCache(modelsCachePath, this.catalog).catch(() => void 0);
4314
4947
  } catch (error) {
4315
4948
  if (signal?.aborted) throw error;
@@ -4340,6 +4973,31 @@ var CommandCodeAdapter = class extends LlmAdapter {
4340
4973
  return allow === void 0 || allow.has(model.id);
4341
4974
  }).map(toInfo).sort(compareByPlan);
4342
4975
  }
4976
+ /**
4977
+ * The per-model allowance bracket this account POOL falls into, or undefined
4978
+ * when there is nothing honest to show.
4979
+ *
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,
4983
+ * Max or Ultra gets undefined rather than a neighbouring tier's figure. Taking
4984
+ * the HIGHEST tier matches the question the picker already answers for a pool
4985
+ * (see `modelVisibleForAnyAccount`): what the user can reach, not what the one
4986
+ * account serving this request happens to be. The fail-open shape is the same
4987
+ * too — an unreachable billing endpoint yields undefined, which hides the
4988
+ * allowance instead of inventing one.
4989
+ */
4990
+ async allowanceTier() {
4991
+ const accesses = await this.loadPoolBillingAccess();
4992
+ if (accesses === void 0) return void 0;
4993
+ let highest;
4994
+ for (const access of accesses) {
4995
+ const weight = access?.tierWeight;
4996
+ if (weight === void 0) continue;
4997
+ if (highest === void 0 || weight > highest) highest = weight;
4998
+ }
4999
+ return allowanceTierForWeight(highest);
5000
+ }
4343
5001
  async resolveModel(provider, model, signal) {
4344
5002
  const entry = this.catalog.find((m) => m.id === model) ?? (await this.loadCatalog(signal)).find((m) => m.id === model);
4345
5003
  const efforts = KNOWN_EFFORTS[model];
@@ -4375,21 +5033,19 @@ var CommandCodeAdapter = class extends LlmAdapter {
4375
5033
  };
4376
5034
  }
4377
5035
  /**
4378
- * Fetch one account endpoint and parse its JSON body. Returns the HTTP
4379
- * status alongside the parsed record so each caller applies its own failure
4380
- * accounting: the billing probe fails open silently, the usage report books
4381
- * failures per endpoint. Non-2xx and invalid JSON bodies come back without a
4382
- * record; only a fetch failure propagates to the caller.
5036
+ * Fetch one JSON GET and return its HTTP status with a parsed record. The
5037
+ * catalog, billing probe, and usage report each apply their own failure
5038
+ * policy. Non-2xx and invalid JSON bodies come back without a record; only
5039
+ * a fetch failure propagates to the caller.
4383
5040
  *
4384
- * `timeoutMs` is the caller's budget for this one request. The default is the
4385
- * CATALOG probe's short cap, which fits the picker's fail-open reads; the
4386
- * usage report deliberately passes the connection's own request budget so an
4387
- * account query is not held to a stricter (and invisible) limit than chat.
5041
+ * `timeoutMs` is the caller's budget, unless it provides its own signal.
5042
+ * The usage report passes the connection's request budget rather than the
5043
+ * catalog/picker's shorter default.
4388
5044
  */
4389
- async fetchEndpointJson(url, headers, timeoutMs = MODELS_TIMEOUT_MS) {
5045
+ async fetchJson(url, headers, timeoutMs = MODELS_TIMEOUT_MS, signal) {
4390
5046
  const response = await this.fetchImpl(url, {
4391
5047
  headers,
4392
- signal: AbortSignal.timeout(timeoutMs)
5048
+ signal: signal ?? AbortSignal.timeout(timeoutMs)
4393
5049
  });
4394
5050
  if (!response.ok) return { status: response.status };
4395
5051
  let parsed;
@@ -4475,7 +5131,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4475
5131
  const connection = this.deps.options();
4476
5132
  const headers = await this.accountHeaders(apiKey);
4477
5133
  const base = connection.apiBase;
4478
- const getJson = async (path) => (await this.fetchEndpointJson(`${base}${path}`, headers)).record;
5134
+ const getJson = async (path) => (await this.fetchJson(`${base}${path}`, headers)).record;
4479
5135
  const whoami = await getJson("/alpha/whoami");
4480
5136
  const orgData = whoami && isRecord(whoami.org) ? whoami.org : void 0;
4481
5137
  const orgId = orgData === void 0 ? void 0 : stringValue(orgData.id);
@@ -4582,7 +5238,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4582
5238
  const failed = [];
4583
5239
  const getJson = async (path) => {
4584
5240
  try {
4585
- const { status, record, invalidResponse } = await this.fetchEndpointJson(`${base}${path}`, headers, connection.requestTimeoutMs);
5241
+ const { status, record, invalidResponse } = await this.fetchJson(`${base}${path}`, headers, connection.requestTimeoutMs);
4586
5242
  if (record === void 0) {
4587
5243
  failures.push(`${path}: HTTP ${status}${invalidResponse ? " returned an unreadable or invalid JSON body" : ""}`);
4588
5244
  failed.push(invalidResponse ? {
@@ -4655,13 +5311,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
4655
5311
  async probeWindowLimits(apiKey) {
4656
5312
  try {
4657
5313
  const connection = this.deps.options();
4658
- const response = await this.fetchImpl(`${connection.apiBase}/alpha/billing/credits`, {
4659
- headers: await this.accountHeaders(apiKey),
4660
- signal: AbortSignal.timeout(MODELS_TIMEOUT_MS)
4661
- });
4662
- if (!response.ok) return void 0;
4663
- const parsed = await response.json();
4664
- if (!isRecord(parsed)) return void 0;
5314
+ const { record: parsed } = await this.fetchJson(`${connection.apiBase}/alpha/billing/credits`, await this.accountHeaders(apiKey));
5315
+ if (parsed === void 0) return void 0;
4665
5316
  const windowLimits = isRecord(parsed.windowLimits) ? parsed.windowLimits : void 0;
4666
5317
  if (windowLimits === void 0) return void 0;
4667
5318
  let reported = false;
@@ -4696,25 +5347,34 @@ var CommandCodeAdapter = class extends LlmAdapter {
4696
5347
  }
4697
5348
  const connection = this.deps.options();
4698
5349
  let apiKey = await this.deps.resolveApiKey(connection, options.model);
4699
- const modelMax = this.catalog.find((m) => m.id === options.model)?.maxTokens ?? 65536;
4700
- const maxTokens = Math.min(options.maxTokens ?? modelMax, modelMax, DEFAULT_GENERATE_MAX_TOKENS);
5350
+ const modelEntry = this.catalog.find((m) => m.id === options.model);
5351
+ const modelMax = modelEntry?.maxTokens ?? 131072;
5352
+ 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
+ };
4701
5358
  const effort = options.reasoningEffort;
4702
5359
  const supported = KNOWN_EFFORTS[options.model];
4703
5360
  const reasoningEffort = effort && effort !== "off" && supported?.includes(effort) ? effort : void 0;
4704
5361
  const systemText = [options.system ?? "", ...options.messages.filter((m) => m.role === "system").map((m) => m.content.map(blockText).filter(Boolean).join("\n"))].filter(Boolean).join("\n\n");
4705
5362
  let protocol = this.resolveProtocol(apiKey, options.model);
4706
- let requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0]);
4707
- const buildBody = async (target) => target === "cli" ? buildCliBody(requestOptions, connection, {
4708
- maxTokens,
4709
- reasoningEffort,
4710
- systemText,
4711
- readImage
4712
- }) : buildOpenAIBody(requestOptions, {
4713
- maxTokens,
4714
- reasoningEffort,
4715
- systemText,
4716
- readImage
4717
- });
5363
+ let requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true && protocol === "cli" ? options.model : void 0);
5364
+ 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;
5377
+ };
4718
5378
  let body = await buildBody(protocol);
4719
5379
  const trace = openStreamTrace(`${options.provider}/${options.model}`);
4720
5380
  const tried = /* @__PURE__ */ new Set();
@@ -4728,6 +5388,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4728
5388
  connection,
4729
5389
  fetchImpl: this.fetchImpl,
4730
5390
  onResponse: (response) => {
5391
+ this.headerTimeoutStreak = void 0;
4731
5392
  responseMetadata = {
4732
5393
  protocol,
4733
5394
  headers: responseIdentifiers(response.headers)
@@ -4763,9 +5424,11 @@ var CommandCodeAdapter = class extends LlmAdapter {
4763
5424
  connected = attempt;
4764
5425
  break;
4765
5426
  }
4766
- if (protocol === "openai" && isUpgradeRequiredError(attempt.status, attempt.errText)) {
5427
+ const providerError = parseProviderError(attempt.errText);
5428
+ if (protocol === "openai" && isUpgradeRequiredError(attempt.status, attempt.errText, providerError)) {
4767
5429
  protocol = "cli";
4768
5430
  this.rememberProtocol(apiKey, true);
5431
+ requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true ? options.model : void 0);
4769
5432
  body = await buildBody("cli");
4770
5433
  continue;
4771
5434
  }
@@ -4774,7 +5437,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4774
5437
  if (missing > 0) throw imageOffloadRequired(missing);
4775
5438
  }
4776
5439
  const rotate = this.deps.rotateApiKey;
4777
- const rejection = classifyAccountRejection(attempt.status, attempt.errText);
5440
+ const rejection = classifyAccountRejection(attempt.status, attempt.errText, providerError);
4778
5441
  if (rejection !== void 0 && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
4779
5442
  const next = await rotate(apiKey, rejection.reason, connection, options.model, {
4780
5443
  tried: [...tried],
@@ -4790,9 +5453,9 @@ var CommandCodeAdapter = class extends LlmAdapter {
4790
5453
  const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
4791
5454
  const when = reset === void 0 ? void 0 : new Date(reset).toISOString();
4792
5455
  const window = rejection.reason === "rate-limit";
4793
- throw new LlmError((window ? "llm-commandcode: the Command Code account is rate limited" : "llm-commandcode: the Command Code account is rate limited (429) — the provider did not report an exhausted usage window") + (when === void 0 ? "" : ` — the provider reports it resets at ${when}`) + (window ? ";当前 Command Code 账户已被限流" : ";当前 Command Code 账户被限流(429),服务商未报告用量窗口用尽") + (when === void 0 ? "" : `,服务商给出的重置时间为 ${when}`), "RATE_LIMIT", wait > 0 ? { providerRetryAfterMs: wait } : void 0);
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);
4794
5457
  }
4795
- throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs);
5458
+ throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs, providerError, contextBudget.sizePressure);
4796
5459
  }
4797
5460
  } catch (error) {
4798
5461
  trace.record("connect-error", {
@@ -4800,7 +5463,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
4800
5463
  responseMetadata
4801
5464
  });
4802
5465
  trace.close();
4803
- throw options.signal?.aborted ? error : failureWithRequestId(error, responseMetadata);
5466
+ if (options.signal?.aborted) throw error;
5467
+ throw failureWithRequestId(this.gradeHeaderTimeout(error, protocol, body, connection.requestTimeoutMs), responseMetadata);
4804
5468
  }
4805
5469
  if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
4806
5470
  const { response, cleanup } = connected;
@@ -4860,7 +5524,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4860
5524
  silentMs: Date.now() - lastChunkAt,
4861
5525
  message: errorChain(error)
4862
5526
  });
4863
- throw new LlmError(`Command Code API stream from ${connection.apiBase} failed while reading: ${errorChain(error)};Command Code API 流式响应中途断开——网络波动所致,重试通常可恢复`, "TRANSPORT", { cause: error });
5527
+ throw bilingual("TRANSPORT", `Command Code API stream from ${connection.apiBase} failed while reading: ${errorChain(error)}`, "Command Code API 流式响应中途断开——网络波动所致,重试通常可恢复", { cause: error });
4864
5528
  } finally {
4865
5529
  clearIdle();
4866
5530
  }
@@ -4875,7 +5539,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4875
5539
  idleFired,
4876
5540
  buffered: buffer.length
4877
5541
  });
4878
- if (idleFired) throw new LlmError(`Command Code API stream from ${connection.apiBase} was idle for ${connection.streamIdleTimeoutMs}ms (no events) and was treated as a dead connection;Command Code API 流式响应已 ${connection.streamIdleTimeoutMs} 毫秒无任何事件,被判定为死连接——长思考模型可在设置中调大流空闲超时`, "TIMEOUT");
5542
+ if (idleFired) throw bilingual("TIMEOUT", `Command Code API stream from ${connection.apiBase} was idle for ${connection.streamIdleTimeoutMs}ms (no events) and was treated as a dead connection`, `Command Code API 流式响应已 ${connection.streamIdleTimeoutMs} 毫秒无任何事件,被判定为死连接——长思考模型可在设置中调大流空闲超时`);
4879
5543
  if (buffer.trim()) yield* handle(parseStreamEventLine(buffer));
4880
5544
  break;
4881
5545
  }
@@ -4933,8 +5597,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
4933
5597
  responseMetadata
4934
5598
  });
4935
5599
  endRecorded = true;
4936
- if (!asm.sawContent) throw new LlmError("Command Code returned an empty response;Command Code 返回了空响应,重试通常可恢复", "EMPTY_RESPONSE");
4937
- throw new LlmError(`Command Code API stream from ${connection.apiBase} ended before its finish event (${detail}) — the response was closed mid-generation, so this is not the model stopping by itself;Command Code API 流式响应在结束事件之前被关闭(生成中途断流),并非模型主动结束——请重试;若频繁出现,可用 ${STREAM_TRACE_ENV}=<文件路径> 抓取原始流以定位断流原因`, "STREAM_CLOSED");
5600
+ if (!asm.sawContent) throw bilingual("EMPTY_RESPONSE", "Command Code returned an empty response", "Command Code 返回了空响应,重试通常可恢复");
5601
+ throw bilingual("STREAM_CLOSED", `Command Code API stream from ${connection.apiBase} ended before its finish event (${detail}) — the response was closed mid-generation, so this is not the model stopping by itself`, `Command Code API 流式响应在结束事件之前被关闭(生成中途断流),并非模型主动结束——请重试;若频繁出现,可用 ${STREAM_TRACE_ENV}=<文件路径> 抓取原始流以定位断流原因`);
4938
5602
  }
4939
5603
  } catch (error) {
4940
5604
  if (!usageEmitted && usage !== void 0) yield {
@@ -4963,44 +5627,39 @@ var CommandCodeAdapter = class extends LlmAdapter {
4963
5627
  }
4964
5628
  }
4965
5629
  };
4966
- /**
4967
- * Map a pre-stream generate HTTP failure onto a stable LlmError. Command
4968
- * Code folds several business rejections into 403 (plan limits, CLI version,
4969
- * model access): prefer the machine-readable `error.code` when present; the
4970
- * status alone cannot distinguish them. The status also decides the failure
4971
- * CODE via {@link httpErrorCode}, which is what the retry policy routes on —
4972
- * a transient 5xx must not be reported as a permanent provider rejection. A
4973
- * 429's `Retry-After` header rides along as `providerRetryAfterMs` so
4974
- * dsh-llm-retry can wait exactly that long instead of guessing at the backoff
4975
- * cadence — capped at RETRY_MAX_DELAY_MS, because in normal mode a longer
4976
- * attached wait makes the executor abandon the retry outright.
4977
- *
4978
- * One body is read, not just its status: a client-side rejection whose
4979
- * `error.code`/`type`/`message` names the model context window is reported as
4980
- * `CONTEXT_WINDOW_EXCEEDED`, because that failure has a recovery the harness
4981
- * performs and a generic provider error does not. Every message built here is
4982
- * bilingual — the harness renders it verbatim in its retry chrome.
4983
- */
4984
- function generateHttpError(status, errText, retryAfterMs) {
4985
- let providerCode;
4986
- let providerDetail = "";
5630
+ /** Parse a rejected body once, retaining the nested/root distinction. */
5631
+ function parseProviderError(errText) {
4987
5632
  try {
4988
5633
  const parsed = JSON.parse(errText);
4989
- if (isRecord(parsed) && isRecord(parsed.error)) {
4990
- providerCode = stringValue(parsed.error.code);
4991
- providerDetail = [
4992
- stringValue(parsed.error.code),
4993
- stringValue(parsed.error.type),
4994
- stringValue(parsed.error.message)
4995
- ].filter((part) => part !== void 0 && part !== "").join(" ");
4996
- }
4997
- } catch {}
5634
+ if (!isRecord(parsed)) return {};
5635
+ const nested = isRecord(parsed.error) ? parsed.error : void 0;
5636
+ const body = nested ?? parsed;
5637
+ return {
5638
+ root: parsed,
5639
+ nested,
5640
+ code: stringValue(body.code),
5641
+ type: stringValue(body.type),
5642
+ message: stringValue(body.message),
5643
+ rateLimit: isRecord(body.rateLimit) ? body.rateLimit : void 0
5644
+ };
5645
+ } catch {
5646
+ return {};
5647
+ }
5648
+ }
5649
+ function generateHttpError(status, errText, retryAfterMs, parsed = parseProviderError(errText), sizePressure = false) {
5650
+ const providerCode = parsed.nested === void 0 ? void 0 : parsed.code;
5651
+ const providerDetail = parsed.nested === void 0 ? "" : [
5652
+ parsed.code,
5653
+ parsed.type,
5654
+ parsed.message
5655
+ ].filter((part) => part !== void 0 && part !== "").join(" ");
4998
5656
  const detail = providerCode ?? `HTTP ${status}`;
4999
5657
  const overflowDetail = providerDetail !== "" ? providerDetail : errText.slice(0, 500);
5000
- if (status < 500 && isContextOverflowDetail(overflowDetail)) return new LlmError(`Command Code API error ${status}: the request exceeds the model's context window — this session is being compacted and the request retried;Command Code API 返回 ${status}:请求内容超出模型上下文窗口——正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, CONTEXT_WINDOW_EXCEEDED_CODE, { status });
5001
- if (status === 422 && isZdrNoProviders(providerCode, providerDetail, errText)) return new LlmError(`Command Code API error 422 (${detail}): zero data retention is enforced but no ZDR-capable upstream is available for this model — turn the ZDR setting off (or use another model);Command Code API 返回 422:已开启零数据保留(ZDR),但该模型当前没有可用的 ZDR 上游——请关闭 ZDR 开关,或改用其他模型`, "PROVIDER_HTTP_ERROR", { status: 422 });
5002
- if (status === 401) return new LlmError(`Command Code API error 401 (${detail}): the API key is missing or invalid — check the key stored for COMMANDCODE_API_KEY (Models page) or the auth file;Command Code API 返回 401:API 密钥缺失或无效——请在设置页检查 COMMANDCODE_API_KEY 存储的密钥,或检查 auth 文件`, "INVALID_CREDENTIAL", { status: 401 });
5003
- if (status === 413) return new LlmError("Command Code API error 413: the request body exceeds the provider's size limit (the cap is undocumented, measured at about 50 MB) — the session history is too large to send, usually because of accumulated image attachments. Start a new session, or drop the image-heavy part of this one, and retry;Command Code API 返回 413:请求体超过服务端的体积上限(官方未公开,实测约 50 MB)——会话历史过大,通常是历史图片累积所致。请新建会话,或移除本会话中图片较多的部分后重试", "PROVIDER_HTTP_ERROR", { status: 413 });
5658
+ 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
+ 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
+ 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
+ if (status === 413) return bilingual("PROVIDER_HTTP_ERROR", "Command Code API error 413: the request body exceeds the provider's size limit (the cap is undocumented, measured at about 50 MB) — the session history is too large to send, usually because of accumulated image attachments. Start a new session, or drop the image-heavy part of this one, and retry", "Command Code API 返回 413:请求体超过服务端的体积上限(官方未公开,实测约 50 MB)——会话历史过大,通常是历史图片累积所致。请新建会话,或移除本会话中图片较多的部分后重试", { status: 413 });
5004
5663
  return new LlmError(`Command Code API error ${status}${detail === `HTTP ${status}` ? "" : ` (${detail})`}: ${errText.slice(0, 500)}`, httpErrorCode(status), {
5005
5664
  status,
5006
5665
  ...retryAfterMs !== void 0 && retryAfterMs > 0 && retryAfterMs <= 9e5 ? { providerRetryAfterMs: retryAfterMs } : {}
@@ -5276,9 +5935,9 @@ function mapFinishReason(reason) {
5276
5935
  /** A terminal marker without text or tools is a failed attempt, not a completed turn. */
5277
5936
  function emptyCompletionError(reason, maxTokens, usage) {
5278
5937
  const detail = `finish_reason=${reason ?? "unknown"}, max_tokens=${maxTokens}, outputTokens=${usage?.outputTokens ?? "unknown"}, reasoningTokens=${usage?.reasoningTokens ?? "unknown"}`;
5279
- if (mapFinishReason(reason).kind === "max-tokens") return new LlmError(`Command Code reached the output token limit without producing answer text or a tool call (${detail});Command Code 已达到输出 token 上限,但没有生成正文或工具调用;请调整思考强度,或在模型和接口上限内增加输出预算`, "OUTPUT_TOKEN_LIMIT");
5280
- if (reason === "content_filter" || reason === "content-filter") return new LlmError(`Command Code filtered the response (${detail});Command Code 拦截了响应,未生成正文或工具调用`, "CONTENT_FILTER");
5281
- return new LlmError(`Command Code finished without producing answer text or a tool call (${detail});Command Code 返回了空响应(可能只有思考内容),重试通常可恢复`, "EMPTY_RESPONSE");
5938
+ if (mapFinishReason(reason).kind === "max-tokens") return bilingual("OUTPUT_TOKEN_LIMIT", `Command Code reached the output token limit without producing answer text or a tool call (${detail})`, "Command Code 已达到输出 token 上限,但没有生成正文或工具调用;请调整思考强度,或在模型和接口上限内增加输出预算");
5939
+ if (reason === "content_filter" || reason === "content-filter") return bilingual("CONTENT_FILTER", `Command Code filtered the response (${detail})`, "Command Code 拦截了响应,未生成正文或工具调用");
5940
+ return bilingual("EMPTY_RESPONSE", `Command Code finished without producing answer text or a tool call (${detail})`, "Command Code 返回了空响应(可能只有思考内容),重试通常可恢复");
5282
5941
  }
5283
5942
  /**
5284
5943
  * Classify a pre-stream rejection that is about the ACCOUNT rather than about
@@ -5322,9 +5981,10 @@ const ACCOUNT_UNAVAILABLE_CODES = /* @__PURE__ */ new Set([
5322
5981
  "PREMIUM_CREDITS_EXHAUSTED",
5323
5982
  "MODEL_NOT_IN_PLAN"
5324
5983
  ]);
5325
- function classifyAccountRejection(status, errText) {
5326
- if (status === 429 || readProviderErrorCode(errText)?.toUpperCase() === "RATE_LIMITED") {
5327
- const evidence = readWindowLimitEvidence(errText);
5984
+ function classifyAccountRejection(status, errText, parsed = parseProviderError(errText)) {
5985
+ const code = (parsed.code ?? parsed.type)?.toUpperCase();
5986
+ if (status === 429 || code === "RATE_LIMITED") {
5987
+ const evidence = readWindowLimitEvidence(errText, parsed);
5328
5988
  if (evidence.window === void 0) return { reason: "throttled" };
5329
5989
  return evidence.resetAtMs === void 0 ? { reason: "rate-limit" } : {
5330
5990
  reason: "rate-limit",
@@ -5332,23 +5992,11 @@ function classifyAccountRejection(status, errText) {
5332
5992
  };
5333
5993
  }
5334
5994
  if (status === 401) return { reason: "invalid-credential" };
5335
- const code = readProviderErrorCode(errText)?.toUpperCase();
5336
5995
  if (code !== void 0 && ACCOUNT_UNAVAILABLE_CODES.has(code)) return { reason: "unavailable" };
5337
5996
  if (status < 400 || status >= 500) return void 0;
5338
5997
  const lower = errText.toLowerCase().replaceAll("_", " ");
5339
5998
  if (lower.includes("insufficient credits") || lower.includes("premium credits exhausted") || lower.includes("model not in plan") || /insufficient (credit|balance)/.test(lower) || /out of credit/.test(lower) || /not (available|included)[^.]{0,40}plan/.test(lower) || lower.includes("upgrade your plan")) return { reason: "unavailable" };
5340
5999
  }
5341
- /** The machine-readable `error.code`/`error.type` of a pre-stream failure body. */
5342
- function readProviderErrorCode(errText) {
5343
- try {
5344
- const parsed = JSON.parse(errText);
5345
- if (!isRecord(parsed)) return void 0;
5346
- const error = isRecord(parsed.error) ? parsed.error : parsed;
5347
- return stringValue(error.code) ?? stringValue(error.type);
5348
- } catch {
5349
- return;
5350
- }
5351
- }
5352
6000
  /**
5353
6001
  * How far ahead a provider-published reset may be and still be trusted.
5354
6002
  *
@@ -5376,20 +6024,10 @@ const MAX_TRUSTED_RESET_MS = 2592e6;
5376
6024
  * message is honored as the fallback. Either can be present without the other;
5377
6025
  * the window label is what decides the classification.
5378
6026
  */
5379
- function readWindowLimitEvidence(errText) {
5380
- let named;
5381
- let message = "";
5382
- let seconds;
5383
- try {
5384
- const parsed = JSON.parse(errText);
5385
- if (isRecord(parsed)) {
5386
- const error = isRecord(parsed.error) ? parsed.error : parsed;
5387
- const rateLimit = isRecord(error.rateLimit) ? error.rateLimit : void 0;
5388
- named = stringValue(rateLimit?.window);
5389
- message = stringValue(error.message) ?? stringValue(parsed.message) ?? "";
5390
- seconds = numberValue(rateLimit?.reset);
5391
- }
5392
- } catch {}
6027
+ function readWindowLimitEvidence(errText, parsed) {
6028
+ const named = stringValue(parsed.rateLimit?.window);
6029
+ const message = parsed.message ?? stringValue(parsed.root?.message) ?? "";
6030
+ const seconds = numberValue(parsed.rateLimit?.reset);
5393
6031
  const window = named === "fiveHour" || named === "weekly" || named === "daily" ? named : /usage limit for your plan/i.test(message) || /usage limit for your plan/i.test(errText) ? /weekly/i.test(message) ? "weekly" : "fiveHour" : void 0;
5394
6032
  const stamp = /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(message)?.[1] ?? /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(errText)?.[1];
5395
6033
  const stamped = stamp === void 0 ? void 0 : Date.parse(stamp);
@@ -5407,24 +6045,15 @@ function readWindowLimitEvidence(errText) {
5407
6045
  * other 4xx/5xx must surface as ordinary errors so real account/model
5408
6046
  * problems are not masked.
5409
6047
  */
5410
- function isUpgradeRequiredError(status, errText) {
6048
+ function isUpgradeRequiredError(status, errText, parsed = parseProviderError(errText)) {
5411
6049
  if (status !== 403) return false;
5412
6050
  const lower = errText.toLowerCase();
5413
6051
  if (lower.includes("upgrade_required")) return true;
5414
6052
  if (lower.includes("go plan") && lower.includes("api access")) return true;
5415
6053
  if (lower.includes("only plan without api access")) return true;
5416
6054
  if (lower.includes("upgrade to goat or higher")) return true;
5417
- try {
5418
- const parsed = JSON.parse(errText);
5419
- if (isRecord(parsed)) {
5420
- const error = isRecord(parsed.error) ? parsed.error : parsed;
5421
- if ((stringValue(error.code) ?? stringValue(error.type))?.toLowerCase() === "upgrade_required") return true;
5422
- const message = stringValue(error.message) ?? "";
5423
- if (message.toLowerCase().includes("go plan") && message.toLowerCase().includes("api access")) return true;
5424
- }
5425
- } catch {
5426
- return false;
5427
- }
6055
+ if ((parsed.code ?? parsed.type)?.toLowerCase() === "upgrade_required") return true;
6056
+ if (parsed.message?.toLowerCase().includes("go plan") && parsed.message.toLowerCase().includes("api access")) return true;
5428
6057
  return false;
5429
6058
  }
5430
6059
  /** Map an OpenAI Chat Completions usage payload to harness disjoint counts. */
@@ -5532,33 +6161,42 @@ function commandCopy(locale, key) {
5532
6161
  return commandcodeCommand[locale][key] ?? commandcodeCommand.en[key] ?? key;
5533
6162
  }
5534
6163
  //#endregion
5535
- //#region src/commands.ts
5536
- function money(value) {
5537
- return `$${value.toFixed(4)}`;
5538
- }
5539
- /** Compact dollar amount (2 decimals), for the summary and window rows. */
5540
- function moneyShort(value) {
6164
+ //#region src/display-format.ts
6165
+ /** Shared, dependency-free formatting for Host commands and browser surfaces. */
6166
+ function formatMoney(value) {
5541
6167
  return `$${value.toFixed(2)}`;
5542
6168
  }
5543
- /** Format a large token count compactly (1.9M style). */
5544
- function tokensCompact(value) {
6169
+ function formatMoneyExact(value) {
6170
+ return `$${value.toFixed(4)}`;
6171
+ }
6172
+ function formatTokensCompact(value) {
5545
6173
  if (value >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
5546
6174
  if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
5547
6175
  if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
5548
6176
  return String(value);
5549
6177
  }
5550
- /**
5551
- * Success-rate percentage (already in percent units): at most two decimals,
5552
- * trailing zeros trimmed — mirrors `formatSuccessRate` in
5553
- * `./client/usage.ts`, which the settings card uses.
5554
- */
5555
- function successRateText(value) {
6178
+ /** The input is already in percent units; callers add the percent sign. */
6179
+ function formatSuccessRate(value) {
5556
6180
  return String(Number(value.toFixed(2)));
5557
6181
  }
6182
+ /** One window's fill ratio in [0, 1]; zero when uncapped. */
6183
+ function windowRatio(used, cap) {
6184
+ if (cap <= 0) return 0;
6185
+ return Math.max(0, Math.min(1, used / cap));
6186
+ }
6187
+ /** Local date-time, with the empty state chosen by the caller. */
6188
+ function formatResetAt(ms) {
6189
+ return ms <= 0 ? "" : new Date(ms).toLocaleString();
6190
+ }
6191
+ //#endregion
6192
+ //#region src/commands.ts
6193
+ const money = formatMoneyExact;
6194
+ const moneyShort = formatMoney;
6195
+ const tokensCompact = formatTokensCompact;
6196
+ const successRateText = formatSuccessRate;
5558
6197
  /** A local date, or `n/a` when the provider published no reset time. */
5559
6198
  function resetLabel(ms) {
5560
- if (ms <= 0) return "n/a";
5561
- return new Date(ms).toLocaleString();
6199
+ return formatResetAt(ms) || "n/a";
5562
6200
  }
5563
6201
  /**
5564
6202
  * A 10-cell horizontal bar: `██████████` for 100%, `███░░░░░░░` for ~33%.
@@ -5566,7 +6204,7 @@ function resetLabel(ms) {
5566
6204
  */
5567
6205
  function bar(used, cap) {
5568
6206
  if (cap <= 0) return "—";
5569
- const ratio = Math.max(0, Math.min(1, used / cap));
6207
+ const ratio = windowRatio(used, cap);
5570
6208
  const filled = Math.round(ratio * 10);
5571
6209
  return "█".repeat(filled) + "░".repeat(10 - filled);
5572
6210
  }
@@ -5849,7 +6487,7 @@ const MODELS_ENDPOINT = "commandcode/models";
5849
6487
  * separate instance so catalog boundary errors name `commandcode/models`,
5850
6488
  * not the report endpoint.
5851
6489
  */
5852
- const { record: catalogRecord, stringField: catalogString } = makeBoundaryValidator("commandcode/models result:");
6490
+ const { record: catalogRecord, stringField: catalogString, numberField: catalogNumber } = makeBoundaryValidator("commandcode/models result:");
5853
6491
  /** Parse one untrusted boundary value into a {@link CommandCodeCatalogModel}. */
5854
6492
  function parseCatalogModel(value) {
5855
6493
  const source = catalogRecord(value, "model");
@@ -5858,6 +6496,7 @@ function parseCatalogModel(value) {
5858
6496
  name: catalogString(source, "name", "model.name")
5859
6497
  };
5860
6498
  if (source.tier !== void 0) model.tier = catalogString(source, "tier", "model.tier");
6499
+ if (source.allowance !== void 0) model.allowance = catalogNumber(source, "allowance", "model.allowance");
5861
6500
  return model;
5862
6501
  }
5863
6502
  /** Parse the wire result into a {@link CommandCodeCatalog}. */
@@ -5999,12 +6638,17 @@ function loginDescriptor(endpoint, method) {
5999
6638
  }]
6000
6639
  } : descriptor;
6001
6640
  }
6002
- /** The three login descriptors, shared verbatim by Host registration and Client mount. */
6003
- const LOGIN_DESCRIPTORS = [
6004
- loginDescriptor(LOGIN_BEGIN_ENDPOINT, "loginBegin"),
6005
- loginDescriptor(LOGIN_STATUS_ENDPOINT, "loginStatus"),
6006
- loginDescriptor(LOGIN_CANCEL_ENDPOINT, "loginCancel")
6007
- ];
6641
+ /** The Host-face contribution fragment registered on `ctx.typert`. */
6642
+ const LOGIN_HOST_CONTRIBUTION = {
6643
+ package: REMOTE_PACKAGE,
6644
+ face: "host",
6645
+ schemas: [],
6646
+ invocations: [
6647
+ loginDescriptor(LOGIN_BEGIN_ENDPOINT, "loginBegin"),
6648
+ loginDescriptor(LOGIN_STATUS_ENDPOINT, "loginStatus"),
6649
+ loginDescriptor(LOGIN_CANCEL_ENDPOINT, "loginCancel")
6650
+ ]
6651
+ };
6008
6652
  //#endregion
6009
6653
  //#region src/usage-remote.ts
6010
6654
  /**
@@ -6093,7 +6737,7 @@ function applyUsageRemote(ctx, deps) {
6093
6737
  ...USAGE_HOST_CONTRIBUTION.invocations,
6094
6738
  MODELS_DESCRIPTOR,
6095
6739
  PRICES_DESCRIPTOR,
6096
- ...LOGIN_DESCRIPTORS
6740
+ ...LOGIN_HOST_CONTRIBUTION.invocations
6097
6741
  ]
6098
6742
  });
6099
6743
  remoteCtx.effect(() => () => void unregister(), "dsh-commandcode-provider: usage remote");
@@ -6810,22 +7454,8 @@ var CommandCodeSearchProvider = class {
6810
7454
  const ACTIVE_ACCOUNT_AUTO = "auto";
6811
7455
  /** The selector value meaning "no language override — follow the shell locale". */
6812
7456
  const LANG_AUTO = "auto";
6813
- /** Tier display names, mirroring the web dropdown's headings (`model-select.ts`). */
6814
- const TIER_TITLES = {
6815
- go: "Go",
6816
- goat: "GOAT",
6817
- pro: "Pro",
6818
- provider: "Provider",
6819
- max: "Max"
6820
- };
6821
7457
  /** Tiers in picker order; a model outside this set joins the "Other" group. */
6822
- const TIER_ORDER = [
6823
- "go",
6824
- "goat",
6825
- "pro",
6826
- "provider",
6827
- "max"
6828
- ];
7458
+ const TIER_ORDER = PLAN_TIER_ORDER;
6829
7459
  /** The group holding models this build's catalog does not know. */
6830
7460
  const OTHER_GROUP_ID = "models-other";
6831
7461
  /**
@@ -6883,8 +7513,8 @@ function buildCommandCodeTuiSection(deps) {
6883
7513
  const extras = [.../* @__PURE__ */ new Set([...stored, ...Object.keys(overrides)])].filter((id) => !known.has(id));
6884
7514
  const tierGroups = TIER_ORDER.filter((tier) => choices.some((choice) => choice.tier === tier)).map((tier) => ({
6885
7515
  id: `models-${tier}`,
6886
- title: `${TIER_TITLES[tier] ?? tier} models`,
6887
- descriptions: { zh: `${TIER_TITLES[tier] ?? tier} 模型` }
7516
+ title: `${PLAN_LABELS[tier] ?? tier} models`,
7517
+ descriptions: { zh: `${PLAN_LABELS[tier] ?? tier} 模型` }
6888
7518
  }));
6889
7519
  const unranked = choices.filter((choice) => tierRank(choice.tier) === TIER_ORDER.length);
6890
7520
  const otherGroup = unranked.length > 0 || extras.length > 0 ? [{
@@ -6989,6 +7619,20 @@ function buildCommandCodeTuiSection(deps) {
6989
7619
  value: text.trim() === "true"
6990
7620
  })
6991
7621
  },
7622
+ {
7623
+ path: ["offloadSeenImagesForCache"],
7624
+ group: "advanced",
7625
+ kind: "boolean",
7626
+ label: "Stop replaying images already seen",
7627
+ descriptions: { zh: "已看过的图片不再重复发送" },
7628
+ hint: "CLI transport only. After this model has answered, durably replace older images with text to keep later prompt text cacheable. Old pixels need a fresh file read or attachment to inspect again. Off by default.",
7629
+ hintDescriptions: { zh: "仅 CLI 传输。模型看过图片并回复后,永久用文字替代旧图,以保留后续文本缓存;重看旧图需要重新读取文件或附加。默认关闭。" },
7630
+ format: (value) => value === true ? "true" : "false",
7631
+ parse: (text) => ({
7632
+ kind: "set",
7633
+ value: text.trim() === "true"
7634
+ })
7635
+ },
6992
7636
  {
6993
7637
  path: ["filterModelsByPlan"],
6994
7638
  group: "models",
@@ -7149,27 +7793,18 @@ function applyCommandCodeTuiSettings(ctx, deps) {
7149
7793
  * A bounded retry budget for `TRANSPORT` failures (issue #39, second report).
7150
7794
  *
7151
7795
  * The route policy (`providerRetryPolicy` in `./adapter.ts`) is near-unbounded
7152
- * on purpose — 1000 attempts doubling to a 15-minute cap — because it exists
7153
- * for failures the provider ASKS to have retried, where the wait is the point.
7154
- * A connection that cannot be established is not that: either a blip the first
7155
- * few attempts absorb, or an outage no in-request waiting fixes (and its waits
7156
- * count BEFORE each attempt, so attempt 11 alone already waits 512 s). So
7157
- * transport failures get their own budget; waiting out a real outage is the
7158
- * user's call, and the next send starts a fresh budget.
7159
- *
7160
- * Not counted here: the pre-stream recovery the ADAPTER performs inside one
7161
- * `stream()` call (account rotation, the 413 image-budget step-down, the
7162
- * Provider-API → CLI switch) — invisible to the caller and bounded on its own.
7163
- * This budget caps only the harness's retry loop.
7796
+ * on purpose — 1000 attempts doubling to a 15-minute cap — for failures the
7797
+ * provider ASKS to have retried. A connection that cannot be established is
7798
+ * not that: a blip the first few attempts absorb, or an outage no in-request
7799
+ * waiting fixes (attempt 11 alone already waits 512 s). So transport failures
7800
+ * get their own budget, cleared at each new STEP (one step = one model
7801
+ * request). `agent/status` → `idle` is NOT the reset (one `running` phase spans
7802
+ * a turn's steps), and neither is `assistant/attempt` (appended for the FAILED
7803
+ * attempt itself, which would clear the budget on every failure).
7164
7804
  *
7165
- * Reset rule: the budget clears at each new STEP (one step = one model request).
7166
- * `agent/status` → `idle` is NOT it, though it looks natural: the loop's
7167
- * `setPhase` emits it only on a status CHANGE, and a turn's steps all run inside
7168
- * one `running` phase, so a reset there would fail every later step of an outage
7169
- * turn at once instead of retrying each. `assistant/attempt` is wrong the other
7170
- * way — the loop appends it for the FAILED attempt itself, before dispatching
7171
- * `agent/request-error`, so resetting on it would clear the budget on every
7172
- * failure and restore the very loop this module exists to stop.
7805
+ * Not counted here: the adapter's in-`stream()` pre-stream recovery (account
7806
+ * rotation, the 413 step-down, the Provider-API → CLI switch) — invisible to
7807
+ * the caller and bounded on its own.
7173
7808
  *
7174
7809
  * @module
7175
7810
  */
@@ -7251,7 +7886,7 @@ function transportBudgetMessage(failureMessage, maxRetries) {
7251
7886
  * Mark one schema field volatile.
7252
7887
  *
7253
7888
  * `.volatile()` is unconditional: it is a schemastery 3.18.3 feature and the
7254
- * only supported engine (`dsh 0.1.7-rc.2`) pins `~3.18.4`, so the method is
7889
+ * only supported engine (`dsh 0.2.0-rc.1`) pins `~3.18.4`, so the method is
7255
7890
  * always present. It is reached through a structural member so the caller's
7256
7891
  * declared field type survives — the mark changes what the schema PARSES to (a
7257
7892
  * live reference), not the plugin-facing `Config` shape.
@@ -7348,6 +7983,7 @@ const Config = z.object(markVolatileFields({
7348
7983
  modelsCachePath: z.string(),
7349
7984
  requestTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
7350
7985
  streamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
7986
+ offloadSeenImagesForCache: z.boolean().default(false),
7351
7987
  transportMaxRetries: z.number().min(0).max(50),
7352
7988
  filterModelsByPlan: z.boolean(),
7353
7989
  visibleModels: z.array(z.string()),
@@ -7380,8 +8016,9 @@ function resolveAdapterOptions(config) {
7380
8016
  apiBase: config.apiBase ?? "https://api.commandcode.ai",
7381
8017
  workingDir: config.workingDir ?? process.cwd(),
7382
8018
  modelsCachePath: config.modelsCachePath ?? DEFAULT_MODELS_CACHE_PATH,
7383
- requestTimeoutMs: config.requestTimeoutMs ?? 6e4,
8019
+ requestTimeoutMs: config.requestTimeoutMs ?? 3e5,
7384
8020
  streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
8021
+ offloadSeenImagesForCache: config.offloadSeenImagesForCache === true,
7385
8022
  filterModelsByPlan: config.filterModelsByPlan ?? true,
7386
8023
  visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : void 0,
7387
8024
  modelVisibility: readModelVisibility(config.modelVisibility),
@@ -7555,12 +8192,16 @@ function apply(ctx, config) {
7555
8192
  });
7556
8193
  ctx.effect(() => () => loginFlow.dispose(), "dsh-commandcode-provider: login flow");
7557
8194
  const catalogForEditors = async () => {
7558
- return { models: (await adapter.listModels(PROVIDER, { unfiltered: true })).map((model) => {
8195
+ const models = await adapter.listModels(PROVIDER, { unfiltered: true });
8196
+ const bracket = await adapter.allowanceTier();
8197
+ return { models: models.map((model) => {
7559
8198
  const tier = KNOWN_PLANS[model.id];
8199
+ const allowance = bracket === void 0 ? void 0 : modelAllowanceFor(model.id)?.[bracket];
7560
8200
  return {
7561
8201
  id: model.id,
7562
8202
  name: model.name.replace(/\s*\(CC\)$/, ""),
7563
- ...tier === void 0 ? {} : { tier }
8203
+ ...tier === void 0 ? {} : { tier },
8204
+ ...allowance === void 0 ? {} : { allowance }
7564
8205
  };
7565
8206
  }) };
7566
8207
  };