opencode-openai-codex-auth-multi 4.3.0-multiaccount.1 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +37 -37
- package/README.md +463 -80
- 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 +391 -135
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts.d.ts +85 -11
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +352 -62
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/auth/auth.d.ts +9 -1
- package/dist/lib/auth/auth.d.ts.map +1 -1
- package/dist/lib/auth/auth.js +26 -13
- 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/auto-update-checker.d.ts +10 -0
- package/dist/lib/auto-update-checker.d.ts.map +1 -0
- package/dist/lib/auto-update-checker.js +129 -0
- package/dist/lib/auto-update-checker.js.map +1 -0
- package/dist/lib/cli.d.ts +1 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +11 -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/logger.d.ts +13 -17
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +89 -24
- package/dist/lib/logger.js.map +1 -1
- 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/refresh-queue.d.ts +100 -0
- package/dist/lib/refresh-queue.d.ts.map +1 -0
- package/dist/lib/refresh-queue.js +196 -0
- package/dist/lib/refresh-queue.js.map +1 -0
- 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 +26 -29
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/rate-limit-backoff.d.ts +17 -0
- package/dist/lib/request/rate-limit-backoff.d.ts.map +1 -0
- package/dist/lib/request/rate-limit-backoff.js +74 -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/rotation.d.ts +121 -0
- package/dist/lib/rotation.d.ts.map +1 -0
- package/dist/lib/rotation.js +248 -0
- package/dist/lib/rotation.js.map +1 -0
- 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,24 @@
|
|
|
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
26
|
import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, REDIRECT_URI, } from "./lib/auth/auth.js";
|
|
27
|
+
import { queuedRefresh } from "./lib/refresh-queue.js";
|
|
27
28
|
import { openBrowserUrl } from "./lib/auth/browser.js";
|
|
28
29
|
import { startLocalOAuthServer } from "./lib/auth/server.js";
|
|
29
30
|
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";
|
|
31
|
+
import { getCodexMode, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getTokenRefreshSkewMs, loadPluginConfig, } from "./lib/config.js";
|
|
32
|
+
import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
|
|
32
33
|
import { logRequest, logDebug } from "./lib/logger.js";
|
|
33
|
-
import {
|
|
34
|
+
import { checkAndNotify } from "./lib/auto-update-checker.js";
|
|
35
|
+
import { AccountManager, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, } from "./lib/accounts.js";
|
|
34
36
|
import { getStoragePath, loadAccounts, saveAccounts } from "./lib/storage.js";
|
|
35
37
|
import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
|
|
39
|
+
import { getModelFamily, MODEL_FAMILIES } from "./lib/prompts/codex.js";
|
|
38
40
|
/**
|
|
39
41
|
* OpenAI Codex OAuth authentication plugin for opencode
|
|
40
42
|
*
|
|
@@ -45,7 +47,7 @@ const AUTH_FAILURE_COOLDOWN_MS = 30_000;
|
|
|
45
47
|
* @example
|
|
46
48
|
* ```json
|
|
47
49
|
* {
|
|
48
|
-
* "plugin": ["opencode-openai-codex-auth-
|
|
50
|
+
* "plugin": ["opencode-openai-codex-auth-multi"],
|
|
49
51
|
|
|
50
52
|
* "model": "openai/gpt-5-codex"
|
|
51
53
|
* }
|
|
@@ -60,7 +62,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
60
62
|
callback: async (input) => {
|
|
61
63
|
const parsed = parseAuthorizationInput(input);
|
|
62
64
|
if (!parsed.code) {
|
|
63
|
-
return { type: "failed" };
|
|
65
|
+
return { type: "failed", reason: "invalid_response", message: "No authorization code provided" };
|
|
64
66
|
}
|
|
65
67
|
const tokens = await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
|
|
66
68
|
if (tokens?.type === "success" && onSuccess) {
|
|
@@ -82,7 +84,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
82
84
|
rl.close();
|
|
83
85
|
}
|
|
84
86
|
};
|
|
85
|
-
const runManualOAuthFlow = async (pkce,
|
|
87
|
+
const runManualOAuthFlow = async (pkce, _url) => {
|
|
86
88
|
console.log("1. Open the URL above in your browser and sign in.");
|
|
87
89
|
console.log("2. After approving, copy the full redirect URL.");
|
|
88
90
|
console.log("3. Paste it back here.\n");
|
|
@@ -93,8 +95,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
93
95
|
}
|
|
94
96
|
return await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
|
|
95
97
|
};
|
|
96
|
-
const runOAuthFlow = async (useManualMode) => {
|
|
97
|
-
const { pkce, state, url } = await createAuthorizationFlow();
|
|
98
|
+
const runOAuthFlow = async (useManualMode, forceNewLogin = false) => {
|
|
99
|
+
const { pkce, state, url } = await createAuthorizationFlow({ forceNewLogin });
|
|
98
100
|
console.log("\nOAuth URL:\n" + url + "\n");
|
|
99
101
|
if (useManualMode) {
|
|
100
102
|
openBrowserUrl(url);
|
|
@@ -104,7 +106,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
104
106
|
try {
|
|
105
107
|
serverInfo = await startLocalOAuthServer({ state });
|
|
106
108
|
}
|
|
107
|
-
catch {
|
|
109
|
+
catch (err) {
|
|
110
|
+
logDebug(`[${PLUGIN_NAME}] Failed to start OAuth server: ${err?.message ?? String(err)}`);
|
|
108
111
|
serverInfo = null;
|
|
109
112
|
}
|
|
110
113
|
openBrowserUrl(url);
|
|
@@ -115,7 +118,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
115
118
|
const result = await serverInfo.waitForCode(state);
|
|
116
119
|
serverInfo.close();
|
|
117
120
|
if (!result) {
|
|
118
|
-
return { type: "failed" };
|
|
121
|
+
return { type: "failed", reason: "unknown", message: "OAuth callback timeout or cancelled" };
|
|
119
122
|
}
|
|
120
123
|
return await exchangeAuthorizationCode(result.code, pkce.verifier, REDIRECT_URI);
|
|
121
124
|
};
|
|
@@ -127,6 +130,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
127
130
|
const accounts = stored?.accounts ? [...stored.accounts] : [];
|
|
128
131
|
const indexByRefreshToken = new Map();
|
|
129
132
|
const indexByAccountId = new Map();
|
|
133
|
+
const indexByEmail = new Map();
|
|
130
134
|
for (let i = 0; i < accounts.length; i += 1) {
|
|
131
135
|
const account = accounts[i];
|
|
132
136
|
if (!account)
|
|
@@ -137,18 +141,26 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
137
141
|
if (account.accountId) {
|
|
138
142
|
indexByAccountId.set(account.accountId, i);
|
|
139
143
|
}
|
|
144
|
+
if (account.email) {
|
|
145
|
+
indexByEmail.set(account.email, i);
|
|
146
|
+
}
|
|
140
147
|
}
|
|
141
148
|
for (const result of results) {
|
|
142
149
|
const accountId = extractAccountId(result.access);
|
|
150
|
+
const accountEmail = sanitizeEmail(extractAccountEmail(result.access));
|
|
151
|
+
const existingByEmail = accountEmail && indexByEmail.has(accountEmail)
|
|
152
|
+
? indexByEmail.get(accountEmail)
|
|
153
|
+
: undefined;
|
|
143
154
|
const existingById = accountId && indexByAccountId.has(accountId)
|
|
144
155
|
? indexByAccountId.get(accountId)
|
|
145
156
|
: undefined;
|
|
146
157
|
const existingByToken = indexByRefreshToken.get(result.refresh);
|
|
147
|
-
const existingIndex = existingById ?? existingByToken;
|
|
158
|
+
const existingIndex = existingById ?? existingByEmail ?? existingByToken;
|
|
148
159
|
if (existingIndex === undefined) {
|
|
149
160
|
const newIndex = accounts.length;
|
|
150
161
|
accounts.push({
|
|
151
162
|
accountId,
|
|
163
|
+
email: accountEmail,
|
|
152
164
|
refreshToken: result.refresh,
|
|
153
165
|
addedAt: now,
|
|
154
166
|
lastUsed: now,
|
|
@@ -157,15 +169,21 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
157
169
|
if (accountId) {
|
|
158
170
|
indexByAccountId.set(accountId, newIndex);
|
|
159
171
|
}
|
|
172
|
+
if (accountEmail) {
|
|
173
|
+
indexByEmail.set(accountEmail, newIndex);
|
|
174
|
+
}
|
|
160
175
|
continue;
|
|
161
176
|
}
|
|
162
177
|
const existing = accounts[existingIndex];
|
|
163
178
|
if (!existing)
|
|
164
179
|
continue;
|
|
165
180
|
const oldToken = existing.refreshToken;
|
|
181
|
+
const oldEmail = existing.email;
|
|
182
|
+
const nextEmail = accountEmail ?? existing.email;
|
|
166
183
|
accounts[existingIndex] = {
|
|
167
184
|
...existing,
|
|
168
185
|
accountId: accountId ?? existing.accountId,
|
|
186
|
+
email: nextEmail,
|
|
169
187
|
refreshToken: result.refresh,
|
|
170
188
|
lastUsed: now,
|
|
171
189
|
};
|
|
@@ -176,6 +194,12 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
176
194
|
if (accountId) {
|
|
177
195
|
indexByAccountId.set(accountId, existingIndex);
|
|
178
196
|
}
|
|
197
|
+
if (oldEmail && oldEmail !== nextEmail) {
|
|
198
|
+
indexByEmail.delete(oldEmail);
|
|
199
|
+
}
|
|
200
|
+
if (nextEmail) {
|
|
201
|
+
indexByEmail.set(nextEmail, existingIndex);
|
|
202
|
+
}
|
|
179
203
|
}
|
|
180
204
|
if (accounts.length === 0)
|
|
181
205
|
return;
|
|
@@ -184,10 +208,22 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
184
208
|
: typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex)
|
|
185
209
|
? stored.activeIndex
|
|
186
210
|
: 0;
|
|
211
|
+
const clampedActiveIndex = Math.max(0, Math.min(activeIndex, accounts.length - 1));
|
|
212
|
+
const activeIndexByFamily = {};
|
|
213
|
+
for (const family of MODEL_FAMILIES) {
|
|
214
|
+
const storedFamilyIndex = stored?.activeIndexByFamily?.[family];
|
|
215
|
+
const rawFamilyIndex = replaceAll
|
|
216
|
+
? 0
|
|
217
|
+
: typeof storedFamilyIndex === "number" && Number.isFinite(storedFamilyIndex)
|
|
218
|
+
? storedFamilyIndex
|
|
219
|
+
: clampedActiveIndex;
|
|
220
|
+
activeIndexByFamily[family] = Math.max(0, Math.min(Math.floor(rawFamilyIndex), accounts.length - 1));
|
|
221
|
+
}
|
|
187
222
|
await saveAccounts({
|
|
188
|
-
version:
|
|
223
|
+
version: 3,
|
|
189
224
|
accounts,
|
|
190
|
-
activeIndex:
|
|
225
|
+
activeIndex: clampedActiveIndex,
|
|
226
|
+
activeIndexByFamily,
|
|
191
227
|
});
|
|
192
228
|
};
|
|
193
229
|
const showToast = async (message, variant = "success") => {
|
|
@@ -203,17 +239,79 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
203
239
|
// Ignore when TUI is not available.
|
|
204
240
|
}
|
|
205
241
|
};
|
|
206
|
-
const resolveActiveIndex = (storage) => {
|
|
242
|
+
const resolveActiveIndex = (storage, family = "codex") => {
|
|
207
243
|
const total = storage.accounts.length;
|
|
208
244
|
if (total === 0)
|
|
209
245
|
return 0;
|
|
210
|
-
const
|
|
246
|
+
const rawCandidate = storage.activeIndexByFamily?.[family] ?? storage.activeIndex;
|
|
247
|
+
const raw = Number.isFinite(rawCandidate) ? rawCandidate : 0;
|
|
211
248
|
return Math.max(0, Math.min(raw, total - 1));
|
|
212
249
|
};
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
250
|
+
const hydrateEmails = async (storage) => {
|
|
251
|
+
if (!storage)
|
|
252
|
+
return storage;
|
|
253
|
+
const skipHydrate = process.env.VITEST_WORKER_ID !== undefined ||
|
|
254
|
+
process.env.NODE_ENV === "test" ||
|
|
255
|
+
process.env.OPENCODE_SKIP_EMAIL_HYDRATE === "1";
|
|
256
|
+
if (skipHydrate)
|
|
257
|
+
return storage;
|
|
258
|
+
const accountsToHydrate = storage.accounts.filter((account) => account && !account.email);
|
|
259
|
+
if (accountsToHydrate.length === 0)
|
|
260
|
+
return storage;
|
|
261
|
+
let changed = false;
|
|
262
|
+
await Promise.all(accountsToHydrate.map(async (account) => {
|
|
263
|
+
try {
|
|
264
|
+
const refreshed = await queuedRefresh(account.refreshToken);
|
|
265
|
+
if (refreshed.type !== "success")
|
|
266
|
+
return;
|
|
267
|
+
const id = extractAccountId(refreshed.access);
|
|
268
|
+
const email = sanitizeEmail(extractAccountEmail(refreshed.access));
|
|
269
|
+
if (id && id !== account.accountId) {
|
|
270
|
+
account.accountId = id;
|
|
271
|
+
changed = true;
|
|
272
|
+
}
|
|
273
|
+
if (email && email !== account.email) {
|
|
274
|
+
account.email = email;
|
|
275
|
+
changed = true;
|
|
276
|
+
}
|
|
277
|
+
if (refreshed.refresh && refreshed.refresh !== account.refreshToken) {
|
|
278
|
+
account.refreshToken = refreshed.refresh;
|
|
279
|
+
changed = true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
logDebug(`[${PLUGIN_NAME}] Failed to hydrate email for account`);
|
|
284
|
+
}
|
|
285
|
+
}));
|
|
286
|
+
if (changed) {
|
|
287
|
+
await saveAccounts(storage);
|
|
288
|
+
}
|
|
289
|
+
return storage;
|
|
290
|
+
};
|
|
291
|
+
const getRateLimitResetTimeForFamily = (account, now, family) => {
|
|
292
|
+
const times = account.rateLimitResetTimes;
|
|
293
|
+
if (!times)
|
|
215
294
|
return null;
|
|
216
|
-
|
|
295
|
+
let minReset = null;
|
|
296
|
+
const prefix = `${family}:`;
|
|
297
|
+
for (const [key, value] of Object.entries(times)) {
|
|
298
|
+
if (typeof value !== "number")
|
|
299
|
+
continue;
|
|
300
|
+
if (value <= now)
|
|
301
|
+
continue;
|
|
302
|
+
if (key !== family && !key.startsWith(prefix))
|
|
303
|
+
continue;
|
|
304
|
+
if (minReset === null || value < minReset) {
|
|
305
|
+
minReset = value;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return minReset;
|
|
309
|
+
};
|
|
310
|
+
const formatRateLimitEntry = (account, now, family = "codex") => {
|
|
311
|
+
const resetAt = getRateLimitResetTimeForFamily(account, now, family);
|
|
312
|
+
if (typeof resetAt !== "number")
|
|
313
|
+
return null;
|
|
314
|
+
const remaining = resetAt - now;
|
|
217
315
|
if (remaining <= 0)
|
|
218
316
|
return null;
|
|
219
317
|
return `resets in ${formatWaitTime(remaining)}`;
|
|
@@ -243,13 +341,9 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
243
341
|
}
|
|
244
342
|
const accountManager = await AccountManager.loadFromDisk(auth);
|
|
245
343
|
cachedAccountManager = accountManager;
|
|
246
|
-
const storedSnapshot = await loadAccounts();
|
|
247
344
|
const refreshToken = auth.type === "oauth" ? auth.refresh : "";
|
|
248
|
-
const needsPersist =
|
|
249
|
-
|
|
250
|
-
accountManager.getAccountCount() ||
|
|
251
|
-
(refreshToken &&
|
|
252
|
-
!storedSnapshot.accounts.some((account) => account.refreshToken === refreshToken));
|
|
345
|
+
const needsPersist = refreshToken &&
|
|
346
|
+
!accountManager.hasRefreshToken(refreshToken);
|
|
253
347
|
if (needsPersist) {
|
|
254
348
|
await accountManager.saveToDisk();
|
|
255
349
|
}
|
|
@@ -267,6 +361,14 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
267
361
|
// Priority: CODEX_MODE env var > config file > default (true)
|
|
268
362
|
const pluginConfig = loadPluginConfig();
|
|
269
363
|
const codexMode = getCodexMode(pluginConfig);
|
|
364
|
+
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
365
|
+
const rateLimitToastDebounceMs = getRateLimitToastDebounceMs(pluginConfig);
|
|
366
|
+
const retryAllAccountsRateLimited = getRetryAllAccountsRateLimited(pluginConfig);
|
|
367
|
+
const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs(pluginConfig);
|
|
368
|
+
const retryAllAccountsMaxRetries = getRetryAllAccountsMaxRetries(pluginConfig);
|
|
369
|
+
checkAndNotify(async (message, variant) => {
|
|
370
|
+
await showToast(message, variant);
|
|
371
|
+
}).catch(() => { });
|
|
270
372
|
// Return SDK configuration
|
|
271
373
|
return {
|
|
272
374
|
apiKey: DUMMY_API_KEY,
|
|
@@ -300,83 +402,138 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
300
402
|
const requestInit = transformation?.updatedInit ?? init;
|
|
301
403
|
const promptCacheKey = transformation?.body?.prompt_cache_key;
|
|
302
404
|
const model = transformation?.body.model;
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
405
|
+
const modelFamily = model ? getModelFamily(model) : "gpt-5.1";
|
|
406
|
+
const quotaKey = model ? `${modelFamily}:${model}` : modelFamily;
|
|
407
|
+
const abortSignal = requestInit?.signal ?? init?.signal ?? null;
|
|
408
|
+
const sleep = (ms) => new Promise((resolve, reject) => {
|
|
409
|
+
if (abortSignal?.aborted) {
|
|
410
|
+
reject(new Error("Aborted"));
|
|
411
|
+
return;
|
|
309
412
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
413
|
+
const timeout = setTimeout(() => {
|
|
414
|
+
cleanup();
|
|
415
|
+
resolve();
|
|
416
|
+
}, ms);
|
|
417
|
+
const onAbort = () => {
|
|
418
|
+
cleanup();
|
|
419
|
+
reject(new Error("Aborted"));
|
|
420
|
+
};
|
|
421
|
+
const cleanup = () => {
|
|
422
|
+
clearTimeout(timeout);
|
|
423
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
424
|
+
};
|
|
425
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
426
|
+
});
|
|
427
|
+
let allRateLimitedRetries = 0;
|
|
428
|
+
while (true) {
|
|
429
|
+
const accountCount = accountManager.getAccountCount();
|
|
430
|
+
const attempted = new Set();
|
|
431
|
+
while (attempted.size < Math.max(1, accountCount)) {
|
|
432
|
+
const account = accountManager.getCurrentOrNextForFamilyHybrid(modelFamily, model);
|
|
433
|
+
if (!account || attempted.has(account.index)) {
|
|
434
|
+
break;
|
|
317
435
|
}
|
|
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);
|
|
436
|
+
attempted.add(account.index);
|
|
437
|
+
let accountAuth = accountManager.toAuthDetails(account);
|
|
438
|
+
try {
|
|
439
|
+
if (shouldRefreshToken(accountAuth, tokenRefreshSkewMs)) {
|
|
440
|
+
accountAuth = (await refreshAndUpdateToken(accountAuth, client));
|
|
441
|
+
accountManager.updateFromAuth(account, accountAuth);
|
|
442
|
+
accountManager.saveToDiskDebounced();
|
|
361
443
|
}
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
logDebug(`[${PLUGIN_NAME}] Auth refresh failed for account: ${err?.message ?? String(err)}`);
|
|
447
|
+
accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
|
|
448
|
+
accountManager.saveToDiskDebounced();
|
|
362
449
|
continue;
|
|
363
450
|
}
|
|
364
|
-
|
|
451
|
+
const accountId = account.accountId ?? extractAccountId(accountAuth.access);
|
|
452
|
+
if (!accountId) {
|
|
453
|
+
accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
|
|
454
|
+
accountManager.saveToDiskDebounced();
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
account.accountId = accountId;
|
|
458
|
+
account.email =
|
|
459
|
+
extractAccountEmail(accountAuth.access) ?? account.email;
|
|
460
|
+
if (accountCount > 1 &&
|
|
461
|
+
accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
462
|
+
const accountLabel = formatAccountLabel(account, account.index);
|
|
463
|
+
await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
|
|
464
|
+
accountManager.markToastShown(account.index);
|
|
465
|
+
}
|
|
466
|
+
const headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
|
|
467
|
+
model,
|
|
468
|
+
promptCacheKey,
|
|
469
|
+
});
|
|
470
|
+
while (true) {
|
|
471
|
+
const response = await fetch(url, {
|
|
472
|
+
...requestInit,
|
|
473
|
+
headers,
|
|
474
|
+
});
|
|
475
|
+
logRequest(LOG_STAGES.RESPONSE, {
|
|
476
|
+
status: response.status,
|
|
477
|
+
ok: response.ok,
|
|
478
|
+
statusText: response.statusText,
|
|
479
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
480
|
+
});
|
|
481
|
+
if (!response.ok) {
|
|
482
|
+
const { response: errorResponse, rateLimit } = await handleErrorResponse(response);
|
|
483
|
+
if (rateLimit) {
|
|
484
|
+
const { attempt, delayMs } = getRateLimitBackoff(account.index, quotaKey, rateLimit.retryAfterMs);
|
|
485
|
+
const waitLabel = formatWaitTime(delayMs);
|
|
486
|
+
if (delayMs <= RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS) {
|
|
487
|
+
if (accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
488
|
+
await showToast(`Rate limited. Retrying in ${waitLabel} (attempt ${attempt})...`, "warning");
|
|
489
|
+
accountManager.markToastShown(account.index);
|
|
490
|
+
}
|
|
491
|
+
await sleep(delayMs);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
accountManager.markRateLimited(account, delayMs, modelFamily, model);
|
|
495
|
+
accountManager.recordRateLimit(account, modelFamily, model);
|
|
496
|
+
account.lastSwitchReason = "rate-limit";
|
|
497
|
+
accountManager.saveToDiskDebounced();
|
|
498
|
+
if (accountManager.getAccountCount() > 1 &&
|
|
499
|
+
accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
500
|
+
await showToast(`Rate limited. Switching accounts (retry in ${waitLabel}).`, "warning");
|
|
501
|
+
accountManager.markToastShown(account.index);
|
|
502
|
+
}
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
return errorResponse;
|
|
506
|
+
}
|
|
507
|
+
resetRateLimitBackoff(account.index, quotaKey);
|
|
508
|
+
accountManager.recordSuccess(account, modelFamily, model);
|
|
509
|
+
return await handleSuccessResponse(response, isStreaming);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const waitMs = accountManager.getMinWaitTimeForFamily(modelFamily, model);
|
|
513
|
+
const count = accountManager.getAccountCount();
|
|
514
|
+
if (retryAllAccountsRateLimited &&
|
|
515
|
+
count > 0 &&
|
|
516
|
+
waitMs > 0 &&
|
|
517
|
+
(retryAllAccountsMaxWaitMs === 0 ||
|
|
518
|
+
waitMs <= retryAllAccountsMaxWaitMs) &&
|
|
519
|
+
allRateLimitedRetries < retryAllAccountsMaxRetries) {
|
|
520
|
+
const waitLabel = formatWaitTime(waitMs);
|
|
521
|
+
await showToast(`All ${count} account(s) are rate-limited. Waiting ${waitLabel}...`, "warning");
|
|
522
|
+
allRateLimitedRetries++;
|
|
523
|
+
await sleep(waitMs);
|
|
524
|
+
continue;
|
|
365
525
|
}
|
|
366
|
-
|
|
526
|
+
const waitLabel = waitMs > 0 ? formatWaitTime(waitMs) : "a bit";
|
|
527
|
+
const message = count === 0
|
|
528
|
+
? "No OpenAI accounts configured. Run `opencode auth login`."
|
|
529
|
+
: `All ${count} account(s) are rate-limited. Try again in ${waitLabel} or add another account with \`opencode auth login\`.`;
|
|
530
|
+
return new Response(JSON.stringify({ error: { message } }), {
|
|
531
|
+
status: 429,
|
|
532
|
+
headers: {
|
|
533
|
+
"content-type": "application/json; charset=utf-8",
|
|
534
|
+
},
|
|
535
|
+
});
|
|
367
536
|
}
|
|
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
537
|
},
|
|
381
538
|
};
|
|
382
539
|
},
|
|
@@ -403,10 +560,11 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
403
560
|
inputs["no-browser"] === "true";
|
|
404
561
|
const useManualMode = noBrowser;
|
|
405
562
|
let startFresh = true;
|
|
406
|
-
const existingStorage = await loadAccounts();
|
|
563
|
+
const existingStorage = await hydrateEmails(await loadAccounts());
|
|
407
564
|
if (existingStorage && existingStorage.accounts.length > 0) {
|
|
408
565
|
const existingAccounts = existingStorage.accounts.map((account, index) => ({
|
|
409
566
|
accountId: account.accountId,
|
|
567
|
+
email: account.email,
|
|
410
568
|
index,
|
|
411
569
|
}));
|
|
412
570
|
const loginMode = await promptLoginMode(existingAccounts);
|
|
@@ -418,9 +576,23 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
418
576
|
console.log("\nAdding to existing accounts.\n");
|
|
419
577
|
}
|
|
420
578
|
}
|
|
421
|
-
while (accounts.length <
|
|
579
|
+
while (accounts.length < ACCOUNT_LIMITS.MAX_ACCOUNTS) {
|
|
422
580
|
console.log(`\n=== OpenAI OAuth (Account ${accounts.length + 1}) ===`);
|
|
423
|
-
const
|
|
581
|
+
const forceNewLogin = accounts.length > 0;
|
|
582
|
+
const result = await runOAuthFlow(useManualMode, forceNewLogin);
|
|
583
|
+
if (result.type === "success") {
|
|
584
|
+
const email = extractAccountEmail(result.access);
|
|
585
|
+
const accountId = extractAccountId(result.access);
|
|
586
|
+
const label = email || accountId || "Unknown account";
|
|
587
|
+
console.log(`\n✓ Authenticated as: ${label}\n`);
|
|
588
|
+
const isDuplicate = accounts.some((acc) => (accountId && extractAccountId(acc.access) === accountId) ||
|
|
589
|
+
(email && extractAccountEmail(acc.access) === email));
|
|
590
|
+
if (isDuplicate) {
|
|
591
|
+
console.warn(`\n⚠️ WARNING: You authenticated with an account that is already in the list (${label}).`);
|
|
592
|
+
console.warn("This usually happens if you didn't log out or use a different browser profile.");
|
|
593
|
+
console.warn("The duplicate will update the existing entry.\n");
|
|
594
|
+
}
|
|
595
|
+
}
|
|
424
596
|
if (result.type === "failed") {
|
|
425
597
|
if (accounts.length === 0) {
|
|
426
598
|
return {
|
|
@@ -439,10 +611,10 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
439
611
|
const isFirstAccount = accounts.length === 1;
|
|
440
612
|
await persistAccountPool([result], isFirstAccount && startFresh);
|
|
441
613
|
}
|
|
442
|
-
catch {
|
|
443
|
-
|
|
614
|
+
catch (err) {
|
|
615
|
+
logDebug(`[${PLUGIN_NAME}] Failed to persist account pool: ${err?.message ?? String(err)}`);
|
|
444
616
|
}
|
|
445
|
-
if (accounts.length >=
|
|
617
|
+
if (accounts.length >= ACCOUNT_LIMITS.MAX_ACCOUNTS) {
|
|
446
618
|
break;
|
|
447
619
|
}
|
|
448
620
|
let currentAccountCount = accounts.length;
|
|
@@ -452,8 +624,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
452
624
|
currentAccountCount = currentStorage.accounts.length;
|
|
453
625
|
}
|
|
454
626
|
}
|
|
455
|
-
catch {
|
|
456
|
-
|
|
627
|
+
catch (err) {
|
|
628
|
+
logDebug(`[${PLUGIN_NAME}] Failed to load accounts for count: ${err?.message ?? String(err)}`);
|
|
457
629
|
}
|
|
458
630
|
const addAnother = await promptAddAnotherAccount(currentAccountCount);
|
|
459
631
|
if (!addAnother) {
|
|
@@ -478,8 +650,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
478
650
|
actualAccountCount = finalStorage.accounts.length;
|
|
479
651
|
}
|
|
480
652
|
}
|
|
481
|
-
catch {
|
|
482
|
-
|
|
653
|
+
catch (err) {
|
|
654
|
+
logDebug(`[${PLUGIN_NAME}] Failed to load final account count: ${err?.message ?? String(err)}`);
|
|
483
655
|
}
|
|
484
656
|
return {
|
|
485
657
|
url: "",
|
|
@@ -493,7 +665,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
493
665
|
try {
|
|
494
666
|
serverInfo = await startLocalOAuthServer({ state });
|
|
495
667
|
}
|
|
496
|
-
catch {
|
|
668
|
+
catch (err) {
|
|
669
|
+
logDebug(`[${PLUGIN_NAME}] Failed to start OAuth server for add flow: ${err?.message ?? String(err)}`);
|
|
497
670
|
serverInfo = null;
|
|
498
671
|
}
|
|
499
672
|
openBrowserUrl(url);
|
|
@@ -558,13 +731,15 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
558
731
|
].join("\n");
|
|
559
732
|
}
|
|
560
733
|
const now = Date.now();
|
|
561
|
-
const activeIndex = resolveActiveIndex(storage);
|
|
734
|
+
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
562
735
|
const lines = [
|
|
563
736
|
`OpenAI Accounts (${storage.accounts.length}):`,
|
|
564
737
|
"",
|
|
738
|
+
" # Label Status",
|
|
739
|
+
"----------------------------------------------- ---------------------",
|
|
565
740
|
];
|
|
566
741
|
storage.accounts.forEach((account, index) => {
|
|
567
|
-
const label = formatAccountLabel(account
|
|
742
|
+
const label = formatAccountLabel(account, index);
|
|
568
743
|
const statuses = [];
|
|
569
744
|
const rateLimit = formatRateLimitEntry(account, now);
|
|
570
745
|
if (index === activeIndex)
|
|
@@ -576,10 +751,9 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
576
751
|
account.coolingDownUntil > now) {
|
|
577
752
|
statuses.push("cooldown");
|
|
578
753
|
}
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
lines.push(` ${index + 1}. ${label}${suffix}`);
|
|
754
|
+
const statusText = statuses.length > 0 ? statuses.join(", ") : "ok";
|
|
755
|
+
const row = `${String(index + 1).padEnd(3)} ${label.padEnd(40)} ${statusText}`;
|
|
756
|
+
lines.push(row);
|
|
583
757
|
});
|
|
584
758
|
lines.push("");
|
|
585
759
|
lines.push(`Storage: ${storePath}`);
|
|
@@ -614,51 +788,133 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
614
788
|
account.lastSwitchReason = "rotation";
|
|
615
789
|
}
|
|
616
790
|
storage.activeIndex = targetIndex;
|
|
791
|
+
storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
|
|
792
|
+
for (const family of MODEL_FAMILIES) {
|
|
793
|
+
storage.activeIndexByFamily[family] = targetIndex;
|
|
794
|
+
}
|
|
617
795
|
await saveAccounts(storage);
|
|
618
796
|
if (cachedAccountManager) {
|
|
619
797
|
cachedAccountManager.setActiveIndex(targetIndex);
|
|
620
798
|
await cachedAccountManager.saveToDisk();
|
|
621
799
|
}
|
|
622
|
-
const label = formatAccountLabel(account
|
|
800
|
+
const label = formatAccountLabel(account, targetIndex);
|
|
623
801
|
return `Switched to account: ${label}`;
|
|
624
802
|
},
|
|
625
803
|
}),
|
|
626
804
|
"openai-accounts-status": tool({
|
|
627
805
|
description: "Show detailed status of OpenAI accounts and rate limits.",
|
|
628
|
-
args: {
|
|
629
|
-
|
|
806
|
+
args: {
|
|
807
|
+
json: tool.schema.boolean().optional().describe("Return JSON instead of text"),
|
|
808
|
+
},
|
|
809
|
+
async execute({ json }) {
|
|
630
810
|
const storage = await loadAccounts();
|
|
631
811
|
if (!storage || storage.accounts.length === 0) {
|
|
632
812
|
return "No OpenAI accounts configured. Run: opencode auth login";
|
|
633
813
|
}
|
|
634
814
|
const now = Date.now();
|
|
635
|
-
const activeIndex = resolveActiveIndex(storage);
|
|
815
|
+
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
816
|
+
if (json) {
|
|
817
|
+
return JSON.stringify({
|
|
818
|
+
total: storage.accounts.length,
|
|
819
|
+
activeIndex,
|
|
820
|
+
activeIndexByFamily: storage.activeIndexByFamily ?? null,
|
|
821
|
+
storagePath: getStoragePath(),
|
|
822
|
+
accounts: storage.accounts.map((account, index) => ({
|
|
823
|
+
index,
|
|
824
|
+
active: index === activeIndex,
|
|
825
|
+
label: formatAccountLabel(account, index),
|
|
826
|
+
accountId: account.accountId ?? null,
|
|
827
|
+
email: account.email ?? null,
|
|
828
|
+
rateLimitResetTimes: account.rateLimitResetTimes ?? null,
|
|
829
|
+
coolingDownUntil: typeof account.coolingDownUntil === "number"
|
|
830
|
+
? account.coolingDownUntil
|
|
831
|
+
: null,
|
|
832
|
+
cooldownReason: account.cooldownReason ?? null,
|
|
833
|
+
lastUsed: typeof account.lastUsed === "number"
|
|
834
|
+
? account.lastUsed
|
|
835
|
+
: null,
|
|
836
|
+
})),
|
|
837
|
+
}, null, 2);
|
|
838
|
+
}
|
|
636
839
|
const lines = [
|
|
637
840
|
`Account Status (${storage.accounts.length} total):`,
|
|
638
841
|
"",
|
|
842
|
+
" # Label Active Rate Limit Cooldown Last Used",
|
|
843
|
+
"----------------------------------------------- ------ ---------------- ---------------- ----------------",
|
|
639
844
|
];
|
|
640
845
|
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
|
-
|
|
846
|
+
const label = formatAccountLabel(account, index).padEnd(42);
|
|
847
|
+
const active = index === activeIndex ? "Yes" : "No";
|
|
848
|
+
const rateLimit = formatRateLimitEntry(account, now) ?? "None";
|
|
849
|
+
const cooldown = formatCooldown(account, now) ?? "No";
|
|
850
|
+
const lastUsed = typeof account.lastUsed === "number" && account.lastUsed > 0
|
|
851
|
+
? `${formatWaitTime(now - account.lastUsed)} ago`
|
|
852
|
+
: "-";
|
|
853
|
+
const row = `${String(index + 1).padEnd(3)} ${label} ${active.padEnd(6)} ${rateLimit.padEnd(16)} ${cooldown.padEnd(16)} ${lastUsed}`;
|
|
854
|
+
lines.push(row);
|
|
855
|
+
});
|
|
856
|
+
lines.push("");
|
|
857
|
+
lines.push("Active index by model family:");
|
|
858
|
+
for (const family of MODEL_FAMILIES) {
|
|
859
|
+
const idx = storage.activeIndexByFamily?.[family];
|
|
860
|
+
const familyIndexLabel = typeof idx === "number" && Number.isFinite(idx) ? String(idx + 1) : "-";
|
|
861
|
+
lines.push(` ${family}: ${familyIndexLabel}`);
|
|
862
|
+
}
|
|
863
|
+
lines.push("");
|
|
864
|
+
lines.push("Rate limits by model family (per account):");
|
|
865
|
+
storage.accounts.forEach((account, index) => {
|
|
866
|
+
const statuses = MODEL_FAMILIES.map((family) => {
|
|
867
|
+
const resetAt = getRateLimitResetTimeForFamily(account, now, family);
|
|
868
|
+
if (typeof resetAt !== "number")
|
|
869
|
+
return `${family}=ok`;
|
|
870
|
+
return `${family}=${formatWaitTime(resetAt - now)}`;
|
|
871
|
+
});
|
|
872
|
+
lines.push(` Account ${index + 1}: ${statuses.join(" | ")}`);
|
|
658
873
|
});
|
|
659
874
|
return lines.join("\n");
|
|
660
875
|
},
|
|
661
876
|
}),
|
|
877
|
+
"openai-accounts-health": tool({
|
|
878
|
+
description: "Check health of all OpenAI accounts by validating refresh tokens.",
|
|
879
|
+
args: {},
|
|
880
|
+
async execute() {
|
|
881
|
+
const storage = await loadAccounts();
|
|
882
|
+
if (!storage || storage.accounts.length === 0) {
|
|
883
|
+
return "No OpenAI accounts configured. Run: opencode auth login";
|
|
884
|
+
}
|
|
885
|
+
const results = [
|
|
886
|
+
`Health Check (${storage.accounts.length} accounts):`,
|
|
887
|
+
"",
|
|
888
|
+
];
|
|
889
|
+
let healthyCount = 0;
|
|
890
|
+
let unhealthyCount = 0;
|
|
891
|
+
for (let i = 0; i < storage.accounts.length; i++) {
|
|
892
|
+
const account = storage.accounts[i];
|
|
893
|
+
if (!account)
|
|
894
|
+
continue;
|
|
895
|
+
const label = formatAccountLabel(account, i);
|
|
896
|
+
try {
|
|
897
|
+
const refreshResult = await queuedRefresh(account.refreshToken);
|
|
898
|
+
if (refreshResult.type === "success") {
|
|
899
|
+
results.push(` ✓ ${label}: Healthy`);
|
|
900
|
+
healthyCount++;
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
results.push(` ✗ ${label}: Token refresh failed`);
|
|
904
|
+
unhealthyCount++;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
909
|
+
results.push(` ✗ ${label}: Error - ${errorMsg.slice(0, 50)}`);
|
|
910
|
+
unhealthyCount++;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
results.push("");
|
|
914
|
+
results.push(`Summary: ${healthyCount} healthy, ${unhealthyCount} unhealthy`);
|
|
915
|
+
return results.join("\n");
|
|
916
|
+
},
|
|
917
|
+
}),
|
|
662
918
|
},
|
|
663
919
|
};
|
|
664
920
|
};
|