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/AGENTS.md +4 -4
- package/README.md +34 -42
- package/README.zh-CN.md +34 -42
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/lib/client.js +354 -201
- package/lib/index.js +478 -45
- package/package.json +5 -1
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.
|
|
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
|
|
1503
|
-
|
|
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
|
|
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)
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
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
|
-
|
|
2370
|
+
retryable = true;
|
|
2371
|
+
throw new Error(UNCERTAIN_RESET_RESULT);
|
|
1971
2372
|
}
|
|
1972
|
-
|
|
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
|
-
|
|
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 =
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
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(() =>
|
|
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 };
|