dsh-codex-subscription 1.2.2 → 1.3.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 +98 -90
- package/README.zh-CN.md +219 -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 +218 -1
- package/lib/index.js +240 -23
- package/package.json +5 -3
- package/README.en.md +0 -210
package/lib/index.js
CHANGED
|
@@ -496,7 +496,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0 } = {
|
|
|
496
496
|
}
|
|
497
497
|
//#endregion
|
|
498
498
|
//#region src/version.js
|
|
499
|
-
const PACKAGE_VERSION = "1.
|
|
499
|
+
const PACKAGE_VERSION = "1.3.0";
|
|
500
500
|
const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
|
|
501
501
|
//#endregion
|
|
502
502
|
//#region src/codex-search.js
|
|
@@ -505,7 +505,7 @@ const CODEX_SEARCH_URL = "https://chatgpt.com/backend-api/codex/alpha/search";
|
|
|
505
505
|
const DEFAULT_MODEL = "gpt-5.6-luna";
|
|
506
506
|
const MAX_OUTPUT_TOKENS = 4096;
|
|
507
507
|
const MAX_SOURCE_DATE = 64;
|
|
508
|
-
const record$
|
|
508
|
+
const record$3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
509
509
|
const nonEmpty$1 = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
510
510
|
const displayText = (value) => {
|
|
511
511
|
const text = nonEmpty$1(value)?.replace(/\s+/gu, " ").trim();
|
|
@@ -518,7 +518,7 @@ const boundedDisplayText = (value, maximum) => {
|
|
|
518
518
|
return `${text.slice(0, maximum - 1)}…`;
|
|
519
519
|
};
|
|
520
520
|
function sourceOf(value) {
|
|
521
|
-
if (!record$
|
|
521
|
+
if (!record$3(value)) return void 0;
|
|
522
522
|
const url = nonEmpty$1(value.url);
|
|
523
523
|
if (url === void 0) return void 0;
|
|
524
524
|
let parsed;
|
|
@@ -539,7 +539,7 @@ function sourceOf(value) {
|
|
|
539
539
|
};
|
|
540
540
|
}
|
|
541
541
|
function parseSearchResponse(value) {
|
|
542
|
-
if (!record$
|
|
542
|
+
if (!record$3(value) || !Array.isArray(value.results)) throw new Error("Codex returned a malformed search response");
|
|
543
543
|
const seen = /* @__PURE__ */ new Set();
|
|
544
544
|
const sources = [];
|
|
545
545
|
for (const result of value.results ?? []) {
|
|
@@ -631,7 +631,7 @@ const PNG_SIGNATURE = Buffer.from([
|
|
|
631
631
|
26,
|
|
632
632
|
10
|
|
633
633
|
]);
|
|
634
|
-
const record$
|
|
634
|
+
const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
635
635
|
const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
636
636
|
function encodedLimit(decodedBytes) {
|
|
637
637
|
return Math.ceil(decodedBytes / 3) * 4;
|
|
@@ -732,7 +732,7 @@ function imageOutputSchema() {
|
|
|
732
732
|
}
|
|
733
733
|
function responseMetadata(value) {
|
|
734
734
|
const data = Array.isArray(value?.data) ? value.data[0] : void 0;
|
|
735
|
-
const encoded = record$
|
|
735
|
+
const encoded = record$2(data) ? data.b64_json : void 0;
|
|
736
736
|
if (typeof encoded !== "string") throw new Error("Codex returned no image data");
|
|
737
737
|
return {
|
|
738
738
|
encoded,
|
|
@@ -865,11 +865,11 @@ async function createSubscriptionDiagnostics({ auth, preferences }) {
|
|
|
865
865
|
//#region src/usage.js
|
|
866
866
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
867
867
|
const DEFAULT_TTL_MS = 6e4;
|
|
868
|
-
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
869
|
-
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
868
|
+
const DEFAULT_TIMEOUT_MS$1 = 15e3;
|
|
869
|
+
const record$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
870
870
|
function windowOf(value) {
|
|
871
871
|
if (value === void 0 || value === null) return void 0;
|
|
872
|
-
if (!record(value)) throw new Error("Codex returned a malformed rate-limit window");
|
|
872
|
+
if (!record$1(value)) throw new Error("Codex returned a malformed rate-limit window");
|
|
873
873
|
const used = value.used_percent;
|
|
874
874
|
const seconds = value.limit_window_seconds;
|
|
875
875
|
if (!Number.isFinite(used) || used < 0 || used > 100) throw new Error("Codex returned an invalid used percentage");
|
|
@@ -884,7 +884,7 @@ function windowOf(value) {
|
|
|
884
884
|
}
|
|
885
885
|
function limitOf(id, name, value) {
|
|
886
886
|
if (value === void 0 || value === null) return void 0;
|
|
887
|
-
if (!record(value)) throw new Error("Codex returned malformed rate-limit details");
|
|
887
|
+
if (!record$1(value)) throw new Error("Codex returned malformed rate-limit details");
|
|
888
888
|
const windows = [windowOf(value.primary_window), windowOf(value.secondary_window)].filter(Boolean);
|
|
889
889
|
return windows.length === 0 ? void 0 : {
|
|
890
890
|
id,
|
|
@@ -903,7 +903,7 @@ function epochSeconds(value, label) {
|
|
|
903
903
|
}
|
|
904
904
|
function creditsOf(value) {
|
|
905
905
|
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");
|
|
906
|
+
if (!record$1(value) || typeof value.has_credits !== "boolean" || typeof value.unlimited !== "boolean") throw new Error("Codex returned malformed credit details");
|
|
907
907
|
if (!value.has_credits) return void 0;
|
|
908
908
|
return {
|
|
909
909
|
unlimited: value.unlimited,
|
|
@@ -912,10 +912,10 @@ function creditsOf(value) {
|
|
|
912
912
|
}
|
|
913
913
|
function individualOf(value) {
|
|
914
914
|
if (value === void 0 || value === null) return void 0;
|
|
915
|
-
if (!record(value)) throw new Error("Codex returned malformed spend control");
|
|
915
|
+
if (!record$1(value)) throw new Error("Codex returned malformed spend control");
|
|
916
916
|
const item = value.individual_limit;
|
|
917
917
|
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");
|
|
918
|
+
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
919
|
const resetsAt = epochSeconds(item.reset_at, "individual-limit reset time");
|
|
920
920
|
return {
|
|
921
921
|
limit: decimal(item.limit, "individual limit"),
|
|
@@ -926,19 +926,19 @@ function individualOf(value) {
|
|
|
926
926
|
}
|
|
927
927
|
function spendControlReachedOf(value) {
|
|
928
928
|
if (value === void 0 || value === null) return void 0;
|
|
929
|
-
if (!record(value)) throw new Error("Codex returned malformed spend control");
|
|
929
|
+
if (!record$1(value)) throw new Error("Codex returned malformed spend control");
|
|
930
930
|
if (value.reached === void 0 || value.reached === null) return void 0;
|
|
931
931
|
if (typeof value.reached !== "boolean") throw new Error("Codex returned an invalid spend-control state");
|
|
932
932
|
return value.reached;
|
|
933
933
|
}
|
|
934
934
|
function resetCreditsOf(value) {
|
|
935
935
|
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");
|
|
936
|
+
if (!record$1(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0) throw new Error("Codex returned malformed reset credit details");
|
|
937
937
|
return { availableCount: value.available_count };
|
|
938
938
|
}
|
|
939
939
|
/** Reduce the provider payload to a browser-safe quota projection. */
|
|
940
940
|
function parseCodexUsage(value) {
|
|
941
|
-
if (!record(value)) throw new Error("Codex returned a malformed usage response");
|
|
941
|
+
if (!record$1(value)) throw new Error("Codex returned a malformed usage response");
|
|
942
942
|
const rateLimits = [];
|
|
943
943
|
const seenLimitIds = /* @__PURE__ */ new Set();
|
|
944
944
|
const addLimit = (limit) => {
|
|
@@ -949,7 +949,7 @@ function parseCodexUsage(value) {
|
|
|
949
949
|
addLimit(limitOf("codex", "Codex", value.rate_limit));
|
|
950
950
|
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
951
|
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");
|
|
952
|
+
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
953
|
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
954
|
addLimit(limitOf(entry.metered_feature, entry.limit_name || void 0, entry.rate_limit));
|
|
955
955
|
}
|
|
@@ -966,7 +966,7 @@ function parseCodexUsage(value) {
|
|
|
966
966
|
...resetCredits === void 0 ? {} : { resetCredits }
|
|
967
967
|
};
|
|
968
968
|
}
|
|
969
|
-
const requestSignal = (signal, timeoutMs) => {
|
|
969
|
+
const requestSignal$1 = (signal, timeoutMs) => {
|
|
970
970
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
971
971
|
return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
|
|
972
972
|
};
|
|
@@ -981,7 +981,7 @@ function createCodexUsageReader(options) {
|
|
|
981
981
|
const fetchUsage = options.fetch ?? fetch;
|
|
982
982
|
const now = options.now ?? Date.now;
|
|
983
983
|
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
984
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
984
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
|
|
985
985
|
let cached;
|
|
986
986
|
let inFlight;
|
|
987
987
|
let generation = 0;
|
|
@@ -1001,7 +1001,7 @@ function createCodexUsageReader(options) {
|
|
|
1001
1001
|
"cache-control": "no-store",
|
|
1002
1002
|
"user-agent": USER_AGENT
|
|
1003
1003
|
},
|
|
1004
|
-
signal: requestSignal(signal, timeoutMs)
|
|
1004
|
+
signal: requestSignal$1(signal, timeoutMs)
|
|
1005
1005
|
});
|
|
1006
1006
|
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
1007
|
let value;
|
|
@@ -1037,6 +1037,185 @@ function createCodexUsageReader(options) {
|
|
|
1037
1037
|
});
|
|
1038
1038
|
}
|
|
1039
1039
|
//#endregion
|
|
1040
|
+
//#region src/reset-credits.js
|
|
1041
|
+
const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
1042
|
+
const CODEX_RESET_CONSUME_URL = `${CODEX_RESET_CREDITS_URL}/consume`;
|
|
1043
|
+
const RESET_CONFIRM_PHRASE = "USE RESET";
|
|
1044
|
+
const DEFAULT_CONFIRM_DELAY_MS = 5e3;
|
|
1045
|
+
const DEFAULT_CHALLENGE_TTL_MS = 6e4;
|
|
1046
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
1047
|
+
const MAX_COPY_LENGTH = 240;
|
|
1048
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1049
|
+
const requestSignal = (signal, timeoutMs) => {
|
|
1050
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
1051
|
+
return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
|
|
1052
|
+
};
|
|
1053
|
+
function safeCopy(value) {
|
|
1054
|
+
return typeof value === "string" && value.length > 0 ? value.slice(0, MAX_COPY_LENGTH) : void 0;
|
|
1055
|
+
}
|
|
1056
|
+
function credentialsOf(auth, credential) {
|
|
1057
|
+
const access = auth?.auth?.apiKey;
|
|
1058
|
+
const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
|
|
1059
|
+
if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
|
|
1060
|
+
return {
|
|
1061
|
+
access,
|
|
1062
|
+
accountId
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
function parseDetails(value, now) {
|
|
1066
|
+
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");
|
|
1067
|
+
if (value.available_count === 0) throw new Error("No quota reset is available");
|
|
1068
|
+
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) => {
|
|
1069
|
+
const expiresAt = credit.expires_at === void 0 || credit.expires_at === null ? void 0 : credit.expires_at * 1e3;
|
|
1070
|
+
if (expiresAt !== void 0 && (!Number.isSafeInteger(credit.expires_at) || credit.expires_at <= 0)) throw new Error("ChatGPT returned malformed quota reset details");
|
|
1071
|
+
return {
|
|
1072
|
+
credit,
|
|
1073
|
+
expiresAt
|
|
1074
|
+
};
|
|
1075
|
+
}).filter(({ expiresAt }) => expiresAt === void 0 || expiresAt > now).sort((a, b) => (a.expiresAt ?? Number.MAX_SAFE_INTEGER) - (b.expiresAt ?? Number.MAX_SAFE_INTEGER));
|
|
1076
|
+
if (available.length === 0) throw new Error("No usable quota reset is available");
|
|
1077
|
+
return {
|
|
1078
|
+
availableCount: value.available_count,
|
|
1079
|
+
creditId: available[0].credit.id,
|
|
1080
|
+
title: safeCopy(available[0].credit.title),
|
|
1081
|
+
description: safeCopy(available[0].credit.description),
|
|
1082
|
+
creditExpiresAt: available[0].expiresAt
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function parseConsumeResult(value) {
|
|
1086
|
+
if (!record(value) || ![
|
|
1087
|
+
"reset",
|
|
1088
|
+
"nothing_to_reset",
|
|
1089
|
+
"no_credit",
|
|
1090
|
+
"already_redeemed"
|
|
1091
|
+
].includes(value.code)) throw new Error("ChatGPT returned an unreadable quota reset response");
|
|
1092
|
+
const windowsReset = Array.isArray(value.windows_reset) ? value.windows_reset.filter((item) => typeof item === "string").slice(0, 16) : [];
|
|
1093
|
+
return {
|
|
1094
|
+
code: value.code,
|
|
1095
|
+
windowsReset
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function quotaIsExhausted(usage) {
|
|
1099
|
+
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));
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Host-only reset redemption. The browser receives an opaque, short-lived
|
|
1103
|
+
* challenge; account ids, bearer tokens, credit ids, and idempotency keys stay
|
|
1104
|
+
* in memory on the host.
|
|
1105
|
+
*/
|
|
1106
|
+
function createCodexResetCreditService(options) {
|
|
1107
|
+
const getAuth = options.getAuth;
|
|
1108
|
+
const readCredential = options.readCredential;
|
|
1109
|
+
const usageReader = options.usageReader;
|
|
1110
|
+
const fetchReset = options.fetch ?? fetch;
|
|
1111
|
+
const now = options.now ?? Date.now;
|
|
1112
|
+
const randomUUID$1 = options.randomUUID ?? randomUUID;
|
|
1113
|
+
const confirmDelayMs = options.confirmDelayMs ?? DEFAULT_CONFIRM_DELAY_MS;
|
|
1114
|
+
const challengeTtlMs = options.challengeTtlMs ?? DEFAULT_CHALLENGE_TTL_MS;
|
|
1115
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1116
|
+
const challenges = /* @__PURE__ */ new Map();
|
|
1117
|
+
const resolveCredentials = async (signal) => credentialsOf(await getAuth({ signal }), await readCredential({ signal }));
|
|
1118
|
+
return Object.freeze({
|
|
1119
|
+
async prepare({ signal } = {}) {
|
|
1120
|
+
const { access, accountId } = await resolveCredentials(signal);
|
|
1121
|
+
const response = await fetchReset(CODEX_RESET_CREDITS_URL, {
|
|
1122
|
+
method: "GET",
|
|
1123
|
+
redirect: "error",
|
|
1124
|
+
headers: {
|
|
1125
|
+
authorization: `Bearer ${access}`,
|
|
1126
|
+
"chatgpt-account-id": accountId,
|
|
1127
|
+
accept: "application/json",
|
|
1128
|
+
"cache-control": "no-store",
|
|
1129
|
+
"user-agent": USER_AGENT
|
|
1130
|
+
},
|
|
1131
|
+
signal: requestSignal(signal, timeoutMs)
|
|
1132
|
+
});
|
|
1133
|
+
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})`);
|
|
1134
|
+
let raw;
|
|
1135
|
+
try {
|
|
1136
|
+
raw = await response.json();
|
|
1137
|
+
} catch {
|
|
1138
|
+
throw new Error("ChatGPT returned unreadable quota reset details");
|
|
1139
|
+
}
|
|
1140
|
+
const details = parseDetails(raw, now());
|
|
1141
|
+
const preparedAt = now();
|
|
1142
|
+
const readyAt = preparedAt + confirmDelayMs;
|
|
1143
|
+
const expiresAt = Math.min(preparedAt + challengeTtlMs, details.creditExpiresAt ?? Number.MAX_SAFE_INTEGER);
|
|
1144
|
+
if (expiresAt <= readyAt) throw new Error("The available quota reset expires too soon");
|
|
1145
|
+
const challengeId = randomUUID$1();
|
|
1146
|
+
challenges.set(challengeId, {
|
|
1147
|
+
state: "prepared",
|
|
1148
|
+
accountId,
|
|
1149
|
+
creditId: details.creditId,
|
|
1150
|
+
redeemRequestId: randomUUID$1(),
|
|
1151
|
+
readyAt,
|
|
1152
|
+
expiresAt
|
|
1153
|
+
});
|
|
1154
|
+
return {
|
|
1155
|
+
challengeId,
|
|
1156
|
+
availableCount: details.availableCount,
|
|
1157
|
+
confirmPhrase: RESET_CONFIRM_PHRASE,
|
|
1158
|
+
readyAt,
|
|
1159
|
+
expiresAt,
|
|
1160
|
+
...details.title === void 0 ? {} : { title: details.title },
|
|
1161
|
+
...details.description === void 0 ? {} : { description: details.description }
|
|
1162
|
+
};
|
|
1163
|
+
},
|
|
1164
|
+
async consume({ challengeId, phrase, signal } = {}) {
|
|
1165
|
+
const challenge = typeof challengeId === "string" ? challenges.get(challengeId) : void 0;
|
|
1166
|
+
if (challenge === void 0) throw new Error("This quota reset confirmation is no longer valid");
|
|
1167
|
+
if (challenge.state === "pending") throw new Error("This quota reset is already in progress");
|
|
1168
|
+
if (now() < challenge.readyAt) throw new Error("Wait before confirming this quota reset");
|
|
1169
|
+
if (now() > challenge.expiresAt) {
|
|
1170
|
+
challenges.delete(challengeId);
|
|
1171
|
+
throw new Error("This quota reset confirmation is no longer valid");
|
|
1172
|
+
}
|
|
1173
|
+
if (phrase !== "USE RESET") throw new Error("The quota reset confirmation phrase does not match");
|
|
1174
|
+
challenge.state = "pending";
|
|
1175
|
+
try {
|
|
1176
|
+
if (!quotaIsExhausted(await usageReader.read({
|
|
1177
|
+
force: true,
|
|
1178
|
+
signal
|
|
1179
|
+
}))) throw new Error("The current Codex quota is not exhausted");
|
|
1180
|
+
const { access, accountId } = await resolveCredentials(signal);
|
|
1181
|
+
if (accountId !== challenge.accountId) throw new Error("The signed-in ChatGPT account changed");
|
|
1182
|
+
const response = await fetchReset(CODEX_RESET_CONSUME_URL, {
|
|
1183
|
+
method: "POST",
|
|
1184
|
+
redirect: "error",
|
|
1185
|
+
headers: {
|
|
1186
|
+
authorization: `Bearer ${access}`,
|
|
1187
|
+
"chatgpt-account-id": accountId,
|
|
1188
|
+
accept: "application/json",
|
|
1189
|
+
"content-type": "application/json",
|
|
1190
|
+
"cache-control": "no-store",
|
|
1191
|
+
"user-agent": USER_AGENT
|
|
1192
|
+
},
|
|
1193
|
+
body: JSON.stringify({
|
|
1194
|
+
redeem_request_id: challenge.redeemRequestId,
|
|
1195
|
+
credit_id: challenge.creditId
|
|
1196
|
+
}),
|
|
1197
|
+
signal: requestSignal(signal, timeoutMs)
|
|
1198
|
+
});
|
|
1199
|
+
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})`);
|
|
1200
|
+
let raw;
|
|
1201
|
+
try {
|
|
1202
|
+
raw = await response.json();
|
|
1203
|
+
} catch {
|
|
1204
|
+
throw new Error("ChatGPT returned an unreadable quota reset response");
|
|
1205
|
+
}
|
|
1206
|
+
const result = parseConsumeResult(raw);
|
|
1207
|
+
usageReader.clear();
|
|
1208
|
+
return result;
|
|
1209
|
+
} finally {
|
|
1210
|
+
challenges.delete(challengeId);
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
clear() {
|
|
1214
|
+
challenges.clear();
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
//#endregion
|
|
1040
1219
|
//#region src/index.js
|
|
1041
1220
|
const name = "codex-subscription";
|
|
1042
1221
|
const inject = [
|
|
@@ -1066,7 +1245,7 @@ const publicError = (code, message) => ({
|
|
|
1066
1245
|
details: { issues: [] }
|
|
1067
1246
|
}
|
|
1068
1247
|
});
|
|
1069
|
-
function createSubscriptionRpcHandler({ authHandler, usageReader, preferences, diagnosticsReader }) {
|
|
1248
|
+
function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, diagnosticsReader }) {
|
|
1070
1249
|
return async (endpoint, payload, signal) => {
|
|
1071
1250
|
if (endpoint === "diagnostics") try {
|
|
1072
1251
|
signal.throwIfAborted();
|
|
@@ -1120,8 +1299,40 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, preferences, d
|
|
|
1120
1299
|
const message = error instanceof Error && known.has(error.message) ? error.message : "Could not read ChatGPT usage";
|
|
1121
1300
|
return publicError("internal", message);
|
|
1122
1301
|
}
|
|
1302
|
+
if (endpoint === "reset-credit/prepare" || endpoint === "reset-credit/consume") try {
|
|
1303
|
+
signal.throwIfAborted();
|
|
1304
|
+
return {
|
|
1305
|
+
ok: true,
|
|
1306
|
+
value: endpoint === "reset-credit/prepare" ? await resetCreditService.prepare({ signal }) : await resetCreditService.consume({
|
|
1307
|
+
challengeId: payload?.challengeId,
|
|
1308
|
+
phrase: payload?.phrase,
|
|
1309
|
+
signal
|
|
1310
|
+
})
|
|
1311
|
+
};
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
if (signal.aborted) throw error;
|
|
1314
|
+
const known = /* @__PURE__ */ new Set([
|
|
1315
|
+
"ChatGPT subscription is not signed in",
|
|
1316
|
+
"ChatGPT sign-in needs to be renewed",
|
|
1317
|
+
"No quota reset is available",
|
|
1318
|
+
"No usable quota reset is available",
|
|
1319
|
+
"The available quota reset expires too soon",
|
|
1320
|
+
"This quota reset confirmation is no longer valid",
|
|
1321
|
+
"This quota reset is already in progress",
|
|
1322
|
+
"Wait before confirming this quota reset",
|
|
1323
|
+
"The quota reset confirmation phrase does not match",
|
|
1324
|
+
"The current Codex quota is not exhausted",
|
|
1325
|
+
"The signed-in ChatGPT account changed"
|
|
1326
|
+
]);
|
|
1327
|
+
const fallback = endpoint === "reset-credit/prepare" ? "Could not prepare a quota reset" : "Could not use the quota reset";
|
|
1328
|
+
const message = error instanceof Error && known.has(error.message) ? error.message : fallback;
|
|
1329
|
+
return publicError("internal", message);
|
|
1330
|
+
}
|
|
1123
1331
|
const result = await authHandler(endpoint, payload, signal);
|
|
1124
|
-
if (endpoint === "logout" && result.ok === true)
|
|
1332
|
+
if (endpoint === "logout" && result.ok === true) {
|
|
1333
|
+
usageReader.clear();
|
|
1334
|
+
resetCreditService.clear();
|
|
1335
|
+
}
|
|
1125
1336
|
return result;
|
|
1126
1337
|
};
|
|
1127
1338
|
}
|
|
@@ -1231,9 +1442,15 @@ function apply(ctx) {
|
|
|
1231
1442
|
getAuth: resolveAuth,
|
|
1232
1443
|
readCredential: (options) => store.read(PROVIDER, options)
|
|
1233
1444
|
});
|
|
1445
|
+
const resetCreditService = createCodexResetCreditService({
|
|
1446
|
+
getAuth: resolveAuth,
|
|
1447
|
+
readCredential: (options) => store.read(PROVIDER, options),
|
|
1448
|
+
usageReader
|
|
1449
|
+
});
|
|
1234
1450
|
const handler = createSubscriptionRpcHandler({
|
|
1235
1451
|
authHandler: createCodexRpcHandler(coordinator, { openExternal: openCodexAuthUrl }),
|
|
1236
1452
|
usageReader,
|
|
1453
|
+
resetCreditService,
|
|
1237
1454
|
preferences,
|
|
1238
1455
|
diagnosticsReader: () => createSubscriptionDiagnostics({
|
|
1239
1456
|
auth,
|
|
@@ -1243,4 +1460,4 @@ function apply(ctx) {
|
|
|
1243
1460
|
ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" }), "codex-subscription: loopback account RPC");
|
|
1244
1461
|
}
|
|
1245
1462
|
//#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 };
|
|
1463
|
+
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.3.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",
|
package/README.en.md
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
|
-
# DSH Codex Subscription
|
|
4
|
-
|
|
5
|
-
**Use your ChatGPT / Codex subscription directly in DeepSeek Harness**
|
|
6
|
-
|
|
7
|
-
No OpenAI API key or Codex CLI. Models, search, quota, and image generation stay inside DSH.
|
|
8
|
-
|
|
9
|
-
[](https://github.com/WSL043/dsh-codex-subscription/actions/workflows/ci.yml)
|
|
10
|
-
[](https://www.npmjs.com/package/dsh-codex-subscription)
|
|
11
|
-
[](https://www.npmjs.com/package/dsh-codex-subscription)
|
|
12
|
-
[](LICENSE)
|
|
13
|
-
[](https://github.com/WSL043/dsh-codex-subscription/stargazers)
|
|
14
|
-
|
|
15
|
-
[Three-step start](#three-step-start) · [Agent install](#let-an-agent-install-it-recommended) · [Update and uninstall](#update-and-uninstall) · [简体中文](https://github.com/WSL043/dsh-codex-subscription/blob/main/README.md)
|
|
16
|
-
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
<p align="center">
|
|
20
|
-
<img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/readme-hero-en.webp" width="900" alt="Your Codex subscription inside DSH: subscription models, web search, live quota, and image generation">
|
|
21
|
-
</p>
|
|
22
|
-
|
|
23
|
-
## Three-step start
|
|
24
|
-
|
|
25
|
-
1. **Install the plugin.** On Windows, open PowerShell and run the line below. Existing `dsh` and DSH-Portable users can use the standard command that follows.
|
|
26
|
-
|
|
27
|
-
```powershell
|
|
28
|
-
irm 'https://github.com/WSL043/dsh-codex-subscription/releases/latest/download/dsh-codex-setup.ps1' | iex
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
2. **Sign in.** Restart DSH yourself, open **Settings -> Codex**, and choose browser sign-in. No Codex CLI and no pasted token are required.
|
|
32
|
-
3. **Use Codex.** Select a Codex model. Quota, subscription search, image generation, and Fast mode remain inside DSH.
|
|
33
|
-
|
|
34
|
-
With an existing `dsh` command, install with:
|
|
35
|
-
|
|
36
|
-
```sh
|
|
37
|
-
dsh plugin --profile web add dsh-codex-subscription
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
From a DSH-Portable product folder, run `./dsh plugin ...` (`.\dsh.exe plugin ...` in Windows PowerShell). See below for the complete official npm, Agent, update, and uninstall routes.
|
|
41
|
-
|
|
42
|
-
## Why this plugin
|
|
43
|
-
|
|
44
|
-
| Capability | What you get |
|
|
45
|
-
| --- | --- |
|
|
46
|
-
| **Subscription models** | Sign in to ChatGPT and use Codex without an OpenAI API key or Codex CLI |
|
|
47
|
-
| **Recoverable and diagnosable** | Sign-in state reconciles automatically; Settings can create a support report without credentials or account identifiers |
|
|
48
|
-
| **Visible quota** | Keep backend-provided standard Codex, Spark, and other limits separate |
|
|
49
|
-
| **Subscription search** | Explicitly choose DSH default search or Codex subscription search |
|
|
50
|
-
| **Codex image generation** | Describe an image in a DSH conversation and view the generated result in that session |
|
|
51
|
-
| **Fast mode (Beta)** | Switch between Standard and Fast directly in the composer |
|
|
52
|
-
|
|
53
|
-
These capabilities reuse the same local ChatGPT sign-in. Subscription routing failures stay visible and never silently switch to another paid route.
|
|
54
|
-
|
|
55
|
-
## Product screen
|
|
56
|
-
|
|
57
|
-
<p align="center">
|
|
58
|
-
<img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/settings-focus-en.png" width="820" alt="Codex subscription settings in DeepSeek Harness">
|
|
59
|
-
</p>
|
|
60
|
-
|
|
61
|
-
<details>
|
|
62
|
-
<summary>View the complete settings screen</summary>
|
|
63
|
-
|
|
64
|
-

|
|
65
|
-
|
|
66
|
-
</details>
|
|
67
|
-
|
|
68
|
-
## Prepare DSH
|
|
69
|
-
|
|
70
|
-
This plugin is currently compatible through DeepSeek Harness `0.1.1-rc.2` and requires a ChatGPT account that currently has Codex access.
|
|
71
|
-
|
|
72
|
-
- Do not want to configure Node.js? Use [DSH-Portable](https://github.com/WSL043/DSH-Portable), a community desktop distribution with portable and installed editions for Windows plus desktop packages for macOS and Linux.
|
|
73
|
-
- Prefer the official route? Follow the [DeepSeek Harness run guide](https://github.com/deepseek-ai/deepseek-harness#run).
|
|
74
|
-
|
|
75
|
-
## Install
|
|
76
|
-
|
|
77
|
-
### Let an Agent install it (recommended)
|
|
78
|
-
|
|
79
|
-
Send this link directly to your Agent:
|
|
80
|
-
|
|
81
|
-
**[Agent install, update, and uninstall guide](https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/AGENTS.md)**
|
|
82
|
-
|
|
83
|
-
```text
|
|
84
|
-
https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/AGENTS.md
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
The guide includes verification steps and tells the Agent to preserve the DSH profile, sign-in, and other plugins.
|
|
88
|
-
|
|
89
|
-
### Manual Windows install
|
|
90
|
-
|
|
91
|
-
Open PowerShell and paste this one line:
|
|
92
|
-
|
|
93
|
-
```powershell
|
|
94
|
-
irm 'https://github.com/WSL043/dsh-codex-subscription/releases/latest/download/dsh-codex-setup.ps1' | iex
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
The lightweight setup checks the current folder, the system command, common locations, and any running official DSH or
|
|
98
|
-
[DSH-Portable](https://github.com/WSL043/DSH-Portable), then invokes the official `plugin add`
|
|
99
|
-
operation once. It does not recursively scan disks, install pnpm, create a resident command, snapshot a
|
|
100
|
-
profile, or download the plugin twice. It needs no administrator access and never restarts DSH. Only
|
|
101
|
-
when no existing DSH is found does it use the official npm route pinned to `0.1.1-rc.2`; the setup warns
|
|
102
|
-
that first-time dependency resolution can take a while.
|
|
103
|
-
|
|
104
|
-
<details>
|
|
105
|
-
<summary>Official npm route (Node.js installed)</summary>
|
|
106
|
-
|
|
107
|
-
The official `npx @deepseek-ai/dsh web` command does not create a global `dsh` command. Keep the full `npx` prefix when installing the plugin:
|
|
108
|
-
|
|
109
|
-
```sh
|
|
110
|
-
npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add dsh-codex-subscription
|
|
111
|
-
npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web list dsh-codex-subscription --depth 0
|
|
112
|
-
npx -y @deepseek-ai/dsh@0.1.1-rc.2 --profile web --dump-config
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
</details>
|
|
116
|
-
|
|
117
|
-
<details>
|
|
118
|
-
<summary>An existing <code>dsh</code> command or a DSH-Portable folder</summary>
|
|
119
|
-
|
|
120
|
-
```sh
|
|
121
|
-
dsh plugin --profile web add dsh-codex-subscription
|
|
122
|
-
dsh plugin --profile web list dsh-codex-subscription --depth 0
|
|
123
|
-
dsh --profile web --dump-config
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
For DSH-Portable, replace `dsh` with `./dsh` (`.\dsh.exe` in PowerShell). The plugin list should contain
|
|
127
|
-
one `dsh-codex-subscription`, and the config should contain one `codex-subscription` entry.
|
|
128
|
-
|
|
129
|
-
</details>
|
|
130
|
-
|
|
131
|
-
Restart DSH manually after installation, then:
|
|
132
|
-
|
|
133
|
-
1. Open **Settings -> Codex**.
|
|
134
|
-
2. Sign in with a ChatGPT account that has Codex access.
|
|
135
|
-
3. Choose a search source.
|
|
136
|
-
4. Select a Codex model.
|
|
137
|
-
|
|
138
|
-
## Features
|
|
139
|
-
|
|
140
|
-
- ChatGPT OAuth sign-in with credentials kept on the host;
|
|
141
|
-
- Codex models and image generation directly inside DSH conversations;
|
|
142
|
-
- A clear choice between DSH default search and Codex subscription search;
|
|
143
|
-
- Actual backend-provided quota, reset time, and freshness;
|
|
144
|
-
- Separate standard Codex, Codex-Spark, Credits, and other independent limits;
|
|
145
|
-
- Optional composer quota for the selected Codex model (Beta, off by default);
|
|
146
|
-
- Standard or Fast mode for supported Codex models directly in the composer (Beta);
|
|
147
|
-
- A copyable support report in Settings that excludes OAuth credentials, account identifiers, and authorization timestamps;
|
|
148
|
-
- Visible errors when subscription routing is unavailable, with no silent paid fallback.
|
|
149
|
-
|
|
150
|
-
### Composer quota
|
|
151
|
-
|
|
152
|
-
<p align="center">
|
|
153
|
-
<img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/composer-quota-en.png" width="800" alt="Codex quota inside the composer">
|
|
154
|
-
</p>
|
|
155
|
-
|
|
156
|
-
The compact percentage appears only for a selected Codex model. Standard Codex uses the lowest remaining
|
|
157
|
-
window returned by the service; Spark uses its independent quota. The plugin does not hard-code a
|
|
158
|
-
“5-hour + weekly” layout or invent Credits and spending caps that the service did not return.
|
|
159
|
-
|
|
160
|
-
### Composer speed (Beta)
|
|
161
|
-
|
|
162
|
-
With a supported Codex model selected, open the composer's model menu to choose Standard or Fast.
|
|
163
|
-
Standard adds no icon; only Fast shows a lightning icon before the model name. Spark does not show the speed entry. Fast mode increases speed and uses more Credits;
|
|
164
|
-
see the [OpenAI Codex Speed documentation](https://learn.chatgpt.com/docs/agent-configuration/speed) for the current rules.
|
|
165
|
-
|
|
166
|
-
## Update and uninstall
|
|
167
|
-
|
|
168
|
-
On Windows, rerun the one-line setup above to update. For the official npm route, use:
|
|
169
|
-
|
|
170
|
-
```sh
|
|
171
|
-
npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web update dsh-codex-subscription
|
|
172
|
-
npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web list dsh-codex-subscription --depth 0
|
|
173
|
-
npx -y @deepseek-ai/dsh@0.1.1-rc.2 --profile web --dump-config
|
|
174
|
-
npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web remove dsh-codex-subscription
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
These operations preserve the DSH profile, other plugins, and saved sign-in.
|
|
178
|
-
|
|
179
|
-
<details>
|
|
180
|
-
<summary>Update or uninstall with an existing <code>dsh</code> command</summary>
|
|
181
|
-
|
|
182
|
-
```sh
|
|
183
|
-
dsh plugin --profile web update dsh-codex-subscription
|
|
184
|
-
dsh plugin --profile web list dsh-codex-subscription --depth 0
|
|
185
|
-
dsh --profile web --dump-config
|
|
186
|
-
dsh plugin --profile web remove dsh-codex-subscription
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
</details>
|
|
190
|
-
|
|
191
|
-
From a DSH-Portable folder, replace `dsh` above with `.\dsh.exe`.
|
|
192
|
-
|
|
193
|
-
## Troubleshooting
|
|
194
|
-
|
|
195
|
-
- **`dsh` is not recognized:** the official npm route does not create a global `dsh` command; use the complete `npx -y @deepseek-ai/dsh@0.1.1-rc.2 ...` command above;
|
|
196
|
-
- **`dsh.exe` is not recognized:** that file is not in the current folder; enter the DSH-Portable folder first or use the one-line Windows setup;
|
|
197
|
-
- **DSH-Portable is not found:** enter its folder and rerun setup, or run `.\dsh.exe plugin --profile web add dsh-codex-subscription` directly;
|
|
198
|
-
- **More than one DSH exists:** the one-line setup combines running copies with Portable folders detected in common locations; enter its number. For an Agent, use the intended folder or pass `-DshPath` explicitly;
|
|
199
|
-
- **Setup still fails:** send the Agent guide above to an Agent. Do not delete the profile or change the system PATH to force an install.
|
|
200
|
-
- **Need to report a problem:** generate a **Support diagnostics** report at the bottom of Settings, then open the [bug report form](https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml). The form collects exact DSH/plugin versions and reproduction steps. The generated report excludes credentials and account identifiers; still do not attach sign-in URLs, authorization codes, or browser callback addresses.
|
|
201
|
-
|
|
202
|
-
The ChatGPT Codex backend and DSH can change independently. This community project is not affiliated with or endorsed by DeepSeek or OpenAI.
|
|
203
|
-
|
|
204
|
-
Use the [bug report form](https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml) for project feedback.
|
|
205
|
-
For DSH plugin discussion, visit [DeepSeek Harness Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions).
|
|
206
|
-
Read [SECURITY.md](SECURITY.md) before reporting sensitive issues.
|
|
207
|
-
|
|
208
|
-
If this project is useful, the [Star button](https://github.com/WSL043/dsh-codex-subscription/stargazers) helps more DSH users find it.
|
|
209
|
-
|
|
210
|
-
[简体中文](README.md) · [MIT](LICENSE)
|