opencode-openai-codex-auth-multi 4.3.0-multiaccount.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.
Files changed (90) hide show
  1. package/LICENSE +37 -0
  2. package/README.md +107 -0
  3. package/assets/opencode-logo-ornate-dark.svg +18 -0
  4. package/assets/readme-hero.svg +31 -0
  5. package/config/README.md +110 -0
  6. package/config/minimal-opencode.json +13 -0
  7. package/config/opencode-legacy.json +572 -0
  8. package/config/opencode-modern.json +240 -0
  9. package/dist/index.d.ts +44 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +666 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/accounts.d.ts +48 -0
  14. package/dist/lib/accounts.d.ts.map +1 -0
  15. package/dist/lib/accounts.js +282 -0
  16. package/dist/lib/accounts.js.map +1 -0
  17. package/dist/lib/auth/auth.d.ts +43 -0
  18. package/dist/lib/auth/auth.d.ts.map +1 -0
  19. package/dist/lib/auth/auth.js +163 -0
  20. package/dist/lib/auth/auth.js.map +1 -0
  21. package/dist/lib/auth/browser.d.ts +17 -0
  22. package/dist/lib/auth/browser.d.ts.map +1 -0
  23. package/dist/lib/auth/browser.js +76 -0
  24. package/dist/lib/auth/browser.js.map +1 -0
  25. package/dist/lib/auth/server.d.ts +10 -0
  26. package/dist/lib/auth/server.d.ts.map +1 -0
  27. package/dist/lib/auth/server.js +78 -0
  28. package/dist/lib/auth/server.js.map +1 -0
  29. package/dist/lib/cli.d.ts +8 -0
  30. package/dist/lib/cli.d.ts.map +1 -0
  31. package/dist/lib/cli.js +44 -0
  32. package/dist/lib/cli.js.map +1 -0
  33. package/dist/lib/config.d.ts +17 -0
  34. package/dist/lib/config.d.ts.map +1 -0
  35. package/dist/lib/config.js +51 -0
  36. package/dist/lib/config.js.map +1 -0
  37. package/dist/lib/constants.d.ts +67 -0
  38. package/dist/lib/constants.d.ts.map +1 -0
  39. package/dist/lib/constants.js +67 -0
  40. package/dist/lib/constants.js.map +1 -0
  41. package/dist/lib/logger.d.ts +26 -0
  42. package/dist/lib/logger.d.ts.map +1 -0
  43. package/dist/lib/logger.js +110 -0
  44. package/dist/lib/logger.js.map +1 -0
  45. package/dist/lib/oauth-success.html +712 -0
  46. package/dist/lib/prompts/codex-opencode-bridge.d.ts +19 -0
  47. package/dist/lib/prompts/codex-opencode-bridge.d.ts.map +1 -0
  48. package/dist/lib/prompts/codex-opencode-bridge.js +152 -0
  49. package/dist/lib/prompts/codex-opencode-bridge.js.map +1 -0
  50. package/dist/lib/prompts/codex.d.ts +27 -0
  51. package/dist/lib/prompts/codex.d.ts.map +1 -0
  52. package/dist/lib/prompts/codex.js +241 -0
  53. package/dist/lib/prompts/codex.js.map +1 -0
  54. package/dist/lib/prompts/opencode-codex.d.ts +21 -0
  55. package/dist/lib/prompts/opencode-codex.d.ts.map +1 -0
  56. package/dist/lib/prompts/opencode-codex.js +91 -0
  57. package/dist/lib/prompts/opencode-codex.js.map +1 -0
  58. package/dist/lib/request/fetch-helpers.d.ts +81 -0
  59. package/dist/lib/request/fetch-helpers.d.ts.map +1 -0
  60. package/dist/lib/request/fetch-helpers.js +321 -0
  61. package/dist/lib/request/fetch-helpers.js.map +1 -0
  62. package/dist/lib/request/helpers/input-utils.d.ts +6 -0
  63. package/dist/lib/request/helpers/input-utils.d.ts.map +1 -0
  64. package/dist/lib/request/helpers/input-utils.js +174 -0
  65. package/dist/lib/request/helpers/input-utils.js.map +1 -0
  66. package/dist/lib/request/helpers/model-map.d.ts +28 -0
  67. package/dist/lib/request/helpers/model-map.d.ts.map +1 -0
  68. package/dist/lib/request/helpers/model-map.js +109 -0
  69. package/dist/lib/request/helpers/model-map.js.map +1 -0
  70. package/dist/lib/request/request-transformer.d.ts +93 -0
  71. package/dist/lib/request/request-transformer.d.ts.map +1 -0
  72. package/dist/lib/request/request-transformer.js +403 -0
  73. package/dist/lib/request/request-transformer.js.map +1 -0
  74. package/dist/lib/request/response-handler.d.ts +14 -0
  75. package/dist/lib/request/response-handler.d.ts.map +1 -0
  76. package/dist/lib/request/response-handler.js +90 -0
  77. package/dist/lib/request/response-handler.js.map +1 -0
  78. package/dist/lib/storage.d.ts +23 -0
  79. package/dist/lib/storage.d.ts.map +1 -0
  80. package/dist/lib/storage.js +153 -0
  81. package/dist/lib/storage.js.map +1 -0
  82. package/dist/lib/types.d.ts +170 -0
  83. package/dist/lib/types.d.ts.map +1 -0
  84. package/dist/lib/types.js +2 -0
  85. package/dist/lib/types.js.map +1 -0
  86. package/package.json +71 -0
  87. package/scripts/copy-oauth-success.js +37 -0
  88. package/scripts/install-opencode-codex-auth.js +193 -0
  89. package/scripts/test-all-models.sh +260 -0
  90. package/scripts/validate-model-map.sh +97 -0
package/dist/index.js ADDED
@@ -0,0 +1,666 @@
1
+ /**
2
+ * OpenAI ChatGPT (Codex) OAuth Authentication Plugin for opencode
3
+ *
4
+ * COMPLIANCE NOTICE:
5
+ * This plugin uses OpenAI's official OAuth authentication flow (the same method
6
+ * used by OpenAI's official Codex CLI at https://github.com/openai/codex).
7
+ *
8
+ * INTENDED USE: Personal development and coding assistance with your own
9
+ * ChatGPT Plus/Pro subscription.
10
+ *
11
+ * NOT INTENDED FOR: Commercial resale, multi-user services, high-volume
12
+ * automated extraction, or any use that violates OpenAI's Terms of Service.
13
+ *
14
+ * Users are responsible for ensuring their usage complies with:
15
+ * - OpenAI Terms of Use: https://openai.com/policies/terms-of-use/
16
+ * - OpenAI Usage Policies: https://openai.com/policies/usage-policies/
17
+ *
18
+ * For production applications, use the OpenAI Platform API: https://platform.openai.com/
19
+ *
20
+ * @license MIT with Usage Disclaimer (see LICENSE file)
21
+ * @author numman-ali
22
+ * @repository https://github.com/ndycode/opencode-openai-codex-auth-multiaccount
23
+
24
+ */
25
+ import { tool } from "@opencode-ai/plugin";
26
+ import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, REDIRECT_URI, } from "./lib/auth/auth.js";
27
+ import { openBrowserUrl } from "./lib/auth/browser.js";
28
+ import { startLocalOAuthServer } from "./lib/auth/server.js";
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";
32
+ import { logRequest, logDebug } from "./lib/logger.js";
33
+ import { AccountManager, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, } from "./lib/accounts.js";
34
+ import { getStoragePath, loadAccounts, saveAccounts } from "./lib/storage.js";
35
+ import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
36
+ const MAX_OAUTH_ACCOUNTS = 10;
37
+ const AUTH_FAILURE_COOLDOWN_MS = 30_000;
38
+ /**
39
+ * OpenAI Codex OAuth authentication plugin for opencode
40
+ *
41
+ * This plugin enables opencode to use OpenAI's Codex backend via ChatGPT Plus/Pro
42
+ * OAuth authentication, allowing users to leverage their ChatGPT subscription
43
+ * instead of OpenAI Platform API credits.
44
+ *
45
+ * @example
46
+ * ```json
47
+ * {
48
+ * "plugin": ["opencode-openai-codex-auth-multiaccount"],
49
+
50
+ * "model": "openai/gpt-5-codex"
51
+ * }
52
+ * ```
53
+ */
54
+ export const OpenAIAuthPlugin = async ({ client }) => {
55
+ let cachedAccountManager = null;
56
+ const buildManualOAuthFlow = (pkce, url, onSuccess) => ({
57
+ url,
58
+ method: "code",
59
+ instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL,
60
+ callback: async (input) => {
61
+ const parsed = parseAuthorizationInput(input);
62
+ if (!parsed.code) {
63
+ return { type: "failed" };
64
+ }
65
+ const tokens = await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
66
+ if (tokens?.type === "success" && onSuccess) {
67
+ await onSuccess(tokens);
68
+ }
69
+ return tokens?.type === "success"
70
+ ? tokens
71
+ : { type: "failed" };
72
+ },
73
+ });
74
+ const promptOAuthCallbackValue = async (message) => {
75
+ const { createInterface } = await import("node:readline/promises");
76
+ const { stdin, stdout } = await import("node:process");
77
+ const rl = createInterface({ input: stdin, output: stdout });
78
+ try {
79
+ return (await rl.question(message)).trim();
80
+ }
81
+ finally {
82
+ rl.close();
83
+ }
84
+ };
85
+ const runManualOAuthFlow = async (pkce, url) => {
86
+ console.log("1. Open the URL above in your browser and sign in.");
87
+ console.log("2. After approving, copy the full redirect URL.");
88
+ console.log("3. Paste it back here.\n");
89
+ const callbackInput = await promptOAuthCallbackValue("Paste the redirect URL (or just the code) here: ");
90
+ const parsed = parseAuthorizationInput(callbackInput);
91
+ if (!parsed.code) {
92
+ return { type: "failed" };
93
+ }
94
+ return await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
95
+ };
96
+ const runOAuthFlow = async (useManualMode) => {
97
+ const { pkce, state, url } = await createAuthorizationFlow();
98
+ console.log("\nOAuth URL:\n" + url + "\n");
99
+ if (useManualMode) {
100
+ openBrowserUrl(url);
101
+ return await runManualOAuthFlow(pkce, url);
102
+ }
103
+ let serverInfo = null;
104
+ try {
105
+ serverInfo = await startLocalOAuthServer({ state });
106
+ }
107
+ catch {
108
+ serverInfo = null;
109
+ }
110
+ openBrowserUrl(url);
111
+ if (!serverInfo || !serverInfo.ready) {
112
+ serverInfo?.close();
113
+ return await runManualOAuthFlow(pkce, url);
114
+ }
115
+ const result = await serverInfo.waitForCode(state);
116
+ serverInfo.close();
117
+ if (!result) {
118
+ return { type: "failed" };
119
+ }
120
+ return await exchangeAuthorizationCode(result.code, pkce.verifier, REDIRECT_URI);
121
+ };
122
+ const persistAccountPool = async (results, replaceAll = false) => {
123
+ if (results.length === 0)
124
+ return;
125
+ const now = Date.now();
126
+ const stored = replaceAll ? null : await loadAccounts();
127
+ const accounts = stored?.accounts ? [...stored.accounts] : [];
128
+ const indexByRefreshToken = new Map();
129
+ const indexByAccountId = new Map();
130
+ for (let i = 0; i < accounts.length; i += 1) {
131
+ const account = accounts[i];
132
+ if (!account)
133
+ continue;
134
+ if (account.refreshToken) {
135
+ indexByRefreshToken.set(account.refreshToken, i);
136
+ }
137
+ if (account.accountId) {
138
+ indexByAccountId.set(account.accountId, i);
139
+ }
140
+ }
141
+ for (const result of results) {
142
+ const accountId = extractAccountId(result.access);
143
+ const existingById = accountId && indexByAccountId.has(accountId)
144
+ ? indexByAccountId.get(accountId)
145
+ : undefined;
146
+ const existingByToken = indexByRefreshToken.get(result.refresh);
147
+ const existingIndex = existingById ?? existingByToken;
148
+ if (existingIndex === undefined) {
149
+ const newIndex = accounts.length;
150
+ accounts.push({
151
+ accountId,
152
+ refreshToken: result.refresh,
153
+ addedAt: now,
154
+ lastUsed: now,
155
+ });
156
+ indexByRefreshToken.set(result.refresh, newIndex);
157
+ if (accountId) {
158
+ indexByAccountId.set(accountId, newIndex);
159
+ }
160
+ continue;
161
+ }
162
+ const existing = accounts[existingIndex];
163
+ if (!existing)
164
+ continue;
165
+ const oldToken = existing.refreshToken;
166
+ accounts[existingIndex] = {
167
+ ...existing,
168
+ accountId: accountId ?? existing.accountId,
169
+ refreshToken: result.refresh,
170
+ lastUsed: now,
171
+ };
172
+ if (oldToken !== result.refresh) {
173
+ indexByRefreshToken.delete(oldToken);
174
+ indexByRefreshToken.set(result.refresh, existingIndex);
175
+ }
176
+ if (accountId) {
177
+ indexByAccountId.set(accountId, existingIndex);
178
+ }
179
+ }
180
+ if (accounts.length === 0)
181
+ return;
182
+ const activeIndex = replaceAll
183
+ ? 0
184
+ : typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex)
185
+ ? stored.activeIndex
186
+ : 0;
187
+ await saveAccounts({
188
+ version: 1,
189
+ accounts,
190
+ activeIndex: Math.max(0, Math.min(activeIndex, accounts.length - 1)),
191
+ });
192
+ };
193
+ const showToast = async (message, variant = "success") => {
194
+ try {
195
+ await client.tui.showToast({
196
+ body: {
197
+ message,
198
+ variant,
199
+ },
200
+ });
201
+ }
202
+ catch {
203
+ // Ignore when TUI is not available.
204
+ }
205
+ };
206
+ const resolveActiveIndex = (storage) => {
207
+ const total = storage.accounts.length;
208
+ if (total === 0)
209
+ return 0;
210
+ const raw = Number.isFinite(storage.activeIndex) ? storage.activeIndex : 0;
211
+ return Math.max(0, Math.min(raw, total - 1));
212
+ };
213
+ const formatRateLimitEntry = (account, now) => {
214
+ if (typeof account.rateLimitResetTime !== "number")
215
+ return null;
216
+ const remaining = account.rateLimitResetTime - now;
217
+ if (remaining <= 0)
218
+ return null;
219
+ return `resets in ${formatWaitTime(remaining)}`;
220
+ };
221
+ return {
222
+ auth: {
223
+ provider: PROVIDER_ID,
224
+ /**
225
+ * Loader function that configures OAuth authentication and request handling
226
+ *
227
+ * This function:
228
+ * 1. Validates OAuth authentication
229
+ * 2. Loads multi-account pool from disk (fallback to current auth)
230
+ * 3. Loads user configuration from opencode.json
231
+ * 4. Fetches Codex system instructions from GitHub (cached)
232
+ * 5. Returns SDK configuration with custom fetch implementation
233
+ *
234
+ * @param getAuth - Function to retrieve current auth state
235
+ * @param provider - Provider configuration from opencode.json
236
+ * @returns SDK configuration object or empty object for non-OAuth auth
237
+ */
238
+ async loader(getAuth, provider) {
239
+ const auth = await getAuth();
240
+ // Only handle OAuth auth type, skip API key auth
241
+ if (auth.type !== "oauth") {
242
+ return {};
243
+ }
244
+ const accountManager = await AccountManager.loadFromDisk(auth);
245
+ cachedAccountManager = accountManager;
246
+ const storedSnapshot = await loadAccounts();
247
+ const refreshToken = auth.type === "oauth" ? auth.refresh : "";
248
+ const needsPersist = !storedSnapshot ||
249
+ storedSnapshot.accounts.length !==
250
+ accountManager.getAccountCount() ||
251
+ (refreshToken &&
252
+ !storedSnapshot.accounts.some((account) => account.refreshToken === refreshToken));
253
+ if (needsPersist) {
254
+ await accountManager.saveToDisk();
255
+ }
256
+ if (accountManager.getAccountCount() === 0) {
257
+ logDebug(`[${PLUGIN_NAME}] No OAuth accounts available (run opencode auth login)`);
258
+ return {};
259
+ }
260
+ // Extract user configuration (global + per-model options)
261
+ const providerConfig = provider;
262
+ const userConfig = {
263
+ global: providerConfig?.options || {},
264
+ models: providerConfig?.models || {},
265
+ };
266
+ // Load plugin configuration and determine CODEX_MODE
267
+ // Priority: CODEX_MODE env var > config file > default (true)
268
+ const pluginConfig = loadPluginConfig();
269
+ const codexMode = getCodexMode(pluginConfig);
270
+ // Return SDK configuration
271
+ return {
272
+ apiKey: DUMMY_API_KEY,
273
+ baseURL: CODEX_BASE_URL,
274
+ /**
275
+ * Custom fetch implementation for Codex API
276
+ *
277
+ * Handles:
278
+ * - Token refresh when expired
279
+ * - URL rewriting for Codex backend
280
+ * - Request body transformation
281
+ * - OAuth header injection
282
+ * - SSE to JSON conversion for non-tool requests
283
+ * - Error handling and logging
284
+ *
285
+ * @param input - Request URL or Request object
286
+ * @param init - Request options
287
+ * @returns Response from Codex API
288
+ */
289
+ async fetch(input, init) {
290
+ // Step 1: Extract and rewrite URL for Codex backend
291
+ const originalUrl = extractRequestUrl(input);
292
+ const url = rewriteUrlForCodex(originalUrl);
293
+ // Step 3: Transform request body with model-specific Codex instructions
294
+ // Instructions are fetched per model family (codex-max, codex, gpt-5.1)
295
+ // Capture original stream value before transformation
296
+ // generateText() sends no stream field, streamText() sends stream=true
297
+ const originalBody = init?.body ? JSON.parse(init.body) : {};
298
+ const isStreaming = originalBody.stream === true;
299
+ const transformation = await transformRequestForCodex(init, url, userConfig, codexMode);
300
+ const requestInit = transformation?.updatedInit ?? init;
301
+ const promptCacheKey = transformation?.body?.prompt_cache_key;
302
+ const model = transformation?.body.model;
303
+ const accountCount = accountManager.getAccountCount();
304
+ const attempted = new Set();
305
+ while (attempted.size < Math.max(1, accountCount)) {
306
+ const account = accountManager.getCurrentOrNext();
307
+ if (!account || attempted.has(account.index)) {
308
+ break;
309
+ }
310
+ attempted.add(account.index);
311
+ let accountAuth = accountManager.toAuthDetails(account);
312
+ try {
313
+ if (shouldRefreshToken(accountAuth)) {
314
+ accountAuth = (await refreshAndUpdateToken(accountAuth, client));
315
+ accountManager.updateFromAuth(account, accountAuth);
316
+ await accountManager.saveToDisk();
317
+ }
318
+ }
319
+ catch (error) {
320
+ accountManager.markAccountCoolingDown(account, AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
321
+ await accountManager.saveToDisk();
322
+ continue;
323
+ }
324
+ const accountId = account.accountId ?? extractAccountId(accountAuth.access);
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);
361
+ }
362
+ continue;
363
+ }
364
+ return errorResponse;
365
+ }
366
+ return await handleSuccessResponse(response, isStreaming);
367
+ }
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
+ },
381
+ };
382
+ },
383
+ methods: [
384
+ {
385
+ label: AUTH_LABELS.OAUTH,
386
+ type: "oauth",
387
+ /**
388
+ * OAuth authorization flow
389
+ *
390
+ * Steps:
391
+ * 1. Generate PKCE challenge and state for security
392
+ * 2. Start local OAuth callback server on port 1455
393
+ * 3. Open browser to OpenAI authorization page
394
+ * 4. Wait for user to complete login
395
+ * 5. Exchange authorization code for tokens
396
+ *
397
+ * @returns Authorization flow configuration
398
+ */
399
+ authorize: async (inputs) => {
400
+ if (inputs) {
401
+ const accounts = [];
402
+ const noBrowser = inputs.noBrowser === "true" ||
403
+ inputs["no-browser"] === "true";
404
+ const useManualMode = noBrowser;
405
+ let startFresh = true;
406
+ const existingStorage = await loadAccounts();
407
+ if (existingStorage && existingStorage.accounts.length > 0) {
408
+ const existingAccounts = existingStorage.accounts.map((account, index) => ({
409
+ accountId: account.accountId,
410
+ index,
411
+ }));
412
+ const loginMode = await promptLoginMode(existingAccounts);
413
+ startFresh = loginMode === "fresh";
414
+ if (startFresh) {
415
+ console.log("\nStarting fresh - existing accounts will be replaced.\n");
416
+ }
417
+ else {
418
+ console.log("\nAdding to existing accounts.\n");
419
+ }
420
+ }
421
+ while (accounts.length < MAX_OAUTH_ACCOUNTS) {
422
+ console.log(`\n=== OpenAI OAuth (Account ${accounts.length + 1}) ===`);
423
+ const result = await runOAuthFlow(useManualMode);
424
+ if (result.type === "failed") {
425
+ if (accounts.length === 0) {
426
+ return {
427
+ url: "",
428
+ instructions: "Authentication failed.",
429
+ method: "auto",
430
+ callback: async () => result,
431
+ };
432
+ }
433
+ console.warn(`[${PLUGIN_NAME}] Skipping failed account ${accounts.length + 1}`);
434
+ break;
435
+ }
436
+ accounts.push(result);
437
+ await showToast(`Account ${accounts.length} authenticated`, "success");
438
+ try {
439
+ const isFirstAccount = accounts.length === 1;
440
+ await persistAccountPool([result], isFirstAccount && startFresh);
441
+ }
442
+ catch {
443
+ // Ignore storage failures
444
+ }
445
+ if (accounts.length >= MAX_OAUTH_ACCOUNTS) {
446
+ break;
447
+ }
448
+ let currentAccountCount = accounts.length;
449
+ try {
450
+ const currentStorage = await loadAccounts();
451
+ if (currentStorage) {
452
+ currentAccountCount = currentStorage.accounts.length;
453
+ }
454
+ }
455
+ catch {
456
+ // Ignore storage read failures
457
+ }
458
+ const addAnother = await promptAddAnotherAccount(currentAccountCount);
459
+ if (!addAnother) {
460
+ break;
461
+ }
462
+ }
463
+ const primary = accounts[0];
464
+ if (!primary) {
465
+ return {
466
+ url: "",
467
+ instructions: "Authentication cancelled",
468
+ method: "auto",
469
+ callback: async () => ({
470
+ type: "failed",
471
+ }),
472
+ };
473
+ }
474
+ let actualAccountCount = accounts.length;
475
+ try {
476
+ const finalStorage = await loadAccounts();
477
+ if (finalStorage) {
478
+ actualAccountCount = finalStorage.accounts.length;
479
+ }
480
+ }
481
+ catch {
482
+ // Ignore storage read failures
483
+ }
484
+ return {
485
+ url: "",
486
+ instructions: `Multi-account setup complete (${actualAccountCount} account(s)).`,
487
+ method: "auto",
488
+ callback: async () => primary,
489
+ };
490
+ }
491
+ const { pkce, state, url } = await createAuthorizationFlow();
492
+ let serverInfo = null;
493
+ try {
494
+ serverInfo = await startLocalOAuthServer({ state });
495
+ }
496
+ catch {
497
+ serverInfo = null;
498
+ }
499
+ openBrowserUrl(url);
500
+ if (!serverInfo || !serverInfo.ready) {
501
+ serverInfo?.close();
502
+ return buildManualOAuthFlow(pkce, url, async (tokens) => {
503
+ await persistAccountPool([tokens], false);
504
+ });
505
+ }
506
+ return {
507
+ url,
508
+ method: "auto",
509
+ instructions: AUTH_LABELS.INSTRUCTIONS,
510
+ callback: async () => {
511
+ const result = await serverInfo.waitForCode(state);
512
+ serverInfo.close();
513
+ if (!result) {
514
+ return { type: "failed" };
515
+ }
516
+ const tokens = await exchangeAuthorizationCode(result.code, pkce.verifier, REDIRECT_URI);
517
+ if (tokens?.type === "success") {
518
+ await persistAccountPool([tokens], false);
519
+ }
520
+ return tokens?.type === "success"
521
+ ? tokens
522
+ : { type: "failed" };
523
+ },
524
+ };
525
+ },
526
+ },
527
+ {
528
+ label: AUTH_LABELS.OAUTH_MANUAL,
529
+ type: "oauth",
530
+ authorize: async () => {
531
+ const { pkce, url } = await createAuthorizationFlow();
532
+ return buildManualOAuthFlow(pkce, url, async (tokens) => {
533
+ await persistAccountPool([tokens], false);
534
+ });
535
+ },
536
+ },
537
+ {
538
+ label: AUTH_LABELS.API_KEY,
539
+ type: "api",
540
+ },
541
+ ],
542
+ },
543
+ tool: {
544
+ "openai-accounts": tool({
545
+ description: "List all OpenAI OAuth accounts and the current active index.",
546
+ args: {},
547
+ async execute() {
548
+ const storage = await loadAccounts();
549
+ const storePath = getStoragePath();
550
+ if (!storage || storage.accounts.length === 0) {
551
+ return [
552
+ "No OpenAI accounts configured.",
553
+ "",
554
+ "Add accounts:",
555
+ " opencode auth login",
556
+ "",
557
+ `Storage: ${storePath}`,
558
+ ].join("\n");
559
+ }
560
+ const now = Date.now();
561
+ const activeIndex = resolveActiveIndex(storage);
562
+ const lines = [
563
+ `OpenAI Accounts (${storage.accounts.length}):`,
564
+ "",
565
+ ];
566
+ storage.accounts.forEach((account, index) => {
567
+ const label = formatAccountLabel(account.accountId, index);
568
+ const statuses = [];
569
+ const rateLimit = formatRateLimitEntry(account, now);
570
+ if (index === activeIndex)
571
+ statuses.push("active");
572
+ if (rateLimit)
573
+ statuses.push("rate-limited");
574
+ if (typeof account.coolingDownUntil ===
575
+ "number" &&
576
+ account.coolingDownUntil > now) {
577
+ statuses.push("cooldown");
578
+ }
579
+ const suffix = statuses.length > 0
580
+ ? ` (${statuses.join(", ")})`
581
+ : "";
582
+ lines.push(` ${index + 1}. ${label}${suffix}`);
583
+ });
584
+ lines.push("");
585
+ lines.push(`Storage: ${storePath}`);
586
+ lines.push("");
587
+ lines.push("Commands:");
588
+ lines.push(" - Add account: opencode auth login");
589
+ lines.push(" - Switch account: openai-accounts-switch");
590
+ lines.push(" - Status details: openai-accounts-status");
591
+ return lines.join("\n");
592
+ },
593
+ }),
594
+ "openai-accounts-switch": tool({
595
+ description: "Switch active OpenAI account by index (1-based).",
596
+ args: {
597
+ index: tool.schema.number().describe("Account number to switch to (1-based, e.g., 1 for first account)"),
598
+ },
599
+ async execute({ index }) {
600
+ const storage = await loadAccounts();
601
+ if (!storage || storage.accounts.length === 0) {
602
+ return "No OpenAI accounts configured. Run: opencode auth login";
603
+ }
604
+ const targetIndex = Math.floor((index ?? 0) - 1);
605
+ if (!Number.isFinite(targetIndex) ||
606
+ targetIndex < 0 ||
607
+ targetIndex >= storage.accounts.length) {
608
+ return `Invalid account number: ${index}\n\nValid range: 1-${storage.accounts.length}`;
609
+ }
610
+ const now = Date.now();
611
+ const account = storage.accounts[targetIndex];
612
+ if (account) {
613
+ account.lastUsed = now;
614
+ account.lastSwitchReason = "rotation";
615
+ }
616
+ storage.activeIndex = targetIndex;
617
+ await saveAccounts(storage);
618
+ if (cachedAccountManager) {
619
+ cachedAccountManager.setActiveIndex(targetIndex);
620
+ await cachedAccountManager.saveToDisk();
621
+ }
622
+ const label = formatAccountLabel(account?.accountId, targetIndex);
623
+ return `Switched to account: ${label}`;
624
+ },
625
+ }),
626
+ "openai-accounts-status": tool({
627
+ description: "Show detailed status of OpenAI accounts and rate limits.",
628
+ args: {},
629
+ async execute() {
630
+ const storage = await loadAccounts();
631
+ if (!storage || storage.accounts.length === 0) {
632
+ return "No OpenAI accounts configured. Run: opencode auth login";
633
+ }
634
+ const now = Date.now();
635
+ const activeIndex = resolveActiveIndex(storage);
636
+ const lines = [
637
+ `Account Status (${storage.accounts.length} total):`,
638
+ "",
639
+ ];
640
+ storage.accounts.forEach((account, index) => {
641
+ const label = formatAccountLabel(account.accountId, index);
642
+ lines.push(`${index + 1}. ${label}`);
643
+ lines.push(` Active: ${index === activeIndex ? "Yes" : "No"}`);
644
+ const rateLimit = formatRateLimitEntry(account, now);
645
+ lines.push(` Rate Limit: ${rateLimit ?? "None"}`);
646
+ const cooldown = formatCooldown(account, now);
647
+ if (cooldown) {
648
+ lines.push(` Cooldown: Yes (${cooldown})`);
649
+ }
650
+ else {
651
+ lines.push(" Cooldown: No");
652
+ }
653
+ if (typeof account.lastUsed === "number" &&
654
+ account.lastUsed > 0) {
655
+ lines.push(` Last Used: ${formatWaitTime(now - account.lastUsed)} ago`);
656
+ }
657
+ lines.push("");
658
+ });
659
+ return lines.join("\n");
660
+ },
661
+ }),
662
+ },
663
+ };
664
+ };
665
+ export default OpenAIAuthPlugin;
666
+ //# sourceMappingURL=index.js.map