opencode-openai-codex-multi-auth 4.5.5 → 4.5.9
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 +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +356 -48
- package/dist/index.js.map +1 -1
- package/dist/lib/account-matching.d.ts.map +1 -1
- package/dist/lib/account-matching.js +3 -37
- package/dist/lib/account-matching.js.map +1 -1
- package/dist/lib/accounts.d.ts +22 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +291 -56
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/auth/auth.d.ts +5 -0
- package/dist/lib/auth/auth.d.ts.map +1 -1
- package/dist/lib/auth/auth.js +19 -1
- package/dist/lib/auth/auth.js.map +1 -1
- package/dist/lib/cli.d.ts +7 -1
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +57 -3
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/config.d.ts +8 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +35 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/formatting.d.ts +9 -0
- package/dist/lib/formatting.d.ts.map +1 -0
- package/dist/lib/formatting.js +71 -0
- package/dist/lib/formatting.js.map +1 -0
- package/dist/lib/oauth-success.html +1 -1
- package/dist/lib/rate-limit.d.ts +36 -0
- package/dist/lib/rate-limit.d.ts.map +1 -0
- package/dist/lib/rate-limit.js +78 -0
- package/dist/lib/rate-limit.js.map +1 -0
- package/dist/lib/refresh-queue.d.ts +37 -0
- package/dist/lib/refresh-queue.d.ts.map +1 -0
- package/dist/lib/refresh-queue.js +83 -0
- package/dist/lib/refresh-queue.js.map +1 -0
- package/dist/lib/rotation.d.ts +1 -1
- package/dist/lib/rotation.d.ts.map +1 -1
- package/dist/lib/rotation.js +15 -3
- package/dist/lib/rotation.js.map +1 -1
- package/dist/lib/storage.d.ts +20 -0
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +502 -108
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/types.d.ts +45 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/package.json +11 -5
package/dist/index.js
CHANGED
|
@@ -22,20 +22,23 @@
|
|
|
22
22
|
* @repository https://github.com/numman-ali/opencode-openai-codex-auth
|
|
23
23
|
*/
|
|
24
24
|
import { tool } from "@opencode-ai/plugin";
|
|
25
|
-
import { createAuthorizationFlow, exchangeAuthorizationCode,
|
|
25
|
+
import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInputForFlow, REDIRECT_URI, } from "./lib/auth/auth.js";
|
|
26
26
|
import { openBrowserUrl } from "./lib/auth/browser.js";
|
|
27
27
|
import { startLocalOAuthServer } from "./lib/auth/server.js";
|
|
28
|
-
import { getAccountSelectionStrategy, getCodexMode, getPidOffsetEnabled, getQuietMode, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getTokenRefreshSkewMs, loadPluginConfig, } from "./lib/config.js";
|
|
28
|
+
import { getAccountSelectionStrategy, getCodexMode, getDefaultRetryAfterMs, getMaxBackoffMs, getMaxCacheFirstWaitSeconds, getPidOffsetEnabled, getQuietMode, getRateLimitDedupWindowMs, getRateLimitStateResetMs, getRateLimitToastDebounceMs, getRequestJitterMaxMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getSchedulingMode, getSwitchOnFirstRateLimit, getTokenRefreshSkewMs, loadPluginConfig, } from "./lib/config.js";
|
|
29
29
|
import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, HTTP_STATUS, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, } from "./lib/constants.js";
|
|
30
30
|
import { logRequest, logDebug } from "./lib/logger.js";
|
|
31
31
|
import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, rewriteUrlForCodex, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
|
|
32
|
-
import { AccountManager, extractAccountEmail, extractAccountId, extractAccountPlan, formatAccountLabel, formatWaitTime, isOAuthAuth, sanitizeEmail, } from "./lib/accounts.js";
|
|
33
|
-
import { promptAddAnotherAccount, promptLoginMode, promptOAuthCallbackValue } from "./lib/cli.js";
|
|
32
|
+
import { AccountManager, extractAccountEmail, extractAccountId, extractAccountPlan, formatAccountLabel, formatWaitTime, isOAuthAuth, needsIdentityHydration, sanitizeEmail, } from "./lib/accounts.js";
|
|
33
|
+
import { promptAddAnotherAccount, promptLoginMode, promptManageAccounts, promptOAuthCallbackValue, promptRepairAccounts, } from "./lib/cli.js";
|
|
34
34
|
import { withTerminalModeRestored } from "./lib/terminal.js";
|
|
35
|
-
import { getStoragePath, loadAccounts, saveAccounts } from "./lib/storage.js";
|
|
35
|
+
import { getStoragePath, autoQuarantineCorruptAccountsFile, inspectAccountsFile, loadAccounts, quarantineAccounts, quarantineCorruptFile, replaceAccountsFile, saveAccounts, toggleAccountEnabled, writeQuarantineFile, } from "./lib/storage.js";
|
|
36
36
|
import { findAccountMatchIndex } from "./lib/account-matching.js";
|
|
37
37
|
import { getModelFamily, MODEL_FAMILIES } from "./lib/prompts/codex.js";
|
|
38
38
|
import { getHealthTracker, getTokenTracker } from "./lib/rotation.js";
|
|
39
|
+
import { RateLimitTracker, decideRateLimitAction, parseRateLimitReason } from "./lib/rate-limit.js";
|
|
40
|
+
import { ProactiveRefreshQueue, createRefreshScheduler, } from "./lib/refresh-queue.js";
|
|
41
|
+
import { formatRateLimitStatusMessage, formatToastMessage } from "./lib/formatting.js";
|
|
39
42
|
const RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS = 5_000;
|
|
40
43
|
const AUTH_FAILURE_COOLDOWN_MS = 60_000;
|
|
41
44
|
const MAX_ACCOUNTS = 10;
|
|
@@ -80,22 +83,26 @@ function parseRetryAfterMs(headers) {
|
|
|
80
83
|
*/
|
|
81
84
|
export const OpenAIAuthPlugin = async ({ client }) => {
|
|
82
85
|
let cachedAccountManager = null;
|
|
86
|
+
let proactiveRefreshScheduler = null;
|
|
83
87
|
const showToast = async (message, variant = "info", quietMode = false) => {
|
|
84
88
|
if (quietMode)
|
|
85
89
|
return;
|
|
86
90
|
try {
|
|
87
|
-
await client.tui.showToast({ body: { message, variant } });
|
|
91
|
+
await client.tui.showToast({ body: { message: formatToastMessage(message), variant } });
|
|
88
92
|
}
|
|
89
93
|
catch {
|
|
90
94
|
// ignore (non-TUI contexts)
|
|
91
95
|
}
|
|
92
96
|
};
|
|
93
|
-
const buildManualOAuthFlow = (pkce, url, onSuccess) => ({
|
|
97
|
+
const buildManualOAuthFlow = (pkce, expectedState, url, onSuccess) => ({
|
|
94
98
|
url,
|
|
95
99
|
method: "code",
|
|
96
100
|
instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL,
|
|
97
101
|
callback: async (input) => {
|
|
98
|
-
const parsed =
|
|
102
|
+
const parsed = parseAuthorizationInputForFlow(input, expectedState);
|
|
103
|
+
if (parsed.stateStatus === "mismatch") {
|
|
104
|
+
return { type: "failed" };
|
|
105
|
+
}
|
|
99
106
|
if (!parsed.code) {
|
|
100
107
|
return { type: "failed" };
|
|
101
108
|
}
|
|
@@ -114,6 +121,9 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
114
121
|
const accountId = extractAccountId(token.access);
|
|
115
122
|
const email = sanitizeEmail(extractAccountEmail(token.idToken ?? token.access));
|
|
116
123
|
const plan = extractAccountPlan(token.idToken ?? token.access);
|
|
124
|
+
if (!accountId || !email || !plan) {
|
|
125
|
+
debugAuth("[PersistAccount] Missing account identity fields; persisting legacy entry");
|
|
126
|
+
}
|
|
117
127
|
debugAuth(`[PersistAccount] Account details - accountId: ${accountId}, email: ${email}, plan: ${plan}, existing accounts: ${accounts.length}`);
|
|
118
128
|
const existingIndex = findAccountMatchIndex(accounts, { accountId, plan, email });
|
|
119
129
|
debugAuth(`[PersistAccount] Match index: ${existingIndex}`);
|
|
@@ -124,6 +134,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
124
134
|
accountId,
|
|
125
135
|
email,
|
|
126
136
|
plan,
|
|
137
|
+
enabled: true,
|
|
127
138
|
addedAt: now,
|
|
128
139
|
lastUsed: now,
|
|
129
140
|
});
|
|
@@ -136,6 +147,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
136
147
|
existing.accountId = accountId ?? existing.accountId;
|
|
137
148
|
existing.email = email ?? existing.email;
|
|
138
149
|
existing.plan = plan ?? existing.plan;
|
|
150
|
+
if (typeof existing.enabled !== "boolean")
|
|
151
|
+
existing.enabled = true;
|
|
139
152
|
existing.lastUsed = now;
|
|
140
153
|
}
|
|
141
154
|
}
|
|
@@ -172,9 +185,15 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
172
185
|
if (!isOAuthAuth(auth)) {
|
|
173
186
|
return {};
|
|
174
187
|
}
|
|
188
|
+
const pluginConfig = loadPluginConfig();
|
|
189
|
+
const quietMode = getQuietMode(pluginConfig);
|
|
175
190
|
const accountManager = await AccountManager.loadFromDisk(auth);
|
|
176
191
|
cachedAccountManager = accountManager;
|
|
177
192
|
if (accountManager.getAccountCount() === 0) {
|
|
193
|
+
const quarantinePath = await autoQuarantineCorruptAccountsFile();
|
|
194
|
+
if (quarantinePath) {
|
|
195
|
+
await showToast("Accounts file was corrupted and has been quarantined. Run `opencode auth login`.", "warning", quietMode);
|
|
196
|
+
}
|
|
178
197
|
logDebug(`[${PLUGIN_NAME}] No OAuth accounts available (run opencode auth login)`);
|
|
179
198
|
return {};
|
|
180
199
|
}
|
|
@@ -184,16 +203,85 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
184
203
|
global: providerConfig?.options || {},
|
|
185
204
|
models: providerConfig?.models || {},
|
|
186
205
|
};
|
|
187
|
-
const pluginConfig = loadPluginConfig();
|
|
188
206
|
const codexMode = getCodexMode(pluginConfig);
|
|
189
207
|
const accountSelectionStrategy = getAccountSelectionStrategy(pluginConfig);
|
|
190
208
|
const pidOffsetEnabled = getPidOffsetEnabled(pluginConfig);
|
|
191
|
-
const quietMode = getQuietMode(pluginConfig);
|
|
192
209
|
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
210
|
+
const proactiveRefreshEnabled = (() => {
|
|
211
|
+
const rawConfig = pluginConfig;
|
|
212
|
+
const configFlag = rawConfig["proactive_token_refresh"] ?? rawConfig["proactiveTokenRefresh"];
|
|
213
|
+
const envFlag = process.env.CODEX_AUTH_PROACTIVE_TOKEN_REFRESH;
|
|
214
|
+
if (envFlag === "1" || envFlag === "true")
|
|
215
|
+
return true;
|
|
216
|
+
if (envFlag === "0" || envFlag === "false")
|
|
217
|
+
return false;
|
|
218
|
+
return Boolean(configFlag);
|
|
219
|
+
})();
|
|
193
220
|
const toastDebounceMs = getRateLimitToastDebounceMs(pluginConfig);
|
|
194
221
|
const retryAllAccountsRateLimited = getRetryAllAccountsRateLimited(pluginConfig);
|
|
195
222
|
const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs(pluginConfig);
|
|
196
223
|
const retryAllAccountsMaxRetries = getRetryAllAccountsMaxRetries(pluginConfig);
|
|
224
|
+
const schedulingMode = getSchedulingMode(pluginConfig);
|
|
225
|
+
const maxCacheFirstWaitSeconds = getMaxCacheFirstWaitSeconds(pluginConfig);
|
|
226
|
+
const switchOnFirstRateLimit = getSwitchOnFirstRateLimit(pluginConfig);
|
|
227
|
+
const rateLimitDedupWindowMs = getRateLimitDedupWindowMs(pluginConfig);
|
|
228
|
+
const rateLimitStateResetMs = getRateLimitStateResetMs(pluginConfig);
|
|
229
|
+
const defaultRetryAfterMs = getDefaultRetryAfterMs(pluginConfig);
|
|
230
|
+
const maxBackoffMs = getMaxBackoffMs(pluginConfig);
|
|
231
|
+
const requestJitterMaxMs = getRequestJitterMaxMs(pluginConfig);
|
|
232
|
+
const maxCacheFirstWaitMs = Math.max(0, Math.floor(maxCacheFirstWaitSeconds * 1000));
|
|
233
|
+
const proactiveRefreshQueue = proactiveRefreshEnabled
|
|
234
|
+
? new ProactiveRefreshQueue({ bufferMs: tokenRefreshSkewMs, intervalMs: 250 })
|
|
235
|
+
: null;
|
|
236
|
+
if (proactiveRefreshScheduler) {
|
|
237
|
+
proactiveRefreshScheduler.stop();
|
|
238
|
+
proactiveRefreshScheduler = null;
|
|
239
|
+
}
|
|
240
|
+
if (proactiveRefreshQueue) {
|
|
241
|
+
proactiveRefreshScheduler = createRefreshScheduler({
|
|
242
|
+
intervalMs: 1000,
|
|
243
|
+
queue: proactiveRefreshQueue,
|
|
244
|
+
getTasks: () => {
|
|
245
|
+
const tasks = [];
|
|
246
|
+
for (const account of accountManager.getAccountsSnapshot()) {
|
|
247
|
+
if (account.enabled === false)
|
|
248
|
+
continue;
|
|
249
|
+
if (!Number.isFinite(account.expires))
|
|
250
|
+
continue;
|
|
251
|
+
tasks.push({
|
|
252
|
+
key: `account-${account.index}`,
|
|
253
|
+
expires: account.expires ?? 0,
|
|
254
|
+
refresh: async () => {
|
|
255
|
+
const live = accountManager.getAccountByIndex(account.index);
|
|
256
|
+
if (!live || live.enabled === false)
|
|
257
|
+
return { type: "failed" };
|
|
258
|
+
const refreshed = await accountManager.refreshAccountWithFallback(live);
|
|
259
|
+
if (refreshed.type !== "success")
|
|
260
|
+
return refreshed;
|
|
261
|
+
const refreshedAuth = {
|
|
262
|
+
type: "oauth",
|
|
263
|
+
access: refreshed.access,
|
|
264
|
+
refresh: refreshed.refresh,
|
|
265
|
+
expires: refreshed.expires,
|
|
266
|
+
};
|
|
267
|
+
accountManager.updateFromAuth(live, refreshedAuth);
|
|
268
|
+
await accountManager.saveToDisk();
|
|
269
|
+
return refreshed;
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return tasks;
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
proactiveRefreshScheduler.start();
|
|
277
|
+
}
|
|
278
|
+
const rateLimitTracker = new RateLimitTracker({
|
|
279
|
+
dedupWindowMs: rateLimitDedupWindowMs,
|
|
280
|
+
resetMs: rateLimitStateResetMs,
|
|
281
|
+
defaultRetryMs: defaultRetryAfterMs,
|
|
282
|
+
maxBackoffMs,
|
|
283
|
+
jitterMaxMs: requestJitterMaxMs,
|
|
284
|
+
});
|
|
197
285
|
// Return SDK configuration
|
|
198
286
|
return {
|
|
199
287
|
apiKey: DUMMY_API_KEY,
|
|
@@ -249,8 +337,35 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
249
337
|
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
250
338
|
});
|
|
251
339
|
let allRateLimitedRetries = 0;
|
|
340
|
+
let autoRepairAttempted = false;
|
|
252
341
|
while (true) {
|
|
253
342
|
const accountCount = accountManager.getAccountCount();
|
|
343
|
+
if (!autoRepairAttempted && accountCount === 0) {
|
|
344
|
+
const legacyAccounts = accountManager.getLegacyAccounts();
|
|
345
|
+
if (legacyAccounts.length > 0) {
|
|
346
|
+
autoRepairAttempted = true;
|
|
347
|
+
const repair = await accountManager.repairLegacyAccounts();
|
|
348
|
+
const snapshot = accountManager.getStorageSnapshot();
|
|
349
|
+
let quarantinePath = null;
|
|
350
|
+
if (repair.quarantined.length > 0) {
|
|
351
|
+
const quarantinedTokens = new Set(repair.quarantined.map((account) => account.refreshToken));
|
|
352
|
+
const quarantineEntries = snapshot.accounts.filter((account) => quarantinedTokens.has(account.refreshToken));
|
|
353
|
+
const quarantineResult = await quarantineAccounts(snapshot, quarantineEntries, "legacy-auto-repair-failed");
|
|
354
|
+
quarantinePath = quarantineResult.quarantinePath;
|
|
355
|
+
accountManager.removeAccountsByRefreshToken(quarantinedTokens);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
await replaceAccountsFile(snapshot);
|
|
359
|
+
}
|
|
360
|
+
if (repair.quarantined.length > 0 && quarantinePath) {
|
|
361
|
+
await showToast(`Auto-repair failed for ${repair.quarantined.length} account(s). Quarantined: ${quarantinePath}`, "warning", quietMode);
|
|
362
|
+
}
|
|
363
|
+
else if (repair.repaired.length > 0) {
|
|
364
|
+
await showToast(`Auto-repaired ${repair.repaired.length} account(s).`, "success", quietMode);
|
|
365
|
+
}
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
254
369
|
const attempted = new Set();
|
|
255
370
|
while (attempted.size < Math.max(1, accountCount)) {
|
|
256
371
|
const account = accountManager.getCurrentOrNextForFamily(modelFamily, model, accountSelectionStrategy, usePidOffset);
|
|
@@ -258,27 +373,48 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
258
373
|
break;
|
|
259
374
|
attempted.add(account.index);
|
|
260
375
|
let accountAuth = accountManager.toAuthDetails(account);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
accountAuth = {
|
|
376
|
+
const tokenExpired = !accountAuth.access || accountAuth.expires <= Date.now();
|
|
377
|
+
const runRefresh = async () => {
|
|
378
|
+
const refreshed = await accountManager.refreshAccountWithFallback(account);
|
|
379
|
+
if (refreshed.type !== "success")
|
|
380
|
+
return refreshed;
|
|
381
|
+
const refreshedAuth = {
|
|
270
382
|
type: "oauth",
|
|
271
383
|
access: refreshed.access,
|
|
272
384
|
refresh: refreshed.refresh,
|
|
273
385
|
expires: refreshed.expires,
|
|
274
386
|
};
|
|
275
|
-
accountManager.updateFromAuth(account,
|
|
387
|
+
accountManager.updateFromAuth(account, refreshedAuth);
|
|
276
388
|
await accountManager.saveToDisk();
|
|
277
|
-
// Keep OpenCode's stored auth aligned with the last-used account.
|
|
278
389
|
await client.auth.set({
|
|
279
390
|
path: { id: PROVIDER_ID },
|
|
280
|
-
body:
|
|
391
|
+
body: refreshedAuth,
|
|
281
392
|
});
|
|
393
|
+
return refreshed;
|
|
394
|
+
};
|
|
395
|
+
if (shouldRefreshToken(accountAuth, tokenRefreshSkewMs)) {
|
|
396
|
+
if (proactiveRefreshQueue && !tokenExpired) {
|
|
397
|
+
void proactiveRefreshQueue.enqueue({
|
|
398
|
+
key: `account-${account.index}`,
|
|
399
|
+
expires: accountAuth.expires,
|
|
400
|
+
refresh: runRefresh,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
const refreshed = await runRefresh();
|
|
405
|
+
if (refreshed.type !== "success") {
|
|
406
|
+
accountManager.markAccountCoolingDown(account, AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
|
|
407
|
+
await accountManager.saveToDisk();
|
|
408
|
+
await showToast(`Auth refresh failed. Cooling down ${formatAccountLabel(account, account.index)}.`, "warning", quietMode);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
accountAuth = {
|
|
412
|
+
type: "oauth",
|
|
413
|
+
access: refreshed.access,
|
|
414
|
+
refresh: refreshed.refresh,
|
|
415
|
+
expires: refreshed.expires,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
282
418
|
}
|
|
283
419
|
const accountId = account.accountId ?? extractAccountId(accountAuth.access);
|
|
284
420
|
if (!accountId) {
|
|
@@ -331,6 +467,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
331
467
|
if (accountSelectionStrategy === "hybrid") {
|
|
332
468
|
getHealthTracker().recordSuccess(account.index);
|
|
333
469
|
}
|
|
470
|
+
accountManager.markAccountUsed(account.index);
|
|
334
471
|
return await handleSuccessResponse(res, isStreaming);
|
|
335
472
|
}
|
|
336
473
|
const handled = await handleErrorResponse(res);
|
|
@@ -340,7 +477,25 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
340
477
|
}
|
|
341
478
|
return handled;
|
|
342
479
|
}
|
|
343
|
-
const retryAfterMs = parseRetryAfterMs(handled.headers)
|
|
480
|
+
const retryAfterMs = parseRetryAfterMs(handled.headers);
|
|
481
|
+
let responseText = "";
|
|
482
|
+
try {
|
|
483
|
+
responseText = await handled.clone().text();
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
responseText = "";
|
|
487
|
+
}
|
|
488
|
+
const reason = parseRateLimitReason(handled.status, responseText);
|
|
489
|
+
const trackerKey = `${account.index}:${modelFamily}:${model ?? ""}`;
|
|
490
|
+
const backoff = rateLimitTracker.getBackoff(trackerKey, reason, retryAfterMs);
|
|
491
|
+
const decision = decideRateLimitAction({
|
|
492
|
+
schedulingMode,
|
|
493
|
+
accountCount,
|
|
494
|
+
maxCacheFirstWaitMs,
|
|
495
|
+
switchOnFirstRateLimit,
|
|
496
|
+
shortRetryThresholdMs: RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS,
|
|
497
|
+
backoff,
|
|
498
|
+
});
|
|
344
499
|
if (tokenConsumed) {
|
|
345
500
|
getTokenTracker().refund(account.index);
|
|
346
501
|
tokenConsumed = false;
|
|
@@ -348,19 +503,27 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
348
503
|
if (accountSelectionStrategy === "hybrid") {
|
|
349
504
|
getHealthTracker().recordRateLimit(account.index);
|
|
350
505
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
506
|
+
accountManager.markRateLimited(account, backoff.delayMs, modelFamily, model);
|
|
507
|
+
const shouldPersistRateLimit = !backoff.isDuplicate;
|
|
508
|
+
if (decision.action === "wait") {
|
|
509
|
+
if (shouldPersistRateLimit) {
|
|
510
|
+
await accountManager.saveToDisk();
|
|
511
|
+
await showToast(`Rate limited. Retrying in ${formatWaitTime(decision.delayMs)}...`, "warning", quietMode);
|
|
512
|
+
}
|
|
513
|
+
if (decision.delayMs > 0) {
|
|
514
|
+
await sleep(decision.delayMs);
|
|
515
|
+
}
|
|
354
516
|
continue;
|
|
355
517
|
}
|
|
356
|
-
accountManager.markRateLimited(account, retryAfterMs, modelFamily, model);
|
|
357
518
|
accountManager.markSwitched(account, "rate-limit", modelFamily);
|
|
358
|
-
|
|
359
|
-
|
|
519
|
+
if (shouldPersistRateLimit) {
|
|
520
|
+
await accountManager.saveToDisk();
|
|
521
|
+
await showToast(`Rate limited. Switching accounts (retry in ${formatWaitTime(decision.delayMs)}).`, "warning", quietMode);
|
|
522
|
+
}
|
|
360
523
|
break;
|
|
361
524
|
}
|
|
362
525
|
}
|
|
363
|
-
const waitMs = accountManager.
|
|
526
|
+
const waitMs = await accountManager.getMinWaitTimeForFamilyWithHydration(modelFamily, model);
|
|
364
527
|
if (retryAllAccountsRateLimited &&
|
|
365
528
|
accountManager.getAccountCount() > 0 &&
|
|
366
529
|
waitMs > 0 &&
|
|
@@ -371,11 +534,11 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
371
534
|
await sleep(waitMs);
|
|
372
535
|
continue;
|
|
373
536
|
}
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
537
|
+
const message = formatRateLimitStatusMessage({
|
|
538
|
+
accountCount: accountManager.getAccountCount(),
|
|
539
|
+
waitMs,
|
|
540
|
+
storagePath: getStoragePath(),
|
|
541
|
+
});
|
|
379
542
|
return new Response(JSON.stringify({ error: { message } }), {
|
|
380
543
|
status: 429,
|
|
381
544
|
headers: { "content-type": "application/json; charset=utf-8" },
|
|
@@ -404,6 +567,71 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
404
567
|
const pluginConfig = loadPluginConfig();
|
|
405
568
|
const quietMode = getQuietMode(pluginConfig);
|
|
406
569
|
const isCliFlow = Boolean(inputs);
|
|
570
|
+
const notifyRepairResult = async (message) => {
|
|
571
|
+
if (isCliFlow) {
|
|
572
|
+
console.log(`\n${message}\n`);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
await showToast(message, "info", quietMode);
|
|
576
|
+
};
|
|
577
|
+
const maybeRepairAccounts = async () => {
|
|
578
|
+
const inspection = await inspectAccountsFile();
|
|
579
|
+
if (inspection.status === "missing" || inspection.status === "ok") {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
const corruptCount = inspection.status === "corrupt-file"
|
|
583
|
+
? 1
|
|
584
|
+
: inspection.corruptEntries.length;
|
|
585
|
+
const legacyCount = inspection.status === "needs-repair" ? inspection.legacyEntries.length : 0;
|
|
586
|
+
const shouldRepair = await promptRepairAccounts({
|
|
587
|
+
legacyCount,
|
|
588
|
+
corruptCount,
|
|
589
|
+
});
|
|
590
|
+
if (!shouldRepair)
|
|
591
|
+
return null;
|
|
592
|
+
const quarantinePaths = [];
|
|
593
|
+
if (inspection.status === "corrupt-file") {
|
|
594
|
+
const quarantinePath = await quarantineCorruptFile();
|
|
595
|
+
if (quarantinePath) {
|
|
596
|
+
quarantinePaths.push(quarantinePath);
|
|
597
|
+
await notifyRepairResult(`Accounts file was corrupted. Quarantined to ${quarantinePath}.`);
|
|
598
|
+
}
|
|
599
|
+
return await loadAccounts();
|
|
600
|
+
}
|
|
601
|
+
if (inspection.corruptEntries.length > 0) {
|
|
602
|
+
const quarantinePath = await writeQuarantineFile(inspection.corruptEntries, "corrupt-entry");
|
|
603
|
+
quarantinePaths.push(quarantinePath);
|
|
604
|
+
}
|
|
605
|
+
const storage = await loadAccounts();
|
|
606
|
+
if (!storage) {
|
|
607
|
+
await notifyRepairResult("Repair skipped: no valid accounts found.");
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
const manager = new AccountManager(undefined, storage);
|
|
611
|
+
const repair = await manager.repairLegacyAccounts();
|
|
612
|
+
const snapshot = manager.getStorageSnapshot();
|
|
613
|
+
let updatedStorage = snapshot;
|
|
614
|
+
if (repair.quarantined.length > 0) {
|
|
615
|
+
const quarantinedTokens = new Set(repair.quarantined.map((account) => account.refreshToken));
|
|
616
|
+
const quarantineEntries = snapshot.accounts.filter((account) => quarantinedTokens.has(account.refreshToken));
|
|
617
|
+
const quarantineResult = await quarantineAccounts(snapshot, quarantineEntries, "legacy-repair-failed");
|
|
618
|
+
updatedStorage = quarantineResult.storage;
|
|
619
|
+
quarantinePaths.push(quarantineResult.quarantinePath);
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
await replaceAccountsFile(snapshot);
|
|
623
|
+
}
|
|
624
|
+
const summaryParts = [
|
|
625
|
+
`Repaired ${repair.repaired.length}`,
|
|
626
|
+
`quarantined ${repair.quarantined.length}`,
|
|
627
|
+
];
|
|
628
|
+
const detail = quarantinePaths.length
|
|
629
|
+
? ` Quarantine: ${quarantinePaths.join(", ")}.`
|
|
630
|
+
: "";
|
|
631
|
+
await notifyRepairResult(`Account repair complete. ${summaryParts.join(", ")}.${detail}`);
|
|
632
|
+
return updatedStorage;
|
|
633
|
+
};
|
|
634
|
+
const repairedStorage = await maybeRepairAccounts();
|
|
407
635
|
// CLI flow (`opencode auth login`) passes inputs; TUI does not.
|
|
408
636
|
if (isCliFlow) {
|
|
409
637
|
debugAuth("[OAuthAuthorize] Starting OAuth flow in CLI mode");
|
|
@@ -415,8 +643,15 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
415
643
|
const { pkce, state, url } = await createAuthorizationFlow();
|
|
416
644
|
console.log("\nOAuth URL:\n" + url + "\n");
|
|
417
645
|
if (noBrowser) {
|
|
418
|
-
const callbackInput = await promptOAuthCallbackValue("Paste the redirect URL (or just the code
|
|
419
|
-
const parsed =
|
|
646
|
+
const callbackInput = await promptOAuthCallbackValue("Paste the full redirect URL (recommended). You can also paste code#state or just the code: ");
|
|
647
|
+
const parsed = parseAuthorizationInputForFlow(callbackInput, state);
|
|
648
|
+
if (parsed.stateStatus === "mismatch") {
|
|
649
|
+
console.log("\nOAuth state mismatch. Paste the redirect URL from this login session (the one shown above).\n");
|
|
650
|
+
return { type: "failed" };
|
|
651
|
+
}
|
|
652
|
+
if (parsed.stateStatus === "missing") {
|
|
653
|
+
console.log("\nWarning: redirect state not provided. For best security, paste the full redirect URL.\n");
|
|
654
|
+
}
|
|
420
655
|
if (!parsed.code)
|
|
421
656
|
return { type: "failed" };
|
|
422
657
|
return await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
|
|
@@ -431,8 +666,15 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
431
666
|
openBrowserUrl(url);
|
|
432
667
|
if (!serverInfo || !serverInfo.ready) {
|
|
433
668
|
serverInfo?.close();
|
|
434
|
-
const callbackInput = await promptOAuthCallbackValue("Paste the redirect URL (or just the code
|
|
435
|
-
const parsed =
|
|
669
|
+
const callbackInput = await promptOAuthCallbackValue("Paste the full redirect URL (recommended). You can also paste code#state or just the code: ");
|
|
670
|
+
const parsed = parseAuthorizationInputForFlow(callbackInput, state);
|
|
671
|
+
if (parsed.stateStatus === "mismatch") {
|
|
672
|
+
console.log("\nOAuth state mismatch. Paste the redirect URL from this login session (the one shown above).\n");
|
|
673
|
+
return { type: "failed" };
|
|
674
|
+
}
|
|
675
|
+
if (parsed.stateStatus === "missing") {
|
|
676
|
+
console.log("\nWarning: redirect state not provided. For best security, paste the full redirect URL.\n");
|
|
677
|
+
}
|
|
436
678
|
if (!parsed.code)
|
|
437
679
|
return { type: "failed" };
|
|
438
680
|
return await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
|
|
@@ -445,9 +687,9 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
445
687
|
};
|
|
446
688
|
const authenticated = [];
|
|
447
689
|
let startFresh = true;
|
|
448
|
-
let existingStorage = await loadAccounts();
|
|
690
|
+
let existingStorage = repairedStorage ?? (await loadAccounts());
|
|
449
691
|
if (existingStorage && existingStorage.accounts.length > 0) {
|
|
450
|
-
const needsHydration = existingStorage.accounts
|
|
692
|
+
const needsHydration = needsIdentityHydration(existingStorage.accounts);
|
|
451
693
|
if (needsHydration) {
|
|
452
694
|
try {
|
|
453
695
|
console.log("\nRefreshing saved accounts to fill missing emails...\n");
|
|
@@ -460,13 +702,43 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
460
702
|
// Best-effort; ignore.
|
|
461
703
|
}
|
|
462
704
|
}
|
|
463
|
-
|
|
705
|
+
let existingLabels = (existingStorage?.accounts ?? []).map((a, index) => ({
|
|
464
706
|
index,
|
|
465
707
|
email: a.email,
|
|
466
708
|
plan: a.plan,
|
|
467
709
|
accountId: a.accountId,
|
|
710
|
+
enabled: a.enabled,
|
|
468
711
|
}));
|
|
469
|
-
|
|
712
|
+
let mode = await promptLoginMode(existingLabels);
|
|
713
|
+
while (mode === "manage") {
|
|
714
|
+
let updatedStorage = existingStorage;
|
|
715
|
+
while (updatedStorage) {
|
|
716
|
+
const labels = (updatedStorage?.accounts ?? []).map((a, index) => ({
|
|
717
|
+
index,
|
|
718
|
+
email: a.email,
|
|
719
|
+
plan: a.plan,
|
|
720
|
+
accountId: a.accountId,
|
|
721
|
+
enabled: a.enabled,
|
|
722
|
+
}));
|
|
723
|
+
const toggleIndex = await promptManageAccounts(labels);
|
|
724
|
+
if (toggleIndex === null)
|
|
725
|
+
break;
|
|
726
|
+
const toggled = toggleAccountEnabled(updatedStorage, toggleIndex);
|
|
727
|
+
if (toggled) {
|
|
728
|
+
await saveAccounts(toggled);
|
|
729
|
+
updatedStorage = toggled;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
existingStorage = await loadAccounts();
|
|
733
|
+
existingLabels = (existingStorage?.accounts ?? []).map((a, index) => ({
|
|
734
|
+
index,
|
|
735
|
+
email: a.email,
|
|
736
|
+
plan: a.plan,
|
|
737
|
+
accountId: a.accountId,
|
|
738
|
+
enabled: a.enabled,
|
|
739
|
+
}));
|
|
740
|
+
mode = await promptLoginMode(existingLabels);
|
|
741
|
+
}
|
|
470
742
|
startFresh = mode === "fresh";
|
|
471
743
|
}
|
|
472
744
|
if (startFresh) {
|
|
@@ -528,7 +800,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
528
800
|
process.env.SSH_TTY ||
|
|
529
801
|
process.env.OPENCODE_HEADLESS);
|
|
530
802
|
const useManualFlow = isHeadless || process.env.OPENCODE_NO_BROWSER === "1";
|
|
531
|
-
const existingStorage = await loadAccounts();
|
|
803
|
+
const existingStorage = repairedStorage ?? (await loadAccounts());
|
|
532
804
|
const existingCount = existingStorage?.accounts.length ?? 0;
|
|
533
805
|
const { pkce, state, url } = await createAuthorizationFlow();
|
|
534
806
|
let serverInfo = null;
|
|
@@ -574,10 +846,14 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
574
846
|
serverInfo?.close();
|
|
575
847
|
return {
|
|
576
848
|
url,
|
|
577
|
-
instructions: "Visit the URL above, complete OAuth, then paste
|
|
849
|
+
instructions: "Visit the URL above, complete OAuth, then paste the full redirect URL (recommended) or the authorization code.",
|
|
578
850
|
method: "code",
|
|
579
851
|
callback: async (input) => {
|
|
580
|
-
const parsed =
|
|
852
|
+
const parsed = parseAuthorizationInputForFlow(input, state);
|
|
853
|
+
if (parsed.stateStatus === "mismatch") {
|
|
854
|
+
await showToast("OAuth state mismatch. Paste the redirect URL from this login session.", "error", quietMode);
|
|
855
|
+
return { type: "failed" };
|
|
856
|
+
}
|
|
581
857
|
if (!parsed.code)
|
|
582
858
|
return { type: "failed" };
|
|
583
859
|
const tokens = await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
|
|
@@ -603,8 +879,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
603
879
|
label: AUTH_LABELS.OAUTH_MANUAL,
|
|
604
880
|
type: "oauth",
|
|
605
881
|
authorize: async () => {
|
|
606
|
-
const { pkce, url } = await createAuthorizationFlow();
|
|
607
|
-
return buildManualOAuthFlow(pkce, url, async (tokens) => {
|
|
882
|
+
const { pkce, state, url } = await createAuthorizationFlow();
|
|
883
|
+
return buildManualOAuthFlow(pkce, state, url, async (tokens) => {
|
|
608
884
|
await persistAccount(tokens);
|
|
609
885
|
});
|
|
610
886
|
},
|
|
@@ -647,6 +923,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
647
923
|
const statuses = [];
|
|
648
924
|
if (index === activeIndex)
|
|
649
925
|
statuses.push("active");
|
|
926
|
+
if (account.enabled === false)
|
|
927
|
+
statuses.push("disabled");
|
|
650
928
|
const rateLimited = account.rateLimitResetTimes &&
|
|
651
929
|
Object.values(account.rateLimitResetTimes).some((t) => typeof t === "number" && t > now);
|
|
652
930
|
if (rateLimited)
|
|
@@ -693,6 +971,36 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
693
971
|
return `Switched to ${formatAccountLabel(account, targetIndex)}`;
|
|
694
972
|
},
|
|
695
973
|
}),
|
|
974
|
+
"openai-accounts-toggle": tool({
|
|
975
|
+
description: "Enable or disable an OpenAI account by index (1-based).",
|
|
976
|
+
args: {
|
|
977
|
+
index: tool.schema.number().describe("Account number (1-based)"),
|
|
978
|
+
},
|
|
979
|
+
async execute({ index }) {
|
|
980
|
+
const storage = await loadAccounts();
|
|
981
|
+
if (!storage || storage.accounts.length === 0) {
|
|
982
|
+
return "No OpenAI accounts configured. Run: opencode auth login";
|
|
983
|
+
}
|
|
984
|
+
const targetIndex = Math.floor((index ?? 0) - 1);
|
|
985
|
+
if (targetIndex < 0 || targetIndex >= storage.accounts.length) {
|
|
986
|
+
return `Invalid account number: ${index}\nValid range: 1-${storage.accounts.length}`;
|
|
987
|
+
}
|
|
988
|
+
const updated = toggleAccountEnabled(storage, targetIndex);
|
|
989
|
+
if (!updated) {
|
|
990
|
+
return `Failed to toggle account number: ${index}`;
|
|
991
|
+
}
|
|
992
|
+
await saveAccounts(updated);
|
|
993
|
+
const account = updated.accounts[targetIndex];
|
|
994
|
+
if (cachedAccountManager) {
|
|
995
|
+
const live = cachedAccountManager.getAccountByIndex(targetIndex);
|
|
996
|
+
if (live)
|
|
997
|
+
live.enabled = account?.enabled !== false;
|
|
998
|
+
}
|
|
999
|
+
const enabled = account?.enabled !== false;
|
|
1000
|
+
const verb = enabled ? "Enabled" : "Disabled";
|
|
1001
|
+
return `${verb} ${formatAccountLabel(account, targetIndex)} (${targetIndex + 1}/${updated.accounts.length})`;
|
|
1002
|
+
},
|
|
1003
|
+
}),
|
|
696
1004
|
},
|
|
697
1005
|
};
|
|
698
1006
|
};
|