dsh-codex-subscription 1.10.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -11,9 +11,13 @@ import { promisify } from "node:util";
11
11
  import { HttpsProxyAgent } from "https-proxy-agent";
12
12
  import { openaiCodexProvider as createOpenAICodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
13
13
  import { createModels } from "@earendil-works/pi-ai";
14
- import { randomUUID } from "node:crypto";
14
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
15
15
  import { WebError } from "@deepseek-ai/dsh-web";
16
16
  import { defineTool } from "@deepseek-ai/dsh-tools";
17
+ import { constants } from "node:fs";
18
+ import { lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
19
+ import { dirname, join, resolve } from "node:path";
20
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
17
21
  //#region src/credential-store.js
18
22
  const PROVIDER$1 = "openai-codex";
19
23
  const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
@@ -725,6 +729,7 @@ const SETTINGS_NAMESPACE = "codex-subscription";
725
729
  const QUICK_QUOTA_MODE_FIELD = "quickQuotaMode";
726
730
  const LEGACY_QUICK_QUOTA_FIELD = "quickQuotaVisible";
727
731
  const QUICK_QUOTA_MODE_PERCENT = "percent";
732
+ const QUICK_QUOTA_MODE_FORECAST = "forecast";
728
733
  const SEARCH_PROVIDER_FIELD = "searchProvider";
729
734
  const SEARCH_PROVIDER_AUTO = "auto";
730
735
  const SEARCH_PROVIDER_CODEX = "codex";
@@ -816,7 +821,8 @@ function contextModelGroups(models) {
816
821
  const normalizeQuickQuotaMode = (value, legacyVisible = false) => [
817
822
  "off",
818
823
  "percent",
819
- "bar"
824
+ "bar",
825
+ "forecast"
820
826
  ].includes(value) ? value : legacyVisible === true ? QUICK_QUOTA_MODE_PERCENT : "off";
821
827
  const supportsCodexFastMode = (modelId) => typeof modelId === "string" && (/^gpt-5\.(?:5|6)(?:$|-)/u.test(modelId) || modelId === "gpt-5.4");
822
828
  //#endregion
@@ -930,7 +936,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
930
936
  }
931
937
  //#endregion
932
938
  //#region src/version.js
933
- const PACKAGE_VERSION = "1.10.0";
939
+ const PACKAGE_VERSION = "1.11.1";
934
940
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
935
941
  //#endregion
936
942
  //#region src/model-catalog.js
@@ -946,7 +952,7 @@ const LEVELS = [
946
952
  ];
947
953
  const record$4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
948
954
  const nonEmpty$2 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
949
- const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0 ? value : void 0;
955
+ const positiveInteger$1 = (value) => Number.isSafeInteger(value) && value > 0 ? value : void 0;
950
956
  function reasoningMap(levels) {
951
957
  const supported = new Set((Array.isArray(levels) ? levels : []).map((level) => nonEmpty$2(record$4(level) ? level.effort : void 0)).filter(Boolean));
952
958
  const map = Object.fromEntries(LEVELS.map((level) => [level, null]));
@@ -966,7 +972,7 @@ function visibleModel(value) {
966
972
  description: nonEmpty$2(value.description),
967
973
  priority: Number.isFinite(value.priority) ? value.priority : 0,
968
974
  input: input.length > 0 ? input : ["text"],
969
- contextWindow: positiveInteger(value.context_window) ?? positiveInteger(value.max_context_window),
975
+ contextWindow: positiveInteger$1(value.context_window) ?? positiveInteger$1(value.max_context_window),
970
976
  reasoning: supported.length > 0,
971
977
  thinkingLevelMap: reasoningMap(supported),
972
978
  supportVerbosity: value.support_verbosity === true,
@@ -1192,6 +1198,42 @@ function createCodexAutoSearchProvider(options) {
1192
1198
  }
1193
1199
  });
1194
1200
  }
1201
+ const ORIGINAL_IMAGE_CHUNK_BYTES = 4 * 1024 * 1024;
1202
+ const ORIGINAL_IMAGE_ID_PATTERN = /^img_[0-9a-f]{32}$/u;
1203
+ const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
1204
+ function decodeOriginalImageRef(value) {
1205
+ if (value === null || typeof value !== "object" || Array.isArray(value) || typeof value.assetId !== "string" || !ORIGINAL_IMAGE_ID_PATTERN.test(value.assetId) || value.mediaType !== "image/png" || !positiveInteger(value.bytes) || value.bytes > 48 * 1024 * 1024 || !positiveInteger(value.width) || !positiveInteger(value.height) || typeof value.name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value.name) || typeof value.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(value.sha256)) return void 0;
1206
+ return {
1207
+ assetId: value.assetId,
1208
+ mediaType: value.mediaType,
1209
+ bytes: value.bytes,
1210
+ width: value.width,
1211
+ height: value.height,
1212
+ name: value.name,
1213
+ sha256: value.sha256
1214
+ };
1215
+ }
1216
+ function decodeImagePresentation(value) {
1217
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value.kind !== "codex-subscription-image" || value.schemaVersion !== 1) return void 0;
1218
+ const original = decodeOriginalImageRef(value.original);
1219
+ return original === void 0 ? void 0 : { original };
1220
+ }
1221
+ function originalImageRefsEqual(left, right) {
1222
+ const a = decodeOriginalImageRef(left);
1223
+ const b = decodeOriginalImageRef(right);
1224
+ return a !== void 0 && b !== void 0 && a.assetId === b.assetId && a.mediaType === b.mediaType && a.bytes === b.bytes && a.width === b.width && a.height === b.height && a.name === b.name && a.sha256 === b.sha256;
1225
+ }
1226
+ /** Resolve only an exact original reference copied into a DSH fork prefix. */
1227
+ function inheritedOriginalImageRef(session, assetId) {
1228
+ const parentSession = session?.header?.parentSession;
1229
+ const seedLength = session?.header?.seedLength;
1230
+ if (typeof parentSession !== "string" || parentSession.length === 0 || !Number.isSafeInteger(seedLength) || seedLength < 0 || !Array.isArray(session?.events) || !ORIGINAL_IMAGE_ID_PATTERN.test(assetId)) return void 0;
1231
+ for (const event of session.events) {
1232
+ if (!Number.isSafeInteger(event?.seq) || event.seq < 0 || event.seq >= seedLength || event.type !== "tool/result") continue;
1233
+ const original = decodeImagePresentation(event.data?.meta)?.original;
1234
+ if (original?.assetId === assetId) return original;
1235
+ }
1236
+ }
1195
1237
  //#endregion
