dsh-subscriptions 0.1.3 → 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 (58) hide show
  1. package/README.md +64 -0
  2. package/lib/auth/rpc.d.ts +3 -1
  3. package/lib/auth/rpc.js +10 -1
  4. package/lib/client/ProviderAccountManager.js +34 -41
  5. package/lib/client/ProviderModelEditor.js +5 -9
  6. package/lib/client/SpeedSelect.js +8 -7
  7. package/lib/client/SubscriptionUsageBadge.js +6 -5
  8. package/lib/client/SubscriptionsSection.d.ts +22 -0
  9. package/lib/client/SubscriptionsSection.js +128 -38
  10. package/lib/client/UsageBadgeDisplaySetting.js +4 -8
  11. package/lib/client/host-contract.d.ts +1 -1
  12. package/lib/client/host-contract.js +22 -0
  13. package/lib/client/locales.d.ts +58 -0
  14. package/lib/client/locales.js +58 -0
  15. package/lib/client/provider-settings-styles.d.ts +3 -0
  16. package/lib/client/provider-settings-styles.js +206 -0
  17. package/lib/client/reset-action.d.ts +14 -0
  18. package/lib/client/reset-action.js +15 -0
  19. package/lib/client/reset-credits-view.d.ts +53 -0
  20. package/lib/client/reset-credits-view.js +103 -0
  21. package/lib/client.js +937 -310
  22. package/lib/client.js.map +1 -1
  23. package/lib/index.d.ts +9 -0
  24. package/lib/index.js +1099 -608
  25. package/lib/providers/account-preferences.js +3 -2
  26. package/lib/providers/catalog-store.d.ts +3 -2
  27. package/lib/providers/catalog-store.js +57 -26
  28. package/lib/providers/claude.d.ts +22 -3
  29. package/lib/providers/claude.js +113 -15
  30. package/lib/providers/codex.d.ts +15 -2
  31. package/lib/providers/codex.js +72 -4
  32. package/lib/providers/common.d.ts +25 -0
  33. package/lib/providers/common.js +22 -18
  34. package/lib/providers/copilot.d.ts +2 -1
  35. package/lib/providers/copilot.js +10 -3
  36. package/lib/providers/grok.d.ts +2 -2
  37. package/lib/providers/grok.js +2 -2
  38. package/lib/providers/pool-health.d.ts +2 -0
  39. package/lib/providers/pool-health.js +9 -0
  40. package/lib/providers/pool-usage.d.ts +13 -1
  41. package/lib/providers/pool-usage.js +39 -7
  42. package/lib/providers/pool.d.ts +7 -5
  43. package/lib/providers/pool.js +89 -12
  44. package/lib/providers/rate-limit.d.ts +0 -14
  45. package/lib/providers/rate-limit.js +0 -26
  46. package/lib/providers/replay.d.ts +4 -0
  47. package/lib/providers/replay.js +50 -0
  48. package/lib/providers/reset-redemption.d.ts +51 -0
  49. package/lib/providers/reset-redemption.js +102 -0
  50. package/lib/translate/anthropic.d.ts +21 -2
  51. package/lib/translate/anthropic.js +35 -11
  52. package/lib/translate/antigravity.js +13 -6
  53. package/lib/translate/chat-completions.js +1 -1
  54. package/lib/translate/resolved.d.ts +21 -0
  55. package/lib/translate/resolved.js +63 -3
  56. package/lib/translate/responses.js +8 -6
  57. package/lib/translate/sse.js +8 -0
  58. 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);
@@ -1367,6 +1442,8 @@ const SUBSCRIPTIONS_AUTH_ENDPOINTS = [
1367
1442
  "cursorSetSettings",
1368
1443
  "image",
1369
1444
  "video",
1445
+ "prepareReset",
1446
+ "consumeReset",
1370
1447
  "speed",
1371
1448
  "setSpeed",
1372
1449
  "modelDefaults",
@@ -1646,6 +1723,13 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
1646
1723
  if (payload === null || typeof payload !== "object") throw new BadRequest("payload must be an object");
1647
1724
  await cursor.setSettings(payload.settings);
1648
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 });
1649
1733
  case "image": return ok(await controller.readImage(readImageRef(payload), signal));
1650
1734
  case "video": return ok(await controller.readVideo(readVideoName(payload), signal));
1651
1735
  case "speed": return ok(await speed.speed(readSessionId(payload)));
@@ -1716,10 +1800,10 @@ function tokenExpiry(token, now) {
1716
1800
  }
1717
1801
  function abortableDelay$2(ms, signal) {
1718
1802
  if (signal.aborted) return Promise.reject(signal.reason);
1719
- return new Promise((resolve$1, reject) => {
1803
+ return new Promise((resolve$2, reject) => {
1720
1804
  const timer = setTimeout(() => {
1721
1805
  signal.removeEventListener("abort", aborted);
1722
- resolve$1();
1806
+ resolve$2();
1723
1807
  }, ms);
1724
1808
  function aborted() {
1725
1809
  clearTimeout(timer);
@@ -2207,7 +2291,7 @@ async function nativeFetch(url, init = {}) {
2207
2291
  const client = target.protocol === "https:" ? https : http;
2208
2292
  const method = (init.method ?? "GET").toUpperCase();
2209
2293
  const body = init.body === void 0 ? void 0 : isBytes(init.body) ? Buffer.from(init.body) : String(init.body);
2210
- const request = (u$1, redirect) => new Promise((resolve$1, reject) => {
2294
+ const request = (u$1, redirect) => new Promise((resolve$2, reject) => {
2211
2295
  const req = client.request(u$1, {
2212
2296
  method,
2213
2297
  headers: sanitizeHeaders(init.headers),
@@ -2224,10 +2308,10 @@ async function nativeFetch(url, init = {}) {
2224
2308
  }
2225
2309
  const location = res.headers.location;
2226
2310
  if ((redirect ?? "follow") === "follow" && location && status >= 300 && status < 400) {
2227
- resolve$1({ redirect: new URL(location, u$1) });
2311
+ resolve$2({ redirect: new URL(location, u$1) });
2228
2312
  return;
2229
2313
  }
2230
- resolve$1({ response: makeResponse(status, res, data) });
2314
+ resolve$2({ response: makeResponse(status, res, data) });
2231
2315
  });
2232
2316
  });
2233
2317
  req.on("error", reject);
@@ -2385,12 +2469,12 @@ function isSuccessfulAgentResponse(status, contentType) {
2385
2469
  function abortableDelay$1(ms, signal) {
2386
2470
  if (signal.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Cursor retry aborted"));
2387
2471
  if (ms === 0) return Promise.resolve();
2388
- return new Promise((resolve$1, reject) => {
2472
+ return new Promise((resolve$2, reject) => {
2389
2473
  const timer = setTimeout(done, ms);
2390
2474
  timer.unref?.();
2391
2475
  function done() {
2392
2476
  signal.removeEventListener("abort", aborted);
2393
- resolve$1();
2477
+ resolve$2();
2394
2478
  }
2395
2479
  function aborted() {
2396
2480
  clearTimeout(timer);
@@ -3170,8 +3254,8 @@ var ConnectFrameReader = class {
3170
3254
  if (this.error !== void 0) throw this.error;
3171
3255
  return;
3172
3256
  }
3173
- return new Promise((resolve$1, reject) => this.waiters.push({
3174
- resolve: resolve$1,
3257
+ return new Promise((resolve$2, reject) => this.waiters.push({
3258
+ resolve: resolve$2,
3175
3259
  reject
3176
3260
  }));
3177
3261
  }
@@ -3209,8 +3293,8 @@ var AgentRun = class {
3209
3293
  authorization: `Bearer ${this.accessToken}`,
3210
3294
  ...AGENT_HEADERS
3211
3295
  };
3212
- this.responsePromise = new Promise((resolve$1, reject) => {
3213
- this.resolveResponse = resolve$1;
3296
+ this.responsePromise = new Promise((resolve$2, reject) => {
3297
+ this.resolveResponse = resolve$2;
3214
3298
  this.rejectResponse = reject;
3215
3299
  });
3216
3300
  this.responsePromise.catch(() => {});
@@ -3239,10 +3323,10 @@ var AgentRun = class {
3239
3323
  });
3240
3324
  }
3241
3325
  #resolveResponse(status) {
3242
- const resolve$1 = this.resolveResponse;
3326
+ const resolve$2 = this.resolveResponse;
3243
3327
  this.resolveResponse = void 0;
3244
3328
  this.rejectResponse = void 0;
3245
- resolve$1?.(status);
3329
+ resolve$2?.(status);
3246
3330
  }
3247
3331
  #rejectResponse(error) {
3248
3332
  const reject = this.rejectResponse;
@@ -3253,10 +3337,10 @@ var AgentRun = class {
3253
3337
  waitForResponse(timeoutMs) {
3254
3338
  if (this.responsePromise === void 0) throw new Error("Cursor AgentRun has not started");
3255
3339
  if (timeoutMs === void 0) return this.responsePromise;
3256
- return new Promise((resolve$1, reject) => {
3340
+ return new Promise((resolve$2, reject) => {
3257
3341
  const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Cursor HTTP response timeout")), timeoutMs);
3258
3342
  timer.unref?.();
3259
- this.responsePromise.then(resolve$1, reject).finally(() => clearTimeout(timer));
3343
+ this.responsePromise.then(resolve$2, reject).finally(() => clearTimeout(timer));
3260
3344
  });
3261
3345
  }
3262
3346
  write(payload) {
@@ -4798,31 +4882,6 @@ function earliestReset(...candidates) {
4798
4882
  function waitFromReset(instant, now) {
4799
4883
  return Math.max(MIN_WAIT_MS, instant - now + RESET_GRACE_MS);
4800
4884
  }
4801
- /** Header names worth showing when a 429 disclosed no reset this code recognizes. */
4802
- const DIAGNOSTIC_HEADER = /rate-?limit|retry|reset|^x-codex-/i;
4803
- /**
4804
- * Render the rate-limit-shaped headers and the head of the body of a 429 whose
4805
- * reset instant nothing parsed. Emitted through the adapter's `onWarn`, this is
4806
- * how an unrecognized provider field gets named from live traffic instead of
4807
- * being guessed at.
4808
- *
4809
- * It is also where the per-bucket rollover snapshots land by design — no reader
4810
- * parks a turn on one, because on a 429 they cannot say which bucket refused —
4811
- * so the operator still sees what the provider disclosed.
4812
- * @param response - the failed response.
4813
- * @param body - the complete response body.
4814
- * @returns a one-line diagnostic.
4815
- */
4816
- function rateLimitDiagnostics(response, body) {
4817
- const headers = [];
4818
- response.headers.forEach((value, key) => {
4819
- if (DIAGNOSTIC_HEADER.test(key)) headers.push(`${key}: ${value}`);
4820
- });
4821
- headers.sort();
4822
- const rendered = headers.length > 0 ? headers.join("; ") : "(none)";
4823
- const head = body.slice(0, 200);
4824
- return `429 disclosed no reset time; headers [${rendered}]; body ${head.length > 0 ? head : "(empty)"}`;
4825
- }
4826
4885
  /**
4827
4886
  * The retry shape every subscription route starts from: Claude Code's own SDK
4828
4887
  * numbers — ten retries after the first attempt, exponential backoff from 1s
@@ -4923,6 +4982,17 @@ function validateModels(models, label) {
4923
4982
  });
4924
4983
  }
4925
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
+ /**
4926
4996
  * Build an LlmError from a non-2xx provider response, mapping the status to a
4927
4997
  * stable code and, for a rate-limited request, the disclosed reset instant to
4928
4998
  * the `providerRetryAfterMs` the retry plugin waits out.
@@ -4943,7 +5013,6 @@ async function httpLlmError(response, label, options = {}) {
4943
5013
  body = await response.text();
4944
5014
  } catch {}
4945
5015
  const shown = body.slice(0, 500);
4946
- const message = shown.length > 0 ? `${label} error (HTTP ${String(response.status)}): ${shown}` : `${label} error (HTTP ${String(response.status)})`;
4947
5016
  let code;
4948
5017
  if (response.status === 401 || response.status === 403) code = "AUTH";
4949
5018
  else if (response.status === 429) code = "RATE_LIMIT";
@@ -4952,10 +5021,11 @@ async function httpLlmError(response, label, options = {}) {
4952
5021
  else if (response.status === 408 || response.status === 504) code = "TIMEOUT";
4953
5022
  else if (response.status >= 500) code = "SERVER";
4954
5023
  else code = `HTTP_${String(response.status)}`;
5024
+ const message = providerDiagnostic(`${label} error (HTTP ${String(response.status)}, ${code})`, body.length > 0);
4955
5025
  const now = Date.now();
4956
5026
  const rateLimited = response.status === 429;
4957
5027
  const reset = rateLimited ? options.rateLimitReset?.(response, body, now) ?? googleQuotaReset(body, now) ?? retryAfterInstant(response, now) : retryAfterInstant(response, now);
4958
- 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));
4959
5029
  return new LlmError(message, code, {
4960
5030
  status: response.status,
4961
5031
  ...reset === void 0 ? {} : { providerRetryAfterMs: waitFromReset(reset, now) }
@@ -5079,19 +5149,17 @@ var OAuthEndpointError = class extends Error {
5079
5149
  */
5080
5150
  async function oauthEndpointError(response, label) {
5081
5151
  let oauthCode;
5082
- let detail = "";
5152
+ let body = "";
5083
5153
  try {
5084
- const parsed = await response.json();
5154
+ body = await response.text();
5155
+ const parsed = JSON.parse(body);
5085
5156
  if (typeof parsed.error === "string") oauthCode = parsed.error;
5086
5157
  else if (typeof parsed.error === "object" && parsed.error !== null) {
5087
5158
  const nested = parsed.error;
5088
5159
  if (typeof nested.code === "string" && nested.code.length > 0) oauthCode = nested.code;
5089
- if (typeof nested.message === "string") detail = nested.message;
5090
5160
  }
5091
- if (typeof parsed.error_description === "string") detail = parsed.error_description;
5092
- if (detail.length === 0) detail = oauthCode ?? "";
5093
5161
  } catch {}
5094
- 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));
5095
5163
  }
5096
5164
  /**
5097
5165
  * Per-provider session freshness: loads the stored session, refreshes
@@ -5170,9 +5238,9 @@ const DISCOVERY_TIMEOUT_MS = 1e4;
5170
5238
  */
5171
5239
  function withTimeout(work, timeoutMs) {
5172
5240
  const signal = AbortSignal.timeout(timeoutMs);
5173
- const aborted = new Promise((resolve$1) => {
5174
- if (signal.aborted) resolve$1(void 0);
5175
- 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 });
5176
5244
  });
5177
5245
  return Promise.race([work(signal).then((value) => signal.aborted ? void 0 : value, (error) => {
5178
5246
  if (signal.aborted) return void 0;
@@ -5531,446 +5599,144 @@ var AccountTokenManager = class {
5531
5599
  };
5532
5600
 
5533
5601
  //#endregion
5534
- //#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
5535
5611
  /**
5536
- * Absolute path of the catalog store file.
5537
- * @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.
5538
5616
  */
5539
- function modelsFilePath() {
5540
- return dshHomePath("plugins", "subscriptions", "models.json");
5541
- }
5542
- /** The per-account section of the file, or undefined when absent or malformed. */
5543
- function accountSection(store, provider) {
5544
- const accounts = store.accounts;
5545
- if (typeof accounts !== "object" || accounts === null || Array.isArray(accounts)) return void 0;
5546
- const section = accounts[provider];
5547
- if (typeof section !== "object" || section === null || Array.isArray(section)) return void 0;
5548
- return section;
5549
- }
5550
- /** Validate one persisted reasoning block, or undefined when malformed. */
5551
- function sanitizeReasoning(value) {
5552
- if (typeof value !== "object" || value === null) return void 0;
5553
- const raw = value;
5554
- if (!Array.isArray(raw.efforts) || raw.efforts.length === 0) return void 0;
5555
- const seen = /* @__PURE__ */ new Set();
5556
- const efforts = [];
5557
- for (const entry of raw.efforts) {
5558
- if (typeof entry !== "object" || entry === null) return void 0;
5559
- const effort = entry;
5560
- 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;
5561
- seen.add(effort.id);
5562
- efforts.push({
5563
- id: ReasoningEffortId(effort.id),
5564
- name: effort.name,
5565
- ...effort.description === void 0 ? {} : { description: effort.description }
5566
- });
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();
5567
5652
  }
5568
- if (raw.defaultEffort !== void 0 && (typeof raw.defaultEffort !== "string" || !seen.has(raw.defaultEffort))) return void 0;
5569
- return {
5570
- efforts,
5571
- ...raw.defaultEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(raw.defaultEffort) }
5572
- };
5573
- }
5574
- /** Validate one persisted model, or undefined when malformed. */
5575
- function sanitizeModel(value) {
5576
- if (typeof value !== "object" || value === null) return void 0;
5577
- const raw = value;
5578
- 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;
5579
- const reasoning = raw.reasoning === void 0 ? void 0 : sanitizeReasoning(raw.reasoning);
5580
- if (raw.reasoning !== void 0 && reasoning === void 0) return void 0;
5581
- const thinkingType = raw.thinkingType;
5582
- if (thinkingType !== void 0 && thinkingType !== "enabled" && thinkingType !== "adaptive") return void 0;
5583
- const fastTier = raw.fastTier;
5584
- if (fastTier !== void 0 && typeof fastTier !== "boolean") return void 0;
5585
- const copilotWire = raw.copilotWire;
5586
- if (copilotWire !== void 0 && copilotWire !== "chat-completions" && copilotWire !== "responses") return;
5587
- const copilotResponses = raw.copilotResponses;
5588
- if (copilotResponses !== void 0 && typeof copilotResponses !== "boolean") return void 0;
5589
- const inputModalities = raw.inputModalities;
5590
- if (inputModalities !== void 0 && (!Array.isArray(inputModalities) || inputModalities.length === 0 || inputModalities.some((modality) => modality !== "text" && modality !== "image"))) return void 0;
5591
- return {
5592
- id: raw.id,
5593
- name: raw.name,
5594
- ...raw.description === void 0 ? {} : { description: raw.description },
5595
- ...raw.contextWindow === void 0 ? {} : { contextWindow: raw.contextWindow },
5596
- ...raw.maxContextWindow === void 0 ? {} : { maxContextWindow: raw.maxContextWindow },
5597
- ...raw.maxOutputTokens === void 0 ? {} : { maxOutputTokens: raw.maxOutputTokens },
5598
- ...raw.priority === void 0 ? {} : { priority: raw.priority },
5599
- ...reasoning === void 0 ? {} : { reasoning },
5600
- ...thinkingType === void 0 ? {} : { thinkingType },
5601
- ...fastTier === void 0 ? {} : { fastTier },
5602
- ...copilotWire === void 0 ? {} : { copilotWire },
5603
- ...copilotResponses === void 0 ? {} : { copilotResponses },
5604
- ...inputModalities === void 0 ? {} : { inputModalities: [...inputModalities] }
5605
- };
5606
5653
  }
5654
+
5655
+ //#endregion
5656
+ //#region src/translate/anthropic.ts
5607
5657
  /**
5608
- * Validate one persisted snapshot. Strict: any malformed field drops the
5609
- * whole snapshot rather than repairing it — the next successful discovery
5610
- * rewrites the entry anyway.
5611
- * @param value - the raw per-provider file entry.
5612
- * @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.
5613
5661
  */
5614
- function sanitizeSnapshot(value) {
5615
- if (typeof value !== "object" || value === null) return void 0;
5616
- const raw = value;
5617
- if (typeof raw.at !== "number" || !Number.isFinite(raw.at)) return void 0;
5618
- if (!Array.isArray(raw.models) || raw.models.length === 0) return void 0;
5619
- const seen = /* @__PURE__ */ new Set();
5620
- const models = [];
5621
- for (const entry of raw.models) {
5622
- const model = sanitizeModel(entry);
5623
- if (model === void 0 || seen.has(model.id)) return void 0;
5624
- seen.add(model.id);
5625
- 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
+ });
5626
5715
  }
5627
- return {
5628
- at: raw.at,
5629
- models
5630
- };
5716
+ return content;
5631
5717
  }
5632
- /** Read the whole file; missing or unparsable reads as an empty cache. */
5633
- async function readCatalogFile(path) {
5634
- let text;
5718
+ /** Parse a tool call's raw JSON arguments into Anthropic's object-shaped `input`. */
5719
+ function parseToolInput(raw) {
5635
5720
  try {
5636
- text = await readFile(path, "utf8");
5637
- } catch {
5721
+ const parsed = JSON.parse(raw);
5722
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
5638
5723
  return {};
5639
- }
5640
- try {
5641
- const parsed = JSON.parse(text);
5642
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
5643
- return parsed;
5644
5724
  } catch {
5645
5725
  return {};
5646
5726
  }
5647
5727
  }
5648
- /** Persist the whole file atomically (tmp file + rename). */
5649
- async function writeCatalogFile(store, path) {
5650
- await mkdir(dirname(path), { recursive: true });
5651
- const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
5652
- try {
5653
- await writeFile(tmp, JSON.stringify(store, null, 2));
5654
- await rename(tmp, path);
5655
- } catch (error) {
5656
- await rm(tmp, { force: true });
5657
- throw error;
5658
- }
5659
- }
5660
5728
  /**
5661
- * Build the durable half of one provider's catalog cache over the shared
5662
- * models.json file (concurrent writers are last-writer-wins, acceptable for
5663
- * a cache).
5664
- * @param provider - the provider route keying the file entry.
5665
- * @param path - store file path; defaults to {@link modelsFilePath}.
5666
- * @returns the persistence hooks for {@link ModelCatalogCache}.
5667
- */
5668
- function catalogStore(provider, path = modelsFilePath()) {
5669
- return {
5670
- async load() {
5671
- return sanitizeSnapshot((await readCatalogFile(path))[provider]);
5672
- },
5673
- async save(snapshot) {
5674
- const store = await readCatalogFile(path);
5675
- store[provider] = snapshot;
5676
- await writeCatalogFile(store, path);
5677
- },
5678
- async clear() {
5679
- const store = await readCatalogFile(path);
5680
- if (store[provider] === void 0) return;
5681
- delete store[provider];
5682
- await writeCatalogFile(store, path);
5683
- }
5684
- };
5685
- }
5686
- /**
5687
- * The durable half of one NON-default account's catalog cache. The provider
5688
- * entry belongs to the default account; every other account keeps its own
5689
- * snapshot under `accounts[provider][account]`, so a working secondary login
5690
- * survives restarts and network failures even when the default login is dead.
5691
- * @param provider - the provider route.
5692
- * @param account - the canonical account key.
5693
- * @param path - store file path; defaults to {@link modelsFilePath}.
5694
- * @returns the persistence hooks for {@link ModelCatalogCache}.
5695
- */
5696
- function accountCatalogStore(provider, account, path = modelsFilePath()) {
5697
- return {
5698
- async load() {
5699
- const section = accountSection(await readCatalogFile(path), provider);
5700
- return section !== void 0 && Object.hasOwn(section, account) ? sanitizeSnapshot(section[account]) : void 0;
5701
- },
5702
- async save(snapshot) {
5703
- const store = await readCatalogFile(path);
5704
- const accounts = typeof store.accounts === "object" && store.accounts !== null && !Array.isArray(store.accounts) ? store.accounts : {};
5705
- const section = {
5706
- ...accountSection(store, provider) ?? {},
5707
- [account]: snapshot
5708
- };
5709
- store.accounts = {
5710
- ...accounts,
5711
- [provider]: section
5712
- };
5713
- await writeCatalogFile(store, path);
5714
- },
5715
- async clear() {
5716
- const store = await readCatalogFile(path);
5717
- const section = accountSection(store, provider);
5718
- if (section === void 0 || !Object.hasOwn(section, account)) return;
5719
- const rest = { ...section };
5720
- delete rest[account];
5721
- store.accounts = {
5722
- ...store.accounts,
5723
- [provider]: rest
5724
- };
5725
- await writeCatalogFile(store, path);
5726
- }
5727
- };
5728
- }
5729
-
5730
- //#endregion
5731
- //#region src/translate/resolved.ts
5732
- /**
5733
- * Wires with text-only tool outputs receive images in a following user turn.
5734
- * Defer that turn until all consecutive user messages have been processed:
5735
- * parallel tool results can arrive in separate harness messages, and a user
5736
- * image message must not interrupt their tool-call/output pairing.
5737
- */
5738
- function withToolResultImages(messages) {
5739
- const out = [];
5740
- let images = [];
5741
- const flush = () => {
5742
- if (images.length > 0) out.push({
5743
- role: "user",
5744
- content: images
5745
- });
5746
- images = [];
5747
- };
5748
- for (const message of messages) {
5749
- if (message.role === "assistant") flush();
5750
- out.push(message);
5751
- if (message.role === "tool") {
5752
- const parts = message.content.filter((part) => part.type === "image" && "dataBase64" in part);
5753
- if (parts.length > 0) images.push({
5754
- type: "text",
5755
- text: `Images from tool result ${String(message.toolCallId)}:`
5756
- }, ...parts);
5757
- }
5758
- for (const block of message.content) {
5759
- if (block.type !== "tool-result") continue;
5760
- const parts = block.content.filter((part) => part.type === "image" && "dataBase64" in part);
5761
- if (parts.length > 0) images.push({
5762
- type: "text",
5763
- text: `Images from tool result ${String(block.toolCallId)}:`
5764
- }, ...parts);
5765
- }
5766
- }
5767
- flush();
5768
- return out;
5769
- }
5770
- /**
5771
- * The request projection for one oversized image, or undefined when it fits.
5772
- * Hosts before DSH 0.1.7 read a pixel budget (`maxPixels`), later ones the
5773
- * target edges (`width`/`height`); each validates only its own fields, so
5774
- * one projection carries both.
5775
- */
5776
- function imageRequestTarget(ref, limit) {
5777
- const longEdge = Math.max(ref.width, ref.height);
5778
- if (!(longEdge > limit.maxEdge)) return void 0;
5779
- const short = (edge) => Math.max(1, Math.floor(edge * limit.maxEdge / longEdge));
5780
- const width = ref.width >= ref.height ? limit.maxEdge : short(ref.width);
5781
- const height = ref.width >= ref.height ? short(ref.height) : limit.maxEdge;
5782
- return {
5783
- width,
5784
- height,
5785
- maxPixels: width * height,
5786
- maxBytes: limit.maxBytes
5787
- };
5788
- }
5789
- /**
5790
- * Resolve every ImageBlock's attachment reference to inline base64 bytes.
5791
- * Messages without images pass through unchanged. A request carrying an image
5792
- * with no attachment service available fails loudly rather than silently
5793
- * dropping the image.
5794
- * @param messages - the request's conversation messages.
5795
- * @param attachments - the deployment's attachment service, when mounted.
5796
- * @param signal - cancellation for the storage reads.
5797
- * @param limit - the route's outgoing image cap; oversized images are sent
5798
- * downscaled while the stored attachment and history stay untouched.
5799
- * @returns the same messages with image blocks resolved for the translators.
5800
- */
5801
- async function resolveImages(messages, attachments, signal, limit) {
5802
- 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");
5803
- const readForRequest = async (ref) => {
5804
- if (attachments === void 0) throw new Error("attachments service is unavailable");
5805
- const target = limit === void 0 ? void 0 : imageRequestTarget(ref, limit);
5806
- if (target !== void 0) try {
5807
- const version = await attachments.readImageRequest(ref, target, signal);
5808
- return {
5809
- data: version.data,
5810
- mediaType: version.mediaType,
5811
- ref: version.attachment
5812
- };
5813
- } catch (error) {
5814
- if (signal?.aborted) throw error;
5815
- }
5816
- const stored = await attachments.readImage(ref, signal);
5817
- return {
5818
- data: stored.data,
5819
- mediaType: stored.ref.mediaType,
5820
- ref: stored.ref
5821
- };
5822
- };
5823
- const resolveBlock = async (block) => {
5824
- if (block.type !== "image") return [block];
5825
- const { data, mediaType: sentType, ref } = await readForRequest(block.attachment);
5826
- const { attachmentId, mediaType, bytes, width, height, name: name$1 } = ref;
5827
- return [{
5828
- type: "image",
5829
- mediaType: sentType,
5830
- dataBase64: Buffer.from(data).toString("base64")
5831
- }, {
5832
- type: "text",
5833
- text: `Image reference (for image_generate.referenceImages): ${JSON.stringify({
5834
- attachmentId,
5835
- mediaType,
5836
- bytes,
5837
- width,
5838
- height,
5839
- ...name$1 === void 0 ? {} : { name: name$1 }
5840
- })}`
5841
- }];
5842
- };
5843
- return Promise.all(messages.map(async (message) => ({
5844
- role: message.role,
5845
- ...message.source === void 0 ? {} : { source: message.source },
5846
- ...message.role === "tool" ? {
5847
- toolCallId: message.toolCallId,
5848
- isError: message.isError
5849
- } : {},
5850
- content: (await Promise.all(message.content.map(resolveBlock))).flat()
5851
- })));
5852
- }
5853
-
5854
- //#endregion
5855
- //#region src/compat.ts
5856
- /** Brand a string as a tool-call id: alpha's `ToolCallId`, rc.2's `CallId`. */
5857
- const ToolCallId$1 = (() => {
5858
- const exports = llm;
5859
- return exports["ToolCallId"] ?? exports["CallId"];
5860
- })();
5861
-
5862
- //#endregion
5863
- //#region src/translate/sse.ts
5864
- /**
5865
- * Decode an SSE byte stream into events.
5866
- * @param stream - raw response bytes; reads may split anywhere, including mid-UTF-8 sequence.
5867
- * @param onActivity - called on every received chunk and comment line; drives the idle watchdog.
5868
- * @returns events in arrival order.
5869
- */
5870
- async function* parseSse(stream, onActivity) {
5871
- const reader = stream.getReader();
5872
- const decoder = new TextDecoder();
5873
- let pending = "";
5874
- let dataLines = [];
5875
- let eventName;
5876
- try {
5877
- while (true) {
5878
- const { done, value } = await reader.read();
5879
- if (done) return;
5880
- onActivity?.();
5881
- pending += decoder.decode(value, { stream: true });
5882
- let newline = pending.indexOf("\n");
5883
- while (newline >= 0) {
5884
- let line = pending.slice(0, newline);
5885
- pending = pending.slice(newline + 1);
5886
- newline = pending.indexOf("\n");
5887
- if (line.endsWith("\r")) line = line.slice(0, -1);
5888
- if (line.length === 0) {
5889
- if (dataLines.length > 0) yield {
5890
- data: dataLines.join("\n"),
5891
- ...eventName === void 0 ? {} : { event: eventName }
5892
- };
5893
- dataLines = [];
5894
- eventName = void 0;
5895
- } else if (line.startsWith(":")) onActivity?.();
5896
- else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
5897
- else if (line.startsWith("event:")) eventName = line.slice(6).replace(/^ /, "");
5898
- }
5899
- }
5900
- } finally {
5901
- reader.releaseLock();
5902
- }
5903
- }
5904
-
5905
- //#endregion
5906
- //#region src/translate/anthropic.ts
5907
- /**
5908
- * The Claude Code identity block. The subscription endpoint rejects requests
5909
- * that do not present as Claude Code, so this block is REQUIRED as the first
5910
- * system entry on every request.
5911
- */
5912
- const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
5913
- /** Tags wrapping a mid-conversation system message where it sits in the history. */
5914
- const SYSTEM_REMINDER_OPEN = "<system-reminder>";
5915
- const SYSTEM_REMINDER_CLOSE = "</system-reminder>";
5916
- /**
5917
- * How far apart consecutive message breakpoints sit, in content blocks.
5918
- *
5919
- * A breakpoint looks back at most 20 blocks for an entry an earlier request
5920
- * wrote, so marks must stay closer than that: one agentic turn can append a
5921
- * dozen tool_use/tool_result blocks at once, and a single trailing mark would
5922
- * silently fall out of range and rebuild the whole prefix.
5923
- */
5924
- const CACHE_BLOCK_STRIDE = 15;
5925
- /**
5926
- * Message breakpoints per request. Anthropic allows four in total and the
5927
- * last `system` block takes the fourth, so three are left for the history —
5928
- * enough to tolerate a turn appending roughly {@link CACHE_BLOCK_STRIDE} × 3
5929
- * blocks before a read is lost.
5930
- */
5931
- const MESSAGE_CACHE_BREAKPOINTS = 3;
5932
- /** Preserve native image blocks, retaining the existing text-only wire shape. */
5933
- function toolResultContent(block) {
5934
- if (!block.content.some((part) => part.type === "image" && "dataBase64" in part)) return block.content.map((part) => part.type === "text" ? part.text : "").join("");
5935
- const content = [];
5936
- for (const part of block.content) {
5937
- if (part.type === "text" && part.text.length > 0) content.push({
5938
- type: "text",
5939
- text: part.text
5940
- });
5941
- if (part.type === "image" && "dataBase64" in part) content.push({
5942
- type: "image",
5943
- source: {
5944
- type: "base64",
5945
- media_type: part.mediaType,
5946
- data: part.dataBase64
5947
- }
5948
- });
5949
- }
5950
- return content;
5951
- }
5952
- /** Parse a tool call's raw JSON arguments into Anthropic's object-shaped `input`. */
5953
- function parseToolInput(raw) {
5954
- try {
5955
- const parsed = JSON.parse(raw);
5956
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
5957
- return {};
5958
- } catch {
5959
- return {};
5960
- }
5961
- }
5962
- /**
5963
- * Move a user message's `tool_result` blocks into one contiguous run at the
5964
- * front, preserving the relative order of both groups.
5965
- *
5966
- * Anthropic answers every `tool_use` against the blocks that *lead* the next
5967
- * message, so a block of any other kind before or between the results reads
5968
- * as a call left unanswered and the request is rejected. The harness merges
5969
- * everything queued for one user turn into a single message, and a parallel
5970
- * tool batch arrives as one result message per call, so any context spliced
5971
- * mid-batch lands between two results. Restoring the run here keeps that
5972
- * independent of delivery order. Order *among* the results does not matter.
5973
- * @param message - one assembled user message, reordered in place.
5729
+ * Move a user message's `tool_result` blocks into one contiguous run at the
5730
+ * front, preserving the relative order of both groups.
5731
+ *
5732
+ * Anthropic answers every `tool_use` against the blocks that *lead* the next
5733
+ * message, so a block of any other kind before or between the results reads
5734
+ * as a call left unanswered and the request is rejected. The harness merges
5735
+ * everything queued for one user turn into a single message, and a parallel
5736
+ * tool batch arrives as one result message per call, so any context spliced
5737
+ * mid-batch lands between two results. Restoring the run here keeps that
5738
+ * independent of delivery order. Order *among* the results does not matter.
5739
+ * @param message - one assembled user message, reordered in place.
5974
5740
  */
5975
5741
  function leadWithToolResults(message) {
5976
5742
  const firstOther = message.content.findIndex((block) => block.type !== "tool_result");
@@ -6095,13 +5861,14 @@ function toAnthropicMessages(messages) {
6095
5861
  * Marks are counted across the flattened block sequence, not per message,
6096
5862
  * because the lookback window Anthropic walks counts blocks the same way.
6097
5863
  * @param messages - assembled Anthropic messages, marked in place.
5864
+ * @param ttl - cache lifetime for every mark; defaults to five minutes.
6098
5865
  */
6099
- function markMessageCache(messages) {
5866
+ function markMessageCache(messages, ttl) {
6100
5867
  const blocks = messages.flatMap((message) => message.content);
6101
5868
  for (let mark = 0; mark < MESSAGE_CACHE_BREAKPOINTS; mark++) {
6102
5869
  const at = blocks.length - 1 - mark * CACHE_BLOCK_STRIDE;
6103
5870
  if (at < 0) return;
6104
- blocks[at].cache_control = { type: "ephemeral" };
5871
+ blocks[at].cache_control = cacheControl(ttl);
6105
5872
  }
6106
5873
  }
6107
5874
  /**
@@ -6110,9 +5877,10 @@ function markMessageCache(messages) {
6110
5877
  * @param system - explicit system prompt, when set.
6111
5878
  * @param messages - conversation messages; the system-role text preceding the
6112
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.
6113
5881
  * @returns the system content blocks.
6114
5882
  */
6115
- function toAnthropicSystem(system, messages) {
5883
+ function toAnthropicSystem(system, messages, ttl) {
6116
5884
  const blocks = [{
6117
5885
  type: "text",
6118
5886
  text: CLAUDE_CODE_IDENTITY
@@ -6126,7 +5894,7 @@ function toAnthropicSystem(system, messages) {
6126
5894
  type: "text",
6127
5895
  text: block.text
6128
5896
  });
6129
- blocks[blocks.length - 1].cache_control = { type: "ephemeral" };
5897
+ blocks[blocks.length - 1].cache_control = cacheControl(ttl);
6130
5898
  return blocks;
6131
5899
  }
6132
5900
  /**
@@ -6174,10 +5942,11 @@ function closeBlock$2(block) {
6174
5942
  function anthropicFailure(error) {
6175
5943
  const type = error?.type ?? "unknown_error";
6176
5944
  const message = error?.message ?? `Anthropic reported ${type}`;
6177
- if (type === "invalid_request_error" && /prompt is too long/i.test(message)) return new LlmError(message, CONTEXT_WINDOW_EXCEEDED_CODE);
6178
- if (type === "rate_limit_error") return new LlmError(message, "RATE_LIMIT");
6179
- if (type === "authentication_error") return new LlmError(message, "AUTH");
6180
- 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);
6181
5950
  }
6182
5951
  /**
6183
5952
  * Push-model Anthropic SSE translator: feed each parsed event object to
@@ -6366,27 +6135,426 @@ var AnthropicStreamTranslator = class {
6366
6135
  case "error": throw anthropicFailure(event.error);
6367
6136
  default: return chunks;
6368
6137
  }
6369
- }
6370
- };
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
6371
6388
  /**
6372
- * Consume an Anthropic SSE byte stream and yield harness StreamChunks.
6373
- * @param stream - raw response body.
6374
- * @param onActivity - transport-activity callback for the idle watchdog.
6375
- * @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.
6376
6393
  */
6377
- async function* streamAnthropic(stream, onActivity) {
6378
- const translator = new AnthropicStreamTranslator();
6379
- for await (const sseEvent of parseSse(stream, onActivity)) {
6380
- let event;
6381
- try {
6382
- event = JSON.parse(sseEvent.data);
6383
- } catch {
6384
- 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);
6385
6421
  }
6386
- yield* translator.push(event);
6387
- if (translator.terminated) return;
6388
6422
  }
6389
- 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
+ })));
6390
6558
  }
6391
6559
 
6392
6560
  //#endregion
@@ -6517,8 +6685,8 @@ const CLAUDE_RESET_FIELDS = [
6517
6685
  * headers are deliberately not read: they are rollover snapshots attached to
6518
6686
  * every response, so on a 429 they cannot say which bucket refused, and the
6519
6687
  * earliest of them is typically the bucket that still had room — a wait that
6520
- * lands straight back in the closed window. They reach the operator through
6521
- * `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.
6522
6690
  */
6523
6691
  const claudeRateLimitReset = (response, body, now) => {
6524
6692
  const unified = earliestReset(resetInstantFromHeader(response, "anthropic-ratelimit-unified-reset", now), resetInstantFromHeader(response, "anthropic-ratelimit-unified-fallback-reset", now));
@@ -6713,11 +6881,49 @@ function isClaudePermanentRefreshError(error) {
6713
6881
  return error instanceof OAuthEndpointError && (error.oauthCode === "invalid_grant" || error.oauthCode === "invalid_token");
6714
6882
  }
6715
6883
  const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
6716
- /** RFC3339 `resets_at` value → epoch ms, or undefined when absent/unparsable. */
6884
+ /** ISO-8601 or numeric epoch (seconds below 1e10 in magnitude, otherwise ms). */
6717
6885
  function claudeResetsAt(value) {
6718
- if (typeof value !== "string" || value.length === 0) return void 0;
6719
- const parsed = Date.parse(value);
6720
- 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;
6721
6927
  }
6722
6928
  /** Claude session and weekly buckets reset as whole five-hour/seven-day windows. */
6723
6929
  function claudeWindowTiming(kind) {
@@ -6769,6 +6975,10 @@ function claudeLimitsWindows(value) {
6769
6975
  * source of Claude Code's `/usage` screen). Newer responses carry a
6770
6976
  * structured `limits` array; older ones the flat `five_hour`/`seven_day*`
6771
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.
6772
6982
  * @param session - the stored session (used as-is; never refreshed here).
6773
6983
  * @param fetchFn - fetch implementation (injectable for tests).
6774
6984
  * @param signal - caller cancellation from the RPC transport.
@@ -6777,7 +6987,7 @@ function claudeLimitsWindows(value) {
6777
6987
  */
6778
6988
  async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion = localClaudeCliVersion) {
6779
6989
  const userAgent = claudeCliUserAgent(await cliVersion());
6780
- const response = await fetchFn(CLAUDE_USAGE_URL, {
6990
+ const init = {
6781
6991
  headers: {
6782
6992
  "authorization": `Bearer ${session.accessToken}`,
6783
6993
  "anthropic-beta": "oauth-2025-04-20",
@@ -6785,13 +6995,25 @@ async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion
6785
6995
  "accept": "application/json"
6786
6996
  },
6787
6997
  ...signal === void 0 ? {} : { signal }
6788
- });
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
+ }
6789
7005
  if (!response.ok) throw await oauthEndpointError(response, "claude usage");
6790
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
+ };
6791
7012
  const modern = claudeLimitsWindows(payload.limits);
6792
7013
  if (modern.length > 0) return {
6793
7014
  supported: true,
6794
- windows: modern
7015
+ windows: modern,
7016
+ ...resets
6795
7017
  };
6796
7018
  const windows = [];
6797
7019
  const legacy = [
@@ -6803,7 +7025,8 @@ async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion
6803
7025
  for (const window of legacy) if (window !== void 0) windows.push(window);
6804
7026
  return {
6805
7027
  supported: true,
6806
- windows
7028
+ windows,
7029
+ ...resets
6807
7030
  };
6808
7031
  }
6809
7032
  function claudeThinkingType(capabilities) {
@@ -6888,6 +7111,14 @@ const CLAUDE_IMAGE_LIMIT = {
6888
7111
  maxEdge: 2e3,
6889
7112
  maxBytes: 375e4
6890
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;
6891
7122
  /** The Claude 4.5 family accepts image input. */
6892
7123
  const CLAUDE_MODALITIES = ["text", "image"];
6893
7124
  /**
@@ -6903,15 +7134,20 @@ const CLAUDE_MODALITIES = ["text", "image"];
6903
7134
  * @param maxTokens - the resolved output cap.
6904
7135
  * @param thinking - the thinking parameter, when the model takes one.
6905
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.
6906
7141
  * @returns the JSON body to POST.
6907
7142
  */
6908
- 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;
6909
7145
  const anthropicMessages = toAnthropicMessages(messages);
6910
- markMessageCache(anthropicMessages);
7146
+ markMessageCache(anthropicMessages, ttl);
6911
7147
  return {
6912
7148
  model: options.model,
6913
7149
  max_tokens: maxTokens,
6914
- system: toAnthropicSystem(options.system, messages),
7150
+ system: toAnthropicSystem(options.system, messages, ttl),
6915
7151
  messages: anthropicMessages,
6916
7152
  ...options.tools !== void 0 && options.tools.length > 0 ? { tools: toAnthropicTools(options.tools) } : {},
6917
7153
  ...thinking === void 0 ? {} : { thinking },
@@ -7097,9 +7333,13 @@ var ClaudeAdapter = class extends LlmAdapter {
7097
7333
  }
7098
7334
  async request(options, session, signal) {
7099
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
+ }
7100
7340
  const disc = await this.discovered(options.model);
7101
7341
  const maxTokens = options.maxTokens ?? claudeMaxTokens(this.options.models.find((entry) => entry.id === options.model), disc);
7102
- 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);
7103
7343
  const cliVersion = await (this.options.resolveCliVersion ?? localClaudeCliVersion)();
7104
7344
  return hostFetch(CLAUDE_API_URL, {
7105
7345
  method: "POST",
@@ -7293,11 +7533,11 @@ function mapResponsesUsage(usage) {
7293
7533
  * @returns the mapped error (context overflow, quota, otherwise SERVER).
7294
7534
  */
7295
7535
  function responsesFailure(code, message) {
7296
- const text = message ?? code ?? "the provider reported a failed response";
7297
7536
  const detail = `${code ?? ""} ${message ?? ""}`;
7298
- if (code === "context_window_exceeded" || isContextWindowExceededError(detail)) return new LlmError(text, CONTEXT_WINDOW_EXCEEDED_CODE);
7299
- if (code !== void 0 && /insufficient|quota/i.test(code) || isQuotaExceededError(detail)) return new LlmError(text, QUOTA_EXCEEDED_CODE);
7300
- 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);
7301
7541
  }
7302
7542
  /** Assemble the final ContentBlock for one open block. */
7303
7543
  function closeBlock$1(block) {
@@ -7516,7 +7756,7 @@ async function* streamResponses(stream, onActivity, transform) {
7516
7756
  try {
7517
7757
  event = JSON.parse(sseEvent.data);
7518
7758
  } catch {
7519
- 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");
7520
7760
  }
7521
7761
  if (transform !== void 0) event = transform(event);
7522
7762
  yield* translator.push(event);
@@ -7555,8 +7795,8 @@ const CODEX_RESET_FIELDS = [
7555
7795
  * rollover snapshots the backend attaches to every response, one per window,
7556
7796
  * so they say nothing about which window refused: a burst 429 that would clear
7557
7797
  * in seconds still carries a primary rollover hours out, and reading it would
7558
- * park the turn for those hours. They reach the operator through
7559
- * `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.
7560
7800
  */
7561
7801
  const codexRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), CODEX_RESET_FIELDS, now);
7562
7802
  /** Default instruction when the request carries no system prompt. */
@@ -7728,6 +7968,63 @@ function isCodexPermanentRefreshError(error) {
7728
7968
  return error instanceof OAuthEndpointError && error.oauthCode !== void 0 && PERMANENT_REFRESH_CODES.has(error.oauthCode);
7729
7969
  }
7730
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
+ }
7731
8028
  /** Seconds of the canonical 5-hour session and 7-day weekly windows. */
7732
8029
  const SESSION_WINDOW_SECONDS = 300 * 60;
7733
8030
  const WEEKLY_WINDOW_SECONDS = 10080 * 60;
@@ -7851,7 +8148,7 @@ async function fetchCodexModels(session, fetchFn = hostFetch, signal, clientVers
7851
8148
  for (const entry of payload.models) {
7852
8149
  if (typeof entry.slug !== "string" || entry.slug.length === 0) continue;
7853
8150
  if (entry.visibility === "hide" || entry.visibility === "none") continue;
7854
- 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) => ({
7855
8152
  id: ReasoningEffortId(level.effort),
7856
8153
  name: effortDisplayName(level.effort),
7857
8154
  ...level.description === void 0 ? {} : { description: level.description }
@@ -7952,6 +8249,16 @@ function reconcileResponsesToolCalls(input) {
7952
8249
  return balanced;
7953
8250
  }
7954
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
+ /**
7955
8262
  * The Responses request body for one generation. A fast-tier request (the
7956
8263
  * composer Speed toggle, the codex CLI's fast mode) carries
7957
8264
  * `service_tier: priority`; the tier field is omitted entirely otherwise,
@@ -7968,7 +8275,7 @@ function codexRequestBody(options, resolved, fast) {
7968
8275
  parallel_tool_calls: true
7969
8276
  } : {},
7970
8277
  ...options.reasoningEffort !== void 0 ? { reasoning: {
7971
- effort: String(options.reasoningEffort),
8278
+ effort: codexWireEffort(String(options.reasoningEffort)),
7972
8279
  summary: "auto"
7973
8280
  } } : {},
7974
8281
  store: false,
@@ -8401,10 +8708,10 @@ function abortableDelay(ms, signal) {
8401
8708
  throwIfAborted(signal);
8402
8709
  return Promise.resolve();
8403
8710
  }
8404
- return new Promise((resolve$1, reject) => {
8711
+ return new Promise((resolve$2, reject) => {
8405
8712
  const timer = setTimeout(() => {
8406
8713
  cleanup();
8407
- resolve$1();
8714
+ resolve$2();
8408
8715
  }, ms);
8409
8716
  const onAbort = () => {
8410
8717
  cleanup();
@@ -8625,6 +8932,12 @@ var PoolHealthRegistry = class {
8625
8932
  }
8626
8933
  return earliest;
8627
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
+ }
8628
8941
  /** Drop records of one provider, or of a single account when given (auth changes). */
8629
8942
  clear(provider, account) {
8630
8943
  const prefix = account === void 0 ? `${provider}/` : `${provider}/${account}/`;
@@ -8632,6 +8945,67 @@ var PoolHealthRegistry = class {
8632
8945
  }
8633
8946
  };
8634
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
+
8635
9009
  //#endregion
8636
9010
  //#region src/providers/pool.ts
8637
9011
  /** Bound on sticky-session memory; oldest entries evict past it. */
@@ -8810,34 +9184,39 @@ var PoolAdapter = class extends LlmAdapter {
8810
9184
  ...modalities === void 0 ? {} : { inputModalities: modalities }
8811
9185
  };
8812
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
+ */
8813
9193
  async *stream(options) {
8814
9194
  const definition = (await this.pools()).get(poolKey(options.provider, options.model));
8815
9195
  if (definition === void 0) throw new LlmError(`unknown pool model "${options.model}"`, "NO_ADAPTER");
8816
9196
  const members = await this.concrete(definition.members);
8817
9197
  const candidates = await this.select(options.model, members, options.sessionId);
8818
9198
  if (candidates.length === 0) throw this.exhausted(options.model, members);
8819
- let lastError;
9199
+ const failures = [];
8820
9200
  for (const member of candidates) {
8821
9201
  const adapter = this.options.adapters[member.provider];
8822
9202
  if (adapter === void 0) continue;
8823
- const iterator = adapter.streamAccount({
9203
+ const iterator = streamAccountWithReplay(adapter, {
8824
9204
  ...options,
8825
- provider: member.provider,
8826
- model: member.model
8827
- }, member.account)[Symbol.asyncIterator]();
9205
+ provider: member.provider
9206
+ }, member.account, member.model)[Symbol.asyncIterator]();
8828
9207
  let first;
8829
9208
  try {
8830
9209
  first = await iterator.next();
8831
9210
  if (first.done === true) throw new LlmError(`${memberLabel(member)} returned an empty stream`, EMPTY_RESPONSE_CODE);
8832
9211
  } catch (error) {
8833
9212
  const classification = classifyPoolFailure(error, member.provider);
8834
- if (classification.action === "throw") throw error;
9213
+ if (classification.action === "throw" || !(error instanceof LlmError)) throw error;
8835
9214
  if ("cooldownMs" in classification) {
8836
9215
  this.options.health.markUnavailable(classification.scope === "account" ? accountKey(member.provider, member.account) : memberKey(member.provider, member.account, member.model), classification.cooldownMs, classification.reason);
8837
9216
  if (classification.reason === QUOTA_EXCEEDED_CODE || classification.reason === "RATE_LIMIT") this.options.usage.invalidate(member.provider, member.account);
8838
9217
  }
8839
9218
  this.options.onWarn(`pool "${options.model}": ${memberLabel(member)} failed before any output (${error instanceof Error ? error.message : String(error)}); trying the next member`);
8840
- lastError = error;
9219
+ failures.push(error);
8841
9220
  continue;
8842
9221
  }
8843
9222
  this.remember(options.model, options.sessionId, member);
@@ -8851,7 +9230,7 @@ var PoolAdapter = class extends LlmAdapter {
8851
9230
  }
8852
9231
  return;
8853
9232
  }
8854
- throw this.exhausted(options.model, members, lastError);
9233
+ throw this.exhausted(options.model, members, failures);
8855
9234
  }
8856
9235
  /**
8857
9236
  * Order the candidates for one request. Health filters both strategies;
@@ -8892,12 +9271,9 @@ var PoolAdapter = class extends LlmAdapter {
8892
9271
  }
8893
9272
  this.sticky.set(key, memberKey(member.provider, member.account, member.model));
8894
9273
  }
8895
- /**
8896
- * The error for an exhausted pool, carrying the earliest recovery hint of
8897
- * THIS pool's members (the health registry is shared across pools, so the
8898
- * hint is scoped to the keys this pool can actually recover through).
8899
- */
8900
- 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");
8901
9277
  const keys = /* @__PURE__ */ new Set();
8902
9278
  for (const member of pool) {
8903
9279
  keys.add(memberKey(member.provider, member.account, member.model));
@@ -8905,12 +9281,53 @@ var PoolAdapter = class extends LlmAdapter {
8905
9281
  }
8906
9282
  const recovery = this.options.health.earliestRecovery(keys);
8907
9283
  const retryAfterMs$1 = recovery === void 0 ? void 0 : Math.max(recovery - Date.now(), 1);
8908
- 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), {
8909
9300
  ...retryAfterMs$1 === void 0 ? {} : { providerRetryAfterMs: retryAfterMs$1 },
8910
9301
  ...cause === void 0 ? {} : { cause }
8911
9302
  });
8912
9303
  }
8913
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
+ }
8914
9331
  function stickyKey(poolId, sessionId) {
8915
9332
  return `${String(sessionId)}|${poolId}`;
8916
9333
  }
@@ -9067,10 +9484,7 @@ var AccountPreferencesAdapter = class extends LlmAdapter {
9067
9484
  const independent = parseAccountModelId(options.model);
9068
9485
  if (independent) {
9069
9486
  await this.requireAccount(independent.account, independent.model, true);
9070
- yield* this.options.adapter.streamAccount({
9071
- ...options,
9072
- model: independent.model
9073
- }, independent.account);
9487
+ yield* streamAccountWithReplay(this.options.adapter, options, independent.account, independent.model);
9074
9488
  return;
9075
9489
  }
9076
9490
  const pool = this.options.pool();
@@ -9078,7 +9492,7 @@ var AccountPreferencesAdapter = class extends LlmAdapter {
9078
9492
  yield* pool.stream(options);
9079
9493
  return;
9080
9494
  }
9081
- yield* this.options.adapter.streamAccount(options, await this.fallback(options.model));
9495
+ yield* streamAccountWithReplay(this.options.adapter, options, await this.fallback(options.model));
9082
9496
  }
9083
9497
  };
9084
9498
 
@@ -9227,6 +9641,11 @@ const FALLBACK_HORIZON_MS = {
9227
9641
  var PoolUsageTracker = class {
9228
9642
  entries = /* @__PURE__ */ new Map();
9229
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();
9230
9649
  constructor(fetcherFor, ttlMs = USAGE_TTL_MS) {
9231
9650
  this.fetcherFor = fetcherFor;
9232
9651
  this.ttlMs = ttlMs;
@@ -9309,13 +9728,25 @@ var PoolUsageTracker = class {
9309
9728
  throw error;
9310
9729
  }
9311
9730
  }
9312
- /** 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
+ */
9313
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
+ };
9314
9745
  if (account !== void 0) {
9315
- this.entries.delete(`${provider}/${account}`);
9746
+ drop(`${provider}/${account}`);
9316
9747
  return;
9317
9748
  }
9318
- 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);
9319
9750
  }
9320
9751
  /**
9321
9752
  * Run (or join) the single in-flight fetch for one account key, caching
@@ -9330,6 +9761,8 @@ var PoolUsageTracker = class {
9330
9761
  if (pending === void 0) {
9331
9762
  const prior = this.entries.get(key);
9332
9763
  const lastSnapshot = prior?.snapshot ?? prior?.lastSnapshot;
9764
+ const generation = this.generation.get(key) ?? 0;
9765
+ const sequence = ++this.nextSequence;
9333
9766
  pending = fetcher().then((value) => {
9334
9767
  const at = Date.now();
9335
9768
  const snapshot = {
@@ -9337,21 +9770,27 @@ var PoolUsageTracker = class {
9337
9770
  observedAt: at,
9338
9771
  stale: false
9339
9772
  };
9340
- this.entries.set(key, {
9341
- snapshot,
9342
- at
9343
- });
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
+ }
9344
9780
  return snapshot;
9345
9781
  }, (error) => {
9346
- if (!isMissingOrInvalidCredential(error)) this.entries.set(key, {
9347
- error,
9348
- at: Date.now(),
9349
- cooldownMs: cooldownFor(error, this.ttlMs),
9350
- ...lastSnapshot === void 0 ? {} : { lastSnapshot }
9351
- });
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
+ }
9352
9791
  throw error;
9353
9792
  }).finally(() => {
9354
- this.inflight.delete(key);
9793
+ if (this.inflight.get(key) === pending) this.inflight.delete(key);
9355
9794
  });
9356
9795
  this.inflight.set(key, pending);
9357
9796
  }
@@ -9433,8 +9872,8 @@ const GROK_RESET_FIELDS = [
9433
9872
  * whose values are rollover durations (`6m0s`) present on every response, one
9434
9873
  * per bucket — on a 429 the earliest of them is usually a bucket with room
9435
9874
  * (`0s` for the request bucket while the token bucket is the one exhausted),
9436
- * which would burn the whole retry budget in seconds. They reach the operator
9437
- * 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.
9438
9877
  */
9439
9878
  const grokRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), GROK_RESET_FIELDS, now);
9440
9879
  /** A discovered URL must be https on x.ai or a subdomain; anything else is a hostile document. */
@@ -10382,7 +10821,7 @@ async function* streamChatCompletions(stream, onActivity) {
10382
10821
  try {
10383
10822
  event = JSON.parse(sseEvent.data);
10384
10823
  } catch {
10385
- 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");
10386
10825
  }
10387
10826
  yield* translator.push(event);
10388
10827
  if (translator.terminated) return;
@@ -10969,13 +11408,16 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
10969
11408
  * account identity is the session's long-lived GitHub token (stable across
10970
11409
  * Copilot-token refreshes, different per GitHub login); the conversation is
10971
11410
  * the loop-stamped `sessionId`, falling back to the first message's id
10972
- * 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
10973
11413
  * wire families. A call id captured in one scope is invisible to every
10974
11414
  * other scope, so reused ids cannot leak reasoning across accounts,
10975
11415
  * conversations, or models.
10976
11416
  */
10977
11417
  replayScope(tokenKey, options) {
10978
- 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}`;
10979
11421
  }
10980
11422
  /**
10981
11423
  * Store one response's completed reasoning items behind every call id it
@@ -10988,6 +11430,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
10988
11430
  * per call.
10989
11431
  */
10990
11432
  captureReasoning(scope, callIds, items) {
11433
+ if (scope === void 0) return;
10991
11434
  let entries = this.replayByScope.get(scope);
10992
11435
  if (entries === void 0) {
10993
11436
  entries = /* @__PURE__ */ new Map();
@@ -11023,6 +11466,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
11023
11466
  * degradation, never an error.
11024
11467
  */
11025
11468
  replayFor(scope, callId) {
11469
+ if (scope === void 0) return void 0;
11026
11470
  const entries = this.replayByScope.get(scope);
11027
11471
  const entry = entries?.get(callId);
11028
11472
  if (entries === void 0 || entry === void 0) return void 0;
@@ -11178,8 +11622,8 @@ function object(value) {
11178
11622
  return typeof value === "object" && value !== null && !Array.isArray(value);
11179
11623
  }
11180
11624
  /** Resolve local references before removing definitions; reject cycles and remote references. */
11181
- function resolve(value, root, refs = /* @__PURE__ */ new Set()) {
11182
- 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));
11183
11627
  if (!object(value)) return value;
11184
11628
  if (typeof value.$ref === "string") {
11185
11629
  const ref = value.$ref;
@@ -11191,7 +11635,7 @@ function resolve(value, root, refs = /* @__PURE__ */ new Set()) {
11191
11635
  }
11192
11636
  if (!object(target)) throw new LlmError(`Antigravity tool schema reference is missing: ${ref}`, "INVALID_REQUEST");
11193
11637
  const { $ref: _ref,...siblings } = value;
11194
- return resolve({
11638
+ return resolve$1({
11195
11639
  ...target,
11196
11640
  ...siblings
11197
11641
  }, root, new Set([...refs, ref]));
@@ -11201,7 +11645,7 @@ function resolve(value, root, refs = /* @__PURE__ */ new Set()) {
11201
11645
  "$id",
11202
11646
  "$defs",
11203
11647
  "definitions"
11204
- ].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)]));
11205
11649
  }
11206
11650
  /** Numeric types widen to `number`: every integer is also a number. */
11207
11651
  const NUMERIC = new Set(["integer", "number"]);
@@ -11287,7 +11731,7 @@ function custom(value) {
11287
11731
  }
11288
11732
  /** Detach and normalize one tool's root object without changing the registry schema. */
11289
11733
  function antigravityToolParameters(parameters, legacy) {
11290
- const resolved = resolve(parameters, parameters);
11734
+ const resolved = resolve$1(parameters, parameters);
11291
11735
  const root = {
11292
11736
  ...resolved,
11293
11737
  type: resolved.type ?? "object"
@@ -11303,9 +11747,11 @@ function antigravityToolParameters(parameters, legacy) {
11303
11747
  * `FunctionResponse.response` field requires. That field maps to a singular
11304
11748
  * protobuf `Struct`, so a bare array, string, number or boolean is rejected
11305
11749
  * by the endpoint with "Proto field is not repeating, cannot start list".
11306
- * 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
11307
11751
  * wrapped in the same "output" envelope the non-JSON path already uses --
11308
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.
11309
11755
  */
11310
11756
  function toolResultValue(block) {
11311
11757
  const text = block.content.map((part) => part.type === "text" ? part.text : "").join("");
@@ -11318,8 +11764,17 @@ function toolResultValue(block) {
11318
11764
  ...block.isError === true ? { isError: true } : {}
11319
11765
  };
11320
11766
  }
11321
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
11322
- 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
+ };
11323
11778
  }
11324
11779
  /** Safely read per-block replay metadata emitted by this adapter. */
11325
11780
  function replayBlocks(message, model) {
@@ -11369,7 +11824,8 @@ function toAntigravityContents(messages, model) {
11369
11824
  response: toolResultValue({
11370
11825
  type: "tool-result",
11371
11826
  toolCallId: ToolCallId$1(id),
11372
- content: message.content
11827
+ content: message.content,
11828
+ ...message.isError === void 0 ? {} : { isError: message.isError }
11373
11829
  })
11374
11830
  } };
11375
11831
  const previous$1 = out.at(-1);
@@ -11657,7 +12113,7 @@ async function* streamAntigravity(stream, onActivity) {
11657
12113
  try {
11658
12114
  parsed = JSON.parse(event.data);
11659
12115
  } catch {
11660
- 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");
11661
12117
  }
11662
12118
  yield* translator.push(parsed);
11663
12119
  if (translator.terminated) return;
@@ -11821,7 +12277,7 @@ async function discoverAntigravityAccount(accessToken, runtime = {}, fetchFn = h
11821
12277
  projectId = projectIdOf(result.response?.cloudaicompanionProject);
11822
12278
  break;
11823
12279
  }
11824
- await new Promise((resolve$1) => setTimeout(resolve$1, 1e3));
12280
+ await new Promise((resolve$2) => setTimeout(resolve$2, 1e3));
11825
12281
  }
11826
12282
  }
11827
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");
@@ -12847,14 +13303,14 @@ function truncate(text, max = 60) {
12847
13303
  /** Abort-aware sleep between two polls. */
12848
13304
  function sleep(ms, signal) {
12849
13305
  if (ms <= 0) return Promise.resolve();
12850
- return new Promise((resolve$1, reject) => {
13306
+ return new Promise((resolve$2, reject) => {
12851
13307
  const onAbort = () => {
12852
13308
  clearTimeout(timer);
12853
13309
  reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("video_generate: aborted"));
12854
13310
  };
12855
13311
  const timer = setTimeout(() => {
12856
13312
  signal.removeEventListener("abort", onAbort);
12857
- resolve$1();
13313
+ resolve$2();
12858
13314
  }, ms);
12859
13315
  if (signal.aborted) {
12860
13316
  onAbort();
@@ -13168,6 +13624,7 @@ const Config = z.object({
13168
13624
  ]),
13169
13625
  codexClientVersion: z.string(),
13170
13626
  streamIdleTimeoutMs: z.number().min(1).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
13627
+ claudePromptCacheTtl: z.union(["5m", "1h"]).default(DEFAULT_PROMPT_CACHE_TTL),
13171
13628
  rateLimit: z.object({
13172
13629
  wait: z.boolean().default(true),
13173
13630
  maxWaitMs: z.number().min(1).default(DEFAULT_RATE_LIMIT_MAX_WAIT_MS)
@@ -13320,16 +13777,16 @@ const DEFAULT_MODELS = {
13320
13777
  };
13321
13778
  /** Validate and detach the model catalog for every provider. */
13322
13779
  function resolveCatalog(models) {
13323
- const resolve$1 = (provider) => {
13780
+ const resolve$2 = (provider) => {
13324
13781
  const configured = models?.[provider];
13325
13782
  return validateModels(configured !== void 0 && configured.length > 0 ? configured : DEFAULT_MODELS[provider], `${name}: models.${provider}`);
13326
13783
  };
13327
13784
  return {
13328
- codex: resolve$1("codex"),
13329
- claude: resolve$1("claude"),
13330
- grok: resolve$1("grok"),
13331
- copilot: resolve$1("copilot"),
13332
- 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")
13333
13790
  };
13334
13791
  }
13335
13792
  /** The display account of a stored session, for the status endpoint. */
@@ -13399,7 +13856,7 @@ var SubscriptionsAuthController = class {
13399
13856
  * cannot be read off the flow manager.
13400
13857
  */
13401
13858
  claims = /* @__PURE__ */ new Map();
13402
- 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) {
13403
13860
  this.flows = flows;
13404
13861
  this.deviceFlows = deviceFlows;
13405
13862
  this.onAuthChanged = onAuthChanged;
@@ -13409,6 +13866,15 @@ var SubscriptionsAuthController = class {
13409
13866
  this.poolUsage = poolUsage;
13410
13867
  this.antigravityConfig = antigravityConfig;
13411
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)]));
13412
13878
  }
13413
13879
  usage(provider, account, signal, force = false) {
13414
13880
  const fetcher = this.usageFetchers[provider];
@@ -13599,8 +14065,8 @@ function presentedVersion(cache, waitMs = STATUS_VERSION_WAIT_MS) {
13599
14065
  return cache.current();
13600
14066
  }
13601
14067
  let timer;
13602
- await Promise.race([cache.resolve(), new Promise((resolve$1) => {
13603
- timer = setTimeout(resolve$1, waitMs);
14068
+ await Promise.race([cache.resolve(), new Promise((resolve$2) => {
14069
+ timer = setTimeout(resolve$2, waitMs);
13604
14070
  })]).finally(() => {
13605
14071
  clearTimeout(timer);
13606
14072
  });
@@ -13619,6 +14085,8 @@ function apply(ctx, config) {
13619
14085
  const providers = [...new Set(config.providers ?? [...PROVIDER_IDS])];
13620
14086
  const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
13621
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'`);
13622
14090
  const rateLimit = resolveRateLimitWait(config.rateLimit, `${name}: rateLimit`);
13623
14091
  const catalog = resolveCatalog(config.models);
13624
14092
  const overridden = new Set(PROVIDER_IDS.filter((provider) => (config.models?.[provider]?.length ?? 0) > 0));
@@ -13686,7 +14154,22 @@ function apply(ctx, config) {
13686
14154
  });
13687
14155
  codexTokens = tokens;
13688
14156
  accountTokens.set("codex", tokens);
13689
- 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
+ };
13690
14173
  let adapter;
13691
14174
  adapter = new CodexAdapter({
13692
14175
  ...config.codexClientVersion === void 0 ? {} : { clientVersion: config.codexClientVersion },
@@ -13737,6 +14220,7 @@ function apply(ctx, config) {
13737
14220
  catalogStore: catalogStore("claude"),
13738
14221
  defaultEffortOf: (model) => defaultEffortOf("claude", model),
13739
14222
  resolveCliVersion: () => claudeVersion.resolve(),
14223
+ promptCacheTtl: claudePromptCacheTtl,
13740
14224
  pool: () => poolAdapter
13741
14225
  });
13742
14226
  adapters.set("claude", adapter);
@@ -13844,8 +14328,8 @@ function apply(ctx, config) {
13844
14328
  const fetcherFor = (provider, account) => {
13845
14329
  switch (provider) {
13846
14330
  case "codex": {
13847
- const tokens = codexTokens;
13848
- 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));
13849
14333
  }
13850
14334
  case "claude": {
13851
14335
  const tokens = claudeTokens;
@@ -14020,13 +14504,20 @@ function apply(ctx, config) {
14020
14504
  cursorHandle = credentialsCtx.llm.registerAdapter(["cursor-subscription"], cursorAdapter);
14021
14505
  });
14022
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));
14023
14514
  registerAuthRpc(ctx, new SubscriptionsAuthController(flows, deviceFlows, authChanged, resolveAttachments, usageFetchers, void 0, poolUsage, config.antigravity, {
14024
14515
  ...providers.includes("codex") ? { codex: config.codexClientVersion === void 0 ? presentedVersion(codexVersion) : async () => ({
14025
14516
  version: config.codexClientVersion,
14026
14517
  source: "config"
14027
14518
  }) } : {},
14028
14519
  ...providers.includes("claude") ? { claude: presentedVersion(claudeVersion) } : {}
14029
- }), speed, modelDefaults, {
14520
+ }, resetRedemption), speed, modelDefaults, {
14030
14521
  async get(provider, force) {
14031
14522
  await loadModelDefaults();
14032
14523
  const adapter = adapters.get(provider);