dsh-codex-subscription 1.2.2 → 1.4.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/AGENTS.md +5 -5
- package/README.md +99 -90
- package/README.zh-CN.md +220 -0
- package/docs/assets/composer-quota-en.png +0 -0
- package/docs/assets/settings-en.png +0 -0
- package/docs/assets/settings-focus-en.png +0 -0
- package/docs/assets/settings-focus.png +0 -0
- package/docs/assets/settings.png +0 -0
- package/lib/client.js +269 -31
- package/lib/index.js +262 -29
- package/package.json +5 -3
- package/README.en.md +0 -210
package/lib/index.js
CHANGED
|
@@ -431,13 +431,20 @@ function createCodexRpcHandler(coordinator, options = {}) {
|
|
|
431
431
|
//#endregion
|
|
432
432
|
//#region src/settings-contract.js
|
|
433
433
|
const SETTINGS_NAMESPACE = "codex-subscription";
|
|
434
|
-
const
|
|
434
|
+
const QUICK_QUOTA_MODE_FIELD = "quickQuotaMode";
|
|
435
|
+
const LEGACY_QUICK_QUOTA_FIELD = "quickQuotaVisible";
|
|
436
|
+
const QUICK_QUOTA_MODE_PERCENT = "percent";
|
|
435
437
|
const SEARCH_PROVIDER_FIELD = "searchProvider";
|
|
436
438
|
const SEARCH_PROVIDER_CODEX = "codex";
|
|
437
439
|
const SPEED_MODE_FIELD = "speedMode";
|
|
438
440
|
const SPEED_MODE_STANDARD = "standard";
|
|
439
441
|
const SPEED_MODE_FAST = "fast";
|
|
440
442
|
const DEFAULT_SPEED_MODE = SPEED_MODE_STANDARD;
|
|
443
|
+
const normalizeQuickQuotaMode = (value, legacyVisible = false) => [
|
|
444
|
+
"off",
|
|
445
|
+
"percent",
|
|
446
|
+
"bar"
|
|
447
|
+
].includes(value) ? value : legacyVisible === true ? QUICK_QUOTA_MODE_PERCENT : "off";
|
|
441
448
|
const supportsCodexFastMode = (modelId) => typeof modelId === "string" && (/^gpt-5\.(?:5|6)(?:$|-)/u.test(modelId) || modelId === "gpt-5.4");
|
|
442
449
|
//#endregion
|
|
443
450
|
//#region src/pi-ai-runtime.js
|
|
@@ -496,7 +503,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0 } = {
|
|
|
496
503
|
}
|
|
497
504
|
//#endregion
|
|
498
505
|
//#region src/version.js
|
|
499
|
-
const PACKAGE_VERSION = "1.
|
|
506
|
+
const PACKAGE_VERSION = "1.4.0";
|
|
500
507
|
const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
|
|
501
508
|
//#endregion
|
|
502
509
|
//#region src/codex-search.js
|
|
@@ -505,7 +512,7 @@ const CODEX_SEARCH_URL = "https://chatgpt.com/backend-api/codex/alpha/search";
|
|
|
505
512
|
const DEFAULT_MODEL = "gpt-5.6-luna";
|
|
506
513
|
const MAX_OUTPUT_TOKENS = 4096;
|
|
507
514
|
const MAX_SOURCE_DATE = 64;
|
|
508
|
-
const record$
|
|
515
|
+
const record$3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
509
516
|
const nonEmpty$1 = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
510
517
|
const displayText = (value) => {
|
|
511
518
|
const text = nonEmpty$1(value)?.replace(/\s+/gu, " ").trim();
|
|
@@ -518,7 +525,7 @@ const boundedDisplayText = (value, maximum) => {
|
|
|
518
525
|
return `${text.slice(0, maximum - 1)}…`;
|
|
519
526
|
};
|
|
520
527
|
function sourceOf(value) {
|
|
521
|
-
if (!record$
|
|
528
|
+
if (!record$3(value)) return void 0;
|
|
522
529
|
const url = nonEmpty$1(value.url);
|
|
523
530
|
if (url === void 0) return void 0;
|
|
524
531
|
let parsed;
|
|
@@ -539,7 +546,7 @@ function sourceOf(value) {
|
|
|
539
546
|
};
|
|
540
547
|
}
|
|
541
548
|
function parseSearchResponse(value) {
|
|
542
|
-
if (!record$
|
|
549
|
+
if (!record$3(value) || !Array.isArray(value.results)) throw new Error("Codex returned a malformed search response");
|
|
543
550
|
const seen = /* @__PURE__ */ new Set();
|
|
544
551
|
const sources = [];
|
|
545
552
|
for (const result of value.results ?? []) {
|
|
@@ -631,7 +638,7 @@ const PNG_SIGNATURE = Buffer.from([
|
|
|
631
638
|
26,
|
|
632
639
|
10
|
|
633
640
|
]);
|
|
634
|
-
const record$
|
|
641
|
+
const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
635
642
|
const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
636
643
|
function encodedLimit(decodedBytes) {
|
|
637
644
|
return Math.ceil(decodedBytes / 3) * 4;
|
|
@@ -732,7 +739,7 @@ function imageOutputSchema() {
|
|
|
732
739
|
}
|
|
733
740
|
function responseMetadata(value) {
|
|
734
741
|
const data = Array.isArray(value?.data) ? value.data[0] : void 0;
|
|
735
|
-
const encoded = record$
|
|
742
|
+
const encoded = record$2(data) ? data.b64_json : void 0;
|
|
736
743
|
if (typeof encoded !== "string") throw new Error("Codex returned no image data");
|
|
737
744
|
return {
|
|
738
745
|
encoded,
|
|
@@ -865,11 +872,11 @@ async function createSubscriptionDiagnostics({ auth, preferences }) {
|
|
|
865
872
|
//#region src/usage.js
|
|
866
873
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
867
874
|
const DEFAULT_TTL_MS = 6e4;
|
|
868
|
-
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
869
|
-
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
875
|
+
const DEFAULT_TIMEOUT_MS$1 = 15e3;
|
|
876
|
+
const record$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
870
877
|
function windowOf(value) {
|
|
871
878
|
if (value === void 0 || value === null) return void 0;
|
|
872
|
-
if (!record(value)) throw new Error("Codex returned a malformed rate-limit window");
|
|
879
|
+
if (!record$1(value)) throw new Error("Codex returned a malformed rate-limit window");
|
|
873
880
|
const used = value.used_percent;
|
|
874
881
|
const seconds = value.limit_window_seconds;
|
|
875
882
|
if (!Number.isFinite(used) || used < 0 || used > 100) throw new Error("Codex returned an invalid used percentage");
|
|
@@ -884,7 +891,7 @@ function windowOf(value) {
|
|
|
884
891
|
}
|
|
885
892
|
function limitOf(id, name, value) {
|
|
886
893
|
if (value === void 0 || value === null) return void 0;
|
|
887
|
-
if (!record(value)) throw new Error("Codex returned malformed rate-limit details");
|
|
894
|
+
if (!record$1(value)) throw new Error("Codex returned malformed rate-limit details");
|
|
888
895
|
const windows = [windowOf(value.primary_window), windowOf(value.secondary_window)].filter(Boolean);
|
|
889
896
|
return windows.length === 0 ? void 0 : {
|
|
890
897
|
id,
|
|
@@ -903,7 +910,7 @@ function epochSeconds(value, label) {
|
|
|
903
910
|
}
|
|
904
911
|
function creditsOf(value) {
|
|
905
912
|
if (value === void 0 || value === null) return void 0;
|
|
906
|
-
if (!record(value) || typeof value.has_credits !== "boolean" || typeof value.unlimited !== "boolean") throw new Error("Codex returned malformed credit details");
|
|
913
|
+
if (!record$1(value) || typeof value.has_credits !== "boolean" || typeof value.unlimited !== "boolean") throw new Error("Codex returned malformed credit details");
|
|
907
914
|
if (!value.has_credits) return void 0;
|
|
908
915
|
return {
|
|
909
916
|
unlimited: value.unlimited,
|
|
@@ -912,10 +919,10 @@ function creditsOf(value) {
|
|
|
912
919
|
}
|
|
913
920
|
function individualOf(value) {
|
|
914
921
|
if (value === void 0 || value === null) return void 0;
|
|
915
|
-
if (!record(value)) throw new Error("Codex returned malformed spend control");
|
|
922
|
+
if (!record$1(value)) throw new Error("Codex returned malformed spend control");
|
|
916
923
|
const item = value.individual_limit;
|
|
917
924
|
if (item === void 0 || item === null) return void 0;
|
|
918
|
-
if (!record(item) || !Number.isFinite(item.remaining_percent) || item.remaining_percent < 0 || item.remaining_percent > 100) throw new Error("Codex returned an invalid individual-limit percentage");
|
|
925
|
+
if (!record$1(item) || !Number.isFinite(item.remaining_percent) || item.remaining_percent < 0 || item.remaining_percent > 100) throw new Error("Codex returned an invalid individual-limit percentage");
|
|
919
926
|
const resetsAt = epochSeconds(item.reset_at, "individual-limit reset time");
|
|
920
927
|
return {
|
|
921
928
|
limit: decimal(item.limit, "individual limit"),
|
|
@@ -926,19 +933,19 @@ function individualOf(value) {
|
|
|
926
933
|
}
|
|
927
934
|
function spendControlReachedOf(value) {
|
|
928
935
|
if (value === void 0 || value === null) return void 0;
|
|
929
|
-
if (!record(value)) throw new Error("Codex returned malformed spend control");
|
|
936
|
+
if (!record$1(value)) throw new Error("Codex returned malformed spend control");
|
|
930
937
|
if (value.reached === void 0 || value.reached === null) return void 0;
|
|
931
938
|
if (typeof value.reached !== "boolean") throw new Error("Codex returned an invalid spend-control state");
|
|
932
939
|
return value.reached;
|
|
933
940
|
}
|
|
934
941
|
function resetCreditsOf(value) {
|
|
935
942
|
if (value === void 0 || value === null) return void 0;
|
|
936
|
-
if (!record(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0) throw new Error("Codex returned malformed reset credit details");
|
|
943
|
+
if (!record$1(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0) throw new Error("Codex returned malformed reset credit details");
|
|
937
944
|
return { availableCount: value.available_count };
|
|
938
945
|
}
|
|
939
946
|
/** Reduce the provider payload to a browser-safe quota projection. */
|
|
940
947
|
function parseCodexUsage(value) {
|
|
941
|
-
if (!record(value)) throw new Error("Codex returned a malformed usage response");
|
|
948
|
+
if (!record$1(value)) throw new Error("Codex returned a malformed usage response");
|
|
942
949
|
const rateLimits = [];
|
|
943
950
|
const seenLimitIds = /* @__PURE__ */ new Set();
|
|
944
951
|
const addLimit = (limit) => {
|
|
@@ -949,7 +956,7 @@ function parseCodexUsage(value) {
|
|
|
949
956
|
addLimit(limitOf("codex", "Codex", value.rate_limit));
|
|
950
957
|
if (value.additional_rate_limits !== void 0 && value.additional_rate_limits !== null && !Array.isArray(value.additional_rate_limits)) throw new Error("Codex returned malformed additional rate limits");
|
|
951
958
|
for (const entry of value.additional_rate_limits ?? []) {
|
|
952
|
-
if (!record(entry) || typeof entry.metered_feature !== "string" || entry.metered_feature.length === 0) throw new Error("Codex returned a malformed additional rate limit");
|
|
959
|
+
if (!record$1(entry) || typeof entry.metered_feature !== "string" || entry.metered_feature.length === 0) throw new Error("Codex returned a malformed additional rate limit");
|
|
953
960
|
if (entry.limit_name !== void 0 && entry.limit_name !== null && typeof entry.limit_name !== "string") throw new Error("Codex returned an invalid additional rate-limit name");
|
|
954
961
|
addLimit(limitOf(entry.metered_feature, entry.limit_name || void 0, entry.rate_limit));
|
|
955
962
|
}
|
|
@@ -966,7 +973,7 @@ function parseCodexUsage(value) {
|
|
|
966
973
|
...resetCredits === void 0 ? {} : { resetCredits }
|
|
967
974
|
};
|
|
968
975
|
}
|
|
969
|
-
const requestSignal = (signal, timeoutMs) => {
|
|
976
|
+
const requestSignal$1 = (signal, timeoutMs) => {
|
|
970
977
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
971
978
|
return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
|
|
972
979
|
};
|
|
@@ -981,7 +988,7 @@ function createCodexUsageReader(options) {
|
|
|
981
988
|
const fetchUsage = options.fetch ?? fetch;
|
|
982
989
|
const now = options.now ?? Date.now;
|
|
983
990
|
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
984
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
991
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
|
|
985
992
|
let cached;
|
|
986
993
|
let inFlight;
|
|
987
994
|
let generation = 0;
|
|
@@ -1001,7 +1008,7 @@ function createCodexUsageReader(options) {
|
|
|
1001
1008
|
"cache-control": "no-store",
|
|
1002
1009
|
"user-agent": USER_AGENT
|
|
1003
1010
|
},
|
|
1004
|
-
signal: requestSignal(signal, timeoutMs)
|
|
1011
|
+
signal: requestSignal$1(signal, timeoutMs)
|
|
1005
1012
|
});
|
|
1006
1013
|
if (!response.ok) throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT usage request failed (HTTP ${response.status})`);
|
|
1007
1014
|
let value;
|
|
@@ -1037,6 +1044,185 @@ function createCodexUsageReader(options) {
|
|
|
1037
1044
|
});
|
|
1038
1045
|
}
|
|
1039
1046
|
//#endregion
|
|
1047
|
+
//#region src/reset-credits.js
|
|
1048
|
+
const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
1049
|
+
const CODEX_RESET_CONSUME_URL = `${CODEX_RESET_CREDITS_URL}/consume`;
|
|
1050
|
+
const RESET_CONFIRM_PHRASE = "USE RESET";
|
|
1051
|
+
const DEFAULT_CONFIRM_DELAY_MS = 5e3;
|
|
1052
|
+
const DEFAULT_CHALLENGE_TTL_MS = 6e4;
|
|
1053
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
1054
|
+
const MAX_COPY_LENGTH = 240;
|
|
1055
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1056
|
+
const requestSignal = (signal, timeoutMs) => {
|
|
1057
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
1058
|
+
return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
|
|
1059
|
+
};
|
|
1060
|
+
function safeCopy(value) {
|
|
1061
|
+
return typeof value === "string" && value.length > 0 ? value.slice(0, MAX_COPY_LENGTH) : void 0;
|
|
1062
|
+
}
|
|
1063
|
+
function credentialsOf(auth, credential) {
|
|
1064
|
+
const access = auth?.auth?.apiKey;
|
|
1065
|
+
const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
|
|
1066
|
+
if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
|
|
1067
|
+
return {
|
|
1068
|
+
access,
|
|
1069
|
+
accountId
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
function parseDetails(value, now) {
|
|
1073
|
+
if (!record(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0 || !Array.isArray(value.credits)) throw new Error("ChatGPT returned malformed quota reset details");
|
|
1074
|
+
if (value.available_count === 0) throw new Error("No quota reset is available");
|
|
1075
|
+
const available = value.credits.filter((credit) => record(credit) && typeof credit.id === "string" && credit.id.length > 0 && credit.id.length <= 256 && typeof credit.status === "string" && credit.status.toLowerCase() === "available").map((credit) => {
|
|
1076
|
+
const expiresAt = credit.expires_at === void 0 || credit.expires_at === null ? void 0 : credit.expires_at * 1e3;
|
|
1077
|
+
if (expiresAt !== void 0 && (!Number.isSafeInteger(credit.expires_at) || credit.expires_at <= 0)) throw new Error("ChatGPT returned malformed quota reset details");
|
|
1078
|
+
return {
|
|
1079
|
+
credit,
|
|
1080
|
+
expiresAt
|
|
1081
|
+
};
|
|
1082
|
+
}).filter(({ expiresAt }) => expiresAt === void 0 || expiresAt > now).sort((a, b) => (a.expiresAt ?? Number.MAX_SAFE_INTEGER) - (b.expiresAt ?? Number.MAX_SAFE_INTEGER));
|
|
1083
|
+
if (available.length === 0) throw new Error("No usable quota reset is available");
|
|
1084
|
+
return {
|
|
1085
|
+
availableCount: value.available_count,
|
|
1086
|
+
creditId: available[0].credit.id,
|
|
1087
|
+
title: safeCopy(available[0].credit.title),
|
|
1088
|
+
description: safeCopy(available[0].credit.description),
|
|
1089
|
+
creditExpiresAt: available[0].expiresAt
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function parseConsumeResult(value) {
|
|
1093
|
+
if (!record(value) || ![
|
|
1094
|
+
"reset",
|
|
1095
|
+
"nothing_to_reset",
|
|
1096
|
+
"no_credit",
|
|
1097
|
+
"already_redeemed"
|
|
1098
|
+
].includes(value.code)) throw new Error("ChatGPT returned an unreadable quota reset response");
|
|
1099
|
+
const windowsReset = Array.isArray(value.windows_reset) ? value.windows_reset.filter((item) => typeof item === "string").slice(0, 16) : [];
|
|
1100
|
+
return {
|
|
1101
|
+
code: value.code,
|
|
1102
|
+
windowsReset
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
function quotaIsExhausted(usage) {
|
|
1106
|
+
return Array.isArray(usage?.rateLimits) && usage.rateLimits.some((limit) => limit?.id !== "code_review" && Array.isArray(limit?.windows) && limit.windows.some((window) => Number.isFinite(window?.usedPercent) && window.usedPercent >= 100));
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Host-only reset redemption. The browser receives an opaque, short-lived
|
|
1110
|
+
* challenge; account ids, bearer tokens, credit ids, and idempotency keys stay
|
|
1111
|
+
* in memory on the host.
|
|
1112
|
+
*/
|
|
1113
|
+
function createCodexResetCreditService(options) {
|
|
1114
|
+
const getAuth = options.getAuth;
|
|
1115
|
+
const readCredential = options.readCredential;
|
|
1116
|
+
const usageReader = options.usageReader;
|
|
1117
|
+
const fetchReset = options.fetch ?? fetch;
|
|
1118
|
+
const now = options.now ?? Date.now;
|
|
1119
|
+
const randomUUID$1 = options.randomUUID ?? randomUUID;
|
|
1120
|
+
const confirmDelayMs = options.confirmDelayMs ?? DEFAULT_CONFIRM_DELAY_MS;
|
|
1121
|
+
const challengeTtlMs = options.challengeTtlMs ?? DEFAULT_CHALLENGE_TTL_MS;
|
|
1122
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1123
|
+
const challenges = /* @__PURE__ */ new Map();
|
|
1124
|
+
const resolveCredentials = async (signal) => credentialsOf(await getAuth({ signal }), await readCredential({ signal }));
|
|
1125
|
+
return Object.freeze({
|
|
1126
|
+
async prepare({ signal } = {}) {
|
|
1127
|
+
const { access, accountId } = await resolveCredentials(signal);
|
|
1128
|
+
const response = await fetchReset(CODEX_RESET_CREDITS_URL, {
|
|
1129
|
+
method: "GET",
|
|
1130
|
+
redirect: "error",
|
|
1131
|
+
headers: {
|
|
1132
|
+
authorization: `Bearer ${access}`,
|
|
1133
|
+
"chatgpt-account-id": accountId,
|
|
1134
|
+
accept: "application/json",
|
|
1135
|
+
"cache-control": "no-store",
|
|
1136
|
+
"user-agent": USER_AGENT
|
|
1137
|
+
},
|
|
1138
|
+
signal: requestSignal(signal, timeoutMs)
|
|
1139
|
+
});
|
|
1140
|
+
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})`);
|
|
1141
|
+
let raw;
|
|
1142
|
+
try {
|
|
1143
|
+
raw = await response.json();
|
|
1144
|
+
} catch {
|
|
1145
|
+
throw new Error("ChatGPT returned unreadable quota reset details");
|
|
1146
|
+
}
|
|
1147
|
+
const details = parseDetails(raw, now());
|
|
1148
|
+
const preparedAt = now();
|
|
1149
|
+
const readyAt = preparedAt + confirmDelayMs;
|
|
1150
|
+
const expiresAt = Math.min(preparedAt + challengeTtlMs, details.creditExpiresAt ?? Number.MAX_SAFE_INTEGER);
|
|
1151
|
+
if (expiresAt <= readyAt) throw new Error("The available quota reset expires too soon");
|
|
1152
|
+
const challengeId = randomUUID$1();
|
|
1153
|
+
challenges.set(challengeId, {
|
|
1154
|
+
state: "prepared",
|
|
1155
|
+
accountId,
|
|
1156
|
+
creditId: details.creditId,
|
|
1157
|
+
redeemRequestId: randomUUID$1(),
|
|
1158
|
+
readyAt,
|
|
1159
|
+
expiresAt
|
|
1160
|
+
});
|
|
1161
|
+
return {
|
|
1162
|
+
challengeId,
|
|
1163
|
+
availableCount: details.availableCount,
|
|
1164
|
+
confirmPhrase: RESET_CONFIRM_PHRASE,
|
|
1165
|
+
readyAt,
|
|
1166
|
+
expiresAt,
|
|
1167
|
+
...details.title === void 0 ? {} : { title: details.title },
|
|
1168
|
+
...details.description === void 0 ? {} : { description: details.description }
|
|
1169
|
+
};
|
|
1170
|
+
},
|
|
1171
|
+
async consume({ challengeId, phrase, signal } = {}) {
|
|
1172
|
+
const challenge = typeof challengeId === "string" ? challenges.get(challengeId) : void 0;
|
|
1173
|
+
if (challenge === void 0) throw new Error("This quota reset confirmation is no longer valid");
|
|
1174
|
+
if (challenge.state === "pending") throw new Error("This quota reset is already in progress");
|
|
1175
|
+
if (now() < challenge.readyAt) throw new Error("Wait before confirming this quota reset");
|
|
1176
|
+
if (now() > challenge.expiresAt) {
|
|
1177
|
+
challenges.delete(challengeId);
|
|
1178
|
+
throw new Error("This quota reset confirmation is no longer valid");
|
|
1179
|
+
}
|
|
1180
|
+
if (phrase !== "USE RESET") throw new Error("The quota reset confirmation phrase does not match");
|
|
1181
|
+
challenge.state = "pending";
|
|
1182
|
+
try {
|
|
1183
|
+
if (!quotaIsExhausted(await usageReader.read({
|
|
1184
|
+
force: true,
|
|
1185
|
+
signal
|
|
1186
|
+
}))) throw new Error("The current Codex quota is not exhausted");
|
|
1187
|
+
const { access, accountId } = await resolveCredentials(signal);
|
|
1188
|
+
if (accountId !== challenge.accountId) throw new Error("The signed-in ChatGPT account changed");
|
|
1189
|
+
const response = await fetchReset(CODEX_RESET_CONSUME_URL, {
|
|
1190
|
+
method: "POST",
|
|
1191
|
+
redirect: "error",
|
|
1192
|
+
headers: {
|
|
1193
|
+
authorization: `Bearer ${access}`,
|
|
1194
|
+
"chatgpt-account-id": accountId,
|
|
1195
|
+
accept: "application/json",
|
|
1196
|
+
"content-type": "application/json",
|
|
1197
|
+
"cache-control": "no-store",
|
|
1198
|
+
"user-agent": USER_AGENT
|
|
1199
|
+
},
|
|
1200
|
+
body: JSON.stringify({
|
|
1201
|
+
redeem_request_id: challenge.redeemRequestId,
|
|
1202
|
+
credit_id: challenge.creditId
|
|
1203
|
+
}),
|
|
1204
|
+
signal: requestSignal(signal, timeoutMs)
|
|
1205
|
+
});
|
|
1206
|
+
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})`);
|
|
1207
|
+
let raw;
|
|
1208
|
+
try {
|
|
1209
|
+
raw = await response.json();
|
|
1210
|
+
} catch {
|
|
1211
|
+
throw new Error("ChatGPT returned an unreadable quota reset response");
|
|
1212
|
+
}
|
|
1213
|
+
const result = parseConsumeResult(raw);
|
|
1214
|
+
usageReader.clear();
|
|
1215
|
+
return result;
|
|
1216
|
+
} finally {
|
|
1217
|
+
challenges.delete(challengeId);
|
|
1218
|
+
}
|
|
1219
|
+
},
|
|
1220
|
+
clear() {
|
|
1221
|
+
challenges.clear();
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
//#endregion
|
|
1040
1226
|
//#region src/index.js
|
|
1041
1227
|
const name = "codex-subscription";
|
|
1042
1228
|
const inject = [
|
|
@@ -1066,7 +1252,7 @@ const publicError = (code, message) => ({
|
|
|
1066
1252
|
details: { issues: [] }
|
|
1067
1253
|
}
|
|
1068
1254
|
});
|
|
1069
|
-
function createSubscriptionRpcHandler({ authHandler, usageReader, preferences, diagnosticsReader }) {
|
|
1255
|
+
function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, diagnosticsReader }) {
|
|
1070
1256
|
return async (endpoint, payload, signal) => {
|
|
1071
1257
|
if (endpoint === "diagnostics") try {
|
|
1072
1258
|
signal.throwIfAborted();
|
|
@@ -1082,9 +1268,13 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, preferences, d
|
|
|
1082
1268
|
signal.throwIfAborted();
|
|
1083
1269
|
if (endpoint === "preferences/update") {
|
|
1084
1270
|
const patch = {};
|
|
1085
|
-
if (Object.hasOwn(payload ?? {}, "
|
|
1086
|
-
if (
|
|
1087
|
-
|
|
1271
|
+
if (Object.hasOwn(payload ?? {}, "quickQuotaMode")) {
|
|
1272
|
+
if (![
|
|
1273
|
+
"off",
|
|
1274
|
+
"percent",
|
|
1275
|
+
"bar"
|
|
1276
|
+
].includes(payload["quickQuotaMode"])) return publicError("internal", "Invalid quick quota preference");
|
|
1277
|
+
patch[QUICK_QUOTA_MODE_FIELD] = payload[QUICK_QUOTA_MODE_FIELD];
|
|
1088
1278
|
}
|
|
1089
1279
|
if (Object.hasOwn(payload ?? {}, "searchProvider")) {
|
|
1090
1280
|
if (!["dsh", "codex"].includes(payload["searchProvider"])) return publicError("internal", "Invalid search provider preference");
|
|
@@ -1120,8 +1310,40 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, preferences, d
|
|
|
1120
1310
|
const message = error instanceof Error && known.has(error.message) ? error.message : "Could not read ChatGPT usage";
|
|
1121
1311
|
return publicError("internal", message);
|
|
1122
1312
|
}
|
|
1313
|
+
if (endpoint === "reset-credit/prepare" || endpoint === "reset-credit/consume") try {
|
|
1314
|
+
signal.throwIfAborted();
|
|
1315
|
+
return {
|
|
1316
|
+
ok: true,
|
|
1317
|
+
value: endpoint === "reset-credit/prepare" ? await resetCreditService.prepare({ signal }) : await resetCreditService.consume({
|
|
1318
|
+
challengeId: payload?.challengeId,
|
|
1319
|
+
phrase: payload?.phrase,
|
|
1320
|
+
signal
|
|
1321
|
+
})
|
|
1322
|
+
};
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
if (signal.aborted) throw error;
|
|
1325
|
+
const known = /* @__PURE__ */ new Set([
|
|
1326
|
+
"ChatGPT subscription is not signed in",
|
|
1327
|
+
"ChatGPT sign-in needs to be renewed",
|
|
1328
|
+
"No quota reset is available",
|
|
1329
|
+
"No usable quota reset is available",
|
|
1330
|
+
"The available quota reset expires too soon",
|
|
1331
|
+
"This quota reset confirmation is no longer valid",
|
|
1332
|
+
"This quota reset is already in progress",
|
|
1333
|
+
"Wait before confirming this quota reset",
|
|
1334
|
+
"The quota reset confirmation phrase does not match",
|
|
1335
|
+
"The current Codex quota is not exhausted",
|
|
1336
|
+
"The signed-in ChatGPT account changed"
|
|
1337
|
+
]);
|
|
1338
|
+
const fallback = endpoint === "reset-credit/prepare" ? "Could not prepare a quota reset" : "Could not use the quota reset";
|
|
1339
|
+
const message = error instanceof Error && known.has(error.message) ? error.message : fallback;
|
|
1340
|
+
return publicError("internal", message);
|
|
1341
|
+
}
|
|
1123
1342
|
const result = await authHandler(endpoint, payload, signal);
|
|
1124
|
-
if (endpoint === "logout" && result.ok === true)
|
|
1343
|
+
if (endpoint === "logout" && result.ok === true) {
|
|
1344
|
+
usageReader.clear();
|
|
1345
|
+
resetCreditService.clear();
|
|
1346
|
+
}
|
|
1125
1347
|
return result;
|
|
1126
1348
|
};
|
|
1127
1349
|
}
|
|
@@ -1144,14 +1366,19 @@ function createSearchProviderSwitcher(loader) {
|
|
|
1144
1366
|
}
|
|
1145
1367
|
function apply(ctx) {
|
|
1146
1368
|
const settings = ctx.settings.register(settingsNamespace(SETTINGS_NAMESPACE), z.object({
|
|
1147
|
-
[
|
|
1369
|
+
[QUICK_QUOTA_MODE_FIELD]: z.union([
|
|
1370
|
+
"off",
|
|
1371
|
+
QUICK_QUOTA_MODE_PERCENT,
|
|
1372
|
+
"bar"
|
|
1373
|
+
]),
|
|
1374
|
+
[LEGACY_QUICK_QUOTA_FIELD]: z.boolean(),
|
|
1148
1375
|
[SEARCH_PROVIDER_FIELD]: z.union(["dsh", SEARCH_PROVIDER_CODEX]).default("dsh"),
|
|
1149
1376
|
[SPEED_MODE_FIELD]: z.union([SPEED_MODE_STANDARD, SPEED_MODE_FAST]).default(DEFAULT_SPEED_MODE)
|
|
1150
1377
|
}));
|
|
1151
1378
|
const searchProvider = createSearchProviderSwitcher(ctx.loader);
|
|
1152
1379
|
const preferences = {
|
|
1153
1380
|
status: () => ({
|
|
1154
|
-
[
|
|
1381
|
+
[QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]),
|
|
1155
1382
|
[SEARCH_PROVIDER_FIELD]: settings.get()[SEARCH_PROVIDER_FIELD],
|
|
1156
1383
|
[SPEED_MODE_FIELD]: settings.get()[SPEED_MODE_FIELD],
|
|
1157
1384
|
writable: ctx.settings.writable
|
|
@@ -1231,9 +1458,15 @@ function apply(ctx) {
|
|
|
1231
1458
|
getAuth: resolveAuth,
|
|
1232
1459
|
readCredential: (options) => store.read(PROVIDER, options)
|
|
1233
1460
|
});
|
|
1461
|
+
const resetCreditService = createCodexResetCreditService({
|
|
1462
|
+
getAuth: resolveAuth,
|
|
1463
|
+
readCredential: (options) => store.read(PROVIDER, options),
|
|
1464
|
+
usageReader
|
|
1465
|
+
});
|
|
1234
1466
|
const handler = createSubscriptionRpcHandler({
|
|
1235
1467
|
authHandler: createCodexRpcHandler(coordinator, { openExternal: openCodexAuthUrl }),
|
|
1236
1468
|
usageReader,
|
|
1469
|
+
resetCreditService,
|
|
1237
1470
|
preferences,
|
|
1238
1471
|
diagnosticsReader: () => createSubscriptionDiagnostics({
|
|
1239
1472
|
auth,
|
|
@@ -1243,4 +1476,4 @@ function apply(ctx) {
|
|
|
1243
1476
|
ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" }), "codex-subscription: loopback account RPC");
|
|
1244
1477
|
}
|
|
1245
1478
|
//#endregion
|
|
1246
|
-
export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_USAGE_URL, CodexLoginCoordinator, DshOAuthCredentialStore, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, openCodexAuthUrl, parseCodexUsage };
|
|
1479
|
+
export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_RESET_CONSUME_URL, CODEX_RESET_CREDITS_URL, CODEX_USAGE_URL, CodexLoginCoordinator, DshOAuthCredentialStore, RESET_CONFIRM_PHRASE, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexResetCreditService, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, openCodexAuthUrl, parseCodexUsage };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-codex-subscription",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "ChatGPT and Codex subscriptions
|
|
3
|
+
"version": "1.4.0",
|
|
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",
|
|
7
7
|
"exports": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"lib/*.js",
|
|
15
15
|
"cordis.patch.yml",
|
|
16
16
|
"README.md",
|
|
17
|
-
"README.
|
|
17
|
+
"README.zh-CN.md",
|
|
18
18
|
"DIRECTORY.md",
|
|
19
19
|
"AGENTS.md",
|
|
20
20
|
"compatibility.json",
|
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
"chatgpt-plus",
|
|
53
53
|
"subscription",
|
|
54
54
|
"quota",
|
|
55
|
+
"quota-reset",
|
|
56
|
+
"fast-mode",
|
|
55
57
|
"usage",
|
|
56
58
|
"oauth",
|
|
57
59
|
"web-search",
|