1196
1238
  //#region src/codex-images.js
1197
1239
  const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
@@ -1360,6 +1402,42 @@ function imageOutputSchema() {
1360
1402
  name: { type: "string" }
1361
1403
  }
1362
1404
  },
1405
+ original: {
1406
+ type: "object",
1407
+ required: true,
1408
+ additionalProperties: false,
1409
+ properties: {
1410
+ assetId: {
1411
+ type: "string",
1412
+ required: true
1413
+ },
1414
+ mediaType: {
1415
+ type: "string",
1416
+ enum: ["image/png"],
1417
+ required: true
1418
+ },
1419
+ bytes: {
1420
+ type: "integer",
1421
+ required: true
1422
+ },
1423
+ width: {
1424
+ type: "integer",
1425
+ required: true
1426
+ },
1427
+ height: {
1428
+ type: "integer",
1429
+ required: true
1430
+ },
1431
+ name: {
1432
+ type: "string",
1433
+ required: true
1434
+ },
1435
+ sha256: {
1436
+ type: "string",
1437
+ required: true
1438
+ }
1439
+ }
1440
+ },
1363
1441
  background: { type: "string" },
1364
1442
  quality: { type: "string" },
1365
1443
  size: { type: "string" }
@@ -1447,7 +1525,12 @@ function createCodexImageTool(options) {
1447
1525
  },
1448
1526
  output: {
1449
1527
  schema: imageOutputSchema(),
1450
- render: (_args, value) => imageContent(value)
1528
+ render: (_args, value) => imageContent(value),
1529
+ presentationMeta: (_args, value) => ({
1530
+ kind: "codex-subscription-image",
1531
+ schemaVersion: 1,
1532
+ original: value.original
1533
+ })
1451
1534
  },
1452
1535
  timeoutMs: 300 * 1e3,
1453
1536
  isConcurrencySafe: () => false,
@@ -1499,12 +1582,23 @@ function createCodexImageTool(options) {
1499
1582
  }
1500
1583
  const metadata = responseMetadata(await readJsonWithin(response, encodedLimit(maximumBytes) + RESPONSE_ENVELOPE_BYTES));
1501
1584
  const data = decodeCodexPng(metadata.encoded, maximumBytes);
1502
- const result = {
1503
- image: imageReference(await attachments.saveImage({
1585
+ const sessionId = exec.agent?.id;
1586
+ if (sessionId === void 0) throw new Error("Codex image generation requires a session-owned tool call");
1587
+ const original = await options.originalImages.save(String(sessionId), data);
1588
+ let ref;
1589
+ try {
1590
+ ref = await attachments.saveImage({
1504
1591
  data,
1505
1592
  mediaType: "image/png",
1506
1593
  name: "codex-generated.png"
1507
- })),
1594
+ });
1595
+ } catch (error) {
1596
+ await options.originalImages.remove(original);
1597
+ throw error;
1598
+ }
1599
+ const result = {
1600
+ image: imageReference(ref),
1601
+ original,
1508
1602
  ...metadata.background === void 0 ? {} : { background: metadata.background },
1509
1603
  ...metadata.quality === void 0 ? {} : { quality: metadata.quality },
1510
1604
  ...metadata.size === void 0 ? {} : { size: metadata.size }
@@ -1521,6 +1615,142 @@ function createCodexImageTool(options) {
1521
1615
  });
1522
1616
  }
1523
1617
  //#endregion
1618
+ //#region src/image-original-store.js
1619
+ const ORIGINAL_IMAGE_DIRECTORY = "dsh-codex-subscription/images/v1";
1620
+ const METADATA_VERSION = 1;
1621
+ const digest = (data) => createHash("sha256").update(data).digest("hex");
1622
+ const validSessionId = (value) => typeof value === "string" && value.length > 0 && value.length <= 512;
1623
+ function pngDimensions(data) {
1624
+ if (!(data instanceof Uint8Array) || data.byteLength < 24 || Buffer.from(data.subarray(0, 8)).toString("hex") !== "89504e470d0a1a0a" || Buffer.from(data.subarray(12, 16)).toString("ascii") !== "IHDR") throw new TypeError("invalid PNG dimensions");
1625
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1626
+ const width = view.getUint32(16, false);
1627
+ const height = view.getUint32(20, false);
1628
+ if (width === 0 || height === 0) throw new TypeError("invalid PNG dimensions");
1629
+ return {
1630
+ width,
1631
+ height
1632
+ };
1633
+ }
1634
+ async function writeExclusive(filename, data) {
1635
+ await mkdir(dirname(filename), {
1636
+ recursive: true,
1637
+ mode: 448
1638
+ });
1639
+ const handle = await open(filename, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
1640
+ try {
1641
+ await handle.writeFile(data);
1642
+ await handle.sync();
1643
+ } finally {
1644
+ await handle.close();
1645
+ }
1646
+ }
1647
+ async function assertPrivateFile(filename) {
1648
+ const stat = await lstat(filename);
1649
+ if (!stat.isFile()) throw new Error("not a regular file");
1650
+ if (process.platform !== "win32" && (stat.mode & 63) !== 0) throw new Error("file is not owner-only");
1651
+ }
1652
+ function parseMetadata(text) {
1653
+ let value;
1654
+ try {
1655
+ value = JSON.parse(text);
1656
+ } catch {
1657
+ return;
1658
+ }
1659
+ if (value?.version !== METADATA_VERSION || !validSessionId(value.sessionId)) return void 0;
1660
+ const image = decodeOriginalImageRef(value.image);
1661
+ return image === void 0 ? void 0 : {
1662
+ sessionId: value.sessionId,
1663
+ image
1664
+ };
1665
+ }
1666
+ var OriginalImageStore = class {
1667
+ constructor(dshHome) {
1668
+ this.root = resolve(join(resolveDshHome(dshHome), ORIGINAL_IMAGE_DIRECTORY));
1669
+ }
1670
+ directory(assetId) {
1671
+ if (!ORIGINAL_IMAGE_ID_PATTERN.test(assetId)) throw new TypeError("invalid original image asset id");
1672
+ return join(this.root, assetId.slice(4, 6), assetId);
1673
+ }
1674
+ async save(sessionId, data, name = "codex-generated-original.png") {
1675
+ if (!validSessionId(sessionId) || !(data instanceof Uint8Array) || data.byteLength === 0 || data.byteLength > 48 * 1024 * 1024) throw new TypeError("invalid original image input");
1676
+ const { width, height } = pngDimensions(data);
1677
+ const assetId = `img_${randomBytes(16).toString("hex")}`;
1678
+ const directory = this.directory(assetId);
1679
+ const ref = {
1680
+ assetId,
1681
+ mediaType: "image/png",
1682
+ bytes: data.byteLength,
1683
+ width,
1684
+ height,
1685
+ name,
1686
+ sha256: digest(data)
1687
+ };
1688
+ try {
1689
+ await mkdir(dirname(directory), {
1690
+ recursive: true,
1691
+ mode: 448
1692
+ });
1693
+ await mkdir(directory, {
1694
+ recursive: false,
1695
+ mode: 448
1696
+ });
1697
+ await writeExclusive(join(directory, "original"), data);
1698
+ const temporary = join(directory, `metadata.${randomBytes(8).toString("hex")}.tmp`);
1699
+ await writeExclusive(temporary, Buffer.from(`${JSON.stringify({
1700
+ version: METADATA_VERSION,
1701
+ sessionId,
1702
+ image: ref
1703
+ }, null, 2)}\n`));
1704
+ await rename(temporary, join(directory, "metadata.json"));
1705
+ return ref;
1706
+ } catch (error) {
1707
+ await rm(directory, {
1708
+ recursive: true,
1709
+ force: true
1710
+ }).catch(() => void 0);
1711
+ throw error;
1712
+ }
1713
+ }
1714
+ async remove(ref) {
1715
+ if (ref !== void 0 && ORIGINAL_IMAGE_ID_PATTERN.test(ref.assetId)) await rm(this.directory(ref.assetId), {
1716
+ recursive: true,
1717
+ force: true
1718
+ }).catch(() => void 0);
1719
+ }
1720
+ async read(sessionId, assetId, inherited) {
1721
+ if (!validSessionId(sessionId) || !ORIGINAL_IMAGE_ID_PATTERN.test(assetId)) return void 0;
1722
+ try {
1723
+ const directory = this.directory(assetId);
1724
+ const metadataFile = join(directory, "metadata.json");
1725
+ const originalFile = join(directory, "original");
1726
+ await Promise.all([assertPrivateFile(metadataFile), assertPrivateFile(originalFile)]);
1727
+ const metadata = parseMetadata(await readFile(metadataFile, "utf8"));
1728
+ if (metadata === void 0 || metadata.image.assetId !== assetId || metadata.sessionId !== sessionId && !originalImageRefsEqual(metadata.image, inherited)) return void 0;
1729
+ const data = new Uint8Array(await readFile(originalFile));
1730
+ const dimensions = pngDimensions(data);
1731
+ if (data.byteLength !== metadata.image.bytes || digest(data) !== metadata.image.sha256 || dimensions.width !== metadata.image.width || dimensions.height !== metadata.image.height) return void 0;
1732
+ return {
1733
+ ref: metadata.image,
1734
+ data
1735
+ };
1736
+ } catch {
1737
+ return;
1738
+ }
1739
+ }
1740
+ async chunk(sessionId, assetId, offset, inherited) {
1741
+ if (!Number.isSafeInteger(offset) || offset < 0) return void 0;
1742
+ const stored = await this.read(sessionId, assetId, inherited);
1743
+ if (stored === void 0 || offset >= stored.data.byteLength || offset % 4194304 !== 0) return void 0;
1744
+ const end = Math.min(stored.data.byteLength, offset + ORIGINAL_IMAGE_CHUNK_BYTES);
1745
+ return {
1746
+ ref: stored.ref,
1747
+ offset,
1748
+ encoded: Buffer.from(stored.data.subarray(offset, end)).toString("base64"),
1749
+ done: end === stored.data.byteLength
1750
+ };
1751
+ }
1752
+ };
1753
+ //#endregion
1524
1754
  //#region src/diagnostics.js
1525
1755
  const requestAreas = /* @__PURE__ */ new Set([
1526
1756
  "login",
@@ -1789,6 +2019,136 @@ function createCodexUsageReader(options) {
1789
2019
  });
1790
2020
  }
1791
2021
  //#endregion
2022
+ //#region src/quota-forecast.js
2023
+ const HOUR_MS = 3600 * 1e3;
2024
+ const HISTORY_MS = 24 * HOUR_MS;
2025
+ const MIN_SPAN_MS = 1800 * 1e3;
2026
+ const MIN_CONSUMED_PERCENT = 1;
2027
+ const PLATEAU_SAMPLE_MS = 900 * 1e3;
2028
+ const finite = (value) => Number.isFinite(Number(value));
2029
+ const clampPercent = (value) => Math.max(0, Math.min(100, Number(value)));
2030
+ const keyFor = (window) => `codex:${Number(window.windowSeconds) || "limit"}`;
2031
+ function observeQuotaForecast(state, windows, now = Date.now()) {
2032
+ const next = { windows: { ...state?.windows ?? {} } };
2033
+ let changed = false;
2034
+ for (const window of windows ?? []) {
2035
+ if (!finite(window?.remainingPercent)) continue;
2036
+ const key = keyFor(window);
2037
+ const resetsAt = finite(window.resetsAt) ? Number(window.resetsAt) : null;
2038
+ const remainingPercent = Math.round(clampPercent(window.remainingPercent) * 1e4) / 1e4;
2039
+ const previous = next.windows[key];
2040
+ const resetChanged = previous !== void 0 && (previous.resetsAt === null !== (resetsAt === null) || previous.resetsAt !== null && Math.abs(previous.resetsAt - resetsAt) > 300);
2041
+ const last = previous?.samples?.at(-1);
2042
+ const quotaIncreased = last !== void 0 && remainingPercent > last.remainingPercent + .5;
2043
+ const record = resetChanged || quotaIncreased ? {
2044
+ resetsAt,
2045
+ samples: []
2046
+ } : {
2047
+ resetsAt,
2048
+ samples: [...previous?.samples ?? []]
2049
+ };
2050
+ const latest = record.samples.at(-1);
2051
+ if (latest === void 0 || now > latest.at && (Math.abs(remainingPercent - latest.remainingPercent) >= .001 || now - latest.at >= PLATEAU_SAMPLE_MS)) {
2052
+ record.samples.push({
2053
+ at: now,
2054
+ remainingPercent
2055
+ });
2056
+ record.samples = record.samples.filter((sample) => sample.at >= now - HISTORY_MS).slice(-192);
2057
+ changed = true;
2058
+ }
2059
+ next.windows[key] = record;
2060
+ }
2061
+ return {
2062
+ state: next,
2063
+ changed
2064
+ };
2065
+ }
2066
+ function estimateQuotaForecast(state, window, now = Date.now()) {
2067
+ if (!finite(window?.remainingPercent)) return { status: "calibrating" };
2068
+ const record = state?.windows?.[keyFor(window)];
2069
+ if (record === void 0) return { status: "calibrating" };
2070
+ const resetsAt = finite(window.resetsAt) ? Number(window.resetsAt) : null;
2071
+ if (record.resetsAt === null !== (resetsAt === null) || resetsAt !== null && Math.abs(record.resetsAt - resetsAt) > 300) return { status: "calibrating" };
2072
+ const samples = record.samples.filter((sample) => sample.at >= now - HISTORY_MS && sample.at <= now + 6e4);
2073
+ if (samples.length < 3) return {
2074
+ status: "calibrating",
2075
+ sampleCount: samples.length
2076
+ };
2077
+ const first = samples[0];
2078
+ const last = samples.at(-1);
2079
+ const spanMs = last.at - first.at;
2080
+ const consumedPercent = Math.max(0, first.remainingPercent - last.remainingPercent);
2081
+ if (spanMs < MIN_SPAN_MS || consumedPercent < MIN_CONSUMED_PERCENT) return {
2082
+ status: "calibrating",
2083
+ sampleCount: samples.length,
2084
+ observedSpanMs: spanMs,
2085
+ consumedPercent
2086
+ };
2087
+ const firstAt = first.at;
2088
+ const weighted = samples.map((sample) => ({
2089
+ x: (sample.at - firstAt) / HOUR_MS,
2090
+ y: first.remainingPercent - sample.remainingPercent,
2091
+ weight: Math.exp((sample.at - last.at) / (6 * HOUR_MS))
2092
+ }));
2093
+ const totalWeight = weighted.reduce((sum, point) => sum + point.weight, 0);
2094
+ const meanX = weighted.reduce((sum, point) => sum + point.x * point.weight, 0) / totalWeight;
2095
+ const meanY = weighted.reduce((sum, point) => sum + point.y * point.weight, 0) / totalWeight;
2096
+ const numerator = weighted.reduce((sum, point) => sum + point.weight * (point.x - meanX) * (point.y - meanY), 0);
2097
+ const denominator = weighted.reduce((sum, point) => sum + point.weight * (point.x - meanX) ** 2, 0);
2098
+ const pacePerHour = denominator > 0 ? numerator / denominator : 0;
2099
+ if (!Number.isFinite(pacePerHour) || pacePerHour < .02) return {
2100
+ status: "idle",
2101
+ pacePerHour: 0
2102
+ };
2103
+ const runwaySeconds = clampPercent(window.remainingPercent) / pacePerHour * 3600;
2104
+ const resetSeconds = resetsAt === null ? null : Math.max(0, resetsAt - now / 1e3);
2105
+ return {
2106
+ status: "ready",
2107
+ pacePerHour,
2108
+ runwaySeconds,
2109
+ survivesReset: resetSeconds !== null && runwaySeconds >= resetSeconds,
2110
+ sampleCount: samples.length,
2111
+ observedSpanMs: spanMs,
2112
+ consumedPercent
2113
+ };
2114
+ }
2115
+ function forecastUsage(usage, state = { windows: {} }, now = Date.now()) {
2116
+ const observed = observeQuotaForecast(state, usage?.rateLimits?.find((limit) => limit.id === "codex")?.windows ?? [], now);
2117
+ return {
2118
+ state: observed.state,
2119
+ changed: observed.changed,
2120
+ usage: {
2121
+ ...usage,
2122
+ rateLimits: (usage?.rateLimits ?? []).map((limit) => limit.id !== "codex" ? limit : {
2123
+ ...limit,
2124
+ windows: limit.windows.map((window) => ({
2125
+ ...window,
2126
+ forecast: estimateQuotaForecast(observed.state, window, now)
2127
+ }))
2128
+ })
2129
+ }
2130
+ };
2131
+ }
2132
+ function createQuotaForecastReader({ reader, enabled, now = Date.now }) {
2133
+ let state = { windows: {} };
2134
+ return Object.freeze({
2135
+ async read(options) {
2136
+ const usage = await reader.read(options);
2137
+ if (!enabled()) {
2138
+ state = { windows: {} };
2139
+ return usage;
2140
+ }
2141
+ const forecast = forecastUsage(usage, state, now());
2142
+ state = forecast.state;
2143
+ return forecast.usage;
2144
+ },
2145
+ clear() {
2146
+ state = { windows: {} };
2147
+ reader.clear();
2148
+ }
2149
+ });
2150
+ }
2151
+ //#endregion
1792
2152
  //#region src/reset-credits.js
1793
2153
  const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
1794
2154
  const CODEX_RESET_CONSUME_URL = `${CODEX_RESET_CREDITS_URL}/consume`;
@@ -1796,6 +2156,7 @@ const DEFAULT_CONFIRM_DELAY_MS = 5e3;
1796
2156
  const DEFAULT_CHALLENGE_TTL_MS = 6e4;
1797
2157
  const DEFAULT_TIMEOUT_MS = 15e3;
1798
2158
  const MAX_COPY_LENGTH = 240;
2159
+ const UNCERTAIN_RESET_RESULT = "Quota reset result is uncertain; retry this confirmation to check the same request";
1799
2160
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1800
2161
  const requestSignal = (signal, timeoutMs) => {
1801
2162
  const timeout = AbortSignal.timeout(timeoutMs);
@@ -1872,8 +2233,8 @@ function createCodexResetCreditService(options) {
1872
2233
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1873
2234
  const challenges = /* @__PURE__ */ new Map();
1874
2235
  const resolveCredentials = async (signal) => credentialsOf(await getAuth({ signal }), await readCredential({ signal }));
1875
- const readDetails = async (signal) => {
1876
- const { access, accountId } = await resolveCredentials(signal);
2236
+ const readDetails = async (signal, credentials) => {
2237
+ const { access, accountId } = credentials ?? await resolveCredentials(signal);
1877
2238
  const response = await fetchReset(CODEX_RESET_CREDITS_URL, {
1878
2239
  method: "GET",
1879
2240
  redirect: "error",
@@ -1907,7 +2268,24 @@ function createCodexResetCreditService(options) {
1907
2268
  };
1908
2269
  },
1909
2270
  async prepare({ signal } = {}) {
1910
- const { accountId, details } = await readDetails(signal);
2271
+ const credentials = await resolveCredentials(signal);
2272
+ for (const [challengeId, challenge] of challenges) {
2273
+ if (challenge.accountId !== credentials.accountId || challenge.uncertain !== true) continue;
2274
+ if (now() > challenge.expiresAt) {
2275
+ challenges.delete(challengeId);
2276
+ continue;
2277
+ }
2278
+ return {
2279
+ challengeId,
2280
+ availableCount: challenge.availableCount,
2281
+ readyAt: challenge.readyAt,
2282
+ expiresAt: challenge.expiresAt,
2283
+ ...challenge.creditExpiresAt === void 0 ? {} : { creditExpiresAt: challenge.creditExpiresAt },
2284
+ ...challenge.title === void 0 ? {} : { title: challenge.title },
2285
+ ...challenge.description === void 0 ? {} : { description: challenge.description }
2286
+ };
2287
+ }
2288
+ const { accountId, details } = await readDetails(signal, credentials);
1911
2289
  const preparedAt = now();
1912
2290
  const readyAt = preparedAt + confirmDelayMs;
1913
2291
  const expiresAt = Math.min(preparedAt + challengeTtlMs, details.creditExpiresAt ?? Number.MAX_SAFE_INTEGER);
@@ -1919,7 +2297,12 @@ function createCodexResetCreditService(options) {
1919
2297
  creditId: details.creditId,
1920
2298
  redeemRequestId: randomUUID$1(),
1921
2299
  readyAt,
1922
- expiresAt
2300
+ expiresAt,
2301
+ availableCount: details.availableCount,
2302
+ creditExpiresAt: details.creditExpiresAt,
2303
+ title: details.title,
2304
+ description: details.description,
2305
+ uncertain: false
1923
2306
  });
1924
2307
  return {
1925
2308
  challengeId,
@@ -1942,38 +2325,66 @@ function createCodexResetCreditService(options) {
1942
2325
  }
1943
2326
  if (acknowledged !== true) throw new Error("You must acknowledge that this may consume one quota reset");
1944
2327
  challenge.state = "pending";
2328
+ let retryable = challenge.uncertain === true;
1945
2329
  try {
1946
2330
  const { access, accountId } = await resolveCredentials(signal);
1947
- if (accountId !== challenge.accountId) throw new Error("The signed-in ChatGPT account changed");
1948
- const response = await fetchReset(CODEX_RESET_CONSUME_URL, {
1949
- method: "POST",
1950
- redirect: "error",
1951
- headers: {
1952
- authorization: `Bearer ${access}`,
1953
- "chatgpt-account-id": accountId,
1954
- accept: "application/json",
1955
- "content-type": "application/json",
1956
- "cache-control": "no-store",
1957
- "user-agent": USER_AGENT
1958
- },
1959
- body: JSON.stringify({
1960
- redeem_request_id: challenge.redeemRequestId,
1961
- credit_id: challenge.creditId
1962
- }),
1963
- signal: requestSignal(signal, timeoutMs)
1964
- });
1965
- if (!response.ok) throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT quota reset request failed (HTTP ${response.status})`);
2331
+ if (accountId !== challenge.accountId) {
2332
+ retryable = false;
2333
+ throw new Error("The signed-in ChatGPT account changed");
2334
+ }
2335
+ let response;
2336
+ try {
2337
+ response = await fetchReset(CODEX_RESET_CONSUME_URL, {
2338
+ method: "POST",
2339
+ redirect: "error",
2340
+ headers: {
2341
+ authorization: `Bearer ${access}`,
2342
+ "chatgpt-account-id": accountId,
2343
+ accept: "application/json",
2344
+ "content-type": "application/json",
2345
+ "cache-control": "no-store",
2346
+ "user-agent": USER_AGENT
2347
+ },
2348
+ body: JSON.stringify({
2349
+ redeem_request_id: challenge.redeemRequestId,
2350
+ credit_id: challenge.creditId
2351
+ }),
2352
+ signal: requestSignal(signal, timeoutMs)
2353
+ });
2354
+ } catch {
2355
+ retryable = true;
2356
+ throw new Error(UNCERTAIN_RESET_RESULT);
2357
+ }
2358
+ if (!response.ok) {
2359
+ if (response.status >= 500) {
2360
+ retryable = true;
2361
+ throw new Error(UNCERTAIN_RESET_RESULT);
2362
+ }
2363
+ if (response.status !== 401 && response.status !== 403) retryable = false;
2364
+ throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT quota reset request failed (HTTP ${response.status})`);
2365
+ }
1966
2366
  let raw;
1967
2367
  try {
1968
2368
  raw = await response.json();
1969
2369
  } catch {
1970
- throw new Error("ChatGPT returned an unreadable quota reset response");
2370
+ retryable = true;
2371
+ throw new Error(UNCERTAIN_RESET_RESULT);
1971
2372
  }
1972
- const result = parseConsumeResult(raw);
2373
+ let result;
2374
+ try {
2375
+ result = parseConsumeResult(raw);
2376
+ } catch {
2377
+ retryable = true;
2378
+ throw new Error(UNCERTAIN_RESET_RESULT);
2379
+ }
2380
+ retryable = false;
1973
2381
  usageReader.clear();
1974
2382
  return result;
1975
2383
  } finally {
1976
- challenges.delete(challengeId);
2384
+ if (retryable && now() <= challenge.expiresAt) {
2385
+ challenge.state = "prepared";
2386
+ challenge.uncertain = true;
2387
+ } else challenges.delete(challengeId);
1977
2388
  }
1978
2389
  },
1979
2390
  clear() {
@@ -1987,7 +2398,6 @@ const name = "codex-subscription";
1987
2398
  const inject = [
1988
2399
  "llm",
1989
2400
  "credentials",
1990
- "connection",
1991
2401
  "settings",
1992
2402
  "web",
1993
2403
  "loader",
@@ -2011,8 +2421,22 @@ const publicError = (code, message) => ({
2011
2421
  details: { issues: [] }
2012
2422
  }
2013
2423
  });
2014
- function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, diagnosticsReader, modelCatalog }) {
2424
+ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal }) {
2015
2425
  return async (endpoint, payload, signal) => {
2426
+ if (endpoint === "image/original/chunk") try {
2427
+ signal.throwIfAborted();
2428
+ if (typeof payload?.sessionId !== "string" || payload.sessionId.length === 0 || payload.sessionId.length > 512 || typeof payload?.assetId !== "string" || !ORIGINAL_IMAGE_ID_PATTERN.test(payload.assetId) || !Number.isSafeInteger(payload?.offset) || payload.offset < 0 || payload.offset % 4194304 !== 0) return publicError("invalid-input", "Invalid original image request");
2429
+ const inherited = resolveInheritedOriginal?.(payload.sessionId, payload.assetId);
2430
+ const chunk = await originalImages?.chunk(payload.sessionId, payload.assetId, payload.offset, inherited);
2431
+ if (chunk === void 0) return publicError("not-found", "Original image is unavailable");
2432
+ return {
2433
+ ok: true,
2434
+ value: chunk
2435
+ };
2436
+ } catch (error) {
2437
+ if (signal.aborted) throw error;
2438
+ return publicError("internal", "Could not read the original image");
2439
+ }
2016
2440
  if (endpoint === "diagnostics") try {
2017
2441
  signal.throwIfAborted();
2018
2442
  return {
@@ -2031,7 +2455,8 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
2031
2455
  if (![
2032
2456
  "off",
2033
2457
  "percent",
2034
- "bar"
2458
+ "bar",
2459
+ "forecast"
2035
2460
  ].includes(payload["quickQuotaMode"])) return publicError("internal", "Invalid quick quota preference");
2036
2461
  patch[QUICK_QUOTA_MODE_FIELD] = payload[QUICK_QUOTA_MODE_FIELD];
2037
2462
  }
@@ -2165,7 +2590,8 @@ function apply(ctx) {
2165
2590
  [QUICK_QUOTA_MODE_FIELD]: z.union([
2166
2591
  "off",
2167
2592
  QUICK_QUOTA_MODE_PERCENT,
2168
- "bar"
2593
+ "bar",
2594
+ QUICK_QUOTA_MODE_FORECAST
2169
2595
  ]),
2170
2596
  [LEGACY_QUICK_QUOTA_FIELD]: z.boolean(),
2171
2597
  [SEARCH_PROVIDER_FIELD]: z.union([
@@ -2190,6 +2616,7 @@ function apply(ctx) {
2190
2616
  }));
2191
2617
  const searchProvider = createSearchProviderSwitcher(ctx.loader);
2192
2618
  const network = createCodexNetworkTransport();
2619
+ const originalImages = new OriginalImageStore();
2193
2620
  const store = new DshOAuthCredentialStore(ctx.credentials, CREDENTIAL_REF, [LEGACY_CREDENTIAL_REF]);
2194
2621
  const baseProvider = createOpenAICodexProvider();
2195
2622
  let resolveAuth = async () => void 0;
@@ -2265,6 +2692,7 @@ function apply(ctx) {
2265
2692
  getAuth: resolveAuth,
2266
2693
  readCredential: (options) => store.read(PROVIDER, options),
2267
2694
  attachments: ctx.attachments,
2695
+ originalImages,
2268
2696
  fetch: (input, init) => network.fetch("image", input, init)
2269
2697
  }));
2270
2698
  const adapter = new PiAiAdapter({
@@ -2313,10 +2741,13 @@ function apply(ctx) {
2313
2741
  }, "codex-subscription: search provider selection");
2314
2742
  const auth = createCodexAuthService(authModels, store, { runLogin: (operation) => network.run("login", operation) });
2315
2743
  const coordinator = new CodexLoginCoordinator(auth);
2316
- const usageReader = createCodexUsageReader({
2317
- getAuth: resolveAuth,
2318
- readCredential: (options) => store.read(PROVIDER, options),
2319
- fetch: (input, init) => network.fetch("quota", input, init)
2744
+ const usageReader = createQuotaForecastReader({
2745
+ reader: createCodexUsageReader({
2746
+ getAuth: resolveAuth,
2747
+ readCredential: (options) => store.read(PROVIDER, options),
2748
+ fetch: (input, init) => network.fetch("quota", input, init)
2749
+ }),
2750
+ enabled: () => normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]) === QUICK_QUOTA_MODE_FORECAST
2320
2751
  });
2321
2752
  const resetCreditService = createCodexResetCreditService({
2322
2753
  getAuth: resolveAuth,
@@ -2335,12 +2766,14 @@ function apply(ctx) {
2335
2766
  login: coordinator.supportState(),
2336
2767
  network
2337
2768
  }),
2338
- modelCatalog
2769
+ modelCatalog,
2770
+ originalImages,
2771
+ resolveInheritedOriginal: (sessionId, assetId) => inheritedOriginalImageRef(ctx.get?.("sessions")?.get?.(sessionId), assetId)
2339
2772
  });
2340
2773
  ctx.effect(() => {
2341
2774
  modelCatalog.refresh().catch((error) => ctx.logger?.debug?.("could not refresh Codex model catalog: %s", error.message));
2342
2775
  }, "codex-subscription: official model catalog");
2343
- ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" }), "codex-subscription: loopback account RPC");
2776
+ ctx.inject(["connection"], (connectionContext) => connectionContext.effect(() => connectionContext.connection.rpc.handle(CHANNEL, handler, { authority: "trusted-host" }), "codex-subscription: DSH-trusted account RPC"));
2344
2777
  }
2345
2778
  //#endregion
2346
2779
  export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_RESET_CONSUME_URL, CODEX_RESET_CREDITS_URL, CODEX_USAGE_URL, CodexLoginCoordinator, DshOAuthCredentialStore, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexResetCreditService, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, normalizeContextMode, normalizeCustomContextWindow, openCodexAuthUrl, parseCodexUsage };