dsh-codex-subscription 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -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.0";
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`;
@@ -2011,8 +2371,22 @@ const publicError = (code, message) => ({
2011
2371
  details: { issues: [] }
2012
2372
  }
2013
2373
  });
2014
- function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, diagnosticsReader, modelCatalog }) {
2374
+ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal }) {
2015
2375
  return async (endpoint, payload, signal) => {
2376
+ if (endpoint === "image/original/chunk") try {
2377
+ signal.throwIfAborted();
2378
+ 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");
2379
+ const inherited = resolveInheritedOriginal?.(payload.sessionId, payload.assetId);
2380
+ const chunk = await originalImages?.chunk(payload.sessionId, payload.assetId, payload.offset, inherited);
2381
+ if (chunk === void 0) return publicError("not-found", "Original image is unavailable");
2382
+ return {
2383
+ ok: true,
2384
+ value: chunk
2385
+ };
2386
+ } catch (error) {
2387
+ if (signal.aborted) throw error;
2388
+ return publicError("internal", "Could not read the original image");
2389
+ }
2016
2390
  if (endpoint === "diagnostics") try {
2017
2391
  signal.throwIfAborted();
2018
2392
  return {
@@ -2031,7 +2405,8 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
2031
2405
  if (![
2032
2406
  "off",
2033
2407
  "percent",
2034
- "bar"
2408
+ "bar",
2409
+ "forecast"
2035
2410
  ].includes(payload["quickQuotaMode"])) return publicError("internal", "Invalid quick quota preference");
2036
2411
  patch[QUICK_QUOTA_MODE_FIELD] = payload[QUICK_QUOTA_MODE_FIELD];
2037
2412
  }
@@ -2165,7 +2540,8 @@ function apply(ctx) {
2165
2540
  [QUICK_QUOTA_MODE_FIELD]: z.union([
2166
2541
  "off",
2167
2542
  QUICK_QUOTA_MODE_PERCENT,
2168
- "bar"
2543
+ "bar",
2544
+ QUICK_QUOTA_MODE_FORECAST
2169
2545
  ]),
2170
2546
  [LEGACY_QUICK_QUOTA_FIELD]: z.boolean(),
2171
2547
  [SEARCH_PROVIDER_FIELD]: z.union([
@@ -2190,6 +2566,7 @@ function apply(ctx) {
2190
2566
  }));
2191
2567
  const searchProvider = createSearchProviderSwitcher(ctx.loader);
2192
2568
  const network = createCodexNetworkTransport();
2569
+ const originalImages = new OriginalImageStore();
2193
2570
  const store = new DshOAuthCredentialStore(ctx.credentials, CREDENTIAL_REF, [LEGACY_CREDENTIAL_REF]);
2194
2571
  const baseProvider = createOpenAICodexProvider();
2195
2572
  let resolveAuth = async () => void 0;
@@ -2265,6 +2642,7 @@ function apply(ctx) {
2265
2642
  getAuth: resolveAuth,
2266
2643
  readCredential: (options) => store.read(PROVIDER, options),
2267
2644
  attachments: ctx.attachments,
2645
+ originalImages,
2268
2646
  fetch: (input, init) => network.fetch("image", input, init)
2269
2647
  }));
2270
2648
  const adapter = new PiAiAdapter({
@@ -2313,10 +2691,13 @@ function apply(ctx) {
2313
2691
  }, "codex-subscription: search provider selection");
2314
2692
  const auth = createCodexAuthService(authModels, store, { runLogin: (operation) => network.run("login", operation) });
2315
2693
  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)
2694
+ const usageReader = createQuotaForecastReader({
2695
+ reader: createCodexUsageReader({
2696
+ getAuth: resolveAuth,
2697
+ readCredential: (options) => store.read(PROVIDER, options),
2698
+ fetch: (input, init) => network.fetch("quota", input, init)
2699
+ }),
2700
+ enabled: () => normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]) === QUICK_QUOTA_MODE_FORECAST
2320
2701
  });
2321
2702
  const resetCreditService = createCodexResetCreditService({
2322
2703
  getAuth: resolveAuth,
@@ -2335,12 +2716,14 @@ function apply(ctx) {
2335
2716
  login: coordinator.supportState(),
2336
2717
  network
2337
2718
  }),
2338
- modelCatalog
2719
+ modelCatalog,
2720
+ originalImages,
2721
+ resolveInheritedOriginal: (sessionId, assetId) => inheritedOriginalImageRef(ctx.get?.("sessions")?.get?.(sessionId), assetId)
2339
2722
  });
2340
2723
  ctx.effect(() => {
2341
2724
  modelCatalog.refresh().catch((error) => ctx.logger?.debug?.("could not refresh Codex model catalog: %s", error.message));
2342
2725
  }, "codex-subscription: official model catalog");
2343
- ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" }), "codex-subscription: loopback account RPC");
2726
+ ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, handler, { authority: "trusted-host" }), "codex-subscription: DSH-trusted account RPC");
2344
2727
  }
2345
2728
  //#endregion
2346
2729
  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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Use ChatGPT and Codex subscriptions in DeepSeek Harness with OAuth, quota, safe resets, web search, images, and Fast mode",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -113,6 +113,7 @@
113
113
  "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
114
114
  "@deepseek-ai/dsh-llm-pi-ai": "0.1.1-rc.2",
115
115
  "@deepseek-ai/dsh-settings": "0.1.1-rc.2",
116
+ "@deepseek-ai/dsh-session": "0.1.1-rc.2",
116
117
  "@deepseek-ai/dsh-tools": "0.1.1-rc.2",
117
118
  "@deepseek-ai/dsh-web": "0.1.1-rc.2",
118
119
  "@deepseek-ai/schemastery": "3.18.1",
@@ -123,10 +124,13 @@
123
124
  "tsdown": "0.22.2"
124
125
  },
125
126
  "dependencies": {
127
+ "@deepseek-ai/dsh-home-paths": "0.1.1-rc.2",
126
128
  "https-proxy-agent": "7.0.6"
127
129
  },
128
130
  "scripts": {
129
131
  "build": "tsdown --config tsdown.config.mjs",
132
+ "test:behavior": "node --test tests/auth-security.test.mjs tests/cache-contract.test.mjs tests/codex-images.test.mjs tests/codex-search.test.mjs tests/image-edit.test.mjs tests/model-catalog.test.mjs tests/oauth-network.test.mjs tests/pi-ai-runtime.test.mjs tests/plugin-integration.test.mjs tests/reset-credits.test.mjs tests/sidebar-quota.test.mjs tests/transport-contract.test.mjs tests/usage.test.mjs",
133
+ "test:delivery": "node --test tests/ci-change-plan.test.mjs tests/client-contract.test.mjs tests/prepare-compat-release.test.mjs tests/publish-idempotency.test.mjs tests/release-contract.test.mjs tests/release-notes.test.mjs tests/support-intake.test.mjs",
130
134
  "test": "node --test tests/*.test.mjs",
131
135
  "check": "pnpm run test && pnpm run build && pnpm pack --pack-destination .artifacts"
132
136
  }