opencode-openai-codex-auth-multi 4.3.0-multiaccount.1 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +37 -37
- package/README.md +461 -79
- package/assets/opencode-logo-ornate-dark.svg +18 -18
- package/assets/readme-hero.svg +31 -31
- package/config/README.md +98 -98
- package/config/minimal-opencode.json +11 -11
- package/config/opencode-legacy.json +568 -568
- package/config/opencode-modern.json +236 -236
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +368 -133
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts.d.ts +78 -11
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +303 -66
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/auth/auth.d.ts.map +1 -1
- package/dist/lib/auth/auth.js +19 -12
- package/dist/lib/auth/auth.js.map +1 -1
- package/dist/lib/auth/browser.d.ts.map +1 -1
- package/dist/lib/auth/browser.js +9 -2
- package/dist/lib/auth/browser.js.map +1 -1
- package/dist/lib/auth/server.d.ts.map +1 -1
- package/dist/lib/auth/server.js +11 -4
- package/dist/lib/auth/server.js.map +1 -1
- package/dist/lib/cli.d.ts +1 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +10 -6
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/config.d.ts +5 -7
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +49 -6
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/constants.d.ts +7 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +7 -0
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/index.d.ts +13 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +13 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/oauth-success.html +712 -712
- package/dist/lib/prompts/codex-opencode-bridge.js +121 -121
- package/dist/lib/prompts/codex.d.ts +5 -0
- package/dist/lib/prompts/codex.d.ts.map +1 -1
- package/dist/lib/prompts/codex.js +114 -93
- package/dist/lib/prompts/codex.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +2 -3
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +24 -27
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/rate-limit-backoff.d.ts +13 -0
- package/dist/lib/request/rate-limit-backoff.d.ts.map +1 -0
- package/dist/lib/request/rate-limit-backoff.js +54 -0
- package/dist/lib/request/rate-limit-backoff.js.map +1 -0
- package/dist/lib/request/request-transformer.d.ts.map +1 -1
- package/dist/lib/request/request-transformer.js +3 -2
- package/dist/lib/request/request-transformer.js.map +1 -1
- package/dist/lib/request/response-handler.js +1 -1
- package/dist/lib/request/response-handler.js.map +1 -1
- package/dist/lib/storage.d.ts +72 -4
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +189 -19
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/types.d.ts +37 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/package.json +85 -71
- package/scripts/install-opencode-codex-auth.js +191 -191
- package/scripts/test-all-models.sh +258 -258
- package/scripts/validate-model-map.sh +97 -97
package/dist/index.js
CHANGED
|
@@ -19,22 +19,22 @@
|
|
|
19
19
|
*
|
|
20
20
|
* @license MIT with Usage Disclaimer (see LICENSE file)
|
|
21
21
|
* @author numman-ali
|
|
22
|
-
* @repository https://github.com/ndycode/opencode-openai-codex-auth-
|
|
22
|
+
* @repository https://github.com/ndycode/opencode-openai-codex-auth-multi
|
|
23
23
|
|
|
24
24
|
*/
|
|
25
25
|
import { tool } from "@opencode-ai/plugin";
|
|
26
|
-
import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, REDIRECT_URI, } from "./lib/auth/auth.js";
|
|
26
|
+
import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, REDIRECT_URI, refreshAccessToken, } from "./lib/auth/auth.js";
|
|
27
27
|
import { openBrowserUrl } from "./lib/auth/browser.js";
|
|
28
28
|
import { startLocalOAuthServer } from "./lib/auth/server.js";
|
|
29
29
|
import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
|
|
30
|
-
import { getCodexMode, loadPluginConfig } from "./lib/config.js";
|
|
31
|
-
import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, } from "./lib/constants.js";
|
|
30
|
+
import { getCodexMode, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getTokenRefreshSkewMs, loadPluginConfig, } from "./lib/config.js";
|
|
31
|
+
import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
|
|
32
32
|
import { logRequest, logDebug } from "./lib/logger.js";
|
|
33
|
-
import { AccountManager, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, } from "./lib/accounts.js";
|
|
33
|
+
import { AccountManager, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, } from "./lib/accounts.js";
|
|
34
34
|
import { getStoragePath, loadAccounts, saveAccounts } from "./lib/storage.js";
|
|
35
35
|
import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
|
|
37
|
+
import { getModelFamily, MODEL_FAMILIES } from "./lib/prompts/codex.js";
|
|
38
38
|
/**
|
|
39
39
|
* OpenAI Codex OAuth authentication plugin for opencode
|
|
40
40
|
*
|
|
@@ -45,7 +45,7 @@ const AUTH_FAILURE_COOLDOWN_MS = 30_000;
|
|
|
45
45
|
* @example
|
|
46
46
|
* ```json
|
|
47
47
|
* {
|
|
48
|
-
* "plugin": ["opencode-openai-codex-auth-
|
|
48
|
+
* "plugin": ["opencode-openai-codex-auth-multi"],
|
|
49
49
|
|
|
50
50
|
* "model": "openai/gpt-5-codex"
|
|
51
51
|
* }
|
|
@@ -60,7 +60,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
60
60
|
callback: async (input) => {
|
|
61
61
|
const parsed = parseAuthorizationInput(input);
|
|
62
62
|
if (!parsed.code) {
|
|
63
|
-
return { type: "failed" };
|
|
63
|
+
return { type: "failed", reason: "invalid_response", message: "No authorization code provided" };
|
|
64
64
|
}
|
|
65
65
|
const tokens = await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
|
|
66
66
|
if (tokens?.type === "success" && onSuccess) {
|
|
@@ -82,7 +82,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
82
82
|
rl.close();
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
|
-
const runManualOAuthFlow = async (pkce,
|
|
85
|
+
const runManualOAuthFlow = async (pkce, _url) => {
|
|
86
86
|
console.log("1. Open the URL above in your browser and sign in.");
|
|
87
87
|
console.log("2. After approving, copy the full redirect URL.");
|
|
88
88
|
console.log("3. Paste it back here.\n");
|
|
@@ -104,7 +104,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
104
104
|
try {
|
|
105
105
|
serverInfo = await startLocalOAuthServer({ state });
|
|
106
106
|
}
|
|
107
|
-
catch {
|
|
107
|
+
catch (err) {
|
|
108
|
+
logDebug(`[${PLUGIN_NAME}] Failed to start OAuth server: ${err?.message ?? String(err)}`);
|
|
108
109
|
serverInfo = null;
|
|
109
110
|
}
|
|
110
111
|
openBrowserUrl(url);
|
|
@@ -115,7 +116,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
115
116
|
const result = await serverInfo.waitForCode(state);
|
|
116
117
|
serverInfo.close();
|
|
117
118
|
if (!result) {
|
|
118
|
-
return { type: "failed" };
|
|
119
|
+
return { type: "failed", reason: "unknown", message: "OAuth callback timeout or cancelled" };
|
|
119
120
|
}
|
|
120
121
|
return await exchangeAuthorizationCode(result.code, pkce.verifier, REDIRECT_URI);
|
|
121
122
|
};
|
|
@@ -127,6 +128,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
127
128
|
const accounts = stored?.accounts ? [...stored.accounts] : [];
|
|
128
129
|
const indexByRefreshToken = new Map();
|
|
129
130
|
const indexByAccountId = new Map();
|
|
131
|
+
const indexByEmail = new Map();
|
|
130
132
|
for (let i = 0; i < accounts.length; i += 1) {
|
|
131
133
|
const account = accounts[i];
|
|
132
134
|
if (!account)
|
|
@@ -137,18 +139,26 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
137
139
|
if (account.accountId) {
|
|
138
140
|
indexByAccountId.set(account.accountId, i);
|
|
139
141
|
}
|
|
142
|
+
if (account.email) {
|
|
143
|
+
indexByEmail.set(account.email, i);
|
|
144
|
+
}
|
|
140
145
|
}
|
|
141
146
|
for (const result of results) {
|
|
142
147
|
const accountId = extractAccountId(result.access);
|
|
148
|
+
const accountEmail = sanitizeEmail(extractAccountEmail(result.access));
|
|
149
|
+
const existingByEmail = accountEmail && indexByEmail.has(accountEmail)
|
|
150
|
+
? indexByEmail.get(accountEmail)
|
|
151
|
+
: undefined;
|
|
143
152
|
const existingById = accountId && indexByAccountId.has(accountId)
|
|
144
153
|
? indexByAccountId.get(accountId)
|
|
145
154
|
: undefined;
|
|
146
155
|
const existingByToken = indexByRefreshToken.get(result.refresh);
|
|
147
|
-
const existingIndex = existingById ?? existingByToken;
|
|
156
|
+
const existingIndex = existingById ?? existingByEmail ?? existingByToken;
|
|
148
157
|
if (existingIndex === undefined) {
|
|
149
158
|
const newIndex = accounts.length;
|
|
150
159
|
accounts.push({
|
|
151
160
|
accountId,
|
|
161
|
+
email: accountEmail,
|
|
152
162
|
refreshToken: result.refresh,
|
|
153
163
|
addedAt: now,
|
|
154
164
|
lastUsed: now,
|
|
@@ -157,15 +167,21 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
157
167
|
if (accountId) {
|
|
158
168
|
indexByAccountId.set(accountId, newIndex);
|
|
159
169
|
}
|
|
170
|
+
if (accountEmail) {
|
|
171
|
+
indexByEmail.set(accountEmail, newIndex);
|
|
172
|
+
}
|
|
160
173
|
continue;
|
|
161
174
|
}
|
|
162
175
|
const existing = accounts[existingIndex];
|
|
163
176
|
if (!existing)
|
|
164
177
|
continue;
|
|
165
178
|
const oldToken = existing.refreshToken;
|
|
179
|
+
const oldEmail = existing.email;
|
|
180
|
+
const nextEmail = accountEmail ?? existing.email;
|
|
166
181
|
accounts[existingIndex] = {
|
|
167
182
|
...existing,
|
|
168
183
|
accountId: accountId ?? existing.accountId,
|
|
184
|
+
email: nextEmail,
|
|
169
185
|
refreshToken: result.refresh,
|
|
170
186
|
lastUsed: now,
|
|
171
187
|
};
|
|
@@ -176,6 +192,12 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
176
192
|
if (accountId) {
|
|
177
193
|
indexByAccountId.set(accountId, existingIndex);
|
|
178
194
|
}
|
|
195
|
+
if (oldEmail && oldEmail !== nextEmail) {
|
|
196
|
+
indexByEmail.delete(oldEmail);
|
|
197
|
+
}
|
|
198
|
+
if (nextEmail) {
|
|
199
|
+
indexByEmail.set(nextEmail, existingIndex);
|
|
200
|
+
}
|
|
179
201
|
}
|
|
180
202
|
if (accounts.length === 0)
|
|
181
203
|
return;
|
|
@@ -184,10 +206,22 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
184
206
|
: typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex)
|
|
185
207
|
? stored.activeIndex
|
|
186
208
|
: 0;
|
|
209
|
+
const clampedActiveIndex = Math.max(0, Math.min(activeIndex, accounts.length - 1));
|
|
210
|
+
const activeIndexByFamily = {};
|
|
211
|
+
for (const family of MODEL_FAMILIES) {
|
|
212
|
+
const storedFamilyIndex = stored?.activeIndexByFamily?.[family];
|
|
213
|
+
const rawFamilyIndex = replaceAll
|
|
214
|
+
? 0
|
|
215
|
+
: typeof storedFamilyIndex === "number" && Number.isFinite(storedFamilyIndex)
|
|
216
|
+
? storedFamilyIndex
|
|
217
|
+
: clampedActiveIndex;
|
|
218
|
+
activeIndexByFamily[family] = Math.max(0, Math.min(Math.floor(rawFamilyIndex), accounts.length - 1));
|
|
219
|
+
}
|
|
187
220
|
await saveAccounts({
|
|
188
|
-
version:
|
|
221
|
+
version: 3,
|
|
189
222
|
accounts,
|
|
190
|
-
activeIndex:
|
|
223
|
+
activeIndex: clampedActiveIndex,
|
|
224
|
+
activeIndexByFamily,
|
|
191
225
|
});
|
|
192
226
|
};
|
|
193
227
|
const showToast = async (message, variant = "success") => {
|
|
@@ -203,17 +237,79 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
203
237
|
// Ignore when TUI is not available.
|
|
204
238
|
}
|
|
205
239
|
};
|
|
206
|
-
const resolveActiveIndex = (storage) => {
|
|
240
|
+
const resolveActiveIndex = (storage, family = "codex") => {
|
|
207
241
|
const total = storage.accounts.length;
|
|
208
242
|
if (total === 0)
|
|
209
243
|
return 0;
|
|
210
|
-
const
|
|
244
|
+
const rawCandidate = storage.activeIndexByFamily?.[family] ?? storage.activeIndex;
|
|
245
|
+
const raw = Number.isFinite(rawCandidate) ? rawCandidate : 0;
|
|
211
246
|
return Math.max(0, Math.min(raw, total - 1));
|
|
212
247
|
};
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
248
|
+
const hydrateEmails = async (storage) => {
|
|
249
|
+
if (!storage)
|
|
250
|
+
return storage;
|
|
251
|
+
const skipHydrate = process.env.VITEST_WORKER_ID !== undefined ||
|
|
252
|
+
process.env.NODE_ENV === "test" ||
|
|
253
|
+
process.env.OPENCODE_SKIP_EMAIL_HYDRATE === "1";
|
|
254
|
+
if (skipHydrate)
|
|
255
|
+
return storage;
|
|
256
|
+
const accountsToHydrate = storage.accounts.filter((account) => account && !account.email);
|
|
257
|
+
if (accountsToHydrate.length === 0)
|
|
258
|
+
return storage;
|
|
259
|
+
let changed = false;
|
|
260
|
+
await Promise.all(accountsToHydrate.map(async (account) => {
|
|
261
|
+
try {
|
|
262
|
+
const refreshed = await refreshAccessToken(account.refreshToken);
|
|
263
|
+
if (refreshed.type !== "success")
|
|
264
|
+
return;
|
|
265
|
+
const id = extractAccountId(refreshed.access);
|
|
266
|
+
const email = sanitizeEmail(extractAccountEmail(refreshed.access));
|
|
267
|
+
if (id && id !== account.accountId) {
|
|
268
|
+
account.accountId = id;
|
|
269
|
+
changed = true;
|
|
270
|
+
}
|
|
271
|
+
if (email && email !== account.email) {
|
|
272
|
+
account.email = email;
|
|
273
|
+
changed = true;
|
|
274
|
+
}
|
|
275
|
+
if (refreshed.refresh && refreshed.refresh !== account.refreshToken) {
|
|
276
|
+
account.refreshToken = refreshed.refresh;
|
|
277
|
+
changed = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
logDebug(`[${PLUGIN_NAME}] Failed to hydrate email for account`);
|
|
282
|
+
}
|
|
283
|
+
}));
|
|
284
|
+
if (changed) {
|
|
285
|
+
await saveAccounts(storage);
|
|
286
|
+
}
|
|
287
|
+
return storage;
|
|
288
|
+
};
|
|
289
|
+
const getRateLimitResetTimeForFamily = (account, now, family) => {
|
|
290
|
+
const times = account.rateLimitResetTimes;
|
|
291
|
+
if (!times)
|
|
292
|
+
return null;
|
|
293
|
+
let minReset = null;
|
|
294
|
+
const prefix = `${family}:`;
|
|
295
|
+
for (const [key, value] of Object.entries(times)) {
|
|
296
|
+
if (typeof value !== "number")
|
|
297
|
+
continue;
|
|
298
|
+
if (value <= now)
|
|
299
|
+
continue;
|
|
300
|
+
if (key !== family && !key.startsWith(prefix))
|
|
301
|
+
continue;
|
|
302
|
+
if (minReset === null || value < minReset) {
|
|
303
|
+
minReset = value;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return minReset;
|
|
307
|
+
};
|
|
308
|
+
const formatRateLimitEntry = (account, now, family = "codex") => {
|
|
309
|
+
const resetAt = getRateLimitResetTimeForFamily(account, now, family);
|
|
310
|
+
if (typeof resetAt !== "number")
|
|
215
311
|
return null;
|
|
216
|
-
const remaining =
|
|
312
|
+
const remaining = resetAt - now;
|
|
217
313
|
if (remaining <= 0)
|
|
218
314
|
return null;
|
|
219
315
|
return `resets in ${formatWaitTime(remaining)}`;
|
|
@@ -243,13 +339,9 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
243
339
|
}
|
|
244
340
|
const accountManager = await AccountManager.loadFromDisk(auth);
|
|
245
341
|
cachedAccountManager = accountManager;
|
|
246
|
-
const storedSnapshot = await loadAccounts();
|
|
247
342
|
const refreshToken = auth.type === "oauth" ? auth.refresh : "";
|
|
248
|
-
const needsPersist =
|
|
249
|
-
|
|
250
|
-
accountManager.getAccountCount() ||
|
|
251
|
-
(refreshToken &&
|
|
252
|
-
!storedSnapshot.accounts.some((account) => account.refreshToken === refreshToken));
|
|
343
|
+
const needsPersist = refreshToken &&
|
|
344
|
+
!accountManager.hasRefreshToken(refreshToken);
|
|
253
345
|
if (needsPersist) {
|
|
254
346
|
await accountManager.saveToDisk();
|
|
255
347
|
}
|
|
@@ -267,6 +359,11 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
267
359
|
// Priority: CODEX_MODE env var > config file > default (true)
|
|
268
360
|
const pluginConfig = loadPluginConfig();
|
|
269
361
|
const codexMode = getCodexMode(pluginConfig);
|
|
362
|
+
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
363
|
+
const rateLimitToastDebounceMs = getRateLimitToastDebounceMs(pluginConfig);
|
|
364
|
+
const retryAllAccountsRateLimited = getRetryAllAccountsRateLimited(pluginConfig);
|
|
365
|
+
const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs(pluginConfig);
|
|
366
|
+
const retryAllAccountsMaxRetries = getRetryAllAccountsMaxRetries(pluginConfig);
|
|
270
367
|
// Return SDK configuration
|
|
271
368
|
return {
|
|
272
369
|
apiKey: DUMMY_API_KEY,
|
|
@@ -300,83 +397,136 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
300
397
|
const requestInit = transformation?.updatedInit ?? init;
|
|
301
398
|
const promptCacheKey = transformation?.body?.prompt_cache_key;
|
|
302
399
|
const model = transformation?.body.model;
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
400
|
+
const modelFamily = model ? getModelFamily(model) : "gpt-5.1";
|
|
401
|
+
const quotaKey = model ? `${modelFamily}:${model}` : modelFamily;
|
|
402
|
+
const abortSignal = requestInit?.signal ?? init?.signal ?? null;
|
|
403
|
+
const sleep = (ms) => new Promise((resolve, reject) => {
|
|
404
|
+
if (abortSignal?.aborted) {
|
|
405
|
+
reject(new Error("Aborted"));
|
|
406
|
+
return;
|
|
309
407
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
408
|
+
const timeout = setTimeout(() => {
|
|
409
|
+
cleanup();
|
|
410
|
+
resolve();
|
|
411
|
+
}, ms);
|
|
412
|
+
const onAbort = () => {
|
|
413
|
+
cleanup();
|
|
414
|
+
reject(new Error("Aborted"));
|
|
415
|
+
};
|
|
416
|
+
const cleanup = () => {
|
|
417
|
+
clearTimeout(timeout);
|
|
418
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
419
|
+
};
|
|
420
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
421
|
+
});
|
|
422
|
+
let allRateLimitedRetries = 0;
|
|
423
|
+
while (true) {
|
|
424
|
+
const accountCount = accountManager.getAccountCount();
|
|
425
|
+
const attempted = new Set();
|
|
426
|
+
while (attempted.size < Math.max(1, accountCount)) {
|
|
427
|
+
const account = accountManager.getCurrentOrNextForFamily(modelFamily, model);
|
|
428
|
+
if (!account || attempted.has(account.index)) {
|
|
429
|
+
break;
|
|
317
430
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (!accountId) {
|
|
326
|
-
accountManager.markAccountCoolingDown(account, AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
|
|
327
|
-
await accountManager.saveToDisk();
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
account.accountId = accountId;
|
|
331
|
-
if (accountCount > 1 &&
|
|
332
|
-
accountManager.shouldShowAccountToast(account.index)) {
|
|
333
|
-
const accountLabel = formatAccountLabel(account.accountId, account.index);
|
|
334
|
-
await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
|
|
335
|
-
accountManager.markToastShown(account.index);
|
|
336
|
-
}
|
|
337
|
-
const headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
|
|
338
|
-
model,
|
|
339
|
-
promptCacheKey,
|
|
340
|
-
});
|
|
341
|
-
const response = await fetch(url, {
|
|
342
|
-
...requestInit,
|
|
343
|
-
headers,
|
|
344
|
-
});
|
|
345
|
-
logRequest(LOG_STAGES.RESPONSE, {
|
|
346
|
-
status: response.status,
|
|
347
|
-
ok: response.ok,
|
|
348
|
-
statusText: response.statusText,
|
|
349
|
-
headers: Object.fromEntries(response.headers.entries()),
|
|
350
|
-
});
|
|
351
|
-
if (!response.ok) {
|
|
352
|
-
const { response: errorResponse, rateLimit } = await handleErrorResponse(response);
|
|
353
|
-
if (rateLimit) {
|
|
354
|
-
accountManager.markRateLimited(account, rateLimit.retryAfterMs);
|
|
355
|
-
accountManager.markSwitched(account, "rate-limit");
|
|
356
|
-
await accountManager.saveToDisk();
|
|
357
|
-
if (accountManager.getAccountCount() > 1 &&
|
|
358
|
-
accountManager.shouldShowAccountToast(account.index)) {
|
|
359
|
-
await showToast("Rate limit reached. Switching accounts.", "warning");
|
|
360
|
-
accountManager.markToastShown(account.index);
|
|
431
|
+
attempted.add(account.index);
|
|
432
|
+
let accountAuth = accountManager.toAuthDetails(account);
|
|
433
|
+
try {
|
|
434
|
+
if (shouldRefreshToken(accountAuth, tokenRefreshSkewMs)) {
|
|
435
|
+
accountAuth = (await refreshAndUpdateToken(accountAuth, client));
|
|
436
|
+
accountManager.updateFromAuth(account, accountAuth);
|
|
437
|
+
accountManager.saveToDiskDebounced();
|
|
361
438
|
}
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
logDebug(`[${PLUGIN_NAME}] Auth refresh failed for account: ${err?.message ?? String(err)}`);
|
|
442
|
+
accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
|
|
443
|
+
accountManager.saveToDiskDebounced();
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const accountId = account.accountId ?? extractAccountId(accountAuth.access);
|
|
447
|
+
if (!accountId) {
|
|
448
|
+
accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
|
|
449
|
+
accountManager.saveToDiskDebounced();
|
|
362
450
|
continue;
|
|
363
451
|
}
|
|
364
|
-
|
|
452
|
+
account.accountId = accountId;
|
|
453
|
+
account.email =
|
|
454
|
+
extractAccountEmail(accountAuth.access) ?? account.email;
|
|
455
|
+
if (accountCount > 1 &&
|
|
456
|
+
accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
457
|
+
const accountLabel = formatAccountLabel(account, account.index);
|
|
458
|
+
await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
|
|
459
|
+
accountManager.markToastShown(account.index);
|
|
460
|
+
}
|
|
461
|
+
const headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
|
|
462
|
+
model,
|
|
463
|
+
promptCacheKey,
|
|
464
|
+
});
|
|
465
|
+
while (true) {
|
|
466
|
+
const response = await fetch(url, {
|
|
467
|
+
...requestInit,
|
|
468
|
+
headers,
|
|
469
|
+
});
|
|
470
|
+
logRequest(LOG_STAGES.RESPONSE, {
|
|
471
|
+
status: response.status,
|
|
472
|
+
ok: response.ok,
|
|
473
|
+
statusText: response.statusText,
|
|
474
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
475
|
+
});
|
|
476
|
+
if (!response.ok) {
|
|
477
|
+
const { response: errorResponse, rateLimit } = await handleErrorResponse(response);
|
|
478
|
+
if (rateLimit) {
|
|
479
|
+
const { attempt, delayMs } = getRateLimitBackoff(account.index, quotaKey, rateLimit.retryAfterMs);
|
|
480
|
+
const waitLabel = formatWaitTime(delayMs);
|
|
481
|
+
if (delayMs <= RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS) {
|
|
482
|
+
if (accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
483
|
+
await showToast(`Rate limited. Retrying in ${waitLabel} (attempt ${attempt})...`, "warning");
|
|
484
|
+
accountManager.markToastShown(account.index);
|
|
485
|
+
}
|
|
486
|
+
await sleep(delayMs);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
accountManager.markRateLimited(account, delayMs, modelFamily, model);
|
|
490
|
+
account.lastSwitchReason = "rate-limit";
|
|
491
|
+
accountManager.saveToDiskDebounced();
|
|
492
|
+
if (accountManager.getAccountCount() > 1 &&
|
|
493
|
+
accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
494
|
+
await showToast(`Rate limited. Switching accounts (retry in ${waitLabel}).`, "warning");
|
|
495
|
+
accountManager.markToastShown(account.index);
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
return errorResponse;
|
|
500
|
+
}
|
|
501
|
+
resetRateLimitBackoff(account.index, quotaKey);
|
|
502
|
+
return await handleSuccessResponse(response, isStreaming);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const waitMs = accountManager.getMinWaitTimeForFamily(modelFamily, model);
|
|
506
|
+
const count = accountManager.getAccountCount();
|
|
507
|
+
if (retryAllAccountsRateLimited &&
|
|
508
|
+
count > 0 &&
|
|
509
|
+
waitMs > 0 &&
|
|
510
|
+
(retryAllAccountsMaxWaitMs === 0 ||
|
|
511
|
+
waitMs <= retryAllAccountsMaxWaitMs) &&
|
|
512
|
+
allRateLimitedRetries < retryAllAccountsMaxRetries) {
|
|
513
|
+
const waitLabel = formatWaitTime(waitMs);
|
|
514
|
+
await showToast(`All ${count} account(s) are rate-limited. Waiting ${waitLabel}...`, "warning");
|
|
515
|
+
allRateLimitedRetries++;
|
|
516
|
+
await sleep(waitMs);
|
|
517
|
+
continue;
|
|
365
518
|
}
|
|
366
|
-
|
|
519
|
+
const waitLabel = waitMs > 0 ? formatWaitTime(waitMs) : "a bit";
|
|
520
|
+
const message = count === 0
|
|
521
|
+
? "No OpenAI accounts configured. Run `opencode auth login`."
|
|
522
|
+
: `All ${count} account(s) are rate-limited. Try again in ${waitLabel} or add another account with \`opencode auth login\`.`;
|
|
523
|
+
return new Response(JSON.stringify({ error: { message } }), {
|
|
524
|
+
status: 429,
|
|
525
|
+
headers: {
|
|
526
|
+
"content-type": "application/json; charset=utf-8",
|
|
527
|
+
},
|
|
528
|
+
});
|
|
367
529
|
}
|
|
368
|
-
const waitMs = accountManager.getMinWaitTime();
|
|
369
|
-
const count = accountManager.getAccountCount();
|
|
370
|
-
const waitLabel = waitMs > 0 ? formatWaitTime(waitMs) : "a bit";
|
|
371
|
-
const message = count === 0
|
|
372
|
-
? "No OpenAI accounts configured. Run `opencode auth login`."
|
|
373
|
-
: `All ${count} account(s) are rate-limited. Try again in ${waitLabel} or add another account with \`opencode auth login\`.`;
|
|
374
|
-
return new Response(JSON.stringify({ error: { message } }), {
|
|
375
|
-
status: 429,
|
|
376
|
-
headers: {
|
|
377
|
-
"content-type": "application/json; charset=utf-8",
|
|
378
|
-
},
|
|
379
|
-
});
|
|
380
530
|
},
|
|
381
531
|
};
|
|
382
532
|
},
|
|
@@ -403,10 +553,11 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
403
553
|
inputs["no-browser"] === "true";
|
|
404
554
|
const useManualMode = noBrowser;
|
|
405
555
|
let startFresh = true;
|
|
406
|
-
const existingStorage = await loadAccounts();
|
|
556
|
+
const existingStorage = await hydrateEmails(await loadAccounts());
|
|
407
557
|
if (existingStorage && existingStorage.accounts.length > 0) {
|
|
408
558
|
const existingAccounts = existingStorage.accounts.map((account, index) => ({
|
|
409
559
|
accountId: account.accountId,
|
|
560
|
+
email: account.email,
|
|
410
561
|
index,
|
|
411
562
|
}));
|
|
412
563
|
const loginMode = await promptLoginMode(existingAccounts);
|
|
@@ -418,7 +569,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
418
569
|
console.log("\nAdding to existing accounts.\n");
|
|
419
570
|
}
|
|
420
571
|
}
|
|
421
|
-
while (accounts.length <
|
|
572
|
+
while (accounts.length < ACCOUNT_LIMITS.MAX_ACCOUNTS) {
|
|
422
573
|
console.log(`\n=== OpenAI OAuth (Account ${accounts.length + 1}) ===`);
|
|
423
574
|
const result = await runOAuthFlow(useManualMode);
|
|
424
575
|
if (result.type === "failed") {
|
|
@@ -439,10 +590,10 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
439
590
|
const isFirstAccount = accounts.length === 1;
|
|
440
591
|
await persistAccountPool([result], isFirstAccount && startFresh);
|
|
441
592
|
}
|
|
442
|
-
catch {
|
|
443
|
-
|
|
593
|
+
catch (err) {
|
|
594
|
+
logDebug(`[${PLUGIN_NAME}] Failed to persist account pool: ${err?.message ?? String(err)}`);
|
|
444
595
|
}
|
|
445
|
-
if (accounts.length >=
|
|
596
|
+
if (accounts.length >= ACCOUNT_LIMITS.MAX_ACCOUNTS) {
|
|
446
597
|
break;
|
|
447
598
|
}
|
|
448
599
|
let currentAccountCount = accounts.length;
|
|
@@ -452,8 +603,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
452
603
|
currentAccountCount = currentStorage.accounts.length;
|
|
453
604
|
}
|
|
454
605
|
}
|
|
455
|
-
catch {
|
|
456
|
-
|
|
606
|
+
catch (err) {
|
|
607
|
+
logDebug(`[${PLUGIN_NAME}] Failed to load accounts for count: ${err?.message ?? String(err)}`);
|
|
457
608
|
}
|
|
458
609
|
const addAnother = await promptAddAnotherAccount(currentAccountCount);
|
|
459
610
|
if (!addAnother) {
|
|
@@ -478,8 +629,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
478
629
|
actualAccountCount = finalStorage.accounts.length;
|
|
479
630
|
}
|
|
480
631
|
}
|
|
481
|
-
catch {
|
|
482
|
-
|
|
632
|
+
catch (err) {
|
|
633
|
+
logDebug(`[${PLUGIN_NAME}] Failed to load final account count: ${err?.message ?? String(err)}`);
|
|
483
634
|
}
|
|
484
635
|
return {
|
|
485
636
|
url: "",
|
|
@@ -493,7 +644,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
493
644
|
try {
|
|
494
645
|
serverInfo = await startLocalOAuthServer({ state });
|
|
495
646
|
}
|
|
496
|
-
catch {
|
|
647
|
+
catch (err) {
|
|
648
|
+
logDebug(`[${PLUGIN_NAME}] Failed to start OAuth server for add flow: ${err?.message ?? String(err)}`);
|
|
497
649
|
serverInfo = null;
|
|
498
650
|
}
|
|
499
651
|
openBrowserUrl(url);
|
|
@@ -558,13 +710,15 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
558
710
|
].join("\n");
|
|
559
711
|
}
|
|
560
712
|
const now = Date.now();
|
|
561
|
-
const activeIndex = resolveActiveIndex(storage);
|
|
713
|
+
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
562
714
|
const lines = [
|
|
563
715
|
`OpenAI Accounts (${storage.accounts.length}):`,
|
|
564
716
|
"",
|
|
717
|
+
" # Label Status",
|
|
718
|
+
"----------------------------------------------- ---------------------",
|
|
565
719
|
];
|
|
566
720
|
storage.accounts.forEach((account, index) => {
|
|
567
|
-
const label = formatAccountLabel(account
|
|
721
|
+
const label = formatAccountLabel(account, index);
|
|
568
722
|
const statuses = [];
|
|
569
723
|
const rateLimit = formatRateLimitEntry(account, now);
|
|
570
724
|
if (index === activeIndex)
|
|
@@ -576,10 +730,9 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
576
730
|
account.coolingDownUntil > now) {
|
|
577
731
|
statuses.push("cooldown");
|
|
578
732
|
}
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
lines.push(` ${index + 1}. ${label}${suffix}`);
|
|
733
|
+
const statusText = statuses.length > 0 ? statuses.join(", ") : "ok";
|
|
734
|
+
const row = `${String(index + 1).padEnd(3)} ${label.padEnd(40)} ${statusText}`;
|
|
735
|
+
lines.push(row);
|
|
583
736
|
});
|
|
584
737
|
lines.push("");
|
|
585
738
|
lines.push(`Storage: ${storePath}`);
|
|
@@ -614,51 +767,133 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
614
767
|
account.lastSwitchReason = "rotation";
|
|
615
768
|
}
|
|
616
769
|
storage.activeIndex = targetIndex;
|
|
770
|
+
storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
|
|
771
|
+
for (const family of MODEL_FAMILIES) {
|
|
772
|
+
storage.activeIndexByFamily[family] = targetIndex;
|
|
773
|
+
}
|
|
617
774
|
await saveAccounts(storage);
|
|
618
775
|
if (cachedAccountManager) {
|
|
619
776
|
cachedAccountManager.setActiveIndex(targetIndex);
|
|
620
777
|
await cachedAccountManager.saveToDisk();
|
|
621
778
|
}
|
|
622
|
-
const label = formatAccountLabel(account
|
|
779
|
+
const label = formatAccountLabel(account, targetIndex);
|
|
623
780
|
return `Switched to account: ${label}`;
|
|
624
781
|
},
|
|
625
782
|
}),
|
|
626
783
|
"openai-accounts-status": tool({
|
|
627
784
|
description: "Show detailed status of OpenAI accounts and rate limits.",
|
|
628
|
-
args: {
|
|
629
|
-
|
|
785
|
+
args: {
|
|
786
|
+
json: tool.schema.boolean().optional().describe("Return JSON instead of text"),
|
|
787
|
+
},
|
|
788
|
+
async execute({ json }) {
|
|
630
789
|
const storage = await loadAccounts();
|
|
631
790
|
if (!storage || storage.accounts.length === 0) {
|
|
632
791
|
return "No OpenAI accounts configured. Run: opencode auth login";
|
|
633
792
|
}
|
|
634
793
|
const now = Date.now();
|
|
635
|
-
const activeIndex = resolveActiveIndex(storage);
|
|
794
|
+
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
795
|
+
if (json) {
|
|
796
|
+
return JSON.stringify({
|
|
797
|
+
total: storage.accounts.length,
|
|
798
|
+
activeIndex,
|
|
799
|
+
activeIndexByFamily: storage.activeIndexByFamily ?? null,
|
|
800
|
+
storagePath: getStoragePath(),
|
|
801
|
+
accounts: storage.accounts.map((account, index) => ({
|
|
802
|
+
index,
|
|
803
|
+
active: index === activeIndex,
|
|
804
|
+
label: formatAccountLabel(account, index),
|
|
805
|
+
accountId: account.accountId ?? null,
|
|
806
|
+
email: account.email ?? null,
|
|
807
|
+
rateLimitResetTimes: account.rateLimitResetTimes ?? null,
|
|
808
|
+
coolingDownUntil: typeof account.coolingDownUntil === "number"
|
|
809
|
+
? account.coolingDownUntil
|
|
810
|
+
: null,
|
|
811
|
+
cooldownReason: account.cooldownReason ?? null,
|
|
812
|
+
lastUsed: typeof account.lastUsed === "number"
|
|
813
|
+
? account.lastUsed
|
|
814
|
+
: null,
|
|
815
|
+
})),
|
|
816
|
+
}, null, 2);
|
|
817
|
+
}
|
|
636
818
|
const lines = [
|
|
637
819
|
`Account Status (${storage.accounts.length} total):`,
|
|
638
820
|
"",
|
|
821
|
+
" # Label Active Rate Limit Cooldown Last Used",
|
|
822
|
+
"----------------------------------------------- ------ ---------------- ---------------- ----------------",
|
|
639
823
|
];
|
|
640
824
|
storage.accounts.forEach((account, index) => {
|
|
641
|
-
const label = formatAccountLabel(account
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
|
|
825
|
+
const label = formatAccountLabel(account, index).padEnd(42);
|
|
826
|
+
const active = index === activeIndex ? "Yes" : "No";
|
|
827
|
+
const rateLimit = formatRateLimitEntry(account, now) ?? "None";
|
|
828
|
+
const cooldown = formatCooldown(account, now) ?? "No";
|
|
829
|
+
const lastUsed = typeof account.lastUsed === "number" && account.lastUsed > 0
|
|
830
|
+
? `${formatWaitTime(now - account.lastUsed)} ago`
|
|
831
|
+
: "-";
|
|
832
|
+
const row = `${String(index + 1).padEnd(3)} ${label} ${active.padEnd(6)} ${rateLimit.padEnd(16)} ${cooldown.padEnd(16)} ${lastUsed}`;
|
|
833
|
+
lines.push(row);
|
|
834
|
+
});
|
|
835
|
+
lines.push("");
|
|
836
|
+
lines.push("Active index by model family:");
|
|
837
|
+
for (const family of MODEL_FAMILIES) {
|
|
838
|
+
const idx = storage.activeIndexByFamily?.[family];
|
|
839
|
+
const familyIndexLabel = typeof idx === "number" && Number.isFinite(idx) ? String(idx + 1) : "-";
|
|
840
|
+
lines.push(` ${family}: ${familyIndexLabel}`);
|
|
841
|
+
}
|
|
842
|
+
lines.push("");
|
|
843
|
+
lines.push("Rate limits by model family (per account):");
|
|
844
|
+
storage.accounts.forEach((account, index) => {
|
|
845
|
+
const statuses = MODEL_FAMILIES.map((family) => {
|
|
846
|
+
const resetAt = getRateLimitResetTimeForFamily(account, now, family);
|
|
847
|
+
if (typeof resetAt !== "number")
|
|
848
|
+
return `${family}=ok`;
|
|
849
|
+
return `${family}=${formatWaitTime(resetAt - now)}`;
|
|
850
|
+
});
|
|
851
|
+
lines.push(` Account ${index + 1}: ${statuses.join(" | ")}`);
|
|
658
852
|
});
|
|
659
853
|
return lines.join("\n");
|
|
660
854
|
},
|
|
661
855
|
}),
|
|
856
|
+
"openai-accounts-health": tool({
|
|
857
|
+
description: "Check health of all OpenAI accounts by validating refresh tokens.",
|
|
858
|
+
args: {},
|
|
859
|
+
async execute() {
|
|
860
|
+
const storage = await loadAccounts();
|
|
861
|
+
if (!storage || storage.accounts.length === 0) {
|
|
862
|
+
return "No OpenAI accounts configured. Run: opencode auth login";
|
|
863
|
+
}
|
|
864
|
+
const results = [
|
|
865
|
+
`Health Check (${storage.accounts.length} accounts):`,
|
|
866
|
+
"",
|
|
867
|
+
];
|
|
868
|
+
let healthyCount = 0;
|
|
869
|
+
let unhealthyCount = 0;
|
|
870
|
+
for (let i = 0; i < storage.accounts.length; i++) {
|
|
871
|
+
const account = storage.accounts[i];
|
|
872
|
+
if (!account)
|
|
873
|
+
continue;
|
|
874
|
+
const label = formatAccountLabel(account, i);
|
|
875
|
+
try {
|
|
876
|
+
const refreshResult = await refreshAccessToken(account.refreshToken);
|
|
877
|
+
if (refreshResult.type === "success") {
|
|
878
|
+
results.push(` ✓ ${label}: Healthy`);
|
|
879
|
+
healthyCount++;
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
results.push(` ✗ ${label}: Token refresh failed`);
|
|
883
|
+
unhealthyCount++;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
888
|
+
results.push(` ✗ ${label}: Error - ${errorMsg.slice(0, 50)}`);
|
|
889
|
+
unhealthyCount++;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
results.push("");
|
|
893
|
+
results.push(`Summary: ${healthyCount} healthy, ${unhealthyCount} unhealthy`);
|
|
894
|
+
return results.join("\n");
|
|
895
|
+
},
|
|
896
|
+
}),
|
|
662
897
|
},
|
|
663
898
|
};
|
|
664
899
|
};
|