oc-chatgpt-multi-auth 5.2.0 → 5.2.3
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/README.md +21 -18
- package/config/README.md +10 -8
- package/config/opencode-legacy.json +47 -73
- package/config/opencode-modern.json +32 -38
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +589 -193
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts.d.ts +8 -0
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +145 -28
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +5 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/logger.d.ts +1 -0
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +25 -2
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/prompts/codex-opencode-bridge.d.ts +4 -3
- package/dist/lib/prompts/codex-opencode-bridge.d.ts.map +1 -1
- package/dist/lib/prompts/codex-opencode-bridge.js +73 -105
- package/dist/lib/prompts/codex-opencode-bridge.js.map +1 -1
- package/dist/lib/prompts/codex.d.ts +4 -4
- package/dist/lib/prompts/codex.d.ts.map +1 -1
- package/dist/lib/prompts/codex.js +35 -37
- package/dist/lib/prompts/codex.js.map +1 -1
- package/dist/lib/prompts/opencode-codex.d.ts.map +1 -1
- package/dist/lib/prompts/opencode-codex.js +100 -25
- package/dist/lib/prompts/opencode-codex.js.map +1 -1
- package/dist/lib/recovery.d.ts.map +1 -1
- package/dist/lib/recovery.js +10 -5
- package/dist/lib/recovery.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +2 -1
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +57 -6
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
- package/dist/lib/request/helpers/model-map.js +35 -25
- package/dist/lib/request/helpers/model-map.js.map +1 -1
- package/dist/lib/request/request-transformer.d.ts +3 -3
- package/dist/lib/request/request-transformer.d.ts.map +1 -1
- package/dist/lib/request/request-transformer.js +73 -35
- package/dist/lib/request/request-transformer.js.map +1 -1
- package/dist/lib/request/response-handler.d.ts.map +1 -1
- package/dist/lib/request/response-handler.js +101 -10
- package/dist/lib/request/response-handler.js.map +1 -1
- package/dist/lib/schemas.d.ts +19 -9
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +5 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/migrations.d.ts +8 -0
- package/dist/lib/storage/migrations.d.ts.map +1 -1
- package/dist/lib/storage/migrations.js +3 -9
- package/dist/lib/storage/migrations.js.map +1 -1
- package/dist/lib/storage/paths.d.ts.map +1 -1
- package/dist/lib/storage/paths.js +14 -2
- package/dist/lib/storage/paths.js.map +1 -1
- package/dist/lib/storage.d.ts +1 -0
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +124 -84
- package/dist/lib/storage.js.map +1 -1
- package/package.json +26 -12
- package/scripts/audit-dev-allowlist.js +114 -0
- package/dist/lib/request/local-fast-path.d.ts +0 -15
- package/dist/lib/request/local-fast-path.d.ts.map +0 -1
- package/dist/lib/request/local-fast-path.js +0 -164
- package/dist/lib/request/local-fast-path.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -28,13 +28,13 @@ import { queuedRefresh } from "./lib/refresh-queue.js";
|
|
|
28
28
|
import { openBrowserUrl } from "./lib/auth/browser.js";
|
|
29
29
|
import { startLocalOAuthServer } from "./lib/auth/server.js";
|
|
30
30
|
import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
|
|
31
|
-
import { getCodexMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getToastDurationMs, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, loadPluginConfig, } from "./lib/config.js";
|
|
31
|
+
import { getCodexMode, getRequestTransformMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getToastDurationMs, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, loadPluginConfig, } from "./lib/config.js";
|
|
32
32
|
import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
|
|
33
33
|
import { initLogger, logRequest, logDebug, logInfo, logWarn, logError, setCorrelationId, clearCorrelationId, } from "./lib/logger.js";
|
|
34
34
|
import { checkAndNotify } from "./lib/auto-update-checker.js";
|
|
35
35
|
import { handleContextOverflow } from "./lib/context-overflow.js";
|
|
36
|
-
import { AccountManager, getAccountIdCandidates, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, } from "./lib/accounts.js";
|
|
37
|
-
import { getStoragePath, loadAccounts, saveAccounts, clearAccounts, setStoragePath, exportAccounts, importAccounts, loadFlaggedAccounts, saveFlaggedAccounts, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
|
|
36
|
+
import { AccountManager, getAccountIdCandidates, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, } from "./lib/accounts.js";
|
|
37
|
+
import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, exportAccounts, importAccounts, loadFlaggedAccounts, saveFlaggedAccounts, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
|
|
38
38
|
import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
|
|
39
39
|
import { applyFastSessionDefaults } from "./lib/request/request-transformer.js";
|
|
40
40
|
import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
|
|
@@ -43,7 +43,7 @@ import { addJitter } from "./lib/rotation.js";
|
|
|
43
43
|
import { buildTableHeader, buildTableRow } from "./lib/table-formatter.js";
|
|
44
44
|
import { setUiRuntimeOptions } from "./lib/ui/runtime.js";
|
|
45
45
|
import { paintUiText, formatUiBadge, formatUiHeader, formatUiItem, formatUiKeyValue, formatUiSection } from "./lib/ui/format.js";
|
|
46
|
-
import { getModelFamily, MODEL_FAMILIES, prewarmCodexInstructions, } from "./lib/prompts/codex.js";
|
|
46
|
+
import { getModelFamily, getCodexInstructions, MODEL_FAMILIES, prewarmCodexInstructions, } from "./lib/prompts/codex.js";
|
|
47
47
|
import { prewarmOpenCodeCodexPrompt } from "./lib/prompts/opencode-codex.js";
|
|
48
48
|
import { createSessionRecoveryHook, isRecoverableError, detectErrorType, getRecoveryToastContent, } from "./lib/recovery.js";
|
|
49
49
|
/**
|
|
@@ -124,7 +124,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
124
124
|
accountLabel: choice.label,
|
|
125
125
|
};
|
|
126
126
|
};
|
|
127
|
-
const buildManualOAuthFlow = (pkce, url, onSuccess) => ({
|
|
127
|
+
const buildManualOAuthFlow = (pkce, url, expectedState, onSuccess) => ({
|
|
128
128
|
url,
|
|
129
129
|
method: "code",
|
|
130
130
|
instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL,
|
|
@@ -133,12 +133,29 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
133
133
|
if (!parsed.code) {
|
|
134
134
|
return "No authorization code found. Paste the full callback URL (e.g., http://localhost:1455/auth/callback?code=...)";
|
|
135
135
|
}
|
|
136
|
+
if (!parsed.state) {
|
|
137
|
+
return "Missing OAuth state. Paste the full callback URL including both code and state parameters.";
|
|
138
|
+
}
|
|
139
|
+
if (parsed.state !== expectedState) {
|
|
140
|
+
return "OAuth state mismatch. Restart login and paste the callback URL generated for this login attempt.";
|
|
141
|
+
}
|
|
136
142
|
return undefined;
|
|
137
143
|
},
|
|
138
144
|
callback: async (input) => {
|
|
139
145
|
const parsed = parseAuthorizationInput(input);
|
|
140
|
-
if (!parsed.code) {
|
|
141
|
-
return {
|
|
146
|
+
if (!parsed.code || !parsed.state) {
|
|
147
|
+
return {
|
|
148
|
+
type: "failed",
|
|
149
|
+
reason: "invalid_response",
|
|
150
|
+
message: "Missing authorization code or OAuth state",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (parsed.state !== expectedState) {
|
|
154
|
+
return {
|
|
155
|
+
type: "failed",
|
|
156
|
+
reason: "invalid_response",
|
|
157
|
+
message: "OAuth state mismatch. Restart login and try again.",
|
|
158
|
+
};
|
|
142
159
|
}
|
|
143
160
|
const tokens = await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
|
|
144
161
|
if (tokens?.type === "success") {
|
|
@@ -182,117 +199,123 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
182
199
|
const persistAccountPool = async (results, replaceAll = false) => {
|
|
183
200
|
if (results.length === 0)
|
|
184
201
|
return;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
await withAccountStorageTransaction(async (loadedStorage, persist) => {
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
const stored = replaceAll ? null : loadedStorage;
|
|
205
|
+
const accounts = stored?.accounts ? [...stored.accounts] : [];
|
|
206
|
+
const indexByRefreshToken = new Map();
|
|
207
|
+
const indexByAccountId = new Map();
|
|
208
|
+
const indexByEmail = new Map();
|
|
209
|
+
for (let i = 0; i < accounts.length; i += 1) {
|
|
210
|
+
const account = accounts[i];
|
|
211
|
+
if (!account)
|
|
212
|
+
continue;
|
|
213
|
+
if (account.refreshToken) {
|
|
214
|
+
indexByRefreshToken.set(account.refreshToken, i);
|
|
215
|
+
}
|
|
216
|
+
if (account.accountId) {
|
|
217
|
+
indexByAccountId.set(account.accountId, i);
|
|
218
|
+
}
|
|
219
|
+
if (account.email) {
|
|
220
|
+
indexByEmail.set(account.email, i);
|
|
221
|
+
}
|
|
203
222
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
223
|
+
for (const result of results) {
|
|
224
|
+
const accountId = result.accountIdOverride ?? extractAccountId(result.access);
|
|
225
|
+
const accountIdSource = accountId
|
|
226
|
+
? result.accountIdSource ??
|
|
227
|
+
(result.accountIdOverride ? "manual" : "token")
|
|
228
|
+
: undefined;
|
|
229
|
+
const accountLabel = result.accountLabel;
|
|
230
|
+
const accountEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
|
|
231
|
+
const existingByEmail = accountEmail && indexByEmail.has(accountEmail)
|
|
232
|
+
? indexByEmail.get(accountEmail)
|
|
233
|
+
: undefined;
|
|
234
|
+
const existingById = accountId && indexByAccountId.has(accountId)
|
|
235
|
+
? indexByAccountId.get(accountId)
|
|
236
|
+
: undefined;
|
|
237
|
+
const existingByToken = indexByRefreshToken.get(result.refresh);
|
|
238
|
+
const existingIndex = existingById ?? existingByEmail ?? existingByToken;
|
|
239
|
+
if (existingIndex === undefined) {
|
|
240
|
+
const newIndex = accounts.length;
|
|
241
|
+
accounts.push({
|
|
242
|
+
accountId,
|
|
243
|
+
accountIdSource,
|
|
244
|
+
accountLabel,
|
|
245
|
+
email: accountEmail,
|
|
246
|
+
refreshToken: result.refresh,
|
|
247
|
+
accessToken: result.access,
|
|
248
|
+
expiresAt: result.expires,
|
|
249
|
+
addedAt: now,
|
|
250
|
+
lastUsed: now,
|
|
251
|
+
});
|
|
252
|
+
indexByRefreshToken.set(result.refresh, newIndex);
|
|
253
|
+
if (accountId) {
|
|
254
|
+
indexByAccountId.set(accountId, newIndex);
|
|
255
|
+
}
|
|
256
|
+
if (accountEmail) {
|
|
257
|
+
indexByEmail.set(accountEmail, newIndex);
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const existing = accounts[existingIndex];
|
|
262
|
+
if (!existing)
|
|
263
|
+
continue;
|
|
264
|
+
const oldToken = existing.refreshToken;
|
|
265
|
+
const oldEmail = existing.email;
|
|
266
|
+
const nextEmail = accountEmail ?? existing.email;
|
|
267
|
+
const nextAccountId = accountId ?? existing.accountId;
|
|
268
|
+
const nextAccountIdSource = accountId ? accountIdSource ?? existing.accountIdSource : existing.accountIdSource;
|
|
269
|
+
const nextAccountLabel = accountLabel ?? existing.accountLabel;
|
|
270
|
+
accounts[existingIndex] = {
|
|
271
|
+
...existing,
|
|
272
|
+
accountId: nextAccountId,
|
|
273
|
+
accountIdSource: nextAccountIdSource,
|
|
274
|
+
accountLabel: nextAccountLabel,
|
|
275
|
+
email: nextEmail,
|
|
228
276
|
refreshToken: result.refresh,
|
|
229
|
-
|
|
277
|
+
accessToken: result.access,
|
|
278
|
+
expiresAt: result.expires,
|
|
230
279
|
lastUsed: now,
|
|
231
|
-
}
|
|
232
|
-
|
|
280
|
+
};
|
|
281
|
+
if (oldToken !== result.refresh) {
|
|
282
|
+
indexByRefreshToken.delete(oldToken);
|
|
283
|
+
indexByRefreshToken.set(result.refresh, existingIndex);
|
|
284
|
+
}
|
|
233
285
|
if (accountId) {
|
|
234
|
-
indexByAccountId.set(accountId,
|
|
286
|
+
indexByAccountId.set(accountId, existingIndex);
|
|
235
287
|
}
|
|
236
|
-
if (
|
|
237
|
-
indexByEmail.
|
|
288
|
+
if (oldEmail && oldEmail !== nextEmail) {
|
|
289
|
+
indexByEmail.delete(oldEmail);
|
|
290
|
+
}
|
|
291
|
+
if (nextEmail) {
|
|
292
|
+
indexByEmail.set(nextEmail, existingIndex);
|
|
238
293
|
}
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
const existing = accounts[existingIndex];
|
|
242
|
-
if (!existing)
|
|
243
|
-
continue;
|
|
244
|
-
const oldToken = existing.refreshToken;
|
|
245
|
-
const oldEmail = existing.email;
|
|
246
|
-
const nextEmail = accountEmail ?? existing.email;
|
|
247
|
-
const nextAccountId = accountId ?? existing.accountId;
|
|
248
|
-
const nextAccountIdSource = accountId ? accountIdSource ?? existing.accountIdSource : existing.accountIdSource;
|
|
249
|
-
const nextAccountLabel = accountLabel ?? existing.accountLabel;
|
|
250
|
-
accounts[existingIndex] = {
|
|
251
|
-
...existing,
|
|
252
|
-
accountId: nextAccountId,
|
|
253
|
-
accountIdSource: nextAccountIdSource,
|
|
254
|
-
accountLabel: nextAccountLabel,
|
|
255
|
-
email: nextEmail,
|
|
256
|
-
refreshToken: result.refresh,
|
|
257
|
-
lastUsed: now,
|
|
258
|
-
};
|
|
259
|
-
if (oldToken !== result.refresh) {
|
|
260
|
-
indexByRefreshToken.delete(oldToken);
|
|
261
|
-
indexByRefreshToken.set(result.refresh, existingIndex);
|
|
262
|
-
}
|
|
263
|
-
if (accountId) {
|
|
264
|
-
indexByAccountId.set(accountId, existingIndex);
|
|
265
|
-
}
|
|
266
|
-
if (oldEmail && oldEmail !== nextEmail) {
|
|
267
|
-
indexByEmail.delete(oldEmail);
|
|
268
|
-
}
|
|
269
|
-
if (nextEmail) {
|
|
270
|
-
indexByEmail.set(nextEmail, existingIndex);
|
|
271
294
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const activeIndex = replaceAll
|
|
276
|
-
? 0
|
|
277
|
-
: typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex)
|
|
278
|
-
? stored.activeIndex
|
|
279
|
-
: 0;
|
|
280
|
-
const clampedActiveIndex = Math.max(0, Math.min(activeIndex, accounts.length - 1));
|
|
281
|
-
const activeIndexByFamily = {};
|
|
282
|
-
for (const family of MODEL_FAMILIES) {
|
|
283
|
-
const storedFamilyIndex = stored?.activeIndexByFamily?.[family];
|
|
284
|
-
const rawFamilyIndex = replaceAll
|
|
295
|
+
if (accounts.length === 0)
|
|
296
|
+
return;
|
|
297
|
+
const activeIndex = replaceAll
|
|
285
298
|
? 0
|
|
286
|
-
: typeof
|
|
287
|
-
?
|
|
288
|
-
:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
299
|
+
: typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex)
|
|
300
|
+
? stored.activeIndex
|
|
301
|
+
: 0;
|
|
302
|
+
const clampedActiveIndex = Math.max(0, Math.min(activeIndex, accounts.length - 1));
|
|
303
|
+
const activeIndexByFamily = {};
|
|
304
|
+
for (const family of MODEL_FAMILIES) {
|
|
305
|
+
const storedFamilyIndex = stored?.activeIndexByFamily?.[family];
|
|
306
|
+
const rawFamilyIndex = replaceAll
|
|
307
|
+
? 0
|
|
308
|
+
: typeof storedFamilyIndex === "number" && Number.isFinite(storedFamilyIndex)
|
|
309
|
+
? storedFamilyIndex
|
|
310
|
+
: clampedActiveIndex;
|
|
311
|
+
activeIndexByFamily[family] = Math.max(0, Math.min(Math.floor(rawFamilyIndex), accounts.length - 1));
|
|
312
|
+
}
|
|
313
|
+
await persist({
|
|
314
|
+
version: 3,
|
|
315
|
+
accounts,
|
|
316
|
+
activeIndex: clampedActiveIndex,
|
|
317
|
+
activeIndexByFamily,
|
|
318
|
+
});
|
|
296
319
|
});
|
|
297
320
|
};
|
|
298
321
|
const showToast = async (message, variant = "success", options) => {
|
|
@@ -349,6 +372,14 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
349
372
|
account.email = email;
|
|
350
373
|
changed = true;
|
|
351
374
|
}
|
|
375
|
+
if (refreshed.access && refreshed.access !== account.accessToken) {
|
|
376
|
+
account.accessToken = refreshed.access;
|
|
377
|
+
changed = true;
|
|
378
|
+
}
|
|
379
|
+
if (typeof refreshed.expires === "number" && refreshed.expires !== account.expiresAt) {
|
|
380
|
+
account.expiresAt = refreshed.expires;
|
|
381
|
+
changed = true;
|
|
382
|
+
}
|
|
352
383
|
if (refreshed.refresh && refreshed.refresh !== account.refreshToken) {
|
|
353
384
|
account.refreshToken = refreshed.refresh;
|
|
354
385
|
changed = true;
|
|
@@ -434,16 +465,31 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
434
465
|
return;
|
|
435
466
|
}
|
|
436
467
|
const index = props.index ?? props.accountIndex;
|
|
437
|
-
if (typeof index === "number"
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
468
|
+
if (typeof index === "number") {
|
|
469
|
+
const storage = await loadAccounts();
|
|
470
|
+
if (!storage || index < 0 || index >= storage.accounts.length) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const now = Date.now();
|
|
474
|
+
const account = storage.accounts[index];
|
|
443
475
|
if (account) {
|
|
444
|
-
|
|
445
|
-
|
|
476
|
+
account.lastUsed = now;
|
|
477
|
+
account.lastSwitchReason = "rotation";
|
|
478
|
+
}
|
|
479
|
+
storage.activeIndex = index;
|
|
480
|
+
storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
|
|
481
|
+
for (const family of MODEL_FAMILIES) {
|
|
482
|
+
storage.activeIndexByFamily[family] = index;
|
|
483
|
+
}
|
|
484
|
+
await saveAccounts(storage);
|
|
485
|
+
// Reload manager from disk so we don't overwrite newer rotated
|
|
486
|
+
// refresh tokens with stale in-memory state.
|
|
487
|
+
if (cachedAccountManager) {
|
|
488
|
+
const reloadedManager = await AccountManager.loadFromDisk();
|
|
489
|
+
cachedAccountManager = reloadedManager;
|
|
490
|
+
accountManagerPromise = Promise.resolve(reloadedManager);
|
|
446
491
|
}
|
|
492
|
+
await showToast(`Switched to account ${index + 1}`, "info");
|
|
447
493
|
}
|
|
448
494
|
}
|
|
449
495
|
}
|
|
@@ -473,6 +519,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
473
519
|
*/
|
|
474
520
|
async loader(getAuth, provider) {
|
|
475
521
|
const auth = await getAuth();
|
|
522
|
+
const pluginConfig = loadPluginConfig();
|
|
523
|
+
applyUiRuntimeFromConfig(pluginConfig);
|
|
524
|
+
const perProjectAccounts = getPerProjectAccounts(pluginConfig);
|
|
525
|
+
setStoragePath(perProjectAccounts ? process.cwd() : null);
|
|
476
526
|
// Only handle OAuth auth type, skip API key auth
|
|
477
527
|
if (auth.type !== "oauth") {
|
|
478
528
|
return {};
|
|
@@ -497,7 +547,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
497
547
|
if (!accountManagerPromise) {
|
|
498
548
|
accountManagerPromise = AccountManager.loadFromDisk(auth);
|
|
499
549
|
}
|
|
500
|
-
|
|
550
|
+
let accountManager = await accountManagerPromise;
|
|
501
551
|
cachedAccountManager = accountManager;
|
|
502
552
|
const refreshToken = auth.type === "oauth" ? auth.refresh : "";
|
|
503
553
|
const needsPersist = refreshToken &&
|
|
@@ -517,9 +567,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
517
567
|
};
|
|
518
568
|
// Load plugin configuration and determine CODEX_MODE
|
|
519
569
|
// Priority: CODEX_MODE env var > config file > default (true)
|
|
520
|
-
const pluginConfig = loadPluginConfig();
|
|
521
|
-
applyUiRuntimeFromConfig(pluginConfig);
|
|
522
570
|
const codexMode = getCodexMode(pluginConfig);
|
|
571
|
+
const requestTransformMode = getRequestTransformMode(pluginConfig);
|
|
572
|
+
const useLegacyRequestTransform = requestTransformMode === "legacy";
|
|
523
573
|
const fastSessionEnabled = getFastSession(pluginConfig);
|
|
524
574
|
const fastSessionStrategy = getFastSessionStrategy(pluginConfig);
|
|
525
575
|
const fastSessionMaxInputItems = getFastSessionMaxInputItems(pluginConfig);
|
|
@@ -533,12 +583,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
533
583
|
const fallbackToGpt52OnUnsupportedGpt53 = getFallbackToGpt52OnUnsupportedGpt53(pluginConfig);
|
|
534
584
|
const unsupportedCodexFallbackChain = getUnsupportedCodexFallbackChain(pluginConfig);
|
|
535
585
|
const toastDurationMs = getToastDurationMs(pluginConfig);
|
|
536
|
-
const perProjectAccounts = getPerProjectAccounts(pluginConfig);
|
|
537
586
|
const fetchTimeoutMs = getFetchTimeoutMs(pluginConfig);
|
|
538
587
|
const streamStallTimeoutMs = getStreamStallTimeoutMs(pluginConfig);
|
|
539
|
-
if (perProjectAccounts) {
|
|
540
|
-
setStoragePath(process.cwd());
|
|
541
|
-
}
|
|
542
588
|
const sessionRecoveryEnabled = getSessionRecovery(pluginConfig);
|
|
543
589
|
const autoResumeEnabled = getAutoResume(pluginConfig);
|
|
544
590
|
const emptyResponseMaxRetries = getEmptyResponseMaxRetries(pluginConfig);
|
|
@@ -559,7 +605,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
559
605
|
const prewarmEnabled = process.env.CODEX_AUTH_PREWARM !== "0" &&
|
|
560
606
|
process.env.VITEST !== "true" &&
|
|
561
607
|
process.env.NODE_ENV !== "test";
|
|
562
|
-
if (!startupPrewarmTriggered && prewarmEnabled) {
|
|
608
|
+
if (!startupPrewarmTriggered && prewarmEnabled && useLegacyRequestTransform) {
|
|
563
609
|
startupPrewarmTriggered = true;
|
|
564
610
|
const configuredModels = Object.keys(userConfig.models ?? {});
|
|
565
611
|
prewarmCodexInstructions(configuredModels);
|
|
@@ -596,6 +642,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
596
642
|
*/
|
|
597
643
|
async fetch(input, init) {
|
|
598
644
|
try {
|
|
645
|
+
if (cachedAccountManager && cachedAccountManager !== accountManager) {
|
|
646
|
+
accountManager = cachedAccountManager;
|
|
647
|
+
}
|
|
599
648
|
// Step 1: Extract and rewrite URL for Codex backend
|
|
600
649
|
const originalUrl = extractRequestUrl(input);
|
|
601
650
|
const url = rewriteUrlForCodex(originalUrl);
|
|
@@ -660,6 +709,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
660
709
|
fastSession: fastSessionEnabled,
|
|
661
710
|
fastSessionStrategy,
|
|
662
711
|
fastSessionMaxInputItems,
|
|
712
|
+
requestTransformMode,
|
|
663
713
|
});
|
|
664
714
|
let requestInit = transformation?.updatedInit ?? baseInit;
|
|
665
715
|
let transformedBody = transformation?.body;
|
|
@@ -720,6 +770,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
720
770
|
while (true) {
|
|
721
771
|
const accountCount = accountManager.getAccountCount();
|
|
722
772
|
const attempted = new Set();
|
|
773
|
+
let restartAccountTraversalWithFallback = false;
|
|
723
774
|
while (attempted.size < Math.max(1, accountCount)) {
|
|
724
775
|
const account = accountManager.getCurrentOrNextForFamilyHybrid(modelFamily, model, { pidOffsetEnabled });
|
|
725
776
|
if (!account || attempted.has(account.index)) {
|
|
@@ -775,12 +826,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
775
826
|
await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
|
|
776
827
|
accountManager.markToastShown(account.index);
|
|
777
828
|
}
|
|
778
|
-
|
|
829
|
+
const headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
|
|
779
830
|
model,
|
|
780
831
|
promptCacheKey,
|
|
781
832
|
});
|
|
782
833
|
// Consume a token before making the request for proactive rate limiting
|
|
783
|
-
accountManager.consumeToken(account, modelFamily, model);
|
|
834
|
+
const tokenConsumed = accountManager.consumeToken(account, modelFamily, model);
|
|
835
|
+
if (!tokenConsumed) {
|
|
836
|
+
accountManager.recordRateLimit(account, modelFamily, model);
|
|
837
|
+
runtimeMetrics.accountRotations++;
|
|
838
|
+
runtimeMetrics.lastError =
|
|
839
|
+
`Local token bucket depleted for account ${account.index + 1} (${modelFamily}${model ? `:${model}` : ""})`;
|
|
840
|
+
logWarn(`Skipping account ${account.index + 1}: local token bucket depleted for ${modelFamily}${model ? `:${model}` : ""}`);
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
784
843
|
while (true) {
|
|
785
844
|
let response;
|
|
786
845
|
const fetchStart = performance.now();
|
|
@@ -868,7 +927,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
868
927
|
customChain: unsupportedCodexFallbackChain,
|
|
869
928
|
});
|
|
870
929
|
if (fallbackModel) {
|
|
871
|
-
const previousModel = model ?? "gpt-5
|
|
930
|
+
const previousModel = model ?? "gpt-5-codex";
|
|
872
931
|
const previousModelFamily = modelFamily;
|
|
873
932
|
attemptedUnsupportedFallbackModels.add(previousModel);
|
|
874
933
|
attemptedUnsupportedFallbackModels.add(fallbackModel);
|
|
@@ -896,11 +955,6 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
896
955
|
...(requestInit ?? {}),
|
|
897
956
|
body: JSON.stringify(transformedBody),
|
|
898
957
|
};
|
|
899
|
-
headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
|
|
900
|
-
model,
|
|
901
|
-
promptCacheKey,
|
|
902
|
-
});
|
|
903
|
-
accountManager.consumeToken(account, modelFamily, model);
|
|
904
958
|
runtimeMetrics.lastError = `Model fallback: ${previousModel} -> ${model}`;
|
|
905
959
|
logWarn(`Model ${previousModel} is unsupported for this ChatGPT account. Falling back to ${model}.`, {
|
|
906
960
|
unsupportedCodexPolicy,
|
|
@@ -910,7 +964,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
910
964
|
fallbackReason: "unsupported-model-entitlement",
|
|
911
965
|
});
|
|
912
966
|
await showToast(`Model ${previousModel} is not available for this account. Retrying with ${model}.`, "warning", { duration: toastDurationMs });
|
|
913
|
-
|
|
967
|
+
restartAccountTraversalWithFallback = true;
|
|
968
|
+
break;
|
|
914
969
|
}
|
|
915
970
|
if (unsupportedModelInfo.isUnsupported && !fallbackOnUnsupportedCodexModel) {
|
|
916
971
|
const blockedModel = unsupportedModelInfo.unsupportedModel ?? model ?? "requested model";
|
|
@@ -975,6 +1030,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
975
1030
|
const successResponse = await handleSuccessResponse(response, isStreaming, {
|
|
976
1031
|
streamStallTimeoutMs,
|
|
977
1032
|
});
|
|
1033
|
+
if (!successResponse.ok) {
|
|
1034
|
+
runtimeMetrics.failedRequests++;
|
|
1035
|
+
runtimeMetrics.lastError = `HTTP ${successResponse.status}`;
|
|
1036
|
+
return successResponse;
|
|
1037
|
+
}
|
|
978
1038
|
if (!isStreaming && emptyResponseMaxRetries > 0) {
|
|
979
1039
|
const clonedResponse = successResponse.clone();
|
|
980
1040
|
try {
|
|
@@ -1003,6 +1063,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1003
1063
|
runtimeMetrics.lastError = null;
|
|
1004
1064
|
return successResponse;
|
|
1005
1065
|
}
|
|
1066
|
+
if (restartAccountTraversalWithFallback) {
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (restartAccountTraversalWithFallback) {
|
|
1071
|
+
continue;
|
|
1006
1072
|
}
|
|
1007
1073
|
const waitMs = accountManager.getMinWaitTimeForFamily(modelFamily, model);
|
|
1008
1074
|
const count = accountManager.getAccountCount();
|
|
@@ -1052,9 +1118,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1052
1118
|
const authPluginConfig = loadPluginConfig();
|
|
1053
1119
|
applyUiRuntimeFromConfig(authPluginConfig);
|
|
1054
1120
|
const authPerProjectAccounts = getPerProjectAccounts(authPluginConfig);
|
|
1055
|
-
|
|
1056
|
-
setStoragePath(process.cwd());
|
|
1057
|
-
}
|
|
1121
|
+
setStoragePath(authPerProjectAccounts ? process.cwd() : null);
|
|
1058
1122
|
const accounts = [];
|
|
1059
1123
|
const noBrowser = inputs?.noBrowser === "true" ||
|
|
1060
1124
|
inputs?.["no-browser"] === "true";
|
|
@@ -1091,6 +1155,211 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1091
1155
|
message.includes("invalid refresh") ||
|
|
1092
1156
|
message.includes("token has been revoked"));
|
|
1093
1157
|
};
|
|
1158
|
+
const parseFiniteNumberHeader = (headers, name) => {
|
|
1159
|
+
const raw = headers.get(name);
|
|
1160
|
+
if (!raw)
|
|
1161
|
+
return undefined;
|
|
1162
|
+
const parsed = Number(raw);
|
|
1163
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1164
|
+
};
|
|
1165
|
+
const parseFiniteIntHeader = (headers, name) => {
|
|
1166
|
+
const raw = headers.get(name);
|
|
1167
|
+
if (!raw)
|
|
1168
|
+
return undefined;
|
|
1169
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1170
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1171
|
+
};
|
|
1172
|
+
const parseResetAtMs = (headers, prefix) => {
|
|
1173
|
+
const resetAfterSeconds = parseFiniteIntHeader(headers, `${prefix}-reset-after-seconds`);
|
|
1174
|
+
if (typeof resetAfterSeconds === "number" &&
|
|
1175
|
+
Number.isFinite(resetAfterSeconds) &&
|
|
1176
|
+
resetAfterSeconds > 0) {
|
|
1177
|
+
return Date.now() + resetAfterSeconds * 1000;
|
|
1178
|
+
}
|
|
1179
|
+
const resetAtRaw = headers.get(`${prefix}-reset-at`);
|
|
1180
|
+
if (!resetAtRaw)
|
|
1181
|
+
return undefined;
|
|
1182
|
+
const trimmed = resetAtRaw.trim();
|
|
1183
|
+
if (/^\d+$/.test(trimmed)) {
|
|
1184
|
+
const parsedNumber = Number.parseInt(trimmed, 10);
|
|
1185
|
+
if (Number.isFinite(parsedNumber) && parsedNumber > 0) {
|
|
1186
|
+
// Upstream sometimes returns seconds since epoch.
|
|
1187
|
+
return parsedNumber < 10_000_000_000 ? parsedNumber * 1000 : parsedNumber;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
const parsedDate = Date.parse(trimmed);
|
|
1191
|
+
return Number.isFinite(parsedDate) ? parsedDate : undefined;
|
|
1192
|
+
};
|
|
1193
|
+
const hasCodexQuotaHeaders = (headers) => {
|
|
1194
|
+
const keys = [
|
|
1195
|
+
"x-codex-primary-used-percent",
|
|
1196
|
+
"x-codex-primary-window-minutes",
|
|
1197
|
+
"x-codex-primary-reset-at",
|
|
1198
|
+
"x-codex-primary-reset-after-seconds",
|
|
1199
|
+
"x-codex-secondary-used-percent",
|
|
1200
|
+
"x-codex-secondary-window-minutes",
|
|
1201
|
+
"x-codex-secondary-reset-at",
|
|
1202
|
+
"x-codex-secondary-reset-after-seconds",
|
|
1203
|
+
];
|
|
1204
|
+
return keys.some((key) => headers.get(key) !== null);
|
|
1205
|
+
};
|
|
1206
|
+
const parseCodexQuotaSnapshot = (headers, status) => {
|
|
1207
|
+
if (!hasCodexQuotaHeaders(headers))
|
|
1208
|
+
return null;
|
|
1209
|
+
const primaryPrefix = "x-codex-primary";
|
|
1210
|
+
const secondaryPrefix = "x-codex-secondary";
|
|
1211
|
+
const primary = {
|
|
1212
|
+
usedPercent: parseFiniteNumberHeader(headers, `${primaryPrefix}-used-percent`),
|
|
1213
|
+
windowMinutes: parseFiniteIntHeader(headers, `${primaryPrefix}-window-minutes`),
|
|
1214
|
+
resetAtMs: parseResetAtMs(headers, primaryPrefix),
|
|
1215
|
+
};
|
|
1216
|
+
const secondary = {
|
|
1217
|
+
usedPercent: parseFiniteNumberHeader(headers, `${secondaryPrefix}-used-percent`),
|
|
1218
|
+
windowMinutes: parseFiniteIntHeader(headers, `${secondaryPrefix}-window-minutes`),
|
|
1219
|
+
resetAtMs: parseResetAtMs(headers, secondaryPrefix),
|
|
1220
|
+
};
|
|
1221
|
+
const planTypeRaw = headers.get("x-codex-plan-type");
|
|
1222
|
+
const planType = planTypeRaw && planTypeRaw.trim() ? planTypeRaw.trim() : undefined;
|
|
1223
|
+
const activeLimit = parseFiniteIntHeader(headers, "x-codex-active-limit");
|
|
1224
|
+
return { status, planType, activeLimit, primary, secondary };
|
|
1225
|
+
};
|
|
1226
|
+
const formatQuotaWindowLabel = (windowMinutes) => {
|
|
1227
|
+
if (!windowMinutes || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
|
|
1228
|
+
return "quota";
|
|
1229
|
+
}
|
|
1230
|
+
if (windowMinutes % 1440 === 0)
|
|
1231
|
+
return `${windowMinutes / 1440}d`;
|
|
1232
|
+
if (windowMinutes % 60 === 0)
|
|
1233
|
+
return `${windowMinutes / 60}h`;
|
|
1234
|
+
return `${windowMinutes}m`;
|
|
1235
|
+
};
|
|
1236
|
+
const formatResetAt = (resetAtMs) => {
|
|
1237
|
+
if (!resetAtMs || !Number.isFinite(resetAtMs) || resetAtMs <= 0)
|
|
1238
|
+
return undefined;
|
|
1239
|
+
const date = new Date(resetAtMs);
|
|
1240
|
+
if (!Number.isFinite(date.getTime()))
|
|
1241
|
+
return undefined;
|
|
1242
|
+
const now = new Date();
|
|
1243
|
+
const sameDay = now.getFullYear() === date.getFullYear() &&
|
|
1244
|
+
now.getMonth() === date.getMonth() &&
|
|
1245
|
+
now.getDate() === date.getDate();
|
|
1246
|
+
const time = date.toLocaleTimeString(undefined, {
|
|
1247
|
+
hour: "2-digit",
|
|
1248
|
+
minute: "2-digit",
|
|
1249
|
+
hour12: false,
|
|
1250
|
+
});
|
|
1251
|
+
if (sameDay)
|
|
1252
|
+
return time;
|
|
1253
|
+
const day = date.toLocaleDateString(undefined, { month: "short", day: "2-digit" });
|
|
1254
|
+
return `${time} on ${day}`;
|
|
1255
|
+
};
|
|
1256
|
+
const formatCodexQuotaLine = (snapshot) => {
|
|
1257
|
+
const summarizeWindow = (label, window) => {
|
|
1258
|
+
const used = window.usedPercent;
|
|
1259
|
+
const left = typeof used === "number" && Number.isFinite(used)
|
|
1260
|
+
? Math.max(0, Math.min(100, Math.round(100 - used)))
|
|
1261
|
+
: undefined;
|
|
1262
|
+
const reset = formatResetAt(window.resetAtMs);
|
|
1263
|
+
let summary = label;
|
|
1264
|
+
if (left !== undefined)
|
|
1265
|
+
summary = `${summary} ${left}% left`;
|
|
1266
|
+
if (reset)
|
|
1267
|
+
summary = `${summary} (resets ${reset})`;
|
|
1268
|
+
return summary;
|
|
1269
|
+
};
|
|
1270
|
+
const primaryLabel = formatQuotaWindowLabel(snapshot.primary.windowMinutes);
|
|
1271
|
+
const secondaryLabel = formatQuotaWindowLabel(snapshot.secondary.windowMinutes);
|
|
1272
|
+
const parts = [
|
|
1273
|
+
summarizeWindow(primaryLabel, snapshot.primary),
|
|
1274
|
+
summarizeWindow(secondaryLabel, snapshot.secondary),
|
|
1275
|
+
];
|
|
1276
|
+
if (snapshot.planType)
|
|
1277
|
+
parts.push(`plan:${snapshot.planType}`);
|
|
1278
|
+
if (typeof snapshot.activeLimit === "number" && Number.isFinite(snapshot.activeLimit)) {
|
|
1279
|
+
parts.push(`active:${snapshot.activeLimit}`);
|
|
1280
|
+
}
|
|
1281
|
+
if (snapshot.status === 429)
|
|
1282
|
+
parts.push("rate-limited");
|
|
1283
|
+
return parts.join(", ");
|
|
1284
|
+
};
|
|
1285
|
+
const fetchCodexQuotaSnapshot = async (params) => {
|
|
1286
|
+
const QUOTA_PROBE_MODELS = ["gpt-5-codex", "gpt-5.3-codex", "gpt-5.2-codex"];
|
|
1287
|
+
let lastError = null;
|
|
1288
|
+
for (const model of QUOTA_PROBE_MODELS) {
|
|
1289
|
+
try {
|
|
1290
|
+
const instructions = await getCodexInstructions(model);
|
|
1291
|
+
const probeBody = {
|
|
1292
|
+
model,
|
|
1293
|
+
stream: true,
|
|
1294
|
+
store: false,
|
|
1295
|
+
include: ["reasoning.encrypted_content"],
|
|
1296
|
+
instructions,
|
|
1297
|
+
input: [
|
|
1298
|
+
{
|
|
1299
|
+
type: "message",
|
|
1300
|
+
role: "user",
|
|
1301
|
+
content: [{ type: "input_text", text: "quota ping" }],
|
|
1302
|
+
},
|
|
1303
|
+
],
|
|
1304
|
+
reasoning: { effort: "none", summary: "auto" },
|
|
1305
|
+
text: { verbosity: "low" },
|
|
1306
|
+
};
|
|
1307
|
+
const headers = createCodexHeaders(undefined, params.accountId, params.accessToken, {
|
|
1308
|
+
model,
|
|
1309
|
+
});
|
|
1310
|
+
headers.set("content-type", "application/json; charset=utf-8");
|
|
1311
|
+
const controller = new AbortController();
|
|
1312
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
1313
|
+
let response;
|
|
1314
|
+
try {
|
|
1315
|
+
response = await fetch(`${CODEX_BASE_URL}/codex/responses`, {
|
|
1316
|
+
method: "POST",
|
|
1317
|
+
headers,
|
|
1318
|
+
body: JSON.stringify(probeBody),
|
|
1319
|
+
signal: controller.signal,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
finally {
|
|
1323
|
+
clearTimeout(timeout);
|
|
1324
|
+
}
|
|
1325
|
+
const snapshot = parseCodexQuotaSnapshot(response.headers, response.status);
|
|
1326
|
+
if (snapshot) {
|
|
1327
|
+
// We only need headers; cancel the SSE stream immediately.
|
|
1328
|
+
try {
|
|
1329
|
+
await response.body?.cancel();
|
|
1330
|
+
}
|
|
1331
|
+
catch {
|
|
1332
|
+
// Ignore cancellation failures.
|
|
1333
|
+
}
|
|
1334
|
+
return snapshot;
|
|
1335
|
+
}
|
|
1336
|
+
if (!response.ok) {
|
|
1337
|
+
const bodyText = await response.text().catch(() => "");
|
|
1338
|
+
let errorBody = undefined;
|
|
1339
|
+
try {
|
|
1340
|
+
errorBody = bodyText ? JSON.parse(bodyText) : undefined;
|
|
1341
|
+
}
|
|
1342
|
+
catch {
|
|
1343
|
+
errorBody = { error: { message: bodyText } };
|
|
1344
|
+
}
|
|
1345
|
+
const unsupportedInfo = getUnsupportedCodexModelInfo(errorBody);
|
|
1346
|
+
if (unsupportedInfo.isUnsupported) {
|
|
1347
|
+
lastError = new Error(unsupportedInfo.message ?? `Model '${model}' unsupported for this account`);
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
const message = (typeof errorBody?.error?.message === "string"
|
|
1351
|
+
? errorBody.error?.message
|
|
1352
|
+
: bodyText) || `HTTP ${response.status}`;
|
|
1353
|
+
throw new Error(message);
|
|
1354
|
+
}
|
|
1355
|
+
lastError = new Error("Codex response did not include quota headers");
|
|
1356
|
+
}
|
|
1357
|
+
catch (error) {
|
|
1358
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
throw lastError ?? new Error("Failed to fetch quotas");
|
|
1362
|
+
};
|
|
1094
1363
|
const runAccountCheck = async (deepProbe) => {
|
|
1095
1364
|
const loadedStorage = await hydrateEmails(await loadAccounts());
|
|
1096
1365
|
const workingStorage = loadedStorage
|
|
@@ -1114,7 +1383,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1114
1383
|
let ok = 0;
|
|
1115
1384
|
let disabled = 0;
|
|
1116
1385
|
let errors = 0;
|
|
1117
|
-
console.log(`\nChecking ${deepProbe ? "full account health" : "quotas
|
|
1386
|
+
console.log(`\nChecking ${deepProbe ? "full account health" : "quotas"} for all accounts...\n`);
|
|
1118
1387
|
for (let i = 0; i < total; i += 1) {
|
|
1119
1388
|
const account = workingStorage.accounts[i];
|
|
1120
1389
|
if (!account)
|
|
@@ -1126,52 +1395,149 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1126
1395
|
continue;
|
|
1127
1396
|
}
|
|
1128
1397
|
try {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1398
|
+
// If we already have a valid cached access token, don't force-refresh.
|
|
1399
|
+
// This avoids flagging accounts where the refresh token has been burned
|
|
1400
|
+
// but the access token is still valid (same behavior as Codex CLI).
|
|
1401
|
+
const nowMs = Date.now();
|
|
1402
|
+
let accessToken = null;
|
|
1403
|
+
let tokenAccountId = undefined;
|
|
1404
|
+
let authDetail = "OK";
|
|
1405
|
+
if (account.accessToken &&
|
|
1406
|
+
(typeof account.expiresAt !== "number" ||
|
|
1407
|
+
!Number.isFinite(account.expiresAt) ||
|
|
1408
|
+
account.expiresAt > nowMs)) {
|
|
1409
|
+
accessToken = account.accessToken;
|
|
1410
|
+
authDetail = "OK (cached access)";
|
|
1411
|
+
tokenAccountId = extractAccountId(account.accessToken);
|
|
1412
|
+
if (tokenAccountId &&
|
|
1413
|
+
shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId) &&
|
|
1414
|
+
tokenAccountId !== account.accountId) {
|
|
1415
|
+
account.accountId = tokenAccountId;
|
|
1416
|
+
account.accountIdSource = "token";
|
|
1417
|
+
storageChanged = true;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
// If Codex CLI has a valid cached access token for this email, use it
|
|
1421
|
+
// instead of forcing a refresh.
|
|
1422
|
+
if (!accessToken) {
|
|
1423
|
+
const cached = await lookupCodexCliTokensByEmail(account.email);
|
|
1424
|
+
if (cached &&
|
|
1425
|
+
(typeof cached.expiresAt !== "number" ||
|
|
1426
|
+
!Number.isFinite(cached.expiresAt) ||
|
|
1427
|
+
cached.expiresAt > nowMs)) {
|
|
1428
|
+
accessToken = cached.accessToken;
|
|
1429
|
+
authDetail = "OK (Codex CLI cache)";
|
|
1430
|
+
if (cached.refreshToken && cached.refreshToken !== account.refreshToken) {
|
|
1431
|
+
account.refreshToken = cached.refreshToken;
|
|
1432
|
+
storageChanged = true;
|
|
1144
1433
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1434
|
+
if (cached.accessToken && cached.accessToken !== account.accessToken) {
|
|
1435
|
+
account.accessToken = cached.accessToken;
|
|
1436
|
+
storageChanged = true;
|
|
1437
|
+
}
|
|
1438
|
+
if (cached.expiresAt !== account.expiresAt) {
|
|
1439
|
+
account.expiresAt = cached.expiresAt;
|
|
1440
|
+
storageChanged = true;
|
|
1441
|
+
}
|
|
1442
|
+
const hydratedEmail = sanitizeEmail(extractAccountEmail(cached.accessToken));
|
|
1443
|
+
if (hydratedEmail && hydratedEmail !== account.email) {
|
|
1444
|
+
account.email = hydratedEmail;
|
|
1445
|
+
storageChanged = true;
|
|
1446
|
+
}
|
|
1447
|
+
tokenAccountId = extractAccountId(cached.accessToken);
|
|
1448
|
+
if (tokenAccountId &&
|
|
1449
|
+
shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId) &&
|
|
1450
|
+
tokenAccountId !== account.accountId) {
|
|
1451
|
+
account.accountId = tokenAccountId;
|
|
1452
|
+
account.accountIdSource = "token";
|
|
1453
|
+
storageChanged = true;
|
|
1147
1454
|
}
|
|
1148
|
-
removeFromActive.add(account.refreshToken);
|
|
1149
|
-
flaggedChanged = true;
|
|
1150
1455
|
}
|
|
1151
|
-
continue;
|
|
1152
1456
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1457
|
+
if (!accessToken) {
|
|
1458
|
+
const refreshResult = await queuedRefresh(account.refreshToken);
|
|
1459
|
+
if (refreshResult.type !== "success") {
|
|
1460
|
+
errors += 1;
|
|
1461
|
+
const message = refreshResult.message ?? refreshResult.reason ?? "refresh failed";
|
|
1462
|
+
console.log(`[${i + 1}/${total}] ${label}: ERROR (${message})`);
|
|
1463
|
+
if (deepProbe && isFlaggableFailure(refreshResult)) {
|
|
1464
|
+
const existingIndex = flaggedStorage.accounts.findIndex((flagged) => flagged.refreshToken === account.refreshToken);
|
|
1465
|
+
const flaggedRecord = {
|
|
1466
|
+
...account,
|
|
1467
|
+
flaggedAt: Date.now(),
|
|
1468
|
+
flaggedReason: "token-invalid",
|
|
1469
|
+
lastError: message,
|
|
1470
|
+
};
|
|
1471
|
+
if (existingIndex >= 0) {
|
|
1472
|
+
flaggedStorage.accounts[existingIndex] = flaggedRecord;
|
|
1473
|
+
}
|
|
1474
|
+
else {
|
|
1475
|
+
flaggedStorage.accounts.push(flaggedRecord);
|
|
1476
|
+
}
|
|
1477
|
+
removeFromActive.add(account.refreshToken);
|
|
1478
|
+
flaggedChanged = true;
|
|
1479
|
+
}
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
accessToken = refreshResult.access;
|
|
1483
|
+
authDetail = "OK";
|
|
1484
|
+
if (refreshResult.refresh !== account.refreshToken) {
|
|
1485
|
+
account.refreshToken = refreshResult.refresh;
|
|
1486
|
+
storageChanged = true;
|
|
1487
|
+
}
|
|
1488
|
+
if (refreshResult.access && refreshResult.access !== account.accessToken) {
|
|
1489
|
+
account.accessToken = refreshResult.access;
|
|
1490
|
+
storageChanged = true;
|
|
1491
|
+
}
|
|
1492
|
+
if (typeof refreshResult.expires === "number" &&
|
|
1493
|
+
refreshResult.expires !== account.expiresAt) {
|
|
1494
|
+
account.expiresAt = refreshResult.expires;
|
|
1495
|
+
storageChanged = true;
|
|
1496
|
+
}
|
|
1497
|
+
const hydratedEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
|
|
1498
|
+
if (hydratedEmail && hydratedEmail !== account.email) {
|
|
1499
|
+
account.email = hydratedEmail;
|
|
1500
|
+
storageChanged = true;
|
|
1501
|
+
}
|
|
1502
|
+
tokenAccountId = extractAccountId(refreshResult.access);
|
|
1503
|
+
if (tokenAccountId &&
|
|
1504
|
+
shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId) &&
|
|
1505
|
+
tokenAccountId !== account.accountId) {
|
|
1506
|
+
account.accountId = tokenAccountId;
|
|
1507
|
+
account.accountIdSource = "token";
|
|
1508
|
+
storageChanged = true;
|
|
1509
|
+
}
|
|
1157
1510
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
account.email = hydratedEmail;
|
|
1161
|
-
storageChanged = true;
|
|
1511
|
+
if (!accessToken) {
|
|
1512
|
+
throw new Error("Missing access token after refresh");
|
|
1162
1513
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1514
|
+
if (deepProbe) {
|
|
1515
|
+
ok += 1;
|
|
1516
|
+
const detail = tokenAccountId
|
|
1517
|
+
? `${authDetail} (id:${tokenAccountId.slice(-6)})`
|
|
1518
|
+
: authDetail;
|
|
1519
|
+
console.log(`[${i + 1}/${total}] ${label}: ${detail}`);
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
try {
|
|
1523
|
+
const requestAccountId = resolveRequestAccountId(account.accountId, account.accountIdSource, tokenAccountId) ??
|
|
1524
|
+
tokenAccountId ??
|
|
1525
|
+
account.accountId;
|
|
1526
|
+
if (!requestAccountId) {
|
|
1527
|
+
throw new Error("Missing accountId for quota probe");
|
|
1528
|
+
}
|
|
1529
|
+
const snapshot = await fetchCodexQuotaSnapshot({
|
|
1530
|
+
accountId: requestAccountId,
|
|
1531
|
+
accessToken,
|
|
1532
|
+
});
|
|
1533
|
+
ok += 1;
|
|
1534
|
+
console.log(`[${i + 1}/${total}] ${label}: ${formatCodexQuotaLine(snapshot)}`);
|
|
1535
|
+
}
|
|
1536
|
+
catch (error) {
|
|
1537
|
+
errors += 1;
|
|
1538
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1539
|
+
console.log(`[${i + 1}/${total}] ${label}: ERROR (${message.slice(0, 160)})`);
|
|
1170
1540
|
}
|
|
1171
|
-
const deepProbeDetail = deepProbe && tokenAccountId
|
|
1172
|
-
? `OK (id:${tokenAccountId.slice(-6)})`
|
|
1173
|
-
: "OK";
|
|
1174
|
-
console.log(`[${i + 1}/${total}] ${label}: ${deepProbeDetail}`);
|
|
1175
1541
|
}
|
|
1176
1542
|
catch (error) {
|
|
1177
1543
|
errors += 1;
|
|
@@ -1213,6 +1579,33 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1213
1579
|
continue;
|
|
1214
1580
|
const label = flagged.email ?? flagged.accountLabel ?? `Flagged ${i + 1}`;
|
|
1215
1581
|
try {
|
|
1582
|
+
const cached = await lookupCodexCliTokensByEmail(flagged.email);
|
|
1583
|
+
const now = Date.now();
|
|
1584
|
+
if (cached &&
|
|
1585
|
+
typeof cached.expiresAt === "number" &&
|
|
1586
|
+
Number.isFinite(cached.expiresAt) &&
|
|
1587
|
+
cached.expiresAt > now) {
|
|
1588
|
+
const refreshToken = typeof cached.refreshToken === "string" && cached.refreshToken.trim()
|
|
1589
|
+
? cached.refreshToken.trim()
|
|
1590
|
+
: flagged.refreshToken;
|
|
1591
|
+
const resolved = resolveAccountSelection({
|
|
1592
|
+
type: "success",
|
|
1593
|
+
access: cached.accessToken,
|
|
1594
|
+
refresh: refreshToken,
|
|
1595
|
+
expires: cached.expiresAt,
|
|
1596
|
+
multiAccount: true,
|
|
1597
|
+
});
|
|
1598
|
+
if (!resolved.accountIdOverride && flagged.accountId) {
|
|
1599
|
+
resolved.accountIdOverride = flagged.accountId;
|
|
1600
|
+
resolved.accountIdSource = flagged.accountIdSource ?? "manual";
|
|
1601
|
+
}
|
|
1602
|
+
if (!resolved.accountLabel && flagged.accountLabel) {
|
|
1603
|
+
resolved.accountLabel = flagged.accountLabel;
|
|
1604
|
+
}
|
|
1605
|
+
restored.push(resolved);
|
|
1606
|
+
console.log(`[${i + 1}/${flaggedStorage.accounts.length}] ${label}: RESTORED (Codex CLI cache)`);
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1216
1609
|
const refreshResult = await queuedRefresh(flagged.refreshToken);
|
|
1217
1610
|
if (refreshResult.type !== "success") {
|
|
1218
1611
|
console.log(`[${i + 1}/${flaggedStorage.accounts.length}] ${label}: STILL FLAGGED (${refreshResult.message ?? refreshResult.reason ?? "refresh failed"})`);
|
|
@@ -1398,8 +1791,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1398
1791
|
targetCount = 1;
|
|
1399
1792
|
}
|
|
1400
1793
|
if (useManualMode) {
|
|
1401
|
-
const { pkce, url } = await createAuthorizationFlow();
|
|
1402
|
-
return buildManualOAuthFlow(pkce, url, async (tokens) => {
|
|
1794
|
+
const { pkce, state, url } = await createAuthorizationFlow();
|
|
1795
|
+
return buildManualOAuthFlow(pkce, url, state, async (tokens) => {
|
|
1403
1796
|
try {
|
|
1404
1797
|
await persistAccountPool([tokens], startFresh);
|
|
1405
1798
|
invalidateAccountManagerCache();
|
|
@@ -1524,11 +1917,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1524
1917
|
const manualPluginConfig = loadPluginConfig();
|
|
1525
1918
|
applyUiRuntimeFromConfig(manualPluginConfig);
|
|
1526
1919
|
const manualPerProjectAccounts = getPerProjectAccounts(manualPluginConfig);
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
const { pkce, url } = await createAuthorizationFlow();
|
|
1531
|
-
return buildManualOAuthFlow(pkce, url, async (tokens) => {
|
|
1920
|
+
setStoragePath(manualPerProjectAccounts ? process.cwd() : null);
|
|
1921
|
+
const { pkce, state, url } = await createAuthorizationFlow();
|
|
1922
|
+
return buildManualOAuthFlow(pkce, url, state, async (tokens) => {
|
|
1532
1923
|
try {
|
|
1533
1924
|
await persistAccountPool([tokens], false);
|
|
1534
1925
|
}
|
|
@@ -1710,8 +2101,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1710
2101
|
return `Switched to ${formatAccountLabel(account, targetIndex)} but failed to persist. Changes may be lost on restart.`;
|
|
1711
2102
|
}
|
|
1712
2103
|
if (cachedAccountManager) {
|
|
1713
|
-
|
|
1714
|
-
|
|
2104
|
+
const reloadedManager = await AccountManager.loadFromDisk();
|
|
2105
|
+
cachedAccountManager = reloadedManager;
|
|
2106
|
+
accountManagerPromise = Promise.resolve(reloadedManager);
|
|
1715
2107
|
}
|
|
1716
2108
|
const label = formatAccountLabel(account, targetIndex);
|
|
1717
2109
|
if (ui.v2Enabled) {
|
|
@@ -2032,12 +2424,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2032
2424
|
return `Removed ${formatAccountLabel(account, targetIndex)} from memory but failed to persist. Changes may be lost on restart.`;
|
|
2033
2425
|
}
|
|
2034
2426
|
if (cachedAccountManager) {
|
|
2035
|
-
const
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
cachedAccountManager.removeAccount(managedAccount);
|
|
2039
|
-
await cachedAccountManager.saveToDisk();
|
|
2040
|
-
}
|
|
2427
|
+
const reloadedManager = await AccountManager.loadFromDisk();
|
|
2428
|
+
cachedAccountManager = reloadedManager;
|
|
2429
|
+
accountManagerPromise = Promise.resolve(reloadedManager);
|
|
2041
2430
|
}
|
|
2042
2431
|
const remaining = storage.accounts.length;
|
|
2043
2432
|
if (ui.v2Enabled) {
|
|
@@ -2090,6 +2479,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2090
2479
|
const refreshResult = await queuedRefresh(account.refreshToken);
|
|
2091
2480
|
if (refreshResult.type === "success") {
|
|
2092
2481
|
account.refreshToken = refreshResult.refresh;
|
|
2482
|
+
account.accessToken = refreshResult.access;
|
|
2483
|
+
account.expiresAt = refreshResult.expires;
|
|
2093
2484
|
results.push(` ${getStatusMarker(ui, "ok")} ${label}: Refreshed`);
|
|
2094
2485
|
refreshedCount++;
|
|
2095
2486
|
}
|
|
@@ -2105,6 +2496,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2105
2496
|
}
|
|
2106
2497
|
}
|
|
2107
2498
|
await saveAccounts(storage);
|
|
2499
|
+
if (cachedAccountManager) {
|
|
2500
|
+
const reloadedManager = await AccountManager.loadFromDisk();
|
|
2501
|
+
cachedAccountManager = reloadedManager;
|
|
2502
|
+
accountManagerPromise = Promise.resolve(reloadedManager);
|
|
2503
|
+
}
|
|
2108
2504
|
results.push("");
|
|
2109
2505
|
results.push(`Summary: ${refreshedCount} refreshed, ${failedCount} failed`);
|
|
2110
2506
|
if (ui.v2Enabled) {
|