dsh-subscriptions 0.1.2 → 0.1.4

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.
Files changed (68) hide show
  1. package/README.md +92 -10
  2. package/lib/auth/rpc.d.ts +3 -1
  3. package/lib/auth/rpc.js +10 -1
  4. package/lib/client/ExternalUsageCards.d.ts +11 -1
  5. package/lib/client/ExternalUsageCards.js +18 -3
  6. package/lib/client/ProviderAccountManager.js +34 -41
  7. package/lib/client/ProviderModelEditor.js +5 -9
  8. package/lib/client/SpeedSelect.js +8 -7
  9. package/lib/client/SubscriptionUsageBadge.d.ts +19 -2
  10. package/lib/client/SubscriptionUsageBadge.js +65 -9
  11. package/lib/client/SubscriptionsSection.d.ts +22 -0
  12. package/lib/client/SubscriptionsSection.js +128 -38
  13. package/lib/client/UsageBadgeDisplaySetting.js +6 -10
  14. package/lib/client/UsageMeter.js +11 -6
  15. package/lib/client/host-contract.d.ts +1 -1
  16. package/lib/client/host-contract.js +22 -0
  17. package/lib/client/locales.d.ts +60 -0
  18. package/lib/client/locales.js +62 -2
  19. package/lib/client/provider-settings-styles.d.ts +3 -0
  20. package/lib/client/provider-settings-styles.js +206 -0
  21. package/lib/client/reset-action.d.ts +14 -0
  22. package/lib/client/reset-action.js +15 -0
  23. package/lib/client/reset-credits-view.d.ts +53 -0
  24. package/lib/client/reset-credits-view.js +103 -0
  25. package/lib/client/usage-badge-preferences.d.ts +7 -2
  26. package/lib/client/usage-badge-preferences.js +3 -2
  27. package/lib/client/usage-pace.d.ts +14 -1
  28. package/lib/client/usage-pace.js +14 -1
  29. package/lib/client.js +1036 -334
  30. package/lib/client.js.map +1 -1
  31. package/lib/index.d.ts +9 -0
  32. package/lib/index.js +1120 -602
  33. package/lib/providers/account-preferences.js +3 -2
  34. package/lib/providers/catalog-store.d.ts +3 -2
  35. package/lib/providers/catalog-store.js +57 -26
  36. package/lib/providers/claude.d.ts +22 -3
  37. package/lib/providers/claude.js +113 -15
  38. package/lib/providers/codex.d.ts +15 -2
  39. package/lib/providers/codex.js +72 -4
  40. package/lib/providers/common.d.ts +25 -0
  41. package/lib/providers/common.js +22 -18
  42. package/lib/providers/copilot.d.ts +2 -1
  43. package/lib/providers/copilot.js +10 -3
  44. package/lib/providers/grok.d.ts +2 -2
  45. package/lib/providers/grok.js +2 -2
  46. package/lib/providers/minimax-usage.d.ts +22 -1
  47. package/lib/providers/minimax-usage.js +37 -7
  48. package/lib/providers/pool-health.d.ts +2 -0
  49. package/lib/providers/pool-health.js +9 -0
  50. package/lib/providers/pool-usage.d.ts +13 -1
  51. package/lib/providers/pool-usage.js +39 -7
  52. package/lib/providers/pool.d.ts +7 -5
  53. package/lib/providers/pool.js +89 -12
  54. package/lib/providers/rate-limit.d.ts +0 -14
  55. package/lib/providers/rate-limit.js +0 -26
  56. package/lib/providers/replay.d.ts +4 -0
  57. package/lib/providers/replay.js +50 -0
  58. package/lib/providers/reset-redemption.d.ts +51 -0
  59. package/lib/providers/reset-redemption.js +102 -0
  60. package/lib/translate/anthropic.d.ts +21 -2
  61. package/lib/translate/anthropic.js +35 -11
  62. package/lib/translate/antigravity.js +13 -6
  63. package/lib/translate/chat-completions.js +1 -1
  64. package/lib/translate/resolved.d.ts +21 -0
  65. package/lib/translate/resolved.js +63 -3
  66. package/lib/translate/responses.js +8 -6
  67. package/lib/translate/sse.js +8 -0
  68. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -5,7 +5,7 @@ import http, { createServer } from "node:http";
5
5
  import { createHash, randomBytes, randomUUID } from "node:crypto";
6
6
  import { getDefaultAutoSelectFamilyAttemptTimeout, setDefaultAutoSelectFamilyAttemptTimeout } from "node:net";
7
7
  import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
8
- import { basename, dirname, join } from "node:path";
8
+ import { basename, dirname, join, resolve } from "node:path";
9
9
  import { execFileSync } from "node:child_process";
10
10
  import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
11
  import { homedir } from "node:os";
@@ -97,12 +97,12 @@ async function listen(handler, spec) {
97
97
  for (const host of hosts) {
98
98
  const server = createServer(handler);
99
99
  try {
100
- await new Promise((resolve$1, reject) => {
100
+ await new Promise((resolve$2, reject) => {
101
101
  const onError = (error) => reject(error);
102
102
  server.once("error", onError);
103
103
  server.listen(port, host, () => {
104
104
  server.removeListener("error", onError);
105
- resolve$1();
105
+ resolve$2();
106
106
  });
107
107
  });
108
108
  const address = server.address();
@@ -169,8 +169,8 @@ var OAuthFlowManager = class {
169
169
  const timeoutMs = spec.timeoutMs ?? DEFAULT_FLOW_TIMEOUT_MS;
170
170
  let resolveCode;
171
171
  let rejectCode;
172
- const codePromise = new Promise((resolve$1, reject) => {
173
- resolveCode = resolve$1;
172
+ const codePromise = new Promise((resolve$2, reject) => {
173
+ resolveCode = resolve$2;
174
174
  rejectCode = reject;
175
175
  });
176
176
  let settled = false;
@@ -280,14 +280,14 @@ const DEFAULT_INTERVAL_SEC = 5;
280
280
  const DEFAULT_EXPIRES_IN_SEC = 900;
281
281
  /** Sleep for `ms`, rejecting early when the signal aborts. */
282
282
  function sleep$1(ms, signal) {
283
- return new Promise((resolve$1, reject) => {
283
+ return new Promise((resolve$2, reject) => {
284
284
  if (signal.aborted) {
285
285
  reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("aborted"));
286
286
  return;
287
287
  }
288
288
  const timer = setTimeout(() => {
289
289
  signal.removeEventListener("abort", onAbort);
290
- resolve$1();
290
+ resolve$2();
291
291
  }, ms);
292
292
  timer.unref();
293
293
  const onAbort = () => {
@@ -349,8 +349,8 @@ var DeviceFlowManager = class {
349
349
  const controller = new AbortController();
350
350
  let resolveToken;
351
351
  let rejectToken;
352
- const tokenPromise = new Promise((resolve$1, reject) => {
353
- resolveToken = resolve$1;
352
+ const tokenPromise = new Promise((resolve$2, reject) => {
353
+ resolveToken = resolve$2;
354
354
  rejectToken = reject;
355
355
  });
356
356
  tokenPromise.catch(() => void 0);
@@ -602,6 +602,81 @@ async function refreshClaudeSynced(session, doRefresh) {
602
602
  return next;
603
603
  }
604
604
 
605
+ //#endregion
606
+ //#region src/providers/reset-redemption.ts
607
+ /**
608
+ * Manual-only redemption with single-use tickets. An ambiguous submission parks
609
+ * the account to prevent an immediate blind retry. This block is process-local,
610
+ * not a durable rate limit: restarting the host clears it. Re-check credits and
611
+ * limits in Codex before retrying; a lost success response may have spent a
612
+ * credit already, and another submission can spend another credit.
613
+ * Rationale: [manual reset note](../../.agents/notes/implemented/provider/2026-10-04-codex-manual-reset.md).
614
+ */
615
+ var ResetRedemption = class {
616
+ tickets = /* @__PURE__ */ new Map();
617
+ busy = /* @__PURE__ */ new Set();
618
+ uncertain = /* @__PURE__ */ new Set();
619
+ constructor(read, consume, invalidate, canonical) {
620
+ this.read = read;
621
+ this.consume = consume;
622
+ this.invalidate = invalidate;
623
+ this.canonical = canonical;
624
+ }
625
+ eligible(usage) {
626
+ const weekly = usage.windows?.find((w) => w.kind === "weekly" && w.scope === void 0);
627
+ if (!weekly || !Number.isFinite(weekly.usedPercent) || weekly.usedPercent < 80 || weekly.resetsAt === void 0 || weekly.resetsAt <= Date.now()) throw new Error("Reset blocked: fresh weekly usage must be at least 80%.");
628
+ if (usage.resetCreditsError) throw new Error("Reset credits could not be verified.");
629
+ return weekly.usedPercent;
630
+ }
631
+ async prepare(account, signal) {
632
+ const who = await this.canonical(account, signal);
633
+ if (this.busy.has(who) || this.uncertain.has(who)) throw new Error("Reset blocked: an operation is pending or its outcome is uncertain. Check Codex before retrying.");
634
+ const usage = await this.read(account, signal);
635
+ if (this.busy.has(who) || this.uncertain.has(who)) throw new Error("Reset blocked: an operation is pending or its outcome is uncertain. Check Codex before retrying.");
636
+ const weeklyUsedPercent = this.eligible(usage);
637
+ const credit = usage.resetCredits?.filter((c) => c.id && (c.expiresAt === void 0 || c.expiresAt > Date.now())).sort((a, b) => (a.expiresAt ?? Infinity) - (b.expiresAt ?? Infinity))[0];
638
+ if (!credit?.id) throw new Error("No available reset credit.");
639
+ for (const [key, value] of this.tickets) if (value.account === who || value.expiresAt <= Date.now()) this.tickets.delete(key);
640
+ const ticket = randomUUID();
641
+ const expiresAt = Date.now() + 6e4;
642
+ this.tickets.set(ticket, {
643
+ account: who,
644
+ creditId: credit.id,
645
+ expiresAt
646
+ });
647
+ return {
648
+ ticket,
649
+ expiresAt,
650
+ weeklyUsedPercent,
651
+ ...credit.expiresAt === void 0 ? {} : { creditExpiresAt: credit.expiresAt }
652
+ };
653
+ }
654
+ async redeem(account, ticket, signal) {
655
+ const who = await this.canonical(account, signal);
656
+ const pending = this.tickets.get(ticket);
657
+ this.tickets.delete(ticket);
658
+ if (!pending || pending.account !== who || pending.expiresAt <= Date.now()) throw new Error("Confirmation expired. Start again.");
659
+ if (this.busy.has(who) || this.uncertain.has(who)) throw new Error("Reset blocked: operation pending or uncertain.");
660
+ this.busy.add(who);
661
+ try {
662
+ const usage = await this.read(account, signal);
663
+ if (pending.expiresAt <= Date.now()) throw new Error("Confirmation expired. Start again.");
664
+ this.eligible(usage);
665
+ if (!usage.resetCredits?.some((c) => c.id === pending.creditId && (c.expiresAt === void 0 || c.expiresAt > Date.now()))) throw new Error("The selected reset is no longer available.");
666
+ this.uncertain.add(who);
667
+ try {
668
+ await this.consume(account, pending.creditId, randomUUID(), signal);
669
+ } catch {
670
+ throw new Error("Reset outcome could not be confirmed. Do not retry or restart to bypass this block; verify credits and limits in Codex first.");
671
+ }
672
+ this.uncertain.delete(who);
673
+ } finally {
674
+ this.invalidate(who);
675
+ this.busy.delete(who);
676
+ }
677
+ }
678
+ };
679
+
605
680
  //#endregion
606
681
  //#region src/auth/jwt.ts
607
682
  /** Minimal JWT payload decoding for claims extraction (no signature verification). */
@@ -905,7 +980,7 @@ async function writeStore(store, path) {
905
980
  * A chain is dropped once nothing is queued behind it, so the map holds an
906
981
  * entry only while writes are in flight.
907
982
  */
908
- const writeChains = /* @__PURE__ */ new Map();
983
+ const writeChains$1 = /* @__PURE__ */ new Map();
909
984
  /**
910
985
  * Run one read-modify-write of a store path after every write already queued
911
986
  * for it. Callers join the chain synchronously, so call order is write order.
@@ -913,14 +988,14 @@ const writeChains = /* @__PURE__ */ new Map();
913
988
  * @param action - the read-modify-write to run.
914
989
  * @returns whatever `action` returns.
915
990
  */
916
- async function serialize(path, action) {
917
- const next = (writeChains.get(path) ?? Promise.resolve()).then(action, action);
991
+ async function serialize$1(path, action) {
992
+ const next = (writeChains$1.get(path) ?? Promise.resolve()).then(action, action);
918
993
  const tail = next.then(() => void 0, () => void 0);
919
- writeChains.set(path, tail);
994
+ writeChains$1.set(path, tail);
920
995
  try {
921
996
  return await next;
922
997
  } finally {
923
- if (writeChains.get(path) === tail) writeChains.delete(path);
998
+ if (writeChains$1.get(path) === tail) writeChains$1.delete(path);
924
999
  }
925
1000
  }
926
1001
  /**
@@ -968,7 +1043,7 @@ async function getAccountSession(provider, account, path = authFilePath()) {
968
1043
  */
969
1044
  async function saveAccountSession(provider, account, session, path = authFilePath()) {
970
1045
  assertSessionShape(provider, account, session);
971
- return serialize(path, async () => {
1046
+ return serialize$1(path, async () => {
972
1047
  const store = await loadStore(path);
973
1048
  const entry = store[provider];
974
1049
  if (entry !== void 0) {
@@ -994,7 +1069,7 @@ async function saveAccountSession(provider, account, session, path = authFilePat
994
1069
  * @param path - store file path; defaults to {@link authFilePath}.
995
1070
  */
996
1071
  async function deleteAccountSession(provider, account, path = authFilePath()) {
997
- return serialize(path, async () => {
1072
+ return serialize$1(path, async () => {
998
1073
  const store = await loadStore(path);
999
1074
  const entry = store[provider];
1000
1075
  if (entry === void 0) return;
@@ -1018,7 +1093,7 @@ async function deleteAccountSession(provider, account, path = authFilePath()) {
1018
1093
  * @param path - store file path; defaults to {@link authFilePath}.
1019
1094
  */
1020
1095
  async function setDefaultAccount(provider, account, path = authFilePath()) {
1021
- return serialize(path, async () => {
1096
+ return serialize$1(path, async () => {
1022
1097
  const store = await loadStore(path);
1023
1098
  const entry = store[provider];
1024
1099
  if (entry !== void 0) account = resolveAccount(entry, account);
@@ -1033,7 +1108,37 @@ async function setDefaultAccount(provider, account, path = authFilePath()) {
1033
1108
 
1034
1109
  //#endregion
1035
1110
  //#region src/providers/minimax-usage.ts
1036
- /** MiniMax subscription quota, not the balance API for sk-api-* inference keys. */
1111
+ /**
1112
+ * Used percentage from the legacy counts, read as the official CLI does when no
1113
+ * explicit percentage exists: the count is the quota remaining. Undefined
1114
+ * unless the total is a positive finite number, so a zero total never yields a
1115
+ * bar.
1116
+ */
1117
+ function usedFromCounts(total, count) {
1118
+ return typeof total === "number" && Number.isFinite(total) && total > 0 && typeof count === "number" ? (1 - count / total) * 100 : void 0;
1119
+ }
1120
+ /**
1121
+ * Read MiniMax subscription quota (M Plan and Token Plan), not the balance API
1122
+ * that `sk-api-*` pay-as-you-go keys use.
1123
+ *
1124
+ * Each `model_remains` row can yield an interval window and a weekly window.
1125
+ * A window needs a finite share: the explicit `*_remaining_percent` (the weekly
1126
+ * one scaled by `weekly_boost_permille`), else the legacy counts. Time-based
1127
+ * plans report zero counts beside the percentage for standard models, so the
1128
+ * percentage stands on its own and a zero total does not hide the window.
1129
+ * Windows that cannot be a finite bar are omitted rather than approximated:
1130
+ * status 3 (unlimited, or no quota in the plan), a weekly pool boosted past
1131
+ * 100% remaining, a percentage outside 0-100, and a row with neither a
1132
+ * percentage nor positive counts. Start and end times are kept only when both
1133
+ * are valid millisecond bounds.
1134
+ * @param key - subscription key for the region; never copied into a result or error.
1135
+ * @param region - `global` or `cn`, which selects the fixed quota host.
1136
+ * @param http - fetch implementation (injectable for tests).
1137
+ * @param signal - caller cancellation from the RPC transport.
1138
+ * @returns the finite windows in the order the API lists them.
1139
+ * @throws when the key is pay-as-you-go, the endpoint or its business status
1140
+ * fails, the body has no `model_remains`, or no window is a finite bar.
1141
+ */
1037
1142
  async function fetchMiniMaxUsage(key, region, http$1 = fetch, signal) {
1038
1143
  if (key.startsWith("sk-api-")) throw new Error("MiniMax requires a subscription key, not a pay-as-you-go key");
1039
1144
  const response = await http$1(`https://www.minimax.${region === "cn" ? "cn" : "io"}/v1/token_plan/remains`, {
@@ -1066,10 +1171,7 @@ async function fetchMiniMaxUsage(key, region, http$1 = fetch, signal) {
1066
1171
  const remaining = row[`${prefix}_remaining_percent`];
1067
1172
  const boost = prefix === "current_weekly" ? row.weekly_boost_permille ?? 1e3 : 1e3;
1068
1173
  if (typeof boost !== "number" || !Number.isFinite(boost) || boost <= 0) continue;
1069
- const total = row[`${prefix}_total_count`];
1070
- const count = row[`${prefix}_usage_count`];
1071
- if (typeof total !== "number" || !Number.isFinite(total) || total <= 0) continue;
1072
- const usedPercent = typeof remaining === "number" ? 100 - remaining * boost / 1e3 : typeof total === "number" && total > 0 && typeof count === "number" ? (1 - count / total) * 100 : void 0;
1174
+ const usedPercent = typeof remaining === "number" ? 100 - remaining * boost / 1e3 : usedFromCounts(row[`${prefix}_total_count`], row[`${prefix}_usage_count`]);
1073
1175
  if (usedPercent === void 0 || !Number.isFinite(usedPercent) || usedPercent < 0 || usedPercent > 100) continue;
1074
1176
  const start = row[startKey];
1075
1177
  const end = row[endKey];
@@ -1340,6 +1442,8 @@ const SUBSCRIPTIONS_AUTH_ENDPOINTS = [
1340
1442
  "cursorSetSettings",
1341
1443
  "image",
1342
1444
  "video",
1445
+ "prepareReset",
1446
+ "consumeReset",
1343
1447
  "speed",
1344
1448
  "setSpeed",
1345
1449
  "modelDefaults",
@@ -1619,6 +1723,13 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
1619
1723
  if (payload === null || typeof payload !== "object") throw new BadRequest("payload must be an object");
1620
1724
  await cursor.setSettings(payload.settings);
1621
1725
  return ok({ ok: true });
1726
+ case "prepareReset":
1727
+ if (!controller.prepareReset) throw new BadRequest("Reset redemption unavailable");
1728
+ return ok(await controller.prepareReset(readString(payload, "account"), signal));
1729
+ case "consumeReset":
1730
+ if (!controller.consumeReset) throw new BadRequest("Reset redemption unavailable");
1731
+ await controller.consumeReset(readString(payload, "account"), readString(payload, "ticket"), signal);
1732
+ return ok({ ok: true });
1622
1733
  case "image": return ok(await controller.readImage(readImageRef(payload), signal));
1623
1734
  case "video": return ok(await controller.readVideo(readVideoName(payload), signal));
1624
1735
  case "speed": return ok(await speed.speed(readSessionId(payload)));
@@ -1689,10 +1800,10 @@ function tokenExpiry(token, now) {
1689
1800
  }
1690
1801
  function abortableDelay$2(ms, signal) {
1691
1802
  if (signal.aborted) return Promise.reject(signal.reason);
1692
- return new Promise((resolve$1, reject) => {
1803
+ return new Promise((resolve$2, reject) => {
1693
1804
  const timer = setTimeout(() => {
1694
1805
  signal.removeEventListener("abort", aborted);
1695
- resolve$1();
1806
+ resolve$2();
1696
1807
  }, ms);
1697
1808
  function aborted() {
1698
1809
  clearTimeout(timer);
@@ -2180,7 +2291,7 @@ async function nativeFetch(url, init = {}) {
2180
2291
  const client = target.protocol === "https:" ? https : http;
2181
2292
  const method = (init.method ?? "GET").toUpperCase();
2182
2293
  const body = init.body === void 0 ? void 0 : isBytes(init.body) ? Buffer.from(init.body) : String(init.body);
2183
- const request = (u$1, redirect) => new Promise((resolve$1, reject) => {
2294
+ const request = (u$1, redirect) => new Promise((resolve$2, reject) => {
2184
2295
  const req = client.request(u$1, {
2185
2296
  method,
2186
2297
  headers: sanitizeHeaders(init.headers),
@@ -2197,10 +2308,10 @@ async function nativeFetch(url, init = {}) {
2197
2308
  }
2198
2309
  const location = res.headers.location;
2199
2310
  if ((redirect ?? "follow") === "follow" && location && status >= 300 && status < 400) {
2200
- resolve$1({ redirect: new URL(location, u$1) });
2311
+ resolve$2({ redirect: new URL(location, u$1) });
2201
2312
  return;
2202
2313
  }
2203
- resolve$1({ response: makeResponse(status, res, data) });
2314
+ resolve$2({ response: makeResponse(status, res, data) });
2204
2315
  });
2205
2316
  });
2206
2317
  req.on("error", reject);
@@ -2358,12 +2469,12 @@ function isSuccessfulAgentResponse(status, contentType) {
2358
2469
  function abortableDelay$1(ms, signal) {
2359
2470
  if (signal.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Cursor retry aborted"));
2360
2471
  if (ms === 0) return Promise.resolve();
2361
- return new Promise((resolve$1, reject) => {
2472
+ return new Promise((resolve$2, reject) => {
2362
2473
  const timer = setTimeout(done, ms);
2363
2474
  timer.unref?.();
2364
2475
  function done() {
2365
2476
  signal.removeEventListener("abort", aborted);
2366
- resolve$1();
2477
+ resolve$2();
2367
2478
  }
2368
2479
  function aborted() {
2369
2480
  clearTimeout(timer);
@@ -3143,8 +3254,8 @@ var ConnectFrameReader = class {
3143
3254
  if (this.error !== void 0) throw this.error;
3144
3255
  return;
3145
3256
  }
3146
- return new Promise((resolve$1, reject) => this.waiters.push({
3147
- resolve: resolve$1,
3257
+ return new Promise((resolve$2, reject) => this.waiters.push({
3258
+ resolve: resolve$2,
3148
3259
  reject
3149
3260
  }));
3150
3261
  }
@@ -3182,8 +3293,8 @@ var AgentRun = class {
3182
3293
  authorization: `Bearer ${this.accessToken}`,
3183
3294
  ...AGENT_HEADERS
3184
3295
  };
3185
- this.responsePromise = new Promise((resolve$1, reject) => {
3186
- this.resolveResponse = resolve$1;
3296
+ this.responsePromise = new Promise((resolve$2, reject) => {
3297
+ this.resolveResponse = resolve$2;
3187
3298
  this.rejectResponse = reject;
3188
3299
  });
3189
3300
  this.responsePromise.catch(() => {});
@@ -3212,10 +3323,10 @@ var AgentRun = class {
3212
3323
  });
3213
3324
  }
3214
3325
  #resolveResponse(status) {
3215
- const resolve$1 = this.resolveResponse;
3326
+ const resolve$2 = this.resolveResponse;
3216
3327
  this.resolveResponse = void 0;
3217
3328
  this.rejectResponse = void 0;
3218
- resolve$1?.(status);
3329
+ resolve$2?.(status);
3219
3330
  }
3220
3331
  #rejectResponse(error) {
3221
3332
  const reject = this.rejectResponse;
@@ -3226,10 +3337,10 @@ var AgentRun = class {
3226
3337
  waitForResponse(timeoutMs) {
3227
3338
  if (this.responsePromise === void 0) throw new Error("Cursor AgentRun has not started");
3228
3339
  if (timeoutMs === void 0) return this.responsePromise;
3229
- return new Promise((resolve$1, reject) => {
3340
+ return new Promise((resolve$2, reject) => {
3230
3341
  const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Cursor HTTP response timeout")), timeoutMs);
3231
3342
  timer.unref?.();
3232
- this.responsePromise.then(resolve$1, reject).finally(() => clearTimeout(timer));
3343
+ this.responsePromise.then(resolve$2, reject).finally(() => clearTimeout(timer));
3233
3344
  });
3234
3345
  }
3235
3346
  write(payload) {
@@ -4771,31 +4882,6 @@ function earliestReset(...candidates) {
4771
4882
  function waitFromReset(instant, now) {
4772
4883
  return Math.max(MIN_WAIT_MS, instant - now + RESET_GRACE_MS);
4773
4884
  }
4774
- /** Header names worth showing when a 429 disclosed no reset this code recognizes. */
4775
- const DIAGNOSTIC_HEADER = /rate-?limit|retry|reset|^x-codex-/i;
4776
- /**
4777
- * Render the rate-limit-shaped headers and the head of the body of a 429 whose
4778
- * reset instant nothing parsed. Emitted through the adapter's `onWarn`, this is
4779
- * how an unrecognized provider field gets named from live traffic instead of
4780
- * being guessed at.
4781
- *
4782
- * It is also where the per-bucket rollover snapshots land by design — no reader
4783
- * parks a turn on one, because on a 429 they cannot say which bucket refused —
4784
- * so the operator still sees what the provider disclosed.
4785
- * @param response - the failed response.
4786
- * @param body - the complete response body.
4787
- * @returns a one-line diagnostic.
4788
- */
4789
- function rateLimitDiagnostics(response, body) {
4790
- const headers = [];
4791
- response.headers.forEach((value, key) => {
4792
- if (DIAGNOSTIC_HEADER.test(key)) headers.push(`${key}: ${value}`);
4793
- });
4794
- headers.sort();
4795
- const rendered = headers.length > 0 ? headers.join("; ") : "(none)";
4796
- const head = body.slice(0, 200);
4797
- return `429 disclosed no reset time; headers [${rendered}]; body ${head.length > 0 ? head : "(empty)"}`;
4798
- }
4799
4885
  /**
4800
4886
  * The retry shape every subscription route starts from: Claude Code's own SDK
4801
4887
  * numbers — ten retries after the first attempt, exponential backoff from 1s
@@ -4896,6 +4982,17 @@ function validateModels(models, label) {
4896
4982
  });
4897
4983
  }
4898
4984
  /**
4985
+ * Provider failures can echo credentials or customer input in any format.
4986
+ * Shape-based filtering cannot make arbitrary response text safe to display.
4987
+ * Here, safe means this diagnostic carries no provider-controlled free text,
4988
+ * not that secrets cannot exist elsewhere in the system. Keep raw responses
4989
+ * internal for classification and reset parsing; display only local summaries.
4990
+ * This deliberately loses model, parameter and plan details useful to support.
4991
+ */
4992
+ function providerDiagnostic(summary, hasBody) {
4993
+ return hasBody ? `${summary}: [provider response body omitted]` : summary;
4994
+ }
4995
+ /**
4899
4996
  * Build an LlmError from a non-2xx provider response, mapping the status to a
4900
4997
  * stable code and, for a rate-limited request, the disclosed reset instant to
4901
4998
  * the `providerRetryAfterMs` the retry plugin waits out.
@@ -4916,7 +5013,6 @@ async function httpLlmError(response, label, options = {}) {
4916
5013
  body = await response.text();
4917
5014
  } catch {}
4918
5015
  const shown = body.slice(0, 500);
4919
- const message = shown.length > 0 ? `${label} error (HTTP ${String(response.status)}): ${shown}` : `${label} error (HTTP ${String(response.status)})`;
4920
5016
  let code;
4921
5017
  if (response.status === 401 || response.status === 403) code = "AUTH";
4922
5018
  else if (response.status === 429) code = "RATE_LIMIT";
@@ -4925,10 +5021,11 @@ async function httpLlmError(response, label, options = {}) {
4925
5021
  else if (response.status === 408 || response.status === 504) code = "TIMEOUT";
4926
5022
  else if (response.status >= 500) code = "SERVER";
4927
5023
  else code = `HTTP_${String(response.status)}`;
5024
+ const message = providerDiagnostic(`${label} error (HTTP ${String(response.status)}, ${code})`, body.length > 0);
4928
5025
  const now = Date.now();
4929
5026
  const rateLimited = response.status === 429;
4930
5027
  const reset = rateLimited ? options.rateLimitReset?.(response, body, now) ?? googleQuotaReset(body, now) ?? retryAfterInstant(response, now) : retryAfterInstant(response, now);
4931
- if (reset === void 0 && rateLimited) options.onWarn?.(`${label}: ${rateLimitDiagnostics(response, body)}`);
5028
+ if (reset === void 0 && rateLimited) options.onWarn?.(providerDiagnostic(`${label}: 429 disclosed no reset time; [provider response headers omitted]`, body.length > 0));
4932
5029
  return new LlmError(message, code, {
4933
5030
  status: response.status,
4934
5031
  ...reset === void 0 ? {} : { providerRetryAfterMs: waitFromReset(reset, now) }
@@ -5052,19 +5149,17 @@ var OAuthEndpointError = class extends Error {
5052
5149
  */
5053
5150
  async function oauthEndpointError(response, label) {
5054
5151
  let oauthCode;
5055
- let detail = "";
5152
+ let body = "";
5056
5153
  try {
5057
- const parsed = await response.json();
5154
+ body = await response.text();
5155
+ const parsed = JSON.parse(body);
5058
5156
  if (typeof parsed.error === "string") oauthCode = parsed.error;
5059
5157
  else if (typeof parsed.error === "object" && parsed.error !== null) {
5060
5158
  const nested = parsed.error;
5061
5159
  if (typeof nested.code === "string" && nested.code.length > 0) oauthCode = nested.code;
5062
- if (typeof nested.message === "string") detail = nested.message;
5063
5160
  }
5064
- if (typeof parsed.error_description === "string") detail = parsed.error_description;
5065
- if (detail.length === 0) detail = oauthCode ?? "";
5066
5161
  } catch {}
5067
- return new OAuthEndpointError(detail.length > 0 ? `${label} token endpoint error (HTTP ${String(response.status)}): ${detail}` : `${label} token endpoint error (HTTP ${String(response.status)})`, response.status, oauthCode, parseRetryAfterMs(response));
5162
+ return new OAuthEndpointError(providerDiagnostic(`${label} token endpoint error (HTTP ${String(response.status)})`, body.length > 0), response.status, oauthCode, parseRetryAfterMs(response));
5068
5163
  }
5069
5164
  /**
5070
5165
  * Per-provider session freshness: loads the stored session, refreshes
@@ -5143,9 +5238,9 @@ const DISCOVERY_TIMEOUT_MS = 1e4;
5143
5238
  */
5144
5239
  function withTimeout(work, timeoutMs) {
5145
5240
  const signal = AbortSignal.timeout(timeoutMs);
5146
- const aborted = new Promise((resolve$1) => {
5147
- if (signal.aborted) resolve$1(void 0);
5148
- else signal.addEventListener("abort", () => resolve$1(void 0), { once: true });
5241
+ const aborted = new Promise((resolve$2) => {
5242
+ if (signal.aborted) resolve$2(void 0);
5243
+ else signal.addEventListener("abort", () => resolve$2(void 0), { once: true });
5149
5244
  });
5150
5245
  return Promise.race([work(signal).then((value) => signal.aborted ? void 0 : value, (error) => {
5151
5246
  if (signal.aborted) return void 0;
@@ -5504,429 +5599,127 @@ var AccountTokenManager = class {
5504
5599
  };
5505
5600
 
5506
5601
  //#endregion
5507
- //#region src/providers/catalog-store.ts
5602
+ //#region src/compat.ts
5603
+ /** Brand a string as a tool-call id: alpha's `ToolCallId`, rc.2's `CallId`. */
5604
+ const ToolCallId$1 = (() => {
5605
+ const exports = llm;
5606
+ return exports["ToolCallId"] ?? exports["CallId"];
5607
+ })();
5608
+
5609
+ //#endregion
5610
+ //#region src/translate/sse.ts
5508
5611
  /**
5509
- * Absolute path of the catalog store file.
5510
- * @returns `dshHomePath('plugins', 'subscriptions', 'models.json')`.
5612
+ * Decode an SSE byte stream into events.
5613
+ * @param stream - raw response bytes; reads may split anywhere, including mid-UTF-8 sequence.
5614
+ * @param onActivity - called on every received chunk and comment line; drives the idle watchdog.
5615
+ * @returns events in arrival order.
5511
5616
  */
5512
- function modelsFilePath() {
5513
- return dshHomePath("plugins", "subscriptions", "models.json");
5514
- }
5515
- /** The per-account section of the file, or undefined when absent or malformed. */
5516
- function accountSection(store, provider) {
5517
- const accounts = store.accounts;
5518
- if (typeof accounts !== "object" || accounts === null || Array.isArray(accounts)) return void 0;
5519
- const section = accounts[provider];
5520
- if (typeof section !== "object" || section === null || Array.isArray(section)) return void 0;
5521
- return section;
5522
- }
5523
- /** Validate one persisted reasoning block, or undefined when malformed. */
5524
- function sanitizeReasoning(value) {
5525
- if (typeof value !== "object" || value === null) return void 0;
5526
- const raw = value;
5527
- if (!Array.isArray(raw.efforts) || raw.efforts.length === 0) return void 0;
5528
- const seen = /* @__PURE__ */ new Set();
5529
- const efforts = [];
5530
- for (const entry of raw.efforts) {
5531
- if (typeof entry !== "object" || entry === null) return void 0;
5532
- const effort = entry;
5533
- if (typeof effort.id !== "string" || effort.id.length === 0 || typeof effort.name !== "string" || effort.name.length === 0 || effort.description !== void 0 && typeof effort.description !== "string" || seen.has(effort.id)) return void 0;
5534
- seen.add(effort.id);
5535
- efforts.push({
5536
- id: ReasoningEffortId(effort.id),
5537
- name: effort.name,
5538
- ...effort.description === void 0 ? {} : { description: effort.description }
5539
- });
5617
+ async function* parseSse(stream, onActivity) {
5618
+ const reader = stream.getReader();
5619
+ const decoder = new TextDecoder();
5620
+ let pending = "";
5621
+ let dataLines = [];
5622
+ let eventName;
5623
+ try {
5624
+ while (true) {
5625
+ const { done, value } = await reader.read();
5626
+ if (done) return;
5627
+ onActivity?.();
5628
+ pending += decoder.decode(value, { stream: true });
5629
+ let newline = pending.indexOf("\n");
5630
+ while (newline >= 0) {
5631
+ let line = pending.slice(0, newline);
5632
+ pending = pending.slice(newline + 1);
5633
+ newline = pending.indexOf("\n");
5634
+ if (line.endsWith("\r")) line = line.slice(0, -1);
5635
+ if (line.length === 0) {
5636
+ if (dataLines.length > 0) yield {
5637
+ data: dataLines.join("\n"),
5638
+ ...eventName === void 0 ? {} : { event: eventName }
5639
+ };
5640
+ dataLines = [];
5641
+ eventName = void 0;
5642
+ } else if (line.startsWith(":")) onActivity?.();
5643
+ else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
5644
+ else if (line.startsWith("event:")) eventName = line.slice(6).replace(/^ /, "");
5645
+ }
5646
+ }
5647
+ } finally {
5648
+ try {
5649
+ await reader.cancel();
5650
+ } catch {}
5651
+ reader.releaseLock();
5540
5652
  }
5541
- if (raw.defaultEffort !== void 0 && (typeof raw.defaultEffort !== "string" || !seen.has(raw.defaultEffort))) return void 0;
5542
- return {
5543
- efforts,
5544
- ...raw.defaultEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(raw.defaultEffort) }
5545
- };
5546
- }
5547
- /** Validate one persisted model, or undefined when malformed. */
5548
- function sanitizeModel(value) {
5549
- if (typeof value !== "object" || value === null) return void 0;
5550
- const raw = value;
5551
- if (typeof raw.id !== "string" || raw.id.length === 0 || typeof raw.name !== "string" || raw.name.length === 0 || raw.description !== void 0 && typeof raw.description !== "string" || raw.contextWindow !== void 0 && (typeof raw.contextWindow !== "number" || !Number.isInteger(raw.contextWindow) || raw.contextWindow <= 0) || raw.maxContextWindow !== void 0 && (typeof raw.maxContextWindow !== "number" || !Number.isSafeInteger(raw.maxContextWindow) || raw.maxContextWindow <= 0) || raw.maxOutputTokens !== void 0 && (typeof raw.maxOutputTokens !== "number" || !Number.isSafeInteger(raw.maxOutputTokens) || raw.maxOutputTokens <= 0) || raw.priority !== void 0 && (typeof raw.priority !== "number" || !Number.isFinite(raw.priority))) return void 0;
5552
- const reasoning = raw.reasoning === void 0 ? void 0 : sanitizeReasoning(raw.reasoning);
5553
- if (raw.reasoning !== void 0 && reasoning === void 0) return void 0;
5554
- const thinkingType = raw.thinkingType;
5555
- if (thinkingType !== void 0 && thinkingType !== "enabled" && thinkingType !== "adaptive") return void 0;
5556
- const fastTier = raw.fastTier;
5557
- if (fastTier !== void 0 && typeof fastTier !== "boolean") return void 0;
5558
- const copilotWire = raw.copilotWire;
5559
- if (copilotWire !== void 0 && copilotWire !== "chat-completions" && copilotWire !== "responses") return;
5560
- const copilotResponses = raw.copilotResponses;
5561
- if (copilotResponses !== void 0 && typeof copilotResponses !== "boolean") return void 0;
5562
- const inputModalities = raw.inputModalities;
5563
- if (inputModalities !== void 0 && (!Array.isArray(inputModalities) || inputModalities.length === 0 || inputModalities.some((modality) => modality !== "text" && modality !== "image"))) return void 0;
5564
- return {
5565
- id: raw.id,
5566
- name: raw.name,
5567
- ...raw.description === void 0 ? {} : { description: raw.description },
5568
- ...raw.contextWindow === void 0 ? {} : { contextWindow: raw.contextWindow },
5569
- ...raw.maxContextWindow === void 0 ? {} : { maxContextWindow: raw.maxContextWindow },
5570
- ...raw.maxOutputTokens === void 0 ? {} : { maxOutputTokens: raw.maxOutputTokens },
5571
- ...raw.priority === void 0 ? {} : { priority: raw.priority },
5572
- ...reasoning === void 0 ? {} : { reasoning },
5573
- ...thinkingType === void 0 ? {} : { thinkingType },
5574
- ...fastTier === void 0 ? {} : { fastTier },
5575
- ...copilotWire === void 0 ? {} : { copilotWire },
5576
- ...copilotResponses === void 0 ? {} : { copilotResponses },
5577
- ...inputModalities === void 0 ? {} : { inputModalities: [...inputModalities] }
5578
- };
5579
5653
  }
5654
+
5655
+ //#endregion
5656
+ //#region src/translate/anthropic.ts
5580
5657
  /**
5581
- * Validate one persisted snapshot. Strict: any malformed field drops the
5582
- * whole snapshot rather than repairing it — the next successful discovery
5583
- * rewrites the entry anyway.
5584
- * @param value - the raw per-provider file entry.
5585
- * @returns the validated snapshot, or undefined when unusable.
5658
+ * The Claude Code identity block. The subscription endpoint rejects requests
5659
+ * that do not present as Claude Code, so this block is REQUIRED as the first
5660
+ * system entry on every request.
5586
5661
  */
5587
- function sanitizeSnapshot(value) {
5588
- if (typeof value !== "object" || value === null) return void 0;
5589
- const raw = value;
5590
- if (typeof raw.at !== "number" || !Number.isFinite(raw.at)) return void 0;
5591
- if (!Array.isArray(raw.models) || raw.models.length === 0) return void 0;
5592
- const seen = /* @__PURE__ */ new Set();
5593
- const models = [];
5594
- for (const entry of raw.models) {
5595
- const model = sanitizeModel(entry);
5596
- if (model === void 0 || seen.has(model.id)) return void 0;
5597
- seen.add(model.id);
5598
- models.push(model);
5662
+ const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
5663
+ /** Tags wrapping a mid-conversation system message where it sits in the history. */
5664
+ const SYSTEM_REMINDER_OPEN = "<system-reminder>";
5665
+ const SYSTEM_REMINDER_CLOSE = "</system-reminder>";
5666
+ /**
5667
+ * How far apart consecutive message breakpoints sit, in content blocks.
5668
+ *
5669
+ * A breakpoint looks back at most 20 blocks for an entry an earlier request
5670
+ * wrote, so marks must stay closer than that: one agentic turn can append a
5671
+ * dozen tool_use/tool_result blocks at once, and a single trailing mark would
5672
+ * silently fall out of range and rebuild the whole prefix.
5673
+ */
5674
+ const CACHE_BLOCK_STRIDE = 15;
5675
+ /**
5676
+ * Message breakpoints per request. Anthropic allows four in total and the
5677
+ * last `system` block takes the fourth, so three are left for the history —
5678
+ * enough to tolerate a turn appending roughly {@link CACHE_BLOCK_STRIDE} × 3
5679
+ * blocks before a read is lost.
5680
+ */
5681
+ const MESSAGE_CACHE_BREAKPOINTS = 3;
5682
+ /** The API default; a marker at this TTL carries no `ttl` field at all. */
5683
+ const DEFAULT_PROMPT_CACHE_TTL = "5m";
5684
+ /**
5685
+ * Build one `cache_control` marker.
5686
+ *
5687
+ * Five minutes is spelled by omitting `ttl`, so a request that does not opt in
5688
+ * stays byte-identical to what earlier releases sent.
5689
+ * @param ttl - cache lifetime; defaults to five minutes.
5690
+ * @returns a fresh marker object (callers attach it to one block each).
5691
+ */
5692
+ function cacheControl(ttl = DEFAULT_PROMPT_CACHE_TTL) {
5693
+ return ttl === "1h" ? {
5694
+ type: "ephemeral",
5695
+ ttl: "1h"
5696
+ } : { type: "ephemeral" };
5697
+ }
5698
+ /** Preserve native image blocks, retaining the existing text-only wire shape. */
5699
+ function toolResultContent(block) {
5700
+ if (!block.content.some((part) => part.type === "image" && "dataBase64" in part)) return block.content.map((part) => part.type === "text" ? part.text : "").join("");
5701
+ const content = [];
5702
+ for (const part of block.content) {
5703
+ if (part.type === "text" && part.text.length > 0) content.push({
5704
+ type: "text",
5705
+ text: part.text
5706
+ });
5707
+ if (part.type === "image" && "dataBase64" in part) content.push({
5708
+ type: "image",
5709
+ source: {
5710
+ type: "base64",
5711
+ media_type: part.mediaType,
5712
+ data: part.dataBase64
5713
+ }
5714
+ });
5599
5715
  }
5600
- return {
5601
- at: raw.at,
5602
- models
5603
- };
5716
+ return content;
5604
5717
  }
5605
- /** Read the whole file; missing or unparsable reads as an empty cache. */
5606
- async function readCatalogFile(path) {
5607
- let text;
5718
+ /** Parse a tool call's raw JSON arguments into Anthropic's object-shaped `input`. */
5719
+ function parseToolInput(raw) {
5608
5720
  try {
5609
- text = await readFile(path, "utf8");
5610
- } catch {
5611
- return {};
5612
- }
5613
- try {
5614
- const parsed = JSON.parse(text);
5615
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
5616
- return parsed;
5617
- } catch {
5618
- return {};
5619
- }
5620
- }
5621
- /** Persist the whole file atomically (tmp file + rename). */
5622
- async function writeCatalogFile(store, path) {
5623
- await mkdir(dirname(path), { recursive: true });
5624
- const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
5625
- try {
5626
- await writeFile(tmp, JSON.stringify(store, null, 2));
5627
- await rename(tmp, path);
5628
- } catch (error) {
5629
- await rm(tmp, { force: true });
5630
- throw error;
5631
- }
5632
- }
5633
- /**
5634
- * Build the durable half of one provider's catalog cache over the shared
5635
- * models.json file (concurrent writers are last-writer-wins, acceptable for
5636
- * a cache).
5637
- * @param provider - the provider route keying the file entry.
5638
- * @param path - store file path; defaults to {@link modelsFilePath}.
5639
- * @returns the persistence hooks for {@link ModelCatalogCache}.
5640
- */
5641
- function catalogStore(provider, path = modelsFilePath()) {
5642
- return {
5643
- async load() {
5644
- return sanitizeSnapshot((await readCatalogFile(path))[provider]);
5645
- },
5646
- async save(snapshot) {
5647
- const store = await readCatalogFile(path);
5648
- store[provider] = snapshot;
5649
- await writeCatalogFile(store, path);
5650
- },
5651
- async clear() {
5652
- const store = await readCatalogFile(path);
5653
- if (store[provider] === void 0) return;
5654
- delete store[provider];
5655
- await writeCatalogFile(store, path);
5656
- }
5657
- };
5658
- }
5659
- /**
5660
- * The durable half of one NON-default account's catalog cache. The provider
5661
- * entry belongs to the default account; every other account keeps its own
5662
- * snapshot under `accounts[provider][account]`, so a working secondary login
5663
- * survives restarts and network failures even when the default login is dead.
5664
- * @param provider - the provider route.
5665
- * @param account - the canonical account key.
5666
- * @param path - store file path; defaults to {@link modelsFilePath}.
5667
- * @returns the persistence hooks for {@link ModelCatalogCache}.
5668
- */
5669
- function accountCatalogStore(provider, account, path = modelsFilePath()) {
5670
- return {
5671
- async load() {
5672
- const section = accountSection(await readCatalogFile(path), provider);
5673
- return section !== void 0 && Object.hasOwn(section, account) ? sanitizeSnapshot(section[account]) : void 0;
5674
- },
5675
- async save(snapshot) {
5676
- const store = await readCatalogFile(path);
5677
- const accounts = typeof store.accounts === "object" && store.accounts !== null && !Array.isArray(store.accounts) ? store.accounts : {};
5678
- const section = {
5679
- ...accountSection(store, provider) ?? {},
5680
- [account]: snapshot
5681
- };
5682
- store.accounts = {
5683
- ...accounts,
5684
- [provider]: section
5685
- };
5686
- await writeCatalogFile(store, path);
5687
- },
5688
- async clear() {
5689
- const store = await readCatalogFile(path);
5690
- const section = accountSection(store, provider);
5691
- if (section === void 0 || !Object.hasOwn(section, account)) return;
5692
- const rest = { ...section };
5693
- delete rest[account];
5694
- store.accounts = {
5695
- ...store.accounts,
5696
- [provider]: rest
5697
- };
5698
- await writeCatalogFile(store, path);
5699
- }
5700
- };
5701
- }
5702
-
5703
- //#endregion
5704
- //#region src/translate/resolved.ts
5705
- /**
5706
- * Wires with text-only tool outputs receive images in a following user turn.
5707
- * Defer that turn until all consecutive user messages have been processed:
5708
- * parallel tool results can arrive in separate harness messages, and a user
5709
- * image message must not interrupt their tool-call/output pairing.
5710
- */
5711
- function withToolResultImages(messages) {
5712
- const out = [];
5713
- let images = [];
5714
- const flush = () => {
5715
- if (images.length > 0) out.push({
5716
- role: "user",
5717
- content: images
5718
- });
5719
- images = [];
5720
- };
5721
- for (const message of messages) {
5722
- if (message.role === "assistant") flush();
5723
- out.push(message);
5724
- if (message.role === "tool") {
5725
- const parts = message.content.filter((part) => part.type === "image" && "dataBase64" in part);
5726
- if (parts.length > 0) images.push({
5727
- type: "text",
5728
- text: `Images from tool result ${String(message.toolCallId)}:`
5729
- }, ...parts);
5730
- }
5731
- for (const block of message.content) {
5732
- if (block.type !== "tool-result") continue;
5733
- const parts = block.content.filter((part) => part.type === "image" && "dataBase64" in part);
5734
- if (parts.length > 0) images.push({
5735
- type: "text",
5736
- text: `Images from tool result ${String(block.toolCallId)}:`
5737
- }, ...parts);
5738
- }
5739
- }
5740
- flush();
5741
- return out;
5742
- }
5743
- /**
5744
- * The request projection for one oversized image, or undefined when it fits.
5745
- * Hosts before DSH 0.1.7 read a pixel budget (`maxPixels`), later ones the
5746
- * target edges (`width`/`height`); each validates only its own fields, so
5747
- * one projection carries both.
5748
- */
5749
- function imageRequestTarget(ref, limit) {
5750
- const longEdge = Math.max(ref.width, ref.height);
5751
- if (!(longEdge > limit.maxEdge)) return void 0;
5752
- const short = (edge) => Math.max(1, Math.floor(edge * limit.maxEdge / longEdge));
5753
- const width = ref.width >= ref.height ? limit.maxEdge : short(ref.width);
5754
- const height = ref.width >= ref.height ? short(ref.height) : limit.maxEdge;
5755
- return {
5756
- width,
5757
- height,
5758
- maxPixels: width * height,
5759
- maxBytes: limit.maxBytes
5760
- };
5761
- }
5762
- /**
5763
- * Resolve every ImageBlock's attachment reference to inline base64 bytes.
5764
- * Messages without images pass through unchanged. A request carrying an image
5765
- * with no attachment service available fails loudly rather than silently
5766
- * dropping the image.
5767
- * @param messages - the request's conversation messages.
5768
- * @param attachments - the deployment's attachment service, when mounted.
5769
- * @param signal - cancellation for the storage reads.
5770
- * @param limit - the route's outgoing image cap; oversized images are sent
5771
- * downscaled while the stored attachment and history stay untouched.
5772
- * @returns the same messages with image blocks resolved for the translators.
5773
- */
5774
- async function resolveImages(messages, attachments, signal, limit) {
5775
- if (messages.some((message) => message.content.some((block) => block.type === "image")) && attachments === void 0) throw new LlmError("dsh-subscriptions: the request carries an image but no attachments service is mounted; image input requires the harness attachment store", "UNSUPPORTED");
5776
- const readForRequest = async (ref) => {
5777
- if (attachments === void 0) throw new Error("attachments service is unavailable");
5778
- const target = limit === void 0 ? void 0 : imageRequestTarget(ref, limit);
5779
- if (target !== void 0) try {
5780
- const version = await attachments.readImageRequest(ref, target, signal);
5781
- return {
5782
- data: version.data,
5783
- mediaType: version.mediaType,
5784
- ref: version.attachment
5785
- };
5786
- } catch (error) {
5787
- if (signal?.aborted) throw error;
5788
- }
5789
- const stored = await attachments.readImage(ref, signal);
5790
- return {
5791
- data: stored.data,
5792
- mediaType: stored.ref.mediaType,
5793
- ref: stored.ref
5794
- };
5795
- };
5796
- const resolveBlock = async (block) => {
5797
- if (block.type !== "image") return [block];
5798
- const { data, mediaType: sentType, ref } = await readForRequest(block.attachment);
5799
- const { attachmentId, mediaType, bytes, width, height, name: name$1 } = ref;
5800
- return [{
5801
- type: "image",
5802
- mediaType: sentType,
5803
- dataBase64: Buffer.from(data).toString("base64")
5804
- }, {
5805
- type: "text",
5806
- text: `Image reference (for image_generate.referenceImages): ${JSON.stringify({
5807
- attachmentId,
5808
- mediaType,
5809
- bytes,
5810
- width,
5811
- height,
5812
- ...name$1 === void 0 ? {} : { name: name$1 }
5813
- })}`
5814
- }];
5815
- };
5816
- return Promise.all(messages.map(async (message) => ({
5817
- role: message.role,
5818
- ...message.source === void 0 ? {} : { source: message.source },
5819
- ...message.role === "tool" ? {
5820
- toolCallId: message.toolCallId,
5821
- isError: message.isError
5822
- } : {},
5823
- content: (await Promise.all(message.content.map(resolveBlock))).flat()
5824
- })));
5825
- }
5826
-
5827
- //#endregion
5828
- //#region src/compat.ts
5829
- /** Brand a string as a tool-call id: alpha's `ToolCallId`, rc.2's `CallId`. */
5830
- const ToolCallId$1 = (() => {
5831
- const exports = llm;
5832
- return exports["ToolCallId"] ?? exports["CallId"];
5833
- })();
5834
-
5835
- //#endregion
5836
- //#region src/translate/sse.ts
5837
- /**
5838
- * Decode an SSE byte stream into events.
5839
- * @param stream - raw response bytes; reads may split anywhere, including mid-UTF-8 sequence.
5840
- * @param onActivity - called on every received chunk and comment line; drives the idle watchdog.
5841
- * @returns events in arrival order.
5842
- */
5843
- async function* parseSse(stream, onActivity) {
5844
- const reader = stream.getReader();
5845
- const decoder = new TextDecoder();
5846
- let pending = "";
5847
- let dataLines = [];
5848
- let eventName;
5849
- try {
5850
- while (true) {
5851
- const { done, value } = await reader.read();
5852
- if (done) return;
5853
- onActivity?.();
5854
- pending += decoder.decode(value, { stream: true });
5855
- let newline = pending.indexOf("\n");
5856
- while (newline >= 0) {
5857
- let line = pending.slice(0, newline);
5858
- pending = pending.slice(newline + 1);
5859
- newline = pending.indexOf("\n");
5860
- if (line.endsWith("\r")) line = line.slice(0, -1);
5861
- if (line.length === 0) {
5862
- if (dataLines.length > 0) yield {
5863
- data: dataLines.join("\n"),
5864
- ...eventName === void 0 ? {} : { event: eventName }
5865
- };
5866
- dataLines = [];
5867
- eventName = void 0;
5868
- } else if (line.startsWith(":")) onActivity?.();
5869
- else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
5870
- else if (line.startsWith("event:")) eventName = line.slice(6).replace(/^ /, "");
5871
- }
5872
- }
5873
- } finally {
5874
- reader.releaseLock();
5875
- }
5876
- }
5877
-
5878
- //#endregion
5879
- //#region src/translate/anthropic.ts
5880
- /**
5881
- * The Claude Code identity block. The subscription endpoint rejects requests
5882
- * that do not present as Claude Code, so this block is REQUIRED as the first
5883
- * system entry on every request.
5884
- */
5885
- const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
5886
- /** Tags wrapping a mid-conversation system message where it sits in the history. */
5887
- const SYSTEM_REMINDER_OPEN = "<system-reminder>";
5888
- const SYSTEM_REMINDER_CLOSE = "</system-reminder>";
5889
- /**
5890
- * How far apart consecutive message breakpoints sit, in content blocks.
5891
- *
5892
- * A breakpoint looks back at most 20 blocks for an entry an earlier request
5893
- * wrote, so marks must stay closer than that: one agentic turn can append a
5894
- * dozen tool_use/tool_result blocks at once, and a single trailing mark would
5895
- * silently fall out of range and rebuild the whole prefix.
5896
- */
5897
- const CACHE_BLOCK_STRIDE = 15;
5898
- /**
5899
- * Message breakpoints per request. Anthropic allows four in total and the
5900
- * last `system` block takes the fourth, so three are left for the history —
5901
- * enough to tolerate a turn appending roughly {@link CACHE_BLOCK_STRIDE} × 3
5902
- * blocks before a read is lost.
5903
- */
5904
- const MESSAGE_CACHE_BREAKPOINTS = 3;
5905
- /** Preserve native image blocks, retaining the existing text-only wire shape. */
5906
- function toolResultContent(block) {
5907
- if (!block.content.some((part) => part.type === "image" && "dataBase64" in part)) return block.content.map((part) => part.type === "text" ? part.text : "").join("");
5908
- const content = [];
5909
- for (const part of block.content) {
5910
- if (part.type === "text" && part.text.length > 0) content.push({
5911
- type: "text",
5912
- text: part.text
5913
- });
5914
- if (part.type === "image" && "dataBase64" in part) content.push({
5915
- type: "image",
5916
- source: {
5917
- type: "base64",
5918
- media_type: part.mediaType,
5919
- data: part.dataBase64
5920
- }
5921
- });
5922
- }
5923
- return content;
5924
- }
5925
- /** Parse a tool call's raw JSON arguments into Anthropic's object-shaped `input`. */
5926
- function parseToolInput(raw) {
5927
- try {
5928
- const parsed = JSON.parse(raw);
5929
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
5721
+ const parsed = JSON.parse(raw);
5722
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
5930
5723
  return {};
5931
5724
  } catch {
5932
5725
  return {};
@@ -6068,13 +5861,14 @@ function toAnthropicMessages(messages) {
6068
5861
  * Marks are counted across the flattened block sequence, not per message,
6069
5862
  * because the lookback window Anthropic walks counts blocks the same way.
6070
5863
  * @param messages - assembled Anthropic messages, marked in place.
5864
+ * @param ttl - cache lifetime for every mark; defaults to five minutes.
6071
5865
  */
6072
- function markMessageCache(messages) {
5866
+ function markMessageCache(messages, ttl) {
6073
5867
  const blocks = messages.flatMap((message) => message.content);
6074
5868
  for (let mark = 0; mark < MESSAGE_CACHE_BREAKPOINTS; mark++) {
6075
5869
  const at = blocks.length - 1 - mark * CACHE_BLOCK_STRIDE;
6076
5870
  if (at < 0) return;
6077
- blocks[at].cache_control = { type: "ephemeral" };
5871
+ blocks[at].cache_control = cacheControl(ttl);
6078
5872
  }
6079
5873
  }
6080
5874
  /**
@@ -6083,9 +5877,10 @@ function markMessageCache(messages) {
6083
5877
  * @param system - explicit system prompt, when set.
6084
5878
  * @param messages - conversation messages; the system-role text preceding the
6085
5879
  * conversation is appended, and a later one is left to {@link toAnthropicMessages}.
5880
+ * @param ttl - cache lifetime for the tools+system mark; defaults to five minutes.
6086
5881
  * @returns the system content blocks.
6087
5882
  */
6088
- function toAnthropicSystem(system, messages) {
5883
+ function toAnthropicSystem(system, messages, ttl) {
6089
5884
  const blocks = [{
6090
5885
  type: "text",
6091
5886
  text: CLAUDE_CODE_IDENTITY
@@ -6099,7 +5894,7 @@ function toAnthropicSystem(system, messages) {
6099
5894
  type: "text",
6100
5895
  text: block.text
6101
5896
  });
6102
- blocks[blocks.length - 1].cache_control = { type: "ephemeral" };
5897
+ blocks[blocks.length - 1].cache_control = cacheControl(ttl);
6103
5898
  return blocks;
6104
5899
  }
6105
5900
  /**
@@ -6147,10 +5942,11 @@ function closeBlock$2(block) {
6147
5942
  function anthropicFailure(error) {
6148
5943
  const type = error?.type ?? "unknown_error";
6149
5944
  const message = error?.message ?? `Anthropic reported ${type}`;
6150
- if (type === "invalid_request_error" && /prompt is too long/i.test(message)) return new LlmError(message, CONTEXT_WINDOW_EXCEEDED_CODE);
6151
- if (type === "rate_limit_error") return new LlmError(message, "RATE_LIMIT");
6152
- if (type === "authentication_error") return new LlmError(message, "AUTH");
6153
- return new LlmError(message, "SERVER");
5945
+ let classification = "SERVER";
5946
+ if (type === "invalid_request_error" && /prompt is too long/i.test(message)) classification = CONTEXT_WINDOW_EXCEEDED_CODE;
5947
+ else if (type === "rate_limit_error") classification = "RATE_LIMIT";
5948
+ else if (type === "authentication_error") classification = "AUTH";
5949
+ return new LlmError(`Anthropic failure (${classification}): [provider response body omitted]`, classification);
6154
5950
  }
6155
5951
  /**
6156
5952
  * Push-model Anthropic SSE translator: feed each parsed event object to
@@ -6339,27 +6135,426 @@ var AnthropicStreamTranslator = class {
6339
6135
  case "error": throw anthropicFailure(event.error);
6340
6136
  default: return chunks;
6341
6137
  }
6342
- }
6343
- };
6138
+ }
6139
+ };
6140
+ /**
6141
+ * Consume an Anthropic SSE byte stream and yield harness StreamChunks.
6142
+ * @param stream - raw response body.
6143
+ * @param onActivity - transport-activity callback for the idle watchdog.
6144
+ * @returns the chunk stream; throws when the stream ends before `message_stop`.
6145
+ */
6146
+ async function* streamAnthropic(stream, onActivity) {
6147
+ const translator = new AnthropicStreamTranslator();
6148
+ for await (const sseEvent of parseSse(stream, onActivity)) {
6149
+ let event;
6150
+ try {
6151
+ event = JSON.parse(sseEvent.data);
6152
+ } catch {
6153
+ throw new LlmError("malformed SSE payload (MALFORMED_RESPONSE): [provider response body omitted]", "MALFORMED_RESPONSE");
6154
+ }
6155
+ yield* translator.push(event);
6156
+ if (translator.terminated) return;
6157
+ }
6158
+ throw new LlmError("Anthropic SSE stream ended before message_stop", "STREAM_CLOSED");
6159
+ }
6160
+
6161
+ //#endregion
6162
+ //#region src/providers/catalog-store.ts
6163
+ /**
6164
+ * Absolute path of the catalog store file.
6165
+ * @returns `dshHomePath('plugins', 'subscriptions', 'models.json')`.
6166
+ */
6167
+ function modelsFilePath() {
6168
+ return dshHomePath("plugins", "subscriptions", "models.json");
6169
+ }
6170
+ /** The per-account section of the file, or undefined when absent or malformed. */
6171
+ function accountSection(store, provider) {
6172
+ const accounts = store.accounts;
6173
+ if (typeof accounts !== "object" || accounts === null || Array.isArray(accounts)) return void 0;
6174
+ const section = accounts[provider];
6175
+ if (typeof section !== "object" || section === null || Array.isArray(section)) return void 0;
6176
+ return section;
6177
+ }
6178
+ /** Validate one persisted reasoning block, or undefined when malformed. */
6179
+ function sanitizeReasoning(value) {
6180
+ if (typeof value !== "object" || value === null) return void 0;
6181
+ const raw = value;
6182
+ if (!Array.isArray(raw.efforts) || raw.efforts.length === 0) return void 0;
6183
+ const seen = /* @__PURE__ */ new Set();
6184
+ const efforts = [];
6185
+ for (const entry of raw.efforts) {
6186
+ if (typeof entry !== "object" || entry === null) return void 0;
6187
+ const effort = entry;
6188
+ if (typeof effort.id !== "string" || effort.id.length === 0 || typeof effort.name !== "string" || effort.name.length === 0 || effort.description !== void 0 && typeof effort.description !== "string" || seen.has(effort.id)) return void 0;
6189
+ seen.add(effort.id);
6190
+ efforts.push({
6191
+ id: ReasoningEffortId(effort.id),
6192
+ name: effort.name,
6193
+ ...effort.description === void 0 ? {} : { description: effort.description }
6194
+ });
6195
+ }
6196
+ if (raw.defaultEffort !== void 0 && (typeof raw.defaultEffort !== "string" || !seen.has(raw.defaultEffort))) return void 0;
6197
+ return {
6198
+ efforts,
6199
+ ...raw.defaultEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(raw.defaultEffort) }
6200
+ };
6201
+ }
6202
+ /** Validate one persisted model, or undefined when malformed. */
6203
+ function sanitizeModel(value) {
6204
+ if (typeof value !== "object" || value === null) return void 0;
6205
+ const raw = value;
6206
+ if (typeof raw.id !== "string" || raw.id.length === 0 || typeof raw.name !== "string" || raw.name.length === 0 || raw.description !== void 0 && typeof raw.description !== "string" || raw.contextWindow !== void 0 && (typeof raw.contextWindow !== "number" || !Number.isInteger(raw.contextWindow) || raw.contextWindow <= 0) || raw.maxContextWindow !== void 0 && (typeof raw.maxContextWindow !== "number" || !Number.isSafeInteger(raw.maxContextWindow) || raw.maxContextWindow <= 0) || raw.maxOutputTokens !== void 0 && (typeof raw.maxOutputTokens !== "number" || !Number.isSafeInteger(raw.maxOutputTokens) || raw.maxOutputTokens <= 0) || raw.priority !== void 0 && (typeof raw.priority !== "number" || !Number.isFinite(raw.priority))) return void 0;
6207
+ const reasoning = raw.reasoning === void 0 ? void 0 : sanitizeReasoning(raw.reasoning);
6208
+ if (raw.reasoning !== void 0 && reasoning === void 0) return void 0;
6209
+ const thinkingType = raw.thinkingType;
6210
+ if (thinkingType !== void 0 && thinkingType !== "enabled" && thinkingType !== "adaptive") return void 0;
6211
+ const fastTier = raw.fastTier;
6212
+ if (fastTier !== void 0 && typeof fastTier !== "boolean") return void 0;
6213
+ const copilotWire = raw.copilotWire;
6214
+ if (copilotWire !== void 0 && copilotWire !== "chat-completions" && copilotWire !== "responses") return;
6215
+ const copilotResponses = raw.copilotResponses;
6216
+ if (copilotResponses !== void 0 && typeof copilotResponses !== "boolean") return void 0;
6217
+ const inputModalities = raw.inputModalities;
6218
+ if (inputModalities !== void 0 && (!Array.isArray(inputModalities) || inputModalities.length === 0 || inputModalities.some((modality) => modality !== "text" && modality !== "image"))) return void 0;
6219
+ return {
6220
+ id: raw.id,
6221
+ name: raw.name,
6222
+ ...raw.description === void 0 ? {} : { description: raw.description },
6223
+ ...raw.contextWindow === void 0 ? {} : { contextWindow: raw.contextWindow },
6224
+ ...raw.maxContextWindow === void 0 ? {} : { maxContextWindow: raw.maxContextWindow },
6225
+ ...raw.maxOutputTokens === void 0 ? {} : { maxOutputTokens: raw.maxOutputTokens },
6226
+ ...raw.priority === void 0 ? {} : { priority: raw.priority },
6227
+ ...reasoning === void 0 ? {} : { reasoning },
6228
+ ...thinkingType === void 0 ? {} : { thinkingType },
6229
+ ...fastTier === void 0 ? {} : { fastTier },
6230
+ ...copilotWire === void 0 ? {} : { copilotWire },
6231
+ ...copilotResponses === void 0 ? {} : { copilotResponses },
6232
+ ...inputModalities === void 0 ? {} : { inputModalities: [...inputModalities] }
6233
+ };
6234
+ }
6235
+ /**
6236
+ * Validate one persisted snapshot. Strict: any malformed field drops the
6237
+ * whole snapshot rather than repairing it — the next successful discovery
6238
+ * rewrites the entry anyway.
6239
+ * @param value - the raw per-provider file entry.
6240
+ * @returns the validated snapshot, or undefined when unusable.
6241
+ */
6242
+ function sanitizeSnapshot(value) {
6243
+ if (typeof value !== "object" || value === null) return void 0;
6244
+ const raw = value;
6245
+ if (typeof raw.at !== "number" || !Number.isFinite(raw.at)) return void 0;
6246
+ if (!Array.isArray(raw.models) || raw.models.length === 0) return void 0;
6247
+ const seen = /* @__PURE__ */ new Set();
6248
+ const models = [];
6249
+ for (const entry of raw.models) {
6250
+ const model = sanitizeModel(entry);
6251
+ if (model === void 0 || seen.has(model.id)) return void 0;
6252
+ seen.add(model.id);
6253
+ models.push(model);
6254
+ }
6255
+ return {
6256
+ at: raw.at,
6257
+ models
6258
+ };
6259
+ }
6260
+ /** Read the whole file; missing or unparsable reads as an empty cache. */
6261
+ async function readCatalogFile(path) {
6262
+ let text;
6263
+ try {
6264
+ text = await readFile(path, "utf8");
6265
+ } catch {
6266
+ return {};
6267
+ }
6268
+ try {
6269
+ const parsed = JSON.parse(text);
6270
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
6271
+ return parsed;
6272
+ } catch {
6273
+ return {};
6274
+ }
6275
+ }
6276
+ /** Persist the whole file atomically (tmp file + rename). */
6277
+ async function writeCatalogFile(store, path) {
6278
+ await mkdir(dirname(path), { recursive: true });
6279
+ const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
6280
+ try {
6281
+ await writeFile(tmp, JSON.stringify(store, null, 2));
6282
+ await rename(tmp, path);
6283
+ } catch (error) {
6284
+ await rm(tmp, { force: true });
6285
+ throw error;
6286
+ }
6287
+ }
6288
+ /**
6289
+ * Like the auth store, mutations of one resolved path share an in-process
6290
+ * write chain in call order. Each reads after the preceding mutation settles;
6291
+ * a failure rejects its caller without blocking later mutations. Idle chains
6292
+ * are removed. Separate processes (or filesystem aliases of a path) are not
6293
+ * locked and remain last-writer-wins.
6294
+ */
6295
+ const writeChains = /* @__PURE__ */ new Map();
6296
+ async function serialize(path, action) {
6297
+ const key = resolve(path);
6298
+ const next = (writeChains.get(key) ?? Promise.resolve()).then(action, action);
6299
+ const tail = next.then(() => void 0, () => void 0);
6300
+ writeChains.set(key, tail);
6301
+ try {
6302
+ return await next;
6303
+ } finally {
6304
+ if (writeChains.get(key) === tail) writeChains.delete(key);
6305
+ }
6306
+ }
6307
+ /**
6308
+ * Build the durable half of one provider's catalog cache over the shared
6309
+ * models.json file. Saves and clears serialize with all accounts at this path
6310
+ * in this process; readers see a complete file before or after an atomic rename.
6311
+ * @param provider - the provider route keying the file entry.
6312
+ * @param path - store file path; defaults to {@link modelsFilePath}.
6313
+ * @returns the persistence hooks for {@link ModelCatalogCache}.
6314
+ */
6315
+ function catalogStore(provider, path = modelsFilePath()) {
6316
+ return {
6317
+ async load() {
6318
+ return sanitizeSnapshot((await readCatalogFile(path))[provider]);
6319
+ },
6320
+ async save(snapshot) {
6321
+ return serialize(path, async () => {
6322
+ const store = await readCatalogFile(path);
6323
+ store[provider] = snapshot;
6324
+ await writeCatalogFile(store, path);
6325
+ });
6326
+ },
6327
+ async clear() {
6328
+ return serialize(path, async () => {
6329
+ const store = await readCatalogFile(path);
6330
+ if (store[provider] === void 0) return;
6331
+ delete store[provider];
6332
+ await writeCatalogFile(store, path);
6333
+ });
6334
+ }
6335
+ };
6336
+ }
6337
+ /**
6338
+ * The durable half of one NON-default account's catalog cache. The provider
6339
+ * entry belongs to the default account; every other account keeps its own
6340
+ * snapshot under `accounts[provider][account]`, so a working secondary login
6341
+ * survives restarts and network failures even when the default login is dead.
6342
+ * Saves and clears share {@link catalogStore}'s in-process write chain.
6343
+ * @param provider - the provider route.
6344
+ * @param account - the canonical account key.
6345
+ * @param path - store file path; defaults to {@link modelsFilePath}.
6346
+ * @returns the persistence hooks for {@link ModelCatalogCache}.
6347
+ */
6348
+ function accountCatalogStore(provider, account, path = modelsFilePath()) {
6349
+ return {
6350
+ async load() {
6351
+ const section = accountSection(await readCatalogFile(path), provider);
6352
+ return section !== void 0 && Object.hasOwn(section, account) ? sanitizeSnapshot(section[account]) : void 0;
6353
+ },
6354
+ async save(snapshot) {
6355
+ return serialize(path, async () => {
6356
+ const store = await readCatalogFile(path);
6357
+ const accounts = typeof store.accounts === "object" && store.accounts !== null && !Array.isArray(store.accounts) ? store.accounts : {};
6358
+ const section = {
6359
+ ...accountSection(store, provider) ?? {},
6360
+ [account]: snapshot
6361
+ };
6362
+ store.accounts = {
6363
+ ...accounts,
6364
+ [provider]: section
6365
+ };
6366
+ await writeCatalogFile(store, path);
6367
+ });
6368
+ },
6369
+ async clear() {
6370
+ return serialize(path, async () => {
6371
+ const store = await readCatalogFile(path);
6372
+ const section = accountSection(store, provider);
6373
+ if (section === void 0 || !Object.hasOwn(section, account)) return;
6374
+ const rest = { ...section };
6375
+ delete rest[account];
6376
+ store.accounts = {
6377
+ ...store.accounts,
6378
+ [provider]: rest
6379
+ };
6380
+ await writeCatalogFile(store, path);
6381
+ });
6382
+ }
6383
+ };
6384
+ }
6385
+
6386
+ //#endregion
6387
+ //#region src/translate/resolved.ts
6344
6388
  /**
6345
- * Consume an Anthropic SSE byte stream and yield harness StreamChunks.
6346
- * @param stream - raw response body.
6347
- * @param onActivity - transport-activity callback for the idle watchdog.
6348
- * @returns the chunk stream; throws when the stream ends before `message_stop`.
6389
+ * Wires with text-only tool outputs receive images in a following user turn.
6390
+ * Defer that turn until all consecutive user messages have been processed:
6391
+ * parallel tool results can arrive in separate harness messages, and a user
6392
+ * image message must not interrupt their tool-call/output pairing.
6349
6393
  */
6350
- async function* streamAnthropic(stream, onActivity) {
6351
- const translator = new AnthropicStreamTranslator();
6352
- for await (const sseEvent of parseSse(stream, onActivity)) {
6353
- let event;
6354
- try {
6355
- event = JSON.parse(sseEvent.data);
6356
- } catch {
6357
- throw new LlmError(`malformed SSE payload: ${sseEvent.data.slice(0, 120)}`, "MALFORMED_RESPONSE");
6394
+ function withToolResultImages(messages) {
6395
+ const out = [];
6396
+ let images = [];
6397
+ const flush = () => {
6398
+ if (images.length > 0) out.push({
6399
+ role: "user",
6400
+ content: images
6401
+ });
6402
+ images = [];
6403
+ };
6404
+ for (const message of messages) {
6405
+ if (message.role === "assistant") flush();
6406
+ out.push(message);
6407
+ if (message.role === "tool") {
6408
+ const parts = message.content.filter((part) => part.type === "image" && "dataBase64" in part);
6409
+ if (parts.length > 0) images.push({
6410
+ type: "text",
6411
+ text: `Images from tool result ${String(message.toolCallId)}:`
6412
+ }, ...parts);
6413
+ }
6414
+ for (const block of message.content) {
6415
+ if (block.type !== "tool-result") continue;
6416
+ const parts = block.content.filter((part) => part.type === "image" && "dataBase64" in part);
6417
+ if (parts.length > 0) images.push({
6418
+ type: "text",
6419
+ text: `Images from tool result ${String(block.toolCallId)}:`
6420
+ }, ...parts);
6358
6421
  }
6359
- yield* translator.push(event);
6360
- if (translator.terminated) return;
6361
6422
  }
6362
- throw new LlmError("Anthropic SSE stream ended before message_stop", "STREAM_CLOSED");
6423
+ flush();
6424
+ return out;
6425
+ }
6426
+ /**
6427
+ * Failure code a host's image-offload executor answers by replacing the oldest
6428
+ * retained images with text and retrying (`IMAGE_OFFLOAD_REQUIRED_CODE` in
6429
+ * dsh-llm 0.2+). Spelled out so the build links against older hosts too.
6430
+ */
6431
+ const IMAGE_OFFLOAD_REQUIRED = "IMAGE_OFFLOAD_REQUIRED";
6432
+ /**
6433
+ * Whether the installed host records image offloads. Hosts that predate the
6434
+ * executor never mark images `offloaded`, so asking them to would fail every
6435
+ * later request instead of shrinking it.
6436
+ */
6437
+ function hostSupportsImageOffload() {
6438
+ const exports = llm;
6439
+ return exports["IMAGE_OFFLOAD_REQUIRED_CODE"] === IMAGE_OFFLOAD_REQUIRED && typeof exports["offloadedImageText"] === "function";
6440
+ }
6441
+ /**
6442
+ * How many of the oldest images still sent inline must be offloaded before
6443
+ * the resolved request fits `maxBase64Bytes` of image data; zero when it fits.
6444
+ * Images are counted depth-first in request order, skipping assistant
6445
+ * messages, the same order the host's executor selects occurrences in.
6446
+ * @param messages - the resolved request, after {@link resolveImages}.
6447
+ * @param maxBase64Bytes - the route's budget for all inline image data.
6448
+ */
6449
+ function requiredImageOffloadCount(messages, maxBase64Bytes) {
6450
+ const lengths = [];
6451
+ const visit = (blocks) => {
6452
+ for (const block of blocks) if (block.type === "image" && "dataBase64" in block) lengths.push(block.dataBase64.length);
6453
+ else if (block.type === "tool-result") visit(block.content);
6454
+ };
6455
+ for (const message of messages) if (message.role !== "assistant") visit(message.content);
6456
+ let excess = lengths.reduce((sum, bytes) => sum + bytes, 0) - maxBase64Bytes;
6457
+ let count = 0;
6458
+ while (excess > 0 && count < lengths.length) {
6459
+ excess -= lengths[count];
6460
+ count += 1;
6461
+ }
6462
+ return count;
6463
+ }
6464
+ /**
6465
+ * The request projection for one oversized image, or undefined when it fits.
6466
+ * Hosts before DSH 0.1.7 read a pixel budget (`maxPixels`), later ones the
6467
+ * target edges (`width`/`height`); each validates only its own fields, so
6468
+ * one projection carries both.
6469
+ */
6470
+ function imageRequestTarget(ref, limit) {
6471
+ const longEdge = Math.max(ref.width, ref.height);
6472
+ if (!(longEdge > limit.maxEdge)) return void 0;
6473
+ const short = (edge) => Math.max(1, Math.floor(edge * limit.maxEdge / longEdge));
6474
+ const width = ref.width >= ref.height ? limit.maxEdge : short(ref.width);
6475
+ const height = ref.width >= ref.height ? short(ref.height) : limit.maxEdge;
6476
+ return {
6477
+ width,
6478
+ height,
6479
+ maxPixels: width * height,
6480
+ maxBytes: limit.maxBytes
6481
+ };
6482
+ }
6483
+ /**
6484
+ * Resolve every ImageBlock's attachment reference to inline base64 bytes.
6485
+ * Messages without images pass through unchanged. A request carrying an image
6486
+ * with no attachment service available fails loudly rather than silently
6487
+ * dropping the image.
6488
+ * @param messages - the request's conversation messages.
6489
+ * @param attachments - the deployment's attachment service, when mounted.
6490
+ * @param signal - cancellation for the storage reads.
6491
+ * @param limit - the route's outgoing image cap; oversized images are sent
6492
+ * downscaled while the stored attachment and history stay untouched.
6493
+ * @returns the same messages with image blocks resolved for the translators.
6494
+ */
6495
+ async function resolveImages(messages, attachments, signal, limit) {
6496
+ const hasRetainedImage = (block) => block.type === "image" ? !("offloaded" in block && block.offloaded === true) : block.type === "tool-result" && block.content.some(hasRetainedImage);
6497
+ if (attachments === void 0 && messages.some((message) => message.content.some(hasRetainedImage))) throw new LlmError("dsh-subscriptions: the request carries an image but no attachments service is mounted; image input requires the harness attachment store", "UNSUPPORTED");
6498
+ const readForRequest = async (ref) => {
6499
+ if (attachments === void 0) throw new Error("attachments service is unavailable");
6500
+ const target = limit === void 0 ? void 0 : imageRequestTarget(ref, limit);
6501
+ if (target !== void 0) try {
6502
+ const version = await attachments.readImageRequest(ref, target, signal);
6503
+ return {
6504
+ data: version.data,
6505
+ mediaType: version.mediaType,
6506
+ ref: version.attachment
6507
+ };
6508
+ } catch (error) {
6509
+ if (signal?.aborted) throw error;
6510
+ }
6511
+ const stored = await attachments.readImage(ref, signal);
6512
+ return {
6513
+ data: stored.data,
6514
+ mediaType: stored.ref.mediaType,
6515
+ ref: stored.ref
6516
+ };
6517
+ };
6518
+ const resolveBlock = async (block) => {
6519
+ if (block.type === "tool-result") return [{
6520
+ ...block,
6521
+ content: (await Promise.all(block.content.map(resolveBlock))).flat()
6522
+ }];
6523
+ if (block.type !== "image" || "dataBase64" in block) return [block];
6524
+ if ("offloaded" in block && block.offloaded === true) {
6525
+ const format = llm.offloadedImageText;
6526
+ return [{
6527
+ type: "text",
6528
+ text: format?.(block.attachment) ?? `[image omitted to fit request image limits; ${block.attachment.attachmentId}]`
6529
+ }];
6530
+ }
6531
+ const { data, mediaType: sentType, ref } = await readForRequest(block.attachment);
6532
+ const { attachmentId, mediaType, bytes, width, height, name: name$1 } = ref;
6533
+ return [{
6534
+ type: "image",
6535
+ mediaType: sentType,
6536
+ dataBase64: Buffer.from(data).toString("base64")
6537
+ }, {
6538
+ type: "text",
6539
+ text: `Image reference (for image_generate.referenceImages): ${JSON.stringify({
6540
+ attachmentId,
6541
+ mediaType,
6542
+ bytes,
6543
+ width,
6544
+ height,
6545
+ ...name$1 === void 0 ? {} : { name: name$1 }
6546
+ })}`
6547
+ }];
6548
+ };
6549
+ return Promise.all(messages.map(async (message) => ({
6550
+ role: message.role,
6551
+ ...message.source === void 0 ? {} : { source: message.source },
6552
+ ...message.role === "tool" ? {
6553
+ toolCallId: message.toolCallId,
6554
+ isError: message.isError
6555
+ } : {},
6556
+ content: (await Promise.all(message.content.map(resolveBlock))).flat()
6557
+ })));
6363
6558
  }
6364
6559
 
6365
6560
  //#endregion
@@ -6490,8 +6685,8 @@ const CLAUDE_RESET_FIELDS = [
6490
6685
  * headers are deliberately not read: they are rollover snapshots attached to
6491
6686
  * every response, so on a 429 they cannot say which bucket refused, and the
6492
6687
  * earliest of them is typically the bucket that still had room — a wait that
6493
- * lands straight back in the closed window. They reach the operator through
6494
- * `rateLimitDiagnostics` instead.
6688
+ * lands straight back in the closed window. The parsed reset supplies the retry
6689
+ * hint; diagnostics omit raw provider headers and body text.
6495
6690
  */
6496
6691
  const claudeRateLimitReset = (response, body, now) => {
6497
6692
  const unified = earliestReset(resetInstantFromHeader(response, "anthropic-ratelimit-unified-reset", now), resetInstantFromHeader(response, "anthropic-ratelimit-unified-fallback-reset", now));
@@ -6686,11 +6881,49 @@ function isClaudePermanentRefreshError(error) {
6686
6881
  return error instanceof OAuthEndpointError && (error.oauthCode === "invalid_grant" || error.oauthCode === "invalid_token");
6687
6882
  }
6688
6883
  const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
6689
- /** RFC3339 `resets_at` value → epoch ms, or undefined when absent/unparsable. */
6884
+ /** ISO-8601 or numeric epoch (seconds below 1e10 in magnitude, otherwise ms). */
6690
6885
  function claudeResetsAt(value) {
6691
- if (typeof value !== "string" || value.length === 0) return void 0;
6692
- const parsed = Date.parse(value);
6693
- return Number.isFinite(parsed) ? parsed : void 0;
6886
+ let parsed;
6887
+ if (typeof value === "number") parsed = Math.trunc(Math.abs(value) < 1e10 ? value * 1e3 : value);
6888
+ else if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) parsed = Date.parse(value);
6889
+ else return;
6890
+ return Number.isFinite(parsed) && Number.isFinite(new Date(parsed).getTime()) ? parsed : void 0;
6891
+ }
6892
+ /**
6893
+ * Read the observed cedar_ember shape from ItsJazii/pane, not an official
6894
+ * contract. Each entry retains its grant id and counts; claimable is only
6895
+ * read-only server-selection metadata, never an implemented redemption path.
6896
+ */
6897
+ function claudeResetCredits(value, now) {
6898
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
6899
+ const ember = value;
6900
+ if (ember.eligible !== true || !Array.isArray(ember.grants)) return void 0;
6901
+ const cooldownUntil = claudeResetsAt(ember.cooldown_until);
6902
+ const cooling = cooldownUntil === void 0 ? ember.cooldown_until !== void 0 && ember.cooldown_until !== null : cooldownUntil > now;
6903
+ const credits = [];
6904
+ let selected = false;
6905
+ for (const raw of ember.grants) {
6906
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
6907
+ const grant = raw;
6908
+ const { id, resets_total: total, resets_left: left, usable_now: usable, paused } = grant;
6909
+ const expiresAt = claudeResetsAt(grant.ends_at);
6910
+ if (typeof id !== "string" || id.trim().length === 0 || typeof total !== "number" || !Number.isSafeInteger(total) || total < 1 || typeof left !== "number" || !Number.isSafeInteger(left) || expiresAt === void 0 || usable !== void 0 && typeof usable !== "boolean" || paused !== void 0 && typeof paused !== "boolean") continue;
6911
+ const resetsLeft = Math.max(0, Math.min(left, total));
6912
+ if (resetsLeft === 0) continue;
6913
+ const claimable = !selected && id === ember.next_grant_id && usable === true && paused !== true && !cooling && expiresAt > now;
6914
+ if (claimable) selected = true;
6915
+ credits.push({
6916
+ id,
6917
+ expiresAt,
6918
+ resetsTotal: total,
6919
+ resetsLeft,
6920
+ ...usable === void 0 ? {} : { usableNow: usable },
6921
+ ...paused === void 0 ? {} : { paused },
6922
+ claimable,
6923
+ ...cooldownUntil === void 0 ? {} : { cooldownUntil }
6924
+ });
6925
+ }
6926
+ return credits.length > 0 ? credits : void 0;
6694
6927
  }
6695
6928
  /** Claude session and weekly buckets reset as whole five-hour/seven-day windows. */
6696
6929
  function claudeWindowTiming(kind) {
@@ -6742,6 +6975,10 @@ function claudeLimitsWindows(value) {
6742
6975
  * source of Claude Code's `/usage` screen). Newer responses carry a
6743
6976
  * structured `limits` array; older ones the flat `five_hour`/`seven_day*`
6744
6977
  * buckets — both shapes are read, the array winning when it has entries.
6978
+ * Requests the optional cedar_ember banked-reset block with the existing CLI
6979
+ * identity. A 400/403 retries once without that parameter; ordinary windows
6980
+ * remain available and resetCreditsError records the optional lookup failure.
6981
+ * Missing/ineligible blocks and empty or unusable grants expose no credits.
6745
6982
  * @param session - the stored session (used as-is; never refreshed here).
6746
6983
  * @param fetchFn - fetch implementation (injectable for tests).
6747
6984
  * @param signal - caller cancellation from the RPC transport.
@@ -6750,7 +6987,7 @@ function claudeLimitsWindows(value) {
6750
6987
  */
6751
6988
  async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion = localClaudeCliVersion) {
6752
6989
  const userAgent = claudeCliUserAgent(await cliVersion());
6753
- const response = await fetchFn(CLAUDE_USAGE_URL, {
6990
+ const init = {
6754
6991
  headers: {
6755
6992
  "authorization": `Bearer ${session.accessToken}`,
6756
6993
  "anthropic-beta": "oauth-2025-04-20",
@@ -6758,13 +6995,25 @@ async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion
6758
6995
  "accept": "application/json"
6759
6996
  },
6760
6997
  ...signal === void 0 ? {} : { signal }
6761
- });
6998
+ };
6999
+ let response = await fetchFn(`${CLAUDE_USAGE_URL}?cedar_ember=1`, init);
7000
+ let resetCreditsError;
7001
+ if (response.status === 400 || response.status === 403) {
7002
+ resetCreditsError = `Claude banked resets lookup failed (HTTP ${response.status}); using plain usage.`;
7003
+ response = await fetchFn(CLAUDE_USAGE_URL, init);
7004
+ }
6762
7005
  if (!response.ok) throw await oauthEndpointError(response, "claude usage");
6763
7006
  const payload = await response.json();
7007
+ const resetCredits = resetCreditsError === void 0 ? claudeResetCredits(payload.cedar_ember, Date.now()) : void 0;
7008
+ const resets = {
7009
+ ...resetCredits === void 0 ? {} : { resetCredits },
7010
+ ...resetCreditsError === void 0 ? {} : { resetCreditsError }
7011
+ };
6764
7012
  const modern = claudeLimitsWindows(payload.limits);
6765
7013
  if (modern.length > 0) return {
6766
7014
  supported: true,
6767
- windows: modern
7015
+ windows: modern,
7016
+ ...resets
6768
7017
  };
6769
7018
  const windows = [];
6770
7019
  const legacy = [
@@ -6776,7 +7025,8 @@ async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion
6776
7025
  for (const window of legacy) if (window !== void 0) windows.push(window);
6777
7026
  return {
6778
7027
  supported: true,
6779
- windows
7028
+ windows,
7029
+ ...resets
6780
7030
  };
6781
7031
  }
6782
7032
  function claudeThinkingType(capabilities) {
@@ -6861,6 +7111,14 @@ const CLAUDE_IMAGE_LIMIT = {
6861
7111
  maxEdge: 2e3,
6862
7112
  maxBytes: 375e4
6863
7113
  };
7114
+ /**
7115
+ * Inline image data a Claude request may carry, in base64 bytes. Every turn
7116
+ * resends the history, so screenshots accumulate until the request passes the
7117
+ * API's 32MB body limit and every later turn and compaction fails with HTTP
7118
+ * 413 `request_too_large`. Past this budget the host offloads the oldest
7119
+ * images to text and retries; the remaining 12MB holds prompts, tools, and text.
7120
+ */
7121
+ const CLAUDE_REQUEST_IMAGE_BUDGET = 20 * 1024 * 1024;
6864
7122
  /** The Claude 4.5 family accepts image input. */
6865
7123
  const CLAUDE_MODALITIES = ["text", "image"];
6866
7124
  /**
@@ -6876,15 +7134,20 @@ const CLAUDE_MODALITIES = ["text", "image"];
6876
7134
  * @param maxTokens - the resolved output cap.
6877
7135
  * @param thinking - the thinking parameter, when the model takes one.
6878
7136
  * @param effort - the reasoning effort, when the model advertises efforts.
7137
+ * @param cacheTtl - lifetime of every cache breakpoint; defaults to five minutes.
7138
+ * Requests that carry a `purpose` (compaction, session titles) always use five
7139
+ * minutes to preserve their existing auxiliary-request behavior. A five-minute
7140
+ * mark can still read an entry a one-hour mark wrote.
6879
7141
  * @returns the JSON body to POST.
6880
7142
  */
6881
- function claudeRequestBody(options, messages, maxTokens, thinking, effort) {
7143
+ function claudeRequestBody(options, messages, maxTokens, thinking, effort, cacheTtl) {
7144
+ const ttl = options.purpose === void 0 ? cacheTtl : void 0;
6882
7145
  const anthropicMessages = toAnthropicMessages(messages);
6883
- markMessageCache(anthropicMessages);
7146
+ markMessageCache(anthropicMessages, ttl);
6884
7147
  return {
6885
7148
  model: options.model,
6886
7149
  max_tokens: maxTokens,
6887
- system: toAnthropicSystem(options.system, messages),
7150
+ system: toAnthropicSystem(options.system, messages, ttl),
6888
7151
  messages: anthropicMessages,
6889
7152
  ...options.tools !== void 0 && options.tools.length > 0 ? { tools: toAnthropicTools(options.tools) } : {},
6890
7153
  ...thinking === void 0 ? {} : { thinking },
@@ -7070,9 +7333,13 @@ var ClaudeAdapter = class extends LlmAdapter {
7070
7333
  }
7071
7334
  async request(options, session, signal) {
7072
7335
  const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal, CLAUDE_IMAGE_LIMIT);
7336
+ if (hostSupportsImageOffload()) {
7337
+ const offloadImages = requiredImageOffloadCount(messages, CLAUDE_REQUEST_IMAGE_BUDGET);
7338
+ if (offloadImages > 0) throw new LlmError(`claude request images exceed the ${CLAUDE_REQUEST_IMAGE_BUDGET}-byte base64 budget; ${offloadImages} more oldest image(s) must be offloaded`, IMAGE_OFFLOAD_REQUIRED, { offloadImages });
7339
+ }
7073
7340
  const disc = await this.discovered(options.model);
7074
7341
  const maxTokens = options.maxTokens ?? claudeMaxTokens(this.options.models.find((entry) => entry.id === options.model), disc);
7075
- const body = claudeRequestBody(options, messages, maxTokens, this.thinkingParam(disc?.thinkingType, maxTokens), options.reasoningEffort !== void 0 && disc?.reasoning !== void 0 ? String(options.reasoningEffort) : void 0);
7342
+ const body = claudeRequestBody(options, messages, maxTokens, this.thinkingParam(disc?.thinkingType, maxTokens), options.reasoningEffort !== void 0 && disc?.reasoning !== void 0 ? String(options.reasoningEffort) : void 0, this.options.promptCacheTtl);
7076
7343
  const cliVersion = await (this.options.resolveCliVersion ?? localClaudeCliVersion)();
7077
7344
  return hostFetch(CLAUDE_API_URL, {
7078
7345
  method: "POST",
@@ -7266,11 +7533,11 @@ function mapResponsesUsage(usage) {
7266
7533
  * @returns the mapped error (context overflow, quota, otherwise SERVER).
7267
7534
  */
7268
7535
  function responsesFailure(code, message) {
7269
- const text = message ?? code ?? "the provider reported a failed response";
7270
7536
  const detail = `${code ?? ""} ${message ?? ""}`;
7271
- if (code === "context_window_exceeded" || isContextWindowExceededError(detail)) return new LlmError(text, CONTEXT_WINDOW_EXCEEDED_CODE);
7272
- if (code !== void 0 && /insufficient|quota/i.test(code) || isQuotaExceededError(detail)) return new LlmError(text, QUOTA_EXCEEDED_CODE);
7273
- return new LlmError(text, "SERVER");
7537
+ let classification = "SERVER";
7538
+ if (code === "context_window_exceeded" || isContextWindowExceededError(detail)) classification = CONTEXT_WINDOW_EXCEEDED_CODE;
7539
+ else if (code !== void 0 && /insufficient|quota/i.test(code) || isQuotaExceededError(detail)) classification = QUOTA_EXCEEDED_CODE;
7540
+ return new LlmError(`Responses failure (${classification}): [provider response body omitted]`, classification);
7274
7541
  }
7275
7542
  /** Assemble the final ContentBlock for one open block. */
7276
7543
  function closeBlock$1(block) {
@@ -7489,7 +7756,7 @@ async function* streamResponses(stream, onActivity, transform) {
7489
7756
  try {
7490
7757
  event = JSON.parse(sseEvent.data);
7491
7758
  } catch {
7492
- throw new LlmError(`malformed SSE payload: ${sseEvent.data.slice(0, 120)}`, "MALFORMED_RESPONSE");
7759
+ throw new LlmError("malformed SSE payload (MALFORMED_RESPONSE): [provider response body omitted]", "MALFORMED_RESPONSE");
7493
7760
  }
7494
7761
  if (transform !== void 0) event = transform(event);
7495
7762
  yield* translator.push(event);
@@ -7528,8 +7795,8 @@ const CODEX_RESET_FIELDS = [
7528
7795
  * rollover snapshots the backend attaches to every response, one per window,
7529
7796
  * so they say nothing about which window refused: a burst 429 that would clear
7530
7797
  * in seconds still carries a primary rollover hours out, and reading it would
7531
- * park the turn for those hours. They reach the operator through
7532
- * `rateLimitDiagnostics` instead.
7798
+ * park the turn for those hours. The parsed body reset supplies the retry hint;
7799
+ * diagnostics omit raw provider headers and body text.
7533
7800
  */
7534
7801
  const codexRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), CODEX_RESET_FIELDS, now);
7535
7802
  /** Default instruction when the request carries no system prompt. */
@@ -7701,6 +7968,63 @@ function isCodexPermanentRefreshError(error) {
7701
7968
  return error instanceof OAuthEndpointError && error.oauthCode !== void 0 && PERMANENT_REFRESH_CODES.has(error.oauthCode);
7702
7969
  }
7703
7970
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
7971
+ const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
7972
+ function parseResetDate(value) {
7973
+ if (typeof value !== "string") return void 0;
7974
+ const timestamp = Date.parse(value);
7975
+ return Number.isFinite(timestamp) ? timestamp : void 0;
7976
+ }
7977
+ /** Read banked resets without spending them; consumption uses a separate explicit POST. */
7978
+ async function fetchCodexResetCredits(session, fetchFn = hostFetch, signal) {
7979
+ const response = await fetchFn(CODEX_RESET_CREDITS_URL, {
7980
+ headers: {
7981
+ authorization: `Bearer ${session.accessToken}`,
7982
+ "chatgpt-account-id": session.accountId,
7983
+ originator: "Codex Desktop",
7984
+ "openai-beta": "codex-1",
7985
+ accept: "application/json",
7986
+ ...attributionHeaders()
7987
+ },
7988
+ ...signal === void 0 ? {} : { signal }
7989
+ });
7990
+ if (!response.ok) throw await oauthEndpointError(response, "codex reset credits");
7991
+ const payload = await response.json();
7992
+ if (!Array.isArray(payload?.credits)) throw new Error("codex reset credits: unexpected response (missing credits array)");
7993
+ return payload.credits.flatMap((value) => {
7994
+ if (typeof value !== "object" || value === null) return [];
7995
+ const credit = value;
7996
+ if (credit.status !== "available" || credit.reset_type !== "codex_rate_limits") return [];
7997
+ const grantedAt = parseResetDate(credit.granted_at);
7998
+ const expiresAt = parseResetDate(credit.expires_at);
7999
+ return [{
8000
+ ...typeof credit.id === "string" ? { id: credit.id } : {},
8001
+ ...grantedAt === void 0 ? {} : { grantedAt },
8002
+ ...expiresAt === void 0 ? {} : { expiresAt }
8003
+ }];
8004
+ });
8005
+ }
8006
+ /** Submit one explicit reset; ambiguous or unconfirmed responses throw without retry. */
8007
+ async function consumeCodexResetCredit(session, creditId, requestId, fetchFn = hostFetch, signal) {
8008
+ const response = await fetchFn(CODEX_RESET_CREDITS_URL + "/consume", {
8009
+ method: "POST",
8010
+ headers: {
8011
+ authorization: "Bearer " + session.accessToken,
8012
+ "chatgpt-account-id": session.accountId,
8013
+ originator: "Codex Desktop",
8014
+ "openai-beta": "codex-1",
8015
+ "content-type": "application/json",
8016
+ accept: "application/json",
8017
+ ...attributionHeaders()
8018
+ },
8019
+ body: JSON.stringify({
8020
+ redeem_request_id: requestId,
8021
+ credit_id: creditId
8022
+ }),
8023
+ ...signal === void 0 ? {} : { signal }
8024
+ });
8025
+ if (!response.ok) throw await oauthEndpointError(response, "codex consume reset");
8026
+ if ((await response.json())?.code !== "reset") throw new Error("Reset was not confirmed. Verify the account in Codex before retrying.");
8027
+ }
7704
8028
  /** Seconds of the canonical 5-hour session and 7-day weekly windows. */
7705
8029
  const SESSION_WINDOW_SECONDS = 300 * 60;
7706
8030
  const WEEKLY_WINDOW_SECONDS = 10080 * 60;
@@ -7824,7 +8148,7 @@ async function fetchCodexModels(session, fetchFn = hostFetch, signal, clientVers
7824
8148
  for (const entry of payload.models) {
7825
8149
  if (typeof entry.slug !== "string" || entry.slug.length === 0) continue;
7826
8150
  if (entry.visibility === "hide" || entry.visibility === "none") continue;
7827
- const efforts = (entry.supported_reasoning_levels ?? []).filter((level) => typeof level.effort === "string" && level.effort.length > 0).map((level) => ({
8151
+ const efforts = (entry.supported_reasoning_levels ?? []).filter((level) => typeof level.effort === "string" && level.effort.length > 0 && level.effort !== "ultra").map((level) => ({
7828
8152
  id: ReasoningEffortId(level.effort),
7829
8153
  name: effortDisplayName(level.effort),
7830
8154
  ...level.description === void 0 ? {} : { description: level.description }
@@ -7925,6 +8249,16 @@ function reconcileResponsesToolCalls(input) {
7925
8249
  return balanced;
7926
8250
  }
7927
8251
  /**
8252
+ * The `reasoning.effort` wire value for a selected effort. Codex's "ultra" is
8253
+ * a client-side selection (max reasoning plus proactive sub-agent delegation)
8254
+ * that the Codex CLI itself lowers to `max` at the Responses API boundary;
8255
+ * the backend answers HTTP 400 to a literal `ultra`. Discovery no longer
8256
+ * lists it, but an effort saved before that change may still arrive here.
8257
+ */
8258
+ function codexWireEffort(effort) {
8259
+ return effort === "ultra" ? "max" : effort;
8260
+ }
8261
+ /**
7928
8262
  * The Responses request body for one generation. A fast-tier request (the
7929
8263
  * composer Speed toggle, the codex CLI's fast mode) carries
7930
8264
  * `service_tier: priority`; the tier field is omitted entirely otherwise,
@@ -7941,7 +8275,7 @@ function codexRequestBody(options, resolved, fast) {
7941
8275
  parallel_tool_calls: true
7942
8276
  } : {},
7943
8277
  ...options.reasoningEffort !== void 0 ? { reasoning: {
7944
- effort: String(options.reasoningEffort),
8278
+ effort: codexWireEffort(String(options.reasoningEffort)),
7945
8279
  summary: "auto"
7946
8280
  } } : {},
7947
8281
  store: false,
@@ -8374,10 +8708,10 @@ function abortableDelay(ms, signal) {
8374
8708
  throwIfAborted(signal);
8375
8709
  return Promise.resolve();
8376
8710
  }
8377
- return new Promise((resolve$1, reject) => {
8711
+ return new Promise((resolve$2, reject) => {
8378
8712
  const timer = setTimeout(() => {
8379
8713
  cleanup();
8380
- resolve$1();
8714
+ resolve$2();
8381
8715
  }, ms);
8382
8716
  const onAbort = () => {
8383
8717
  cleanup();
@@ -8598,6 +8932,12 @@ var PoolHealthRegistry = class {
8598
8932
  }
8599
8933
  return earliest;
8600
8934
  }
8935
+ /** Active failure codes for the given keys; reading them does not change cooldowns. */
8936
+ unavailableReasons(keys, now = Date.now()) {
8937
+ const reasons = [];
8938
+ for (const [key, record$4] of this.records) if (keys.has(key) && record$4.unavailableUntil > now) reasons.push(record$4.reason);
8939
+ return reasons;
8940
+ }
8601
8941
  /** Drop records of one provider, or of a single account when given (auth changes). */
8602
8942
  clear(provider, account) {
8603
8943
  const prefix = account === void 0 ? `${provider}/` : `${provider}/${account}/`;
@@ -8605,6 +8945,67 @@ var PoolHealthRegistry = class {
8605
8945
  }
8606
8946
  };
8607
8947
 
8948
+ //#endregion
8949
+ //#region src/providers/replay.ts
8950
+ const REPLAY_KIND = "subscriptions-account-replay";
8951
+ /** Bind opaque replay to the concrete account/model behind a route alias. */
8952
+ async function* streamAccountWithReplay(adapter, options, account, model = options.model) {
8953
+ const key = createHash("sha256").update(JSON.stringify([
8954
+ options.provider,
8955
+ account,
8956
+ model
8957
+ ])).digest("hex");
8958
+ const messages = options.messages.map((message) => {
8959
+ if (message.role !== "assistant" || message.source?.replayState === void 0) return message;
8960
+ const source = message.source;
8961
+ if (source.replayState === null || typeof source.replayState !== "object" || !("response" in source.replayState)) return message;
8962
+ const replay = source.replayState;
8963
+ const response = replay.response;
8964
+ if (response !== null && typeof response === "object" && "kind" in response && response.kind === REPLAY_KIND) {
8965
+ if ("version" in response && response.version === 1 && "key" in response && response.key === key && "state" in response) return {
8966
+ ...message,
8967
+ source: {
8968
+ ...source,
8969
+ provider: options.provider,
8970
+ model,
8971
+ replayState: {
8972
+ ...replay,
8973
+ response: response.state
8974
+ }
8975
+ }
8976
+ };
8977
+ const { replayState: _discarded$1,...withoutReplay$1 } = source;
8978
+ return {
8979
+ ...message,
8980
+ source: withoutReplay$1
8981
+ };
8982
+ }
8983
+ if (source.provider === options.provider && source.model === model) return message;
8984
+ const { replayState: _discarded,...withoutReplay } = source;
8985
+ return {
8986
+ ...message,
8987
+ source: withoutReplay
8988
+ };
8989
+ });
8990
+ for await (const chunk of adapter.streamAccount({
8991
+ ...options,
8992
+ model,
8993
+ messages
8994
+ }, account)) if (chunk.type === "finish" && chunk.replayState !== void 0) yield {
8995
+ ...chunk,
8996
+ replayState: {
8997
+ ...chunk.replayState,
8998
+ response: {
8999
+ kind: REPLAY_KIND,
9000
+ version: 1,
9001
+ key,
9002
+ state: chunk.replayState.response
9003
+ }
9004
+ }
9005
+ };
9006
+ else yield chunk;
9007
+ }
9008
+
8608
9009
  //#endregion
8609
9010
  //#region src/providers/pool.ts
8610
9011
  /** Bound on sticky-session memory; oldest entries evict past it. */
@@ -8783,34 +9184,39 @@ var PoolAdapter = class extends LlmAdapter {
8783
9184
  ...modalities === void 0 ? {} : { inputModalities: modalities }
8784
9185
  };
8785
9186
  }
9187
+ /**
9188
+ * Fail over before output. A single non-quota failure keeps its identity
9189
+ * and shared provider message, with scoped pool recovery or its own hint as fallback.
9190
+ * Multiple failures carry a bounded summary and all attempts in an AggregateError cause.
9191
+ * Pure quota/rate exhaustion retains RATE_LIMIT and this pool's earliest recovery hint.
9192
+ */
8786
9193
  async *stream(options) {
8787
9194
  const definition = (await this.pools()).get(poolKey(options.provider, options.model));
8788
9195
  if (definition === void 0) throw new LlmError(`unknown pool model "${options.model}"`, "NO_ADAPTER");
8789
9196
  const members = await this.concrete(definition.members);
8790
9197
  const candidates = await this.select(options.model, members, options.sessionId);
8791
9198
  if (candidates.length === 0) throw this.exhausted(options.model, members);
8792
- let lastError;
9199
+ const failures = [];
8793
9200
  for (const member of candidates) {
8794
9201
  const adapter = this.options.adapters[member.provider];
8795
9202
  if (adapter === void 0) continue;
8796
- const iterator = adapter.streamAccount({
9203
+ const iterator = streamAccountWithReplay(adapter, {
8797
9204
  ...options,
8798
- provider: member.provider,
8799
- model: member.model
8800
- }, member.account)[Symbol.asyncIterator]();
9205
+ provider: member.provider
9206
+ }, member.account, member.model)[Symbol.asyncIterator]();
8801
9207
  let first;
8802
9208
  try {
8803
9209
  first = await iterator.next();
8804
9210
  if (first.done === true) throw new LlmError(`${memberLabel(member)} returned an empty stream`, EMPTY_RESPONSE_CODE);
8805
9211
  } catch (error) {
8806
9212
  const classification = classifyPoolFailure(error, member.provider);
8807
- if (classification.action === "throw") throw error;
9213
+ if (classification.action === "throw" || !(error instanceof LlmError)) throw error;
8808
9214
  if ("cooldownMs" in classification) {
8809
9215
  this.options.health.markUnavailable(classification.scope === "account" ? accountKey(member.provider, member.account) : memberKey(member.provider, member.account, member.model), classification.cooldownMs, classification.reason);
8810
9216
  if (classification.reason === QUOTA_EXCEEDED_CODE || classification.reason === "RATE_LIMIT") this.options.usage.invalidate(member.provider, member.account);
8811
9217
  }
8812
9218
  this.options.onWarn(`pool "${options.model}": ${memberLabel(member)} failed before any output (${error instanceof Error ? error.message : String(error)}); trying the next member`);
8813
- lastError = error;
9219
+ failures.push(error);
8814
9220
  continue;
8815
9221
  }
8816
9222
  this.remember(options.model, options.sessionId, member);
@@ -8824,7 +9230,7 @@ var PoolAdapter = class extends LlmAdapter {
8824
9230
  }
8825
9231
  return;
8826
9232
  }
8827
- throw this.exhausted(options.model, members, lastError);
9233
+ throw this.exhausted(options.model, members, failures);
8828
9234
  }
8829
9235
  /**
8830
9236
  * Order the candidates for one request. Health filters both strategies;
@@ -8865,12 +9271,9 @@ var PoolAdapter = class extends LlmAdapter {
8865
9271
  }
8866
9272
  this.sticky.set(key, memberKey(member.provider, member.account, member.model));
8867
9273
  }
8868
- /**
8869
- * The error for an exhausted pool, carrying the earliest recovery hint of
8870
- * THIS pool's members (the health registry is shared across pools, so the
8871
- * hint is scoped to the keys this pool can actually recover through).
8872
- */
8873
- exhausted(model, pool, cause) {
9274
+ /** Report attempt failures or this pool's active cooldowns, never unrelated health records. */
9275
+ exhausted(model, pool, failures = []) {
9276
+ if (pool.length === 0) return new LlmError(`pool "${model}" has no usable member: no account could be resolved`, "NO_ADAPTER");
8874
9277
  const keys = /* @__PURE__ */ new Set();
8875
9278
  for (const member of pool) {
8876
9279
  keys.add(memberKey(member.provider, member.account, member.model));
@@ -8878,12 +9281,53 @@ var PoolAdapter = class extends LlmAdapter {
8878
9281
  }
8879
9282
  const recovery = this.options.health.earliestRecovery(keys);
8880
9283
  const retryAfterMs$1 = recovery === void 0 ? void 0 : Math.max(recovery - Date.now(), 1);
8881
- return new LlmError(`pool "${model}" exhausted: every member is unavailable or failed`, "RATE_LIMIT", {
9284
+ const coolingReasons = this.options.health.unavailableReasons(keys);
9285
+ const codes = [...failures.map((error) => error.code), ...coolingReasons];
9286
+ if (codes.length === 0) return new LlmError(`pool "${model}" has no usable member`, "NO_ADAPTER");
9287
+ const quotaOnly = codes.every(isQuotaFailure);
9288
+ if (failures.length === 1 && !isQuotaFailure(failures[0].code)) {
9289
+ const failure$1 = failures[0];
9290
+ if (retryAfterMs$1 !== void 0 && retryAfterMs$1 !== failure$1.failure.providerRetryAfterMs) Object.defineProperty(failure$1, "failure", { value: Object.freeze({
9291
+ ...failure$1.failure,
9292
+ providerRetryAfterMs: retryAfterMs$1
9293
+ }) });
9294
+ return failure$1;
9295
+ }
9296
+ const attemptedCodes = failures.map((error) => error.code);
9297
+ const summary = failureSummary([...attemptedCodes, ...coolingReasons.filter((code) => !attemptedCodes.includes(code))]);
9298
+ const cause = failures.length === 0 ? void 0 : failures.length === 1 ? failures[0] : new AggregateError(failures, `${failures.length} pool members failed: ${summary}`);
9299
+ return new LlmError(quotaOnly ? `pool "${model}" exhausted: every member is unavailable or failed` : failures.length > 0 ? `pool "${model}" exhausted: ${failures.length} ${failures.length === 1 ? "member" : "members"} failed (${summary})` : `pool "${model}" unavailable: ${pool.length} members are cooling down (${summary})`, quotaOnly ? "RATE_LIMIT" : attemptedCodes.filter((code) => !isQuotaFailure(code)).at(-1) ?? coolingReasons.filter((code) => !isQuotaFailure(code)).at(-1), {
8882
9300
  ...retryAfterMs$1 === void 0 ? {} : { providerRetryAfterMs: retryAfterMs$1 },
8883
9301
  ...cause === void 0 ? {} : { cause }
8884
9302
  });
8885
9303
  }
8886
9304
  };
9305
+ function isQuotaFailure(code) {
9306
+ return code === QUOTA_EXCEEDED_CODE || code === "RATE_LIMIT";
9307
+ }
9308
+ /** Summaries use stable codes only: adapter messages and nested causes can contain raw payloads. */
9309
+ function failureSummary(codes) {
9310
+ const descriptions = {
9311
+ AUTH: "authentication failed",
9312
+ INVALID_CREDENTIAL: "credential is invalid",
9313
+ MISSING_CREDENTIAL: "credential is missing",
9314
+ SERVER: "provider server failed",
9315
+ TIMEOUT: "provider request timed out",
9316
+ EMPTY_RESPONSE: "provider returned no output",
9317
+ TRANSPORT: "provider connection failed",
9318
+ HTTP_402: "provider plan unavailable",
9319
+ HTTP_404: "provider model unavailable",
9320
+ QUOTA: "provider quota exhausted",
9321
+ RATE_LIMIT: "provider rate limit reached"
9322
+ };
9323
+ const limit = 5;
9324
+ const shown = codes.slice(0, limit).map((code) => {
9325
+ const description = descriptions[code];
9326
+ return description === void 0 ? "member unavailable" : `${code}: ${description}`;
9327
+ });
9328
+ if (codes.length > limit) shown.push(`${codes.length - limit} more`);
9329
+ return shown.join("; ");
9330
+ }
8887
9331
  function stickyKey(poolId, sessionId) {
8888
9332
  return `${String(sessionId)}|${poolId}`;
8889
9333
  }
@@ -9040,10 +9484,7 @@ var AccountPreferencesAdapter = class extends LlmAdapter {
9040
9484
  const independent = parseAccountModelId(options.model);
9041
9485
  if (independent) {
9042
9486
  await this.requireAccount(independent.account, independent.model, true);
9043
- yield* this.options.adapter.streamAccount({
9044
- ...options,
9045
- model: independent.model
9046
- }, independent.account);
9487
+ yield* streamAccountWithReplay(this.options.adapter, options, independent.account, independent.model);
9047
9488
  return;
9048
9489
  }
9049
9490
  const pool = this.options.pool();
@@ -9051,7 +9492,7 @@ var AccountPreferencesAdapter = class extends LlmAdapter {
9051
9492
  yield* pool.stream(options);
9052
9493
  return;
9053
9494
  }
9054
- yield* this.options.adapter.streamAccount(options, await this.fallback(options.model));
9495
+ yield* streamAccountWithReplay(this.options.adapter, options, await this.fallback(options.model));
9055
9496
  }
9056
9497
  };
9057
9498
 
@@ -9200,6 +9641,11 @@ const FALLBACK_HORIZON_MS = {
9200
9641
  var PoolUsageTracker = class {
9201
9642
  entries = /* @__PURE__ */ new Map();
9202
9643
  inflight = /* @__PURE__ */ new Map();
9644
+ /** Bumped by {@link invalidate} so a fetch started before it cannot repopulate the cache. */
9645
+ generation = /* @__PURE__ */ new Map();
9646
+ /** Monotonic per-key write order: only the newest settled fetch may cache. */
9647
+ nextSequence = 0;
9648
+ lastWritten = /* @__PURE__ */ new Map();
9203
9649
  constructor(fetcherFor, ttlMs = USAGE_TTL_MS) {
9204
9650
  this.fetcherFor = fetcherFor;
9205
9651
  this.ttlMs = ttlMs;
@@ -9282,13 +9728,25 @@ var PoolUsageTracker = class {
9282
9728
  throw error;
9283
9729
  }
9284
9730
  }
9285
- /** Drop cached snapshots: one account, or a whole provider when `account` is omitted. */
9731
+ /**
9732
+ * Drop cached snapshots: one account, or a whole provider when `account` is
9733
+ * omitted. A fetch already in flight is abandoned too. It was started against
9734
+ * state the caller has just declared changed — a redeemed credit, a completed
9735
+ * login — so joining it would return the pre-change numbers and cache them for
9736
+ * the full freshness window. Its result is discarded on arrival by the
9737
+ * generation check in {@link refresh}.
9738
+ */
9286
9739
  invalidate(provider, account) {
9740
+ const drop = (key) => {
9741
+ this.entries.delete(key);
9742
+ this.inflight.delete(key);
9743
+ this.generation.set(key, (this.generation.get(key) ?? 0) + 1);
9744
+ };
9287
9745
  if (account !== void 0) {
9288
- this.entries.delete(`${provider}/${account}`);
9746
+ drop(`${provider}/${account}`);
9289
9747
  return;
9290
9748
  }
9291
- for (const key of [...this.entries.keys()]) if (key.startsWith(`${provider}/`)) this.entries.delete(key);
9749
+ for (const key of [...this.entries.keys(), ...this.inflight.keys()]) if (key.startsWith(`${provider}/`)) drop(key);
9292
9750
  }
9293
9751
  /**
9294
9752
  * Run (or join) the single in-flight fetch for one account key, caching
@@ -9303,6 +9761,8 @@ var PoolUsageTracker = class {
9303
9761
  if (pending === void 0) {
9304
9762
  const prior = this.entries.get(key);
9305
9763
  const lastSnapshot = prior?.snapshot ?? prior?.lastSnapshot;
9764
+ const generation = this.generation.get(key) ?? 0;
9765
+ const sequence = ++this.nextSequence;
9306
9766
  pending = fetcher().then((value) => {
9307
9767
  const at = Date.now();
9308
9768
  const snapshot = {
@@ -9310,21 +9770,27 @@ var PoolUsageTracker = class {
9310
9770
  observedAt: at,
9311
9771
  stale: false
9312
9772
  };
9313
- this.entries.set(key, {
9314
- snapshot,
9315
- at
9316
- });
9773
+ if ((this.generation.get(key) ?? 0) === generation && (this.lastWritten.get(key) ?? 0) < sequence) {
9774
+ this.lastWritten.set(key, sequence);
9775
+ this.entries.set(key, {
9776
+ snapshot,
9777
+ at
9778
+ });
9779
+ }
9317
9780
  return snapshot;
9318
9781
  }, (error) => {
9319
- if (!isMissingOrInvalidCredential(error)) this.entries.set(key, {
9320
- error,
9321
- at: Date.now(),
9322
- cooldownMs: cooldownFor(error, this.ttlMs),
9323
- ...lastSnapshot === void 0 ? {} : { lastSnapshot }
9324
- });
9782
+ if (!isMissingOrInvalidCredential(error) && (this.generation.get(key) ?? 0) === generation && (this.lastWritten.get(key) ?? 0) < sequence) {
9783
+ this.lastWritten.set(key, sequence);
9784
+ this.entries.set(key, {
9785
+ error,
9786
+ at: Date.now(),
9787
+ cooldownMs: cooldownFor(error, this.ttlMs),
9788
+ ...lastSnapshot === void 0 ? {} : { lastSnapshot }
9789
+ });
9790
+ }
9325
9791
  throw error;
9326
9792
  }).finally(() => {
9327
- this.inflight.delete(key);
9793
+ if (this.inflight.get(key) === pending) this.inflight.delete(key);
9328
9794
  });
9329
9795
  this.inflight.set(key, pending);
9330
9796
  }
@@ -9406,8 +9872,8 @@ const GROK_RESET_FIELDS = [
9406
9872
  * whose values are rollover durations (`6m0s`) present on every response, one
9407
9873
  * per bucket — on a 429 the earliest of them is usually a bucket with room
9408
9874
  * (`0s` for the request bucket while the token bucket is the one exhausted),
9409
- * which would burn the whole retry budget in seconds. They reach the operator
9410
- * through `rateLimitDiagnostics` instead.
9875
+ * which would burn the whole retry budget in seconds. The parsed body reset
9876
+ * supplies the retry hint; diagnostics omit raw provider headers and body text.
9411
9877
  */
9412
9878
  const grokRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), GROK_RESET_FIELDS, now);
9413
9879
  /** A discovered URL must be https on x.ai or a subdomain; anything else is a hostile document. */
@@ -10355,7 +10821,7 @@ async function* streamChatCompletions(stream, onActivity) {
10355
10821
  try {
10356
10822
  event = JSON.parse(sseEvent.data);
10357
10823
  } catch {
10358
- throw new LlmError(`malformed SSE payload: ${sseEvent.data.slice(0, 120)}`, "MALFORMED_RESPONSE");
10824
+ throw new LlmError("malformed SSE payload (MALFORMED_RESPONSE): [provider response body omitted]", "MALFORMED_RESPONSE");
10359
10825
  }
10360
10826
  yield* translator.push(event);
10361
10827
  if (translator.terminated) return;
@@ -10942,13 +11408,16 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
10942
11408
  * account identity is the session's long-lived GitHub token (stable across
10943
11409
  * Copilot-token refreshes, different per GitHub login); the conversation is
10944
11410
  * the loop-stamped `sessionId`, falling back to the first message's id
10945
- * when a hand-built request carries no session stamp; the model separates
11411
+ * when a hand-built request carries no session stamp. Identity-free
11412
+ * requests have no replay scope; the model separates
10946
11413
  * wire families. A call id captured in one scope is invisible to every
10947
11414
  * other scope, so reused ids cannot leak reasoning across accounts,
10948
11415
  * conversations, or models.
10949
11416
  */
10950
11417
  replayScope(tokenKey, options) {
10951
- return `${tokenKey}\u0000${options.sessionId !== void 0 ? `session:${String(options.sessionId)}` : options.messages[0] !== void 0 ? `anchor:${String(options.messages[0].id)}` : "conversation:none"}\u0000${options.model}`;
11418
+ const conversation = options.sessionId !== void 0 ? `session:${String(options.sessionId)}` : options.messages[0]?.id !== void 0 ? `anchor:${String(options.messages[0].id)}` : void 0;
11419
+ if (conversation === void 0) return void 0;
11420
+ return `${tokenKey}\u0000${conversation}\u0000${options.model}`;
10952
11421
  }
10953
11422
  /**
10954
11423
  * Store one response's completed reasoning items behind every call id it
@@ -10961,6 +11430,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
10961
11430
  * per call.
10962
11431
  */
10963
11432
  captureReasoning(scope, callIds, items) {
11433
+ if (scope === void 0) return;
10964
11434
  let entries = this.replayByScope.get(scope);
10965
11435
  if (entries === void 0) {
10966
11436
  entries = /* @__PURE__ */ new Map();
@@ -10996,6 +11466,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
10996
11466
  * degradation, never an error.
10997
11467
  */
10998
11468
  replayFor(scope, callId) {
11469
+ if (scope === void 0) return void 0;
10999
11470
  const entries = this.replayByScope.get(scope);
11000
11471
  const entry = entries?.get(callId);
11001
11472
  if (entries === void 0 || entry === void 0) return void 0;
@@ -11151,8 +11622,8 @@ function object(value) {
11151
11622
  return typeof value === "object" && value !== null && !Array.isArray(value);
11152
11623
  }
11153
11624
  /** Resolve local references before removing definitions; reject cycles and remote references. */
11154
- function resolve(value, root, refs = /* @__PURE__ */ new Set()) {
11155
- if (Array.isArray(value)) return value.map((item) => resolve(item, root, refs));
11625
+ function resolve$1(value, root, refs = /* @__PURE__ */ new Set()) {
11626
+ if (Array.isArray(value)) return value.map((item) => resolve$1(item, root, refs));
11156
11627
  if (!object(value)) return value;
11157
11628
  if (typeof value.$ref === "string") {
11158
11629
  const ref = value.$ref;
@@ -11164,7 +11635,7 @@ function resolve(value, root, refs = /* @__PURE__ */ new Set()) {
11164
11635
  }
11165
11636
  if (!object(target)) throw new LlmError(`Antigravity tool schema reference is missing: ${ref}`, "INVALID_REQUEST");
11166
11637
  const { $ref: _ref,...siblings } = value;
11167
- return resolve({
11638
+ return resolve$1({
11168
11639
  ...target,
11169
11640
  ...siblings
11170
11641
  }, root, new Set([...refs, ref]));
@@ -11174,7 +11645,7 @@ function resolve(value, root, refs = /* @__PURE__ */ new Set()) {
11174
11645
  "$id",
11175
11646
  "$defs",
11176
11647
  "definitions"
11177
- ].includes(key)).map(([key, item]) => [key, key === "properties" && object(item) ? Object.fromEntries(Object.entries(item).map(([name$1, property]) => [name$1, resolve(property, root, refs)])) : resolve(item, root, refs)]));
11648
+ ].includes(key)).map(([key, item]) => [key, key === "properties" && object(item) ? Object.fromEntries(Object.entries(item).map(([name$1, property]) => [name$1, resolve$1(property, root, refs)])) : resolve$1(item, root, refs)]));
11178
11649
  }
11179
11650
  /** Numeric types widen to `number`: every integer is also a number. */
11180
11651
  const NUMERIC = new Set(["integer", "number"]);
@@ -11260,7 +11731,7 @@ function custom(value) {
11260
11731
  }
11261
11732
  /** Detach and normalize one tool's root object without changing the registry schema. */
11262
11733
  function antigravityToolParameters(parameters, legacy) {
11263
- const resolved = resolve(parameters, parameters);
11734
+ const resolved = resolve$1(parameters, parameters);
11264
11735
  const root = {
11265
11736
  ...resolved,
11266
11737
  type: resolved.type ?? "object"
@@ -11276,9 +11747,11 @@ function antigravityToolParameters(parameters, legacy) {
11276
11747
  * `FunctionResponse.response` field requires. That field maps to a singular
11277
11748
  * protobuf `Struct`, so a bare array, string, number or boolean is rejected
11278
11749
  * by the endpoint with "Proto field is not repeating, cannot start list".
11279
- * Only a non-array JSON object survives verbatim; every other JSON value is
11750
+ * Non-array JSON objects use their own fields; every other JSON value is
11280
11751
  * wrapped in the same "output" envelope the non-JSON path already uses --
11281
11752
  * the shape other Cloud Code Assist clients send for every tool result.
11753
+ * Failed results carry top-level `isError: true`, overriding a payload field;
11754
+ * successful results retain their existing serialized shape.
11282
11755
  */
11283
11756
  function toolResultValue(block) {
11284
11757
  const text = block.content.map((part) => part.type === "text" ? part.text : "").join("");
@@ -11291,8 +11764,17 @@ function toolResultValue(block) {
11291
11764
  ...block.isError === true ? { isError: true } : {}
11292
11765
  };
11293
11766
  }
11294
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
11295
- return { output: parsed };
11767
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
11768
+ const value = parsed;
11769
+ return block.isError === true ? {
11770
+ ...value,
11771
+ isError: true
11772
+ } : value;
11773
+ }
11774
+ return {
11775
+ output: parsed,
11776
+ ...block.isError === true ? { isError: true } : {}
11777
+ };
11296
11778
  }
11297
11779
  /** Safely read per-block replay metadata emitted by this adapter. */
11298
11780
  function replayBlocks(message, model) {
@@ -11342,7 +11824,8 @@ function toAntigravityContents(messages, model) {
11342
11824
  response: toolResultValue({
11343
11825
  type: "tool-result",
11344
11826
  toolCallId: ToolCallId$1(id),
11345
- content: message.content
11827
+ content: message.content,
11828
+ ...message.isError === void 0 ? {} : { isError: message.isError }
11346
11829
  })
11347
11830
  } };
11348
11831
  const previous$1 = out.at(-1);
@@ -11630,7 +12113,7 @@ async function* streamAntigravity(stream, onActivity) {
11630
12113
  try {
11631
12114
  parsed = JSON.parse(event.data);
11632
12115
  } catch {
11633
- throw new LlmError(`malformed Antigravity SSE payload: ${event.data.slice(0, 120)}`, "MALFORMED_RESPONSE");
12116
+ throw new LlmError("malformed Antigravity SSE payload (MALFORMED_RESPONSE): [provider response body omitted]", "MALFORMED_RESPONSE");
11634
12117
  }
11635
12118
  yield* translator.push(parsed);
11636
12119
  if (translator.terminated) return;
@@ -11794,7 +12277,7 @@ async function discoverAntigravityAccount(accessToken, runtime = {}, fetchFn = h
11794
12277
  projectId = projectIdOf(result.response?.cloudaicompanionProject);
11795
12278
  break;
11796
12279
  }
11797
- await new Promise((resolve$1) => setTimeout(resolve$1, 1e3));
12280
+ await new Promise((resolve$2) => setTimeout(resolve$2, 1e3));
11798
12281
  }
11799
12282
  }
11800
12283
  if (projectId === void 0) throw new Error("Antigravity account has no Cloud AI Companion project; open Antigravity and complete onboarding, then log in again");
@@ -12820,14 +13303,14 @@ function truncate(text, max = 60) {
12820
13303
  /** Abort-aware sleep between two polls. */
12821
13304
  function sleep(ms, signal) {
12822
13305
  if (ms <= 0) return Promise.resolve();
12823
- return new Promise((resolve$1, reject) => {
13306
+ return new Promise((resolve$2, reject) => {
12824
13307
  const onAbort = () => {
12825
13308
  clearTimeout(timer);
12826
13309
  reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("video_generate: aborted"));
12827
13310
  };
12828
13311
  const timer = setTimeout(() => {
12829
13312
  signal.removeEventListener("abort", onAbort);
12830
- resolve$1();
13313
+ resolve$2();
12831
13314
  }, ms);
12832
13315
  if (signal.aborted) {
12833
13316
  onAbort();
@@ -13141,6 +13624,7 @@ const Config = z.object({
13141
13624
  ]),
13142
13625
  codexClientVersion: z.string(),
13143
13626
  streamIdleTimeoutMs: z.number().min(1).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
13627
+ claudePromptCacheTtl: z.union(["5m", "1h"]).default(DEFAULT_PROMPT_CACHE_TTL),
13144
13628
  rateLimit: z.object({
13145
13629
  wait: z.boolean().default(true),
13146
13630
  maxWaitMs: z.number().min(1).default(DEFAULT_RATE_LIMIT_MAX_WAIT_MS)
@@ -13293,16 +13777,16 @@ const DEFAULT_MODELS = {
13293
13777
  };
13294
13778
  /** Validate and detach the model catalog for every provider. */
13295
13779
  function resolveCatalog(models) {
13296
- const resolve$1 = (provider) => {
13780
+ const resolve$2 = (provider) => {
13297
13781
  const configured = models?.[provider];
13298
13782
  return validateModels(configured !== void 0 && configured.length > 0 ? configured : DEFAULT_MODELS[provider], `${name}: models.${provider}`);
13299
13783
  };
13300
13784
  return {
13301
- codex: resolve$1("codex"),
13302
- claude: resolve$1("claude"),
13303
- grok: resolve$1("grok"),
13304
- copilot: resolve$1("copilot"),
13305
- antigravity: resolve$1("antigravity")
13785
+ codex: resolve$2("codex"),
13786
+ claude: resolve$2("claude"),
13787
+ grok: resolve$2("grok"),
13788
+ copilot: resolve$2("copilot"),
13789
+ antigravity: resolve$2("antigravity")
13306
13790
  };
13307
13791
  }
13308
13792
  /** The display account of a stored session, for the status endpoint. */
@@ -13372,7 +13856,7 @@ var SubscriptionsAuthController = class {
13372
13856
  * cannot be read off the flow manager.
13373
13857
  */
13374
13858
  claims = /* @__PURE__ */ new Map();
13375
- constructor(flows, deviceFlows, onAuthChanged, resolveAttachments, usageFetchers = {}, readClaudeCreds = readClaudeCodeCredentials, poolUsage = void 0, antigravityConfig = {}, clientVersions = {}) {
13859
+ constructor(flows, deviceFlows, onAuthChanged, resolveAttachments, usageFetchers = {}, readClaudeCreds = readClaudeCodeCredentials, poolUsage = void 0, antigravityConfig = {}, clientVersions = {}, resetRedemption) {
13376
13860
  this.flows = flows;
13377
13861
  this.deviceFlows = deviceFlows;
13378
13862
  this.onAuthChanged = onAuthChanged;
@@ -13382,6 +13866,15 @@ var SubscriptionsAuthController = class {
13382
13866
  this.poolUsage = poolUsage;
13383
13867
  this.antigravityConfig = antigravityConfig;
13384
13868
  this.clientVersions = clientVersions;
13869
+ this.resetRedemption = resetRedemption;
13870
+ }
13871
+ prepareReset(account, signal) {
13872
+ if (!this.resetRedemption) throw new BadRequest("Reset redemption unavailable");
13873
+ return this.resetRedemption.prepare(account, AbortSignal.any([signal, AbortSignal.timeout(2e4)]));
13874
+ }
13875
+ async consumeReset(account, ticket, signal) {
13876
+ if (!this.resetRedemption) throw new BadRequest("Reset redemption unavailable");
13877
+ await this.resetRedemption.redeem(account, ticket, AbortSignal.any([signal, AbortSignal.timeout(2e4)]));
13385
13878
  }
13386
13879
  usage(provider, account, signal, force = false) {
13387
13880
  const fetcher = this.usageFetchers[provider];
@@ -13572,8 +14065,8 @@ function presentedVersion(cache, waitMs = STATUS_VERSION_WAIT_MS) {
13572
14065
  return cache.current();
13573
14066
  }
13574
14067
  let timer;
13575
- await Promise.race([cache.resolve(), new Promise((resolve$1) => {
13576
- timer = setTimeout(resolve$1, waitMs);
14068
+ await Promise.race([cache.resolve(), new Promise((resolve$2) => {
14069
+ timer = setTimeout(resolve$2, waitMs);
13577
14070
  })]).finally(() => {
13578
14071
  clearTimeout(timer);
13579
14072
  });
@@ -13592,6 +14085,8 @@ function apply(ctx, config) {
13592
14085
  const providers = [...new Set(config.providers ?? [...PROVIDER_IDS])];
13593
14086
  const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
13594
14087
  if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0) throw new Error(`${name}: streamIdleTimeoutMs must be a positive finite number`);
14088
+ const claudePromptCacheTtl = config.claudePromptCacheTtl ?? DEFAULT_PROMPT_CACHE_TTL;
14089
+ if (claudePromptCacheTtl !== "5m" && claudePromptCacheTtl !== "1h") throw new Error(`${name}: claudePromptCacheTtl must be '5m' or '1h'`);
13595
14090
  const rateLimit = resolveRateLimitWait(config.rateLimit, `${name}: rateLimit`);
13596
14091
  const catalog = resolveCatalog(config.models);
13597
14092
  const overridden = new Set(PROVIDER_IDS.filter((provider) => (config.models?.[provider]?.length ?? 0) > 0));
@@ -13659,7 +14154,22 @@ function apply(ctx, config) {
13659
14154
  });
13660
14155
  codexTokens = tokens;
13661
14156
  accountTokens.set("codex", tokens);
13662
- usageFetchers.codex = async (account, signal) => fetchCodexUsage(await tokens.session(account), hostFetch, signal);
14157
+ usageFetchers.codex = async (account, signal) => {
14158
+ const session = await tokens.session(account);
14159
+ const usage = await fetchCodexUsage(session, hostFetch, signal);
14160
+ try {
14161
+ const resetCredits = await fetchCodexResetCredits(session, hostFetch, signal);
14162
+ return {
14163
+ ...usage,
14164
+ resetCredits
14165
+ };
14166
+ } catch {
14167
+ return {
14168
+ ...usage,
14169
+ resetCreditsError: "the usage-limit reset lookup did not answer"
14170
+ };
14171
+ }
14172
+ };
13663
14173
  let adapter;
13664
14174
  adapter = new CodexAdapter({
13665
14175
  ...config.codexClientVersion === void 0 ? {} : { clientVersion: config.codexClientVersion },
@@ -13710,6 +14220,7 @@ function apply(ctx, config) {
13710
14220
  catalogStore: catalogStore("claude"),
13711
14221
  defaultEffortOf: (model) => defaultEffortOf("claude", model),
13712
14222
  resolveCliVersion: () => claudeVersion.resolve(),
14223
+ promptCacheTtl: claudePromptCacheTtl,
13713
14224
  pool: () => poolAdapter
13714
14225
  });
13715
14226
  adapters.set("claude", adapter);
@@ -13817,8 +14328,8 @@ function apply(ctx, config) {
13817
14328
  const fetcherFor = (provider, account) => {
13818
14329
  switch (provider) {
13819
14330
  case "codex": {
13820
- const tokens = codexTokens;
13821
- return tokens === void 0 ? void 0 : async () => fetchCodexUsage(await tokens.session(account), hostFetch, AbortSignal.timeout(POOL_USAGE_TIMEOUT_MS));
14331
+ const fetcher = usageFetchers.codex;
14332
+ return fetcher === void 0 ? void 0 : () => fetcher(account, AbortSignal.timeout(POOL_USAGE_TIMEOUT_MS));
13822
14333
  }
13823
14334
  case "claude": {
13824
14335
  const tokens = claudeTokens;
@@ -13993,13 +14504,20 @@ function apply(ctx, config) {
13993
14504
  cursorHandle = credentialsCtx.llm.registerAdapter(["cursor-subscription"], cursorAdapter);
13994
14505
  });
13995
14506
  const externalUsage = new ExternalUsageController(async (name$1) => resolveExternalCredential?.(name$1), hostFetch);
14507
+ const resetRedemption = new ResetRedemption(async (account, signal) => {
14508
+ if (!usageFetchers.codex) throw new BadRequest("Codex unavailable");
14509
+ return usageFetchers.codex(account, signal);
14510
+ }, async (account, creditId, requestId, signal) => {
14511
+ if (!codexTokens) throw new BadRequest("Codex unavailable");
14512
+ await consumeCodexResetCredit(await codexTokens.session(account), creditId, requestId, hostFetch, signal);
14513
+ }, (account) => poolUsage?.invalidate("codex", account), async (account) => resolveAccountKey("codex", account));
13996
14514
  registerAuthRpc(ctx, new SubscriptionsAuthController(flows, deviceFlows, authChanged, resolveAttachments, usageFetchers, void 0, poolUsage, config.antigravity, {
13997
14515
  ...providers.includes("codex") ? { codex: config.codexClientVersion === void 0 ? presentedVersion(codexVersion) : async () => ({
13998
14516
  version: config.codexClientVersion,
13999
14517
  source: "config"
14000
14518
  }) } : {},
14001
14519
  ...providers.includes("claude") ? { claude: presentedVersion(claudeVersion) } : {}
14002
- }), speed, modelDefaults, {
14520
+ }, resetRedemption), speed, modelDefaults, {
14003
14521
  async get(provider, force) {
14004
14522
  await loadModelDefaults();
14005
14523
  const adapter = adapters.get(provider);