@mars-sea/dsh-commandcode-provider 0.15.4 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -123,7 +123,7 @@ const PLAN_ORDER = Object.fromEntries(PLAN_TIER_ORDER.map((tier, index) => [tier
123
123
  * subscription-plan labels, deals, and hourly (peak/off-peak) pricing.
124
124
  *
125
125
  * Everything here is synced from official sources — the command-code CLI
126
- * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.88.0), the
126
+ * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.88.1), the
127
127
  * live model catalog (`GET /provider/v1/models?view=client`, the authority for
128
128
  * capability fields since 1.82.4), and the official plan/pricing/model docs; see
129
129
  * the dsh-commandcode-upstream skill for the extraction procedures. The
@@ -139,7 +139,13 @@ const PLAN_ORDER = Object.fromEntries(PLAN_TIER_ORDER.map((tier, index) => [tier
139
139
  * efforts `low/high/max`, Go tier, `$2/$10/$0.2`). The 1.88.0 artifact does not
140
140
  * name that row yet; the default catalog, `?view=client`, the models docs page
141
141
  * and the pricing page all do, so the capability fields come from the catalog
142
- * as usual. Keeping the snapshot in its own module
142
+ * as usual. The 2026-10-27 check finds the CLI still at 1.88.0 and one more
143
+ * catalog-only addition, GPT-6.1 Terra (`gpt-6.1-terra`): 1.05M context, text +
144
+ * image, the same five-level effort set, Pro tier, `$1/$5` up to a 272K context
145
+ * step and `$2/$7.5` beyond. The 2026-10-28 check moves the constant to 1.88.1,
146
+ * whose registry now names both catalog-only rows (Kimi K3.1 Pro and GPT-6.1
147
+ * Terra) with efforts and context identical to this snapshot, so no table
148
+ * changes. Keeping the snapshot in its own module
143
149
  * confines those frequent sync diffs here: src/adapter.ts holds only the stable
144
150
  * wire/runtime logic. The read helpers live here too, for the same reason.
145
151
  *
@@ -499,6 +505,13 @@ const KNOWN_EFFORTS = {
499
505
  "xhigh",
500
506
  "max"
501
507
  ],
508
+ "gpt-6.1-terra": [
509
+ "low",
510
+ "medium",
511
+ "high",
512
+ "xhigh",
513
+ "max"
514
+ ],
502
515
  "inclusionai/ling-3.1-flash": [
503
516
  "low",
504
517
  "medium",
@@ -581,6 +594,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
581
594
  "gpt-6-luna",
582
595
  "gpt-6-sol",
583
596
  "gpt-6.1-sol",
597
+ "gpt-6.1-terra",
584
598
  "meta/muse-spark-1.1",
585
599
  "meta/muse-spark-1.2",
586
600
  "meta/muse-spark-1.2-contributor",
@@ -934,10 +948,9 @@ const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
934
948
  * `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
935
949
  * list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
936
950
  * Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
937
- * Provider-tier. Re-verified at command-code@1.88.0 (2026-10-24) against the
938
- * live `?view=client` catalog: 105 ids, still a strict superset chain. Mistral
939
- * Large 4
940
- * (1.75.0) was the only addition at the 1.77.0 check; the 1.78.0/1.79.0 pair
951
+ * Provider-tier. Re-verified at command-code@1.88.1 (2026-10-28) against the
952
+ * live `?view=client` catalog: 106 ids, still a strict superset chain. Mistral
953
+ * Large 4 (1.75.0) was the only addition at the 1.77.0 check; the 1.78.0/1.79.0 pair
941
954
  * added Claude Haiku 5.5 at Pro and the free Glyph Cluster preview at Go.
942
955
  * 1.79.2 lowers Haiku 5.5's minimum plan to GOAT; 1.80.0 adds Muse Wonder at
943
956
  * Max; 1.82.0 adds Gemini 4 Argon at Pro and nothing else moves. The 1.82.4
@@ -946,7 +959,8 @@ const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
946
959
  * which left the live catalog. Every other
947
960
  * tier is unchanged. The 1.86.1 sync (2026-10-21) adds only the Ornith 1.5 122B
948
961
  * launch pair at Go; the 1.87.1 check (2026-10-22) adds no model and moves no
949
- * tier, and the 2026-10-24 check adds only Kimi K3.1 Pro at Go.
962
+ * tier, the 2026-10-24 check adds only Kimi K3.1 Pro at Go, and the 2026-10-27
963
+ * check adds only GPT-6.1 Terra at Pro.
950
964
  * The only removals since 1.49.0 are the two retired stealth previews (Pixel
951
965
  * Canary at 1.73.1, Space Bunny Alpha at 1.74.3), Ling 3.0 Flash Sante
952
966
  * (2026-10-16) and the retired Cat Jam preview (2026-10-20). Per-entry tags
@@ -1043,6 +1057,7 @@ const KNOWN_PLANS = {
1043
1057
  "gpt-5.5": "pro",
1044
1058
  "gpt-5.6-terra": "pro",
1045
1059
  "gpt-6-sol": "pro",
1060
+ "gpt-6.1-terra": "pro",
1046
1061
  "meta/muse-spark-1.1": "pro",
1047
1062
  "claude-fable-5-1": "provider",
1048
1063
  "claude-fable-5": "provider",
@@ -2106,6 +2121,34 @@ const MODEL_PRICE_ROWS = [
2106
2121
  5
2107
2122
  ] }]
2108
2123
  },
2124
+ {
2125
+ id: "gpt-6.1-terra",
2126
+ rates: [
2127
+ 1,
2128
+ 5,
2129
+ .05,
2130
+ 1.25
2131
+ ],
2132
+ allowance: {
2133
+ go: 6,
2134
+ goat: 20,
2135
+ pro: 20
2136
+ },
2137
+ contextTiers: [{
2138
+ "maxContext": 272e3,
2139
+ "rates": [
2140
+ 1,
2141
+ 5,
2142
+ .05,
2143
+ 1.25
2144
+ ]
2145
+ }, { "rates": [
2146
+ 2,
2147
+ 7.5,
2148
+ .1,
2149
+ 2.5
2150
+ ] }]
2151
+ },
2109
2152
  {
2110
2153
  id: "grok-4.5",
2111
2154
  rates: [
@@ -4224,6 +4267,29 @@ var CommandCodeAccountPool = class CommandCodeAccountPool {
4224
4267
  });
4225
4268
  }
4226
4269
  /**
4270
+ * 用量报告读到的窗口结论(设置页账号卡片、`/commandcode` 拉取的
4271
+ * `/alpha/billing/credits`):某个窗口已超限,就按探测的同一口径给这个密钥
4272
+ * 打标记——有可信重置时刻记为到期自动恢复的 `cooldown`,否则为 `unknown`。
4273
+ *
4274
+ * 为什么需要(issue #105):轮转原本只认真实请求的拒绝,进程重启或冷却过早
4275
+ * 到期后,卡片明明显示「每周 100%」,自动模式和「当前使用」徽标仍选列表第一个
4276
+ * 账号,下一个请求白撞一次拒绝才换号。报告本来就要拉这个接口,把结论记进池里
4277
+ * 不增加任何请求。
4278
+ *
4279
+ * 只加不减:窗口未超限时不清除已有标记。限流(throttle)或消费上限这类拒绝
4280
+ * 在窗口数据里看不到(issue #54),据此清标记会让每次刷新卡片都换来一次注定
4281
+ * 被拒的请求;清除仍只走有节流的探测路径。401 禁用的密钥不改。
4282
+ */
4283
+ noteWindowReport(apiKey, verdict) {
4284
+ if (verdict === void 0 || !verdict.exceeded) return;
4285
+ const state = this.states.get(apiKey);
4286
+ if (state?.kind === "disabled") return;
4287
+ this.stampWindowMark({
4288
+ key: apiKey,
4289
+ state
4290
+ }, verdict);
4291
+ }
4292
+ /**
4227
4293
  * Record a model-scoped `unavailable` rejection (no credits, or a model
4228
4294
  * outside this account's plan) as a SHORT-LIVED PREFERENCE demotion.
4229
4295
  *
@@ -6369,7 +6435,8 @@ var GatewayFacts = class {
6369
6435
  if (this.state?.source !== identity) this.state = {
6370
6436
  source: identity,
6371
6437
  rows: [],
6372
- expiresAt: 0
6438
+ expiresAt: 0,
6439
+ origin: "none"
6373
6440
  };
6374
6441
  return this.state;
6375
6442
  }
@@ -6378,6 +6445,20 @@ var GatewayFacts = class {
6378
6445
  const row = this.forSource(source).rows.find((candidate) => candidate.id === model);
6379
6446
  return row === void 0 ? void 0 : copyRows([row])[0];
6380
6447
  }
6448
+ /**
6449
+ * 本进程**联网成功过**的目录;否则返回 undefined。
6450
+ *
6451
+ * 判据是来源而不是「内存里有行」:`snapshot()` 会把磁盘缓存写进同一份
6452
+ * `state.rows`(生成路径需要它),若只看行数,冷启动先发过一次聊天再打开
6453
+ * 选择器时,首屏拿到的就是没有年龄校验的磁盘副本——正是本方法要排除的东西。
6454
+ * 空目录同样返回 undefined:刷新失败后的空行(`expiresAt` 已进入重试窗口)
6455
+ * 不能让选择器把「暂时读不到」画成「没有模型」,调用方应回落到 `refresh()`。
6456
+ */
6457
+ loaded(source) {
6458
+ const state = this.forSource(source);
6459
+ if (state.origin !== "network" || state.rows.length === 0) return void 0;
6460
+ return copyRows(state.rows);
6461
+ }
6381
6462
  /** 生成只读取本地目录;捕获状态后不再转向实例的新来源。 */
6382
6463
  async snapshot(source) {
6383
6464
  const state = this.forSource(source);
@@ -6385,7 +6466,10 @@ var GatewayFacts = class {
6385
6466
  state.disk ??= readCache(source).catch(() => []);
6386
6467
  const rows = await state.disk;
6387
6468
  if (state.rows.length > 0 || state.expiresAt > 0) return copyRows(state.rows);
6388
- if (state.inflight === void 0) state.rows = rows;
6469
+ if (state.inflight === void 0) {
6470
+ state.rows = rows;
6471
+ if (rows.length > 0 && state.origin === "none") state.origin = "disk";
6472
+ }
6389
6473
  return copyRows(rows);
6390
6474
  }
6391
6475
  async refresh(source, signal) {
@@ -6398,8 +6482,10 @@ var GatewayFacts = class {
6398
6482
  const mayPublish = () => this.state === state && generations.get(source.apiBase) === generation;
6399
6483
  state.inflight = (async () => {
6400
6484
  let rows, ttl = MODEL_CATALOG_TTL_MS;
6485
+ let network = false;
6401
6486
  try {
6402
6487
  rows = parseCatalog$1(await this.fetchCatalog(source), source.apiBase);
6488
+ network = true;
6403
6489
  if (mayPublish()) {
6404
6490
  publish(source.apiBase, rows);
6405
6491
  await writeCache(source, rows, mayPublish).catch(() => void 0);
@@ -6411,6 +6497,8 @@ var GatewayFacts = class {
6411
6497
  }
6412
6498
  state.rows = rows;
6413
6499
  state.expiresAt = Date.now() + ttl;
6500
+ if (network) state.origin = "network";
6501
+ else if (state.origin === "none" && rows.length > 0) state.origin = "disk";
6414
6502
  return rows;
6415
6503
  })().finally(() => {
6416
6504
  delete state.inflight;
@@ -6447,7 +6535,7 @@ var GatewayFacts = class {
6447
6535
  * per-request options thunk and an API-key resolver from the plugin entry
6448
6536
  * (src/index.ts), so a settings change reaches the very next request.
6449
6537
  */
6450
- const COMMAND_CODE_CLI_VERSION = "1.88.0";
6538
+ const COMMAND_CODE_CLI_VERSION = "1.88.1";
6451
6539
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
6452
6540
  /**
6453
6541
  * The output budget this bundle asks for, and the ceiling it will never exceed.
@@ -6461,6 +6549,17 @@ const MODELS_TIMEOUT_MS = 1e4;
6461
6549
  /** How long the picker's plan-filter billing facts stay cached before refetching. */
6462
6550
  const BILLING_ACCESS_TTL_MS = 3e5;
6463
6551
  /**
6552
+ * How long the raw account facts captured by one usage-report run stay usable
6553
+ * for the picker's advisory plan filter.
6554
+ *
6555
+ * This is much shorter than {@link BILLING_ACCESS_TTL_MS} because it only ever
6556
+ * removes a DUPLICATE read: the picker and the settings card ask the same three
6557
+ * endpoints about the same key, and whichever ran last already paid for them.
6558
+ * Anything older than this window is simply fetched again — the filter is
6559
+ * fail-open advisory data, so a miss costs one round trip and nothing else.
6560
+ */
6561
+ const ACCOUNT_FACTS_TTL_MS = 3e4;
6562
+ /**
6464
6563
  * How long a learned CLI-only protocol preference stays cached per account.
6465
6564
  * After this TTL a request may probe Provider API again, so an upgraded Go
6466
6565
  * account can recover without a restart.
@@ -6552,9 +6651,13 @@ function fingerprintValue(value) {
6552
6651
  }
6553
6652
  return {
6554
6653
  bytes: Buffer.byteLength(serialized),
6555
- sha256: createHash("sha256").update(serialized).digest("hex")
6654
+ sha256: sha256Hex(serialized)
6556
6655
  };
6557
6656
  }
6657
+ /** Digest of already-serialized text: no second stringify, no retained copy. */
6658
+ function sha256Hex(serialized) {
6659
+ return createHash("sha256").update(serialized).digest("hex");
6660
+ }
6558
6661
  /**
6559
6662
  * Structural request facts for issue #64's cache diagnosis.
6560
6663
  *
@@ -6670,6 +6773,16 @@ function headersTimeoutError(input) {
6670
6773
  /** How deep a root `$ref` / combinator chain is followed while normalizing. */
6671
6774
  const SCHEMA_NORMALIZE_MAX_DEPTH = 4;
6672
6775
  /**
6776
+ * Depth-0 normalization results, keyed by the caller's own `parameters` object.
6777
+ *
6778
+ * {@link toolParametersSchema} only ever reads the root, so a schema that has
6779
+ * already been normalized returns the identical answer however many times it is
6780
+ * asked. The tool list is rebuilt for EVERY attempt — a rotation or a protocol
6781
+ * fallback rebuilds it again — and the objects come from the harness, so they
6782
+ * stay identical for the life of a turn: identity is an exact key.
6783
+ */
6784
+ const TOOL_SCHEMA_CACHE = /* @__PURE__ */ new WeakMap();
6785
+ /**
6673
6786
  * Whether a type-less node is object-shaped enough to be one with the type
6674
6787
  * declared: `properties`/`required`/`additionalProperties` only make sense on
6675
6788
  * an object, and a schema carrying them was meant to be one.
@@ -6715,7 +6828,7 @@ function mergeCombinatorBranches(node, depth) {
6715
6828
  const objectBranches = [];
6716
6829
  for (const branch of branches) {
6717
6830
  if (!isRecord(branch)) continue;
6718
- objectBranches.push(toolParametersSchema(branch, depth + 1));
6831
+ objectBranches.push(normalizeToolParameters(branch, depth + 1));
6719
6832
  }
6720
6833
  if (objectBranches.length === 0) continue;
6721
6834
  sawBranch = true;
@@ -6745,10 +6858,32 @@ function mergeCombinatorBranches(node, depth) {
6745
6858
  * resolved/merged; anything else (an array/scalar root, or no schema at all)
6746
6859
  * degrades to a permissive free-form object, because a request the provider
6747
6860
  * refuses helps no one and the tool's own description is still in the prompt.
6748
- * Only the root is touched and every path returns a copy: the harness may
6749
- * deep-freeze the caller's schema.
6750
- */
6751
- function toolParametersSchema(parameters, depth = 0) {
6861
+ *
6862
+ * Only the ROOT is touched. The object-root fast path deliberately returns the
6863
+ * caller's own object: an already-normalized schema is the common case, the
6864
+ * function has always answered with that same identity, and callers rely on it
6865
+ * to get the same frozen schema they handed in. Everything this function
6866
+ * CONSTRUCTS is a fresh object, so no caller can mutate another's schema.
6867
+ *
6868
+ * The ROOT is memoized per `parameters` object, because every transport rebuilds
6869
+ * its tool list on every attempt — on a rotating or downshifting request the
6870
+ * same handful of tool schemas is walked four, five, six times for an answer
6871
+ * that cannot have changed. The tool objects come from the harness and are
6872
+ * stable for the life of a turn, so a WeakMap keyed on identity is exact: a
6873
+ * genuinely different schema is a different object and misses. Nested
6874
+ * `$ref`/combinator branches are reached through {@link normalizeToolParameters}
6875
+ * and are not memoized; only the root is asked for on a hot path.
6876
+ */
6877
+ function toolParametersSchema(parameters) {
6878
+ if (!isRecord(parameters)) return normalizeToolParameters(parameters, 0);
6879
+ const cached = TOOL_SCHEMA_CACHE.get(parameters);
6880
+ if (cached !== void 0) return cached;
6881
+ const normalized = normalizeToolParameters(parameters, 0);
6882
+ TOOL_SCHEMA_CACHE.set(parameters, normalized);
6883
+ return normalized;
6884
+ }
6885
+ /** Depth-0 normalization of {@link toolParametersSchema}; see its memo there. */
6886
+ function normalizeToolParameters(parameters, depth) {
6752
6887
  if (!isRecord(parameters)) return {
6753
6888
  type: "object",
6754
6889
  properties: {},
@@ -6764,7 +6899,7 @@ function toolParametersSchema(parameters, depth = 0) {
6764
6899
  const target = resolveLocalRef(parameters, parameters.$ref);
6765
6900
  if (target !== void 0) {
6766
6901
  const { $ref: _ref, ...rest } = parameters;
6767
- return toolParametersSchema({
6902
+ return normalizeToolParameters({
6768
6903
  ...target,
6769
6904
  ...rest
6770
6905
  }, depth + 1);
@@ -7000,26 +7135,47 @@ function coreBudget(budget) {
7000
7135
  */
7001
7136
  const imageVersionBytes = (block) => block.attachment.bytes;
7002
7137
  /**
7138
+ * The accounting half of the image gate: project the surface's durable offload
7139
+ * marks and count what the byte budget still needs — WITHOUT throwing.
7140
+ *
7141
+ * The throw has to wait until the credential is known. A request that cannot
7142
+ * authenticate must not trigger a durable (irreversible) image offload before
7143
+ * the caller learns the key is missing; keeping the count pure lets
7144
+ * `streamAttempt()` run it before ANY I/O and report ONE error later, once the
7145
+ * real transport is known.
7146
+ */
7147
+ function projectSurfaceBudget(options, policy, budget) {
7148
+ const marked = policy.projectOffloadedImages(options.messages, (ref) => offloadedImageText(ref));
7149
+ const messages = marked === options.messages ? options.messages : [...marked];
7150
+ return {
7151
+ options: messages === options.messages ? options : {
7152
+ ...options,
7153
+ messages
7154
+ },
7155
+ budgetMissing: policy.requiredImageOffload(messages, coreBudget(budget), imageVersionBytes)
7156
+ };
7157
+ }
7158
+ /**
7003
7159
  * Request options under one budget rung: the surface's durable marks become
7004
7160
  * placeholder text, and an over-budget history FAILS with the count to offload
7005
7161
  * instead of being evicted here. Throws rather than returning, so no caller can
7006
7162
  * accidentally send a body this route already knows is too large.
7007
7163
  *
7164
+ * This is the one-shot form (budget + cache in a single error) used by the 413
7165
+ * downshift and the protocol fallback. `streamAttempt()`'s first pass uses
7166
+ * {@link projectSurfaceBudget} instead, because there the throw must wait for
7167
+ * the credential.
7168
+ *
7008
7169
  * The next rung is applied to the CURRENT projection, never to the raw history —
7009
7170
  * otherwise the 413 retry could reintroduce images the first rung had already
7010
7171
  * evicted.
7011
7172
  */
7012
7173
  function withSurfaceOffload(options, policy, budget, cacheOffloadModel) {
7013
- const marked = policy.projectOffloadedImages(options.messages, (ref) => offloadedImageText(ref));
7014
- const messages = marked === options.messages ? options.messages : [...marked];
7015
- const budgetMissing = policy.requiredImageOffload(messages, coreBudget(budget), imageVersionBytes);
7016
- const cacheMissing = cacheOffloadModel === void 0 ? 0 : seenImageOffloadCount(messages, cacheOffloadModel);
7017
- const missing = Math.max(budgetMissing, cacheMissing);
7018
- if (missing > 0) throw imageOffloadRequired(missing, cacheMissing > budgetMissing ? "cache" : "budget");
7019
- return messages === options.messages ? options : {
7020
- ...options,
7021
- messages
7022
- };
7174
+ const projected = projectSurfaceBudget(options, policy, budget);
7175
+ const cacheMissing = cacheOffloadModel === void 0 ? 0 : seenImageOffloadCount(projected.options.messages, cacheOffloadModel);
7176
+ const missing = Math.max(projected.budgetMissing, cacheMissing);
7177
+ if (missing > 0) throw imageOffloadRequired(missing, cacheMissing > projected.budgetMissing ? "cache" : "budget");
7178
+ return projected.options;
7023
7179
  }
7024
7180
  /**
7025
7181
  * Count retained images already followed by an answer from this same model.
@@ -7494,6 +7650,33 @@ function parseWindowLimit(value) {
7494
7650
  };
7495
7651
  }
7496
7652
  /**
7653
+ * 把已解析的用量窗口归结为账号池能用的结论:任一窗口超限即 `exceeded`,
7654
+ * `resetAt` 取超限窗口里最晚的重置时刻(真正卡住账号的那个)。
7655
+ *
7656
+ * 职责:`probeWindowLimits()`(池的探测)与用量报告(设置页卡片、`/commandcode`)
7657
+ * 读的是同一个 `/alpha/billing/credits`,两条路径必须得出同一结论,否则卡片显示
7658
+ * 「每周 100%」而池仍把它当可用(issue #105)。
7659
+ *
7660
+ * 一个窗口都没报告时返回 undefined:无从判断,调用方保持原标记不动。
7661
+ */
7662
+ function windowVerdict(windows, now = Date.now()) {
7663
+ let reported = false;
7664
+ let exceeded = false;
7665
+ let resetAt = 0;
7666
+ for (const window of windows) {
7667
+ if (window === void 0) continue;
7668
+ reported = true;
7669
+ if (!window.exceeded) continue;
7670
+ exceeded = true;
7671
+ if (window.resetAt > 0 && window.resetAt - now <= 2592e6) resetAt = Math.max(resetAt, window.resetAt);
7672
+ }
7673
+ if (!reported) return void 0;
7674
+ return {
7675
+ exceeded,
7676
+ resetAt: exceeded ? resetAt : 0
7677
+ };
7678
+ }
7679
+ /**
7497
7680
  * Parse one billing/credits payload into the report's credits section.
7498
7681
  * Returns undefined when the endpoint answered nothing usable.
7499
7682
  */
@@ -7977,6 +8160,25 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
7977
8160
  surfaceOffload;
7978
8161
  billingAccess = /* @__PURE__ */ new Map();
7979
8162
  billingAccessInflight = /* @__PURE__ */ new Map();
8163
+ /**
8164
+ * Successful account-endpoint responses, keyed by gateway + key + exact URL.
8165
+ *
8166
+ * The settings card and the picker's advisory plan filter ask the SAME three
8167
+ * endpoints about the SAME account (`/alpha/whoami`, then credits and
8168
+ * subscriptions in parallel). Here that is four round trips per surface for
8169
+ * two answers — and the card refreshes on a timer, so the picker keeps paying
8170
+ * for rows it already has. Sharing the raw responses removes the duplicate
8171
+ * without either surface having to know about the other: whoever asks second
8172
+ * within {@link ACCOUNT_FACTS_TTL_MS} reads the same record instead of the
8173
+ * wire.
8174
+ *
8175
+ * Only SUCCESSFUL records are kept. A failure must be retried rather than
8176
+ * replayed, or an endpoint that recovered a second ago would keep reporting
8177
+ * yesterday's outage — and the usage report's per-endpoint `failures` and
8178
+ * {@link classifyTotalFailure} verdict would describe a request this process
8179
+ * never made.
8180
+ */
8181
+ accountJson = /* @__PURE__ */ new Map();
7980
8182
  protocolCache = /* @__PURE__ */ new Map();
7981
8183
  /** Namespace account-derived facts by gateway without exposing the key. */
7982
8184
  accountCacheKey(apiBase, apiKey) {
@@ -8016,10 +8218,10 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8016
8218
  * @returns the error to surface: unchanged for any other failure, the
8017
8219
  * original timeout below the threshold, and a terminal one at it.
8018
8220
  */
8019
- gradeHeaderTimeout(error, protocol, body, timeoutMs, sessionId) {
8221
+ gradeHeaderTimeout(error, protocol, body, timeoutMs, sessionId, sent) {
8020
8222
  if (!(error instanceof LlmError && error.code === "TIMEOUT")) return error;
8021
8223
  const key = sessionId ?? GLOBAL_HEADER_TIMEOUT_KEY;
8022
- const fingerprint = `${protocol}:${fingerprintValue(body).sha256}`;
8224
+ const fingerprint = `${protocol}:${sent === void 0 ? fingerprintValue(body).sha256 : sha256Hex(sent)}`;
8023
8225
  const previous = this.headerTimeoutStreaks.get(key);
8024
8226
  const count = previous?.fingerprint === fingerprint ? previous.count + 1 : 1;
8025
8227
  if (previous === void 0) {
@@ -8122,7 +8324,9 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8122
8324
  }
8123
8325
  async listModels(provider, opts) {
8124
8326
  const connection = { ...this.deps.options() };
8125
- const catalog = await this.gatewayFacts.refresh(connection);
8327
+ const cached = this.gatewayFacts.loaded(connection);
8328
+ const catalog = cached ?? await this.gatewayFacts.refresh(connection);
8329
+ if (cached !== void 0) this.gatewayFacts.refresh(connection).catch(() => void 0);
8126
8330
  const toInfo = (model) => {
8127
8331
  const vision = KNOWN_IMAGE_MODELS.has(model.id);
8128
8332
  return {
@@ -8187,13 +8391,33 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8187
8391
  })) } } : {}
8188
8392
  };
8189
8393
  }
8190
- /** The headers every authenticated account endpoint shares. */
8191
- async accountHeaders(apiKey) {
8394
+ /**
8395
+ * Resolve and validate the key one account-endpoint call will use.
8396
+ *
8397
+ * Every account endpoint runs the chat path's own `assertUsableApiKey` check,
8398
+ * or a key carrying a character no HTTP header can hold throws inside `fetch`
8399
+ * BEFORE any I/O — once per endpoint — and the report reads as "the network is
8400
+ * down". The credential reference is read structurally: it belongs to the
8401
+ * host's resolved options, not to this adapter's connection type, and it only
8402
+ * names the key in the rejection message.
8403
+ *
8404
+ * Separate from {@link accountHeadersFor} so a caller that also needs to key a
8405
+ * cache by the same identity does not have to resolve the key twice.
8406
+ */
8407
+ async usableAccountKey(apiKey) {
8192
8408
  const connection = this.deps.options();
8193
8409
  const raw = apiKey ?? await this.deps.resolveApiKey(connection);
8194
8410
  const named = connection.apiKeyEnv;
8411
+ return assertUsableApiKey(raw, "llm-commandcode", typeof named === "string" && named !== "" ? named : "the stored credential");
8412
+ }
8413
+ /** The headers every authenticated account endpoint shares. */
8414
+ async accountHeaders(apiKey) {
8415
+ return this.accountHeadersFor(await this.usableAccountKey(apiKey));
8416
+ }
8417
+ /** Headers for an already-validated key; see {@link usableAccountKey}. */
8418
+ accountHeadersFor(key) {
8195
8419
  return {
8196
- Authorization: `Bearer ${assertUsableApiKey(raw, "llm-commandcode", typeof named === "string" && named !== "" ? named : "the stored credential")}`,
8420
+ Authorization: `Bearer ${key}`,
8197
8421
  ...IDENTITY_ENCODING_HEADER,
8198
8422
  "x-command-code-version": COMMAND_CODE_CLI_VERSION,
8199
8423
  "x-cli-environment": "production",
@@ -8313,6 +8537,42 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8313
8537
  return inflight;
8314
8538
  }
8315
8539
  /**
8540
+ * One account-endpoint GET, served from {@link accountJson} when `share` and
8541
+ * the same URL answered successfully within {@link ACCOUNT_FACTS_TTL_MS}.
8542
+ *
8543
+ * `timeoutMs` is the CALLER's budget; a cache hit spends none of it. Failures
8544
+ * are never cached, so this can only ever save a request that would have
8545
+ * re-read an answer this process already had.
8546
+ *
8547
+ * `share: false` marks a VOLATILE endpoint — one whose answer is the number
8548
+ * the user is actually looking at, and which must therefore never be replayed
8549
+ * from an earlier request. The usage card's summary is exactly that: its
8550
+ * headline counts and cost are the reason the card exists, and stamping a
8551
+ * cached copy with the current time would misreport when they were read.
8552
+ * Identity and billing facts are the opposite: a plan tier or an org id that
8553
+ * is 30 s old is still the truth, and those are what the picker duplicates.
8554
+ */
8555
+ async accountJsonGet(apiBase, apiKey, path, timeoutMs, share = true) {
8556
+ const cacheKey = JSON.stringify([
8557
+ apiBase,
8558
+ apiKey,
8559
+ path
8560
+ ]);
8561
+ if (share) {
8562
+ const hit = this.accountJson.get(cacheKey);
8563
+ if (hit !== void 0 && Date.now() - hit.at < ACCOUNT_FACTS_TTL_MS) return {
8564
+ status: 200,
8565
+ record: hit.record
8566
+ };
8567
+ }
8568
+ const answer = await this.fetchJson(`${apiBase}${path}`, this.accountHeadersFor(apiKey), timeoutMs);
8569
+ if (share && answer.record !== void 0) this.accountJson.set(cacheKey, {
8570
+ record: answer.record,
8571
+ at: Date.now()
8572
+ });
8573
+ return answer;
8574
+ }
8575
+ /**
8316
8576
  * The billing facts behind the picker's plan filter, mirroring the CLI's
8317
8577
  * `createBilling` flow: whoami yields the org id, then the subscriptions
8318
8578
  * and credits endpoints answer in parallel. The plan id is honored only
@@ -8323,8 +8583,8 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8323
8583
  */
8324
8584
  async fetchBillingAccess(apiBase, apiKey) {
8325
8585
  try {
8326
- const headers = await this.accountHeaders(apiKey);
8327
- const getJson = async (path) => (await this.fetchJson(`${apiBase}${path}`, headers)).record;
8586
+ const key = await this.usableAccountKey(apiKey);
8587
+ const getJson = async (path) => (await this.accountJsonGet(apiBase, key, path, MODELS_TIMEOUT_MS)).record;
8328
8588
  const whoami = await getJson("/alpha/whoami");
8329
8589
  const orgData = whoami && isRecord(whoami.org) ? whoami.org : void 0;
8330
8590
  const orgId = orgData === void 0 ? void 0 : stringValue(orgData.id);
@@ -8431,9 +8691,9 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8431
8691
  async getUsage(apiKey, capturedConnection) {
8432
8692
  const connection = capturedConnection ?? { ...this.deps.options() };
8433
8693
  const base = connection.apiBase;
8434
- let headers;
8694
+ let key;
8435
8695
  try {
8436
- headers = await this.accountHeaders(apiKey);
8696
+ key = await this.usableAccountKey(apiKey);
8437
8697
  } catch (error) {
8438
8698
  if (error instanceof LlmError && error.code === "INVALID_CREDENTIAL") return {
8439
8699
  failures: [error.message],
@@ -8443,9 +8703,9 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8443
8703
  }
8444
8704
  const failures = [];
8445
8705
  const failed = [];
8446
- const getJson = async (path) => {
8706
+ const getJson = async (path, share = true) => {
8447
8707
  try {
8448
- const { status, record, responseFailure } = await this.fetchJson(`${base}${path}`, headers, connection.requestTimeoutMs);
8708
+ const { status, record, responseFailure } = await this.accountJsonGet(base, key, path, connection.requestTimeoutMs, share);
8449
8709
  if (record === void 0) {
8450
8710
  failures.push(`${path}: HTTP ${status}${responseFailure ? `;${responseFailure}` : ""}`);
8451
8711
  failed.push(responseFailure ? {
@@ -8479,7 +8739,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8479
8739
  const rawStart = subData?.currentPeriodStart;
8480
8740
  const periodStart = periodTimestampValue(rawStart);
8481
8741
  const validStart = periodStart > 0 && Number.isFinite(new Date(periodStart).getTime());
8482
- const totals = parseUsageTotals(await getJson(scopedPath("/alpha/usage/summary", validStart ? String(rawStart) : void 0)));
8742
+ const totals = parseUsageTotals(await getJson(scopedPath("/alpha/usage/summary", validStart ? String(rawStart) : void 0), false));
8483
8743
  if (totals !== void 0) report.usage = totals;
8484
8744
  const limits = parseCreditLimits(credits);
8485
8745
  if (limits !== void 0) report.credits = limits;
@@ -8528,22 +8788,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8528
8788
  if (parsed === void 0) return void 0;
8529
8789
  const windowLimits = isRecord(parsed.windowLimits) ? parsed.windowLimits : void 0;
8530
8790
  if (windowLimits === void 0) return void 0;
8531
- let reported = false;
8532
- let exceeded = false;
8533
- let resetAt = 0;
8534
- for (const raw of [windowLimits.fiveHour, windowLimits.weekly]) {
8535
- const window = isRecord(raw) ? parseWindowLimit(raw) : void 0;
8536
- if (window === void 0) continue;
8537
- reported = true;
8538
- if (!window.exceeded) continue;
8539
- exceeded = true;
8540
- if (window.resetAt > 0 && window.resetAt - Date.now() <= 2592e6) resetAt = Math.max(resetAt, window.resetAt);
8541
- }
8542
- if (!reported) return void 0;
8543
- return {
8544
- exceeded,
8545
- resetAt: exceeded ? resetAt : 0
8546
- };
8791
+ return windowVerdict([windowLimits.fiveHour, windowLimits.weekly].map((raw) => isRecord(raw) ? parseWindowLimit(raw) : void 0));
8547
8792
  } catch {
8548
8793
  return;
8549
8794
  }
@@ -8634,23 +8879,15 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8634
8879
  if (attachments === void 0) throw new LlmError("Command Code image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
8635
8880
  readImage = requestImages(attachments, options.signal, timing);
8636
8881
  }
8637
- const finishCredentials = timing.phase("credentials");
8638
- let apiKey;
8639
- try {
8640
- apiKey = await this.deps.resolveApiKey(connection, options.model);
8641
- } finally {
8642
- finishCredentials();
8643
- }
8644
- options.signal?.throwIfAborted();
8645
8882
  let maxTokens = Math.min(budget.max, learnedOutputLimit(connection.apiBase, options.model));
8883
+ const builtMaxTokens = maxTokens;
8646
8884
  const supported = KNOWN_EFFORTS[options.model];
8647
8885
  const effort = options.purpose === "session-title" && supported?.includes("off") ? "off" : options.reasoningEffort;
8648
8886
  const declared = effort !== void 0 && supported?.includes(effort) ? effort : void 0;
8649
8887
  const reasoningEffort = effort === "off" && declared === void 0 ? void 0 : declared;
8650
8888
  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");
8651
- let protocol = this.resolveProtocol(connection, apiKey, options.model, modelEntry);
8652
- let messagesPermissionRejection;
8653
- let requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true && protocol === "cli" ? options.model : void 0);
8889
+ const projected = projectSurfaceBudget(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0]);
8890
+ let requestOptions = projected.options;
8654
8891
  const buildBody = async (target) => {
8655
8892
  const finishBody = timing.phase("body");
8656
8893
  try {
@@ -8668,7 +8905,30 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8668
8905
  finishBody();
8669
8906
  }
8670
8907
  };
8671
- let body = await buildBody(protocol);
8908
+ const provisionalProtocol = connection.protocol === "cli" ? "cli" : this.routesToMessages(options.model, modelEntry) ? "messages" : "openai";
8909
+ const finishCredentials = timing.phase("credentials");
8910
+ const credentials = this.deps.resolveApiKey(connection, options.model);
8911
+ credentials.catch(() => void 0);
8912
+ const provisionalBody = connection.offloadSeenImagesForCache === true || projected.budgetMissing > 0 ? void 0 : buildBody(provisionalProtocol);
8913
+ provisionalBody?.catch(() => void 0);
8914
+ let apiKey;
8915
+ try {
8916
+ apiKey = await credentials;
8917
+ } finally {
8918
+ finishCredentials();
8919
+ }
8920
+ options.signal?.throwIfAborted();
8921
+ maxTokens = Math.min(budget.max, learnedOutputLimit(connection.apiBase, options.model));
8922
+ let protocol = this.resolveProtocol(connection, apiKey, options.model, modelEntry);
8923
+ const cacheMissing = protocol === "cli" && connection.offloadSeenImagesForCache === true ? seenImageOffloadCount(requestOptions.messages, options.model) : 0;
8924
+ const missing = Math.max(projected.budgetMissing, cacheMissing);
8925
+ if (missing > 0) throw imageOffloadRequired(missing, cacheMissing > projected.budgetMissing ? "cache" : "budget");
8926
+ let messagesPermissionRejection;
8927
+ let body = provisionalBody !== void 0 && protocol === provisionalProtocol ? await provisionalBody : await buildBody(protocol);
8928
+ if (maxTokens !== builtMaxTokens) {
8929
+ budget.max = Math.min(budget.max, maxTokens);
8930
+ if (!omitOutputTokens(connection.maxOutputTokens, protocol, options.maxTokens)) assignMaxTokens(body, protocol, maxTokens);
8931
+ }
8672
8932
  const serializedBodies = /* @__PURE__ */ new WeakMap();
8673
8933
  const trace = openStreamTrace(`${options.provider}/${options.model}`);
8674
8934
  const tried = /* @__PURE__ */ new Set();
@@ -8790,7 +9050,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8790
9050
  });
8791
9051
  trace.close();
8792
9052
  if (options.signal?.aborted) throw error;
8793
- throw failureWithRequestId(this.gradeHeaderTimeout(error, protocol, body, connection.requestTimeoutMs, options.sessionId), responseMetadata);
9053
+ throw failureWithRequestId(this.gradeHeaderTimeout(error, protocol, body, connection.requestTimeoutMs, options.sessionId, serializedBodies.get(body)), responseMetadata);
8794
9054
  }
8795
9055
  if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
8796
9056
  const { response, cleanup } = connected;
@@ -11888,18 +12148,24 @@ function apply(ctx, config) {
11888
12148
  const usageReports = async () => {
11889
12149
  const connection = options();
11890
12150
  const accounts = scopedPool(connection);
12151
+ const keyed = new Map((await accounts.describeAccounts()).map((account) => [account.slot.id, account.key]));
12152
+ const reports = await Promise.all(connection.accountSelection.slots.map(async (slot) => {
12153
+ const key = keyed.get(slot.id);
12154
+ if (key === void 0) return { failures: [] };
12155
+ try {
12156
+ const report = await adapter.getUsage(key, connection);
12157
+ if (report.credits !== void 0) accounts.noteWindowReport(key, windowVerdict([report.credits.fiveHour, report.credits.weekly]));
12158
+ return report;
12159
+ } catch (error) {
12160
+ return { failures: [readableErrorText(error)] };
12161
+ }
12162
+ }));
11891
12163
  const active = await accounts.activeAccount();
11892
12164
  const described = await accounts.describeAccounts();
11893
12165
  const byId = new Map(described.map((account) => [account.slot.id, account]));
11894
- return { accounts: await Promise.all(connection.accountSelection.slots.map(async (slot) => {
12166
+ return { accounts: connection.accountSelection.slots.map((slot, index) => {
11895
12167
  const account = byId.get(slot.id);
11896
- let report;
11897
- if (account === void 0) report = { failures: [] };
11898
- else try {
11899
- report = await adapter.getUsage(account.key, connection);
11900
- } catch (error) {
11901
- report = { failures: [readableErrorText(error)] };
11902
- }
12168
+ const report = reports[index] ?? { failures: [] };
11903
12169
  const state = account?.state;
11904
12170
  const usable = accountUsable(state);
11905
12171
  return {
@@ -11911,7 +12177,7 @@ function apply(ctx, config) {
11911
12177
  cooldownUntil: !usable && state?.kind === "cooldown" ? state.until : 0,
11912
12178
  report
11913
12179
  };
11914
- })) };
12180
+ }) };
11915
12181
  };
11916
12182
  const commandLocale = () => pickCommandLocale(current().lang);
11917
12183
  ctx.inject(["commands"], (commandCtx) => {