oc-codex-multi-auth 6.1.2 → 6.1.6

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 (84) hide show
  1. package/README.md +27 -23
  2. package/config/README.md +35 -35
  3. package/config/opencode-legacy.json +80 -30
  4. package/config/opencode-modern.json +14 -4
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +157 -11
  7. package/dist/index.js.map +1 -1
  8. package/dist/lib/accounts/persistence.d.ts.map +1 -1
  9. package/dist/lib/accounts/persistence.js +1 -0
  10. package/dist/lib/accounts/persistence.js.map +1 -1
  11. package/dist/lib/accounts/state.d.ts +2 -2
  12. package/dist/lib/accounts/state.d.ts.map +1 -1
  13. package/dist/lib/accounts/state.js +87 -19
  14. package/dist/lib/accounts/state.js.map +1 -1
  15. package/dist/lib/auth/auth.d.ts +2 -2
  16. package/dist/lib/auth/auth.d.ts.map +1 -1
  17. package/dist/lib/auth/auth.js +9 -6
  18. package/dist/lib/auth/auth.js.map +1 -1
  19. package/dist/lib/auth/login-runner.d.ts +1 -0
  20. package/dist/lib/auth/login-runner.d.ts.map +1 -1
  21. package/dist/lib/auth/login-runner.js +3 -0
  22. package/dist/lib/auth/login-runner.js.map +1 -1
  23. package/dist/lib/auth/scopes.d.ts +5 -0
  24. package/dist/lib/auth/scopes.d.ts.map +1 -0
  25. package/dist/lib/auth/scopes.js +23 -0
  26. package/dist/lib/auth/scopes.js.map +1 -0
  27. package/dist/lib/auth/server.d.ts +6 -1
  28. package/dist/lib/auth/server.d.ts.map +1 -1
  29. package/dist/lib/auth/server.js +95 -51
  30. package/dist/lib/auth/server.js.map +1 -1
  31. package/dist/lib/codex-usage.d.ts +91 -0
  32. package/dist/lib/codex-usage.d.ts.map +1 -0
  33. package/dist/lib/codex-usage.js +368 -0
  34. package/dist/lib/codex-usage.js.map +1 -0
  35. package/dist/lib/oauth-constants.d.ts +5 -5
  36. package/dist/lib/oauth-constants.d.ts.map +1 -1
  37. package/dist/lib/oauth-constants.js +5 -5
  38. package/dist/lib/oauth-constants.js.map +1 -1
  39. package/dist/lib/prompts/codex.d.ts +2 -0
  40. package/dist/lib/prompts/codex.d.ts.map +1 -1
  41. package/dist/lib/prompts/codex.js +43 -11
  42. package/dist/lib/prompts/codex.js.map +1 -1
  43. package/dist/lib/request/fetch-helpers.d.ts +2 -2
  44. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  45. package/dist/lib/request/fetch-helpers.js +37 -25
  46. package/dist/lib/request/fetch-helpers.js.map +1 -1
  47. package/dist/lib/request/helpers/model-map.d.ts +1 -0
  48. package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
  49. package/dist/lib/request/helpers/model-map.js +28 -25
  50. package/dist/lib/request/helpers/model-map.js.map +1 -1
  51. package/dist/lib/request/request-transformer.d.ts +1 -0
  52. package/dist/lib/request/request-transformer.d.ts.map +1 -1
  53. package/dist/lib/request/request-transformer.js +46 -22
  54. package/dist/lib/request/request-transformer.js.map +1 -1
  55. package/dist/lib/schemas.d.ts +8 -0
  56. package/dist/lib/schemas.d.ts.map +1 -1
  57. package/dist/lib/schemas.js +3 -0
  58. package/dist/lib/schemas.js.map +1 -1
  59. package/dist/lib/storage/migrations.d.ts +4 -0
  60. package/dist/lib/storage/migrations.d.ts.map +1 -1
  61. package/dist/lib/storage/migrations.js +1 -0
  62. package/dist/lib/storage/migrations.js.map +1 -1
  63. package/dist/lib/tools/codex-limits.d.ts.map +1 -1
  64. package/dist/lib/tools/codex-limits.js +39 -340
  65. package/dist/lib/tools/codex-limits.js.map +1 -1
  66. package/dist/lib/tools/codex-switch.d.ts.map +1 -1
  67. package/dist/lib/tools/codex-switch.js +9 -0
  68. package/dist/lib/tools/codex-switch.js.map +1 -1
  69. package/dist/lib/tui-quota-cache.d.ts +33 -0
  70. package/dist/lib/tui-quota-cache.d.ts.map +1 -0
  71. package/dist/lib/tui-quota-cache.js +247 -0
  72. package/dist/lib/tui-quota-cache.js.map +1 -0
  73. package/dist/lib/tui-status.d.ts +55 -0
  74. package/dist/lib/tui-status.d.ts.map +1 -0
  75. package/dist/lib/tui-status.js +343 -0
  76. package/dist/lib/tui-status.js.map +1 -0
  77. package/dist/lib/types.d.ts +3 -1
  78. package/dist/lib/types.d.ts.map +1 -1
  79. package/dist/tui.d.ts +6 -0
  80. package/dist/tui.d.ts.map +1 -0
  81. package/dist/tui.js +363 -0
  82. package/dist/tui.js.map +1 -0
  83. package/package.json +16 -2
  84. package/scripts/install-oc-codex-multi-auth-core.js +119 -18
package/dist/index.js CHANGED
@@ -22,6 +22,9 @@
22
22
  * @repository https://github.com/ndycode/oc-codex-multi-auth
23
23
 
24
24
  */
25
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
26
+ import { homedir } from "node:os";
27
+ import { join } from "node:path";
25
28
  import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, REDIRECT_URI, } from "./lib/auth/auth.js";
26
29
  import { buildDeviceCodeInstructions, completeDeviceCodeSession, createDeviceCodeSession, } from "./lib/auth/device-code.js";
27
30
  import { applyAccountSelectionFallbacks, persistResolvedAccountSelection, persistAccountPool, resolveAndPersistAccountSelection, resolveAccountSelection, } from "./lib/auth/login-runner.js";
@@ -38,7 +41,7 @@ import { AccountManager, extractAccountEmail, extractAccountId, formatAccountLab
38
41
  import { getStoragePath, loadAccounts, saveAccounts, clearAccounts, setStoragePath, loadFlaggedAccounts, saveFlaggedAccounts, withFlaggedAccountStorageTransaction, clearFlaggedAccounts, getWorkspaceIdentityKey, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
39
42
  import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, isDeactivatedWorkspaceError, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
40
43
  import { createDeactivatedWorkspaceError, DEACTIVATED_WORKSPACE_ERROR_CODE, isDeactivatedWorkspaceErrorMessage, } from "./lib/error-sentinels.js";
41
- import { applyFastSessionDefaults } from "./lib/request/request-transformer.js";
44
+ import { applyFastSessionDefaults, upsertBackendModelIdentityMessage, } from "./lib/request/request-transformer.js";
42
45
  import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
43
46
  import { isEmptyResponse } from "./lib/request/response-handler.js";
44
47
  import { getCircuitBreaker } from "./lib/circuit-breaker.js";
@@ -52,6 +55,8 @@ import { prewarmOpenCodeCodexPrompt } from "./lib/prompts/opencode-codex.js";
52
55
  import { createSessionRecoveryHook, isRecoverableError, detectErrorType, getRecoveryToastContent, } from "./lib/recovery.js";
53
56
  import { matchesWorkspaceIdentity, upsertFlaggedAccountRecord, createRetryBudgetUsage, serializeSelectionExplainability, formatRoutingValue, formatExplainabilitySummary, } from "./lib/runtime.js";
54
57
  import { createToolRegistry, } from "./lib/tools/index.js";
58
+ import { createUsageAccountFingerprint } from "./lib/codex-usage.js";
59
+ import { clearTuiQuotaSnapshot, parseTuiQuotaSnapshotFromHeaders, writeTuiQuotaSnapshot, } from "./lib/tui-quota-cache.js";
55
60
  /**
56
61
  * OpenAI Codex OAuth authentication plugin for opencode
57
62
  *
@@ -68,7 +73,6 @@ import { createToolRegistry, } from "./lib/tools/index.js";
68
73
  * }
69
74
  * ```
70
75
  */
71
- // eslint-disable-next-line @typescript-eslint/require-await
72
76
  export const OpenAIOAuthPlugin = async ({ client }) => {
73
77
  initLogger(client);
74
78
  let cachedAccountManager = null;
@@ -252,7 +256,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
252
256
  openBrowserUrl(url);
253
257
  if (!serverInfo || !serverInfo.ready) {
254
258
  serverInfo?.close();
255
- const message = `OAuth callback server failed to start on http://127.0.0.1:1455. ` +
259
+ const message = `OAuth callback server failed to start on localhost loopback port 1455. ` +
256
260
  `Retry with "${AUTH_LABELS.OAUTH_DEVICE_CODE}" or "${AUTH_LABELS.OAUTH_MANUAL}".`;
257
261
  logWarn(`\n[${PLUGIN_NAME}] ${message}\n`);
258
262
  return {
@@ -288,6 +292,31 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
288
292
  // Ignore when TUI is not available.
289
293
  }
290
294
  };
295
+ const clearPromptQuotaCache = async () => {
296
+ try {
297
+ await clearTuiQuotaSnapshot();
298
+ }
299
+ catch (error) {
300
+ logDebug(`[${PLUGIN_NAME}] Failed to clear TUI quota cache: ${error instanceof Error ? error.message : String(error)}`);
301
+ }
302
+ };
303
+ const recordPromptQuotaHeaders = async (response, account, accountCount) => {
304
+ try {
305
+ const snapshot = parseTuiQuotaSnapshotFromHeaders(response.headers, {
306
+ fingerprint: createUsageAccountFingerprint(account),
307
+ accountIndex: account.index + 1,
308
+ accountCount,
309
+ accountEmail: account.email?.trim() || undefined,
310
+ accountLabel: formatAccountLabel(account, account.index),
311
+ });
312
+ if (!snapshot)
313
+ return;
314
+ await writeTuiQuotaSnapshot(snapshot);
315
+ }
316
+ catch (error) {
317
+ logDebug(`[${PLUGIN_NAME}] Failed to record TUI quota headers: ${error instanceof Error ? error.message : String(error)}`);
318
+ }
319
+ };
291
320
  const resolveActiveIndex = (storage, family = "codex") => {
292
321
  const total = storage.accounts.length;
293
322
  if (total === 0)
@@ -296,6 +325,67 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
296
325
  const raw = Number.isFinite(rawCandidate) ? rawCandidate : 0;
297
326
  return Math.max(0, Math.min(raw, total - 1));
298
327
  };
328
+ const backfillHostOpenAIAuthFromPool = async () => {
329
+ const authPath = join(homedir(), ".local", "share", "opencode", "auth.json");
330
+ let authStore = {};
331
+ try {
332
+ const authRaw = await readFile(authPath, "utf8");
333
+ const parsed = JSON.parse(authRaw);
334
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
335
+ authStore = parsed;
336
+ }
337
+ }
338
+ catch (error) {
339
+ const errorCode = error?.code;
340
+ if (errorCode !== "ENOENT") {
341
+ logWarn(`[${PLUGIN_NAME}] Failed to read host auth store for OpenAI backfill: ${error?.message ?? String(error)}`);
342
+ return;
343
+ }
344
+ }
345
+ const existing = authStore[PROVIDER_ID];
346
+ const hasExistingOAuth = existing?.type === "oauth" &&
347
+ typeof existing.access === "string" &&
348
+ existing.access.trim().length > 0 &&
349
+ typeof existing.refresh === "string" &&
350
+ existing.refresh.trim().length > 0 &&
351
+ typeof existing.expires === "number" &&
352
+ Number.isFinite(existing.expires);
353
+ if (hasExistingOAuth) {
354
+ return;
355
+ }
356
+ const storage = await loadAccounts();
357
+ if (!storage || storage.accounts.length === 0) {
358
+ return;
359
+ }
360
+ const hasUsableTokens = (account) => typeof account?.accessToken === "string" &&
361
+ account.accessToken.trim().length > 0 &&
362
+ typeof account?.refreshToken === "string" &&
363
+ account.refreshToken.trim().length > 0 &&
364
+ typeof account?.expiresAt === "number" &&
365
+ Number.isFinite(account.expiresAt);
366
+ const activeIndex = resolveActiveIndex(storage, "codex");
367
+ const activeAccount = storage.accounts[activeIndex];
368
+ const candidate = hasUsableTokens(activeAccount)
369
+ ? activeAccount
370
+ : storage.accounts.find(hasUsableTokens);
371
+ if (!candidate || !hasUsableTokens(candidate)) {
372
+ return;
373
+ }
374
+ authStore[PROVIDER_ID] = {
375
+ type: "oauth",
376
+ access: candidate.accessToken,
377
+ refresh: candidate.refreshToken,
378
+ expires: candidate.expiresAt,
379
+ };
380
+ try {
381
+ await mkdir(join(homedir(), ".local", "share", "opencode"), { recursive: true });
382
+ await writeFile(authPath, `${JSON.stringify(authStore, null, 2)}\n`, "utf8");
383
+ logInfo(`[${PLUGIN_NAME}] Restored missing host OpenAI auth entry from stored account pool`);
384
+ }
385
+ catch (error) {
386
+ logWarn(`[${PLUGIN_NAME}] Failed to backfill host OpenAI auth entry: ${error?.message ?? String(error)}`);
387
+ }
388
+ };
299
389
  const hydrateEmails = async (storage) => {
300
390
  if (!storage)
301
391
  return storage;
@@ -339,6 +429,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
339
429
  account.expiresAt = refreshed.expires;
340
430
  changed = true;
341
431
  }
432
+ if (refreshed.scope && refreshed.scope !== account.oauthScope) {
433
+ account.oauthScope = refreshed.scope;
434
+ changed = true;
435
+ }
342
436
  if (refreshed.refresh && refreshed.refresh !== account.refreshToken) {
343
437
  account.refreshToken = refreshed.refresh;
344
438
  changed = true;
@@ -777,6 +871,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
777
871
  storage.activeIndexByFamily[family] = index;
778
872
  }
779
873
  await saveAccounts(storage);
874
+ await clearPromptQuotaCache();
780
875
  // Reload manager from disk so we don't overwrite newer rotated
781
876
  // refresh tokens with stale in-memory state.
782
877
  if (cachedAccountManager) {
@@ -844,6 +939,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
844
939
  invalidateAccountManagerCache,
845
940
  upsertFlaggedAccountRecord,
846
941
  };
942
+ const startupPluginConfig = loadPluginConfig();
943
+ const startupPerProjectAccounts = getPerProjectAccounts(startupPluginConfig);
944
+ setStoragePath(startupPerProjectAccounts ? process.cwd() : null);
945
+ await backfillHostOpenAIAuthFromPool();
847
946
  return {
848
947
  event: eventHandler,
849
948
  auth: {
@@ -900,9 +999,17 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
900
999
  if (needsPersist) {
901
1000
  await accountManager.saveToDisk();
902
1001
  }
903
- if (accountManager.getAccountCount() === 0) {
904
- logDebug(`[${PLUGIN_NAME}] No Codex accounts available (run opencode auth login)`);
905
- return {};
1002
+ const accountCount = accountManager.getAccountCount();
1003
+ const storagePath = getStoragePath();
1004
+ logDebug(`[${PLUGIN_NAME}] Loader auth bootstrap`, {
1005
+ authType: auth.type,
1006
+ authHasRefresh: !!authFallback?.refresh,
1007
+ authHasMultiAccount: !!authWithMulti?.multiAccount,
1008
+ accountCount,
1009
+ storagePath,
1010
+ });
1011
+ if (accountCount === 0) {
1012
+ logWarn(`[${PLUGIN_NAME}] No Codex accounts available (run opencode auth login)`);
906
1013
  }
907
1014
  // Extract user configuration (global + per-model options)
908
1015
  const providerConfig = provider;
@@ -1278,6 +1385,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1278
1385
  }
1279
1386
  account.email =
1280
1387
  extractAccountEmail(accountAuth.access) ?? account.email;
1388
+ // Keep the persisted active identity in step with the account used
1389
+ // for this request so the TUI quota cache accepts the next real
1390
+ // response-header snapshot instead of filtering it as stale.
1391
+ accountManager.saveToDiskDebounced();
1281
1392
  if (accountCount > 1 &&
1282
1393
  accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
1283
1394
  const accountLabel = formatAccountLabel(account, account.index);
@@ -1401,6 +1512,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1401
1512
  latencyMs: fetchLatencyMs,
1402
1513
  headers: Object.fromEntries(response.headers.entries()),
1403
1514
  });
1515
+ void recordPromptQuotaHeaders(response, account, accountCount);
1404
1516
  if (!response.ok) {
1405
1517
  const contextOverflowResult = await handleContextOverflow(response, model);
1406
1518
  if (contextOverflowResult.handled) {
@@ -1500,15 +1612,31 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1500
1612
  fallbackFrom = previousModel;
1501
1613
  fallbackTo = model;
1502
1614
  fallbackReason = "fallback-unsupported-model-entitlement";
1615
+ const fallbackInstructions = await getCodexInstructions(model);
1503
1616
  if (transformedBody && typeof transformedBody === "object") {
1504
- transformedBody = { ...transformedBody, model };
1617
+ transformedBody = {
1618
+ ...transformedBody,
1619
+ model,
1620
+ instructions: fallbackInstructions,
1621
+ input: upsertBackendModelIdentityMessage(transformedBody.input, model),
1622
+ };
1505
1623
  }
1506
1624
  else {
1507
- let fallbackBody = { model };
1625
+ let fallbackBody = {
1626
+ model,
1627
+ instructions: fallbackInstructions,
1628
+ };
1508
1629
  if (requestInit?.body && typeof requestInit.body === "string") {
1509
1630
  try {
1510
1631
  const parsed = JSON.parse(requestInit.body);
1511
- fallbackBody = { ...parsed, model };
1632
+ fallbackBody = {
1633
+ ...parsed,
1634
+ model,
1635
+ instructions: fallbackInstructions,
1636
+ };
1637
+ if (Array.isArray(fallbackBody.input)) {
1638
+ fallbackBody.input = upsertBackendModelIdentityMessage(fallbackBody.input, model);
1639
+ }
1512
1640
  }
1513
1641
  catch {
1514
1642
  // Keep minimal fallback body if parsing fails.
@@ -1715,14 +1843,28 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1715
1843
  continue;
1716
1844
  }
1717
1845
  const waitLabel = waitMs > 0 ? formatWaitTime(waitMs) : "a bit";
1846
+ const wasEntitlementExhaustion = runtimeMetrics.lastErrorCategory === "unsupported-model";
1847
+ const entitlementModel = typeof runtimeMetrics.lastError === "string"
1848
+ ? runtimeMetrics.lastError.replace(/^Unsupported model.*?:\s*/i, "").trim()
1849
+ : "";
1850
+ const entitlementDetail = entitlementModel.length > 0
1851
+ ? ` The backend rejected '${entitlementModel}' as not entitled for Codex OAuth on every pooled account.`
1852
+ : "";
1718
1853
  const message = count === 0
1719
1854
  ? "No Codex accounts configured. Run `opencode auth login`."
1720
1855
  : waitMs > 0
1721
1856
  ? `All ${count} account(s) are rate-limited. Try again in ${waitLabel} or add another account with \`opencode auth login\`.`
1722
- : `All ${count} account(s) failed (server errors or auth issues). Check account health with \`codex-health\`.`;
1857
+ : wasEntitlementExhaustion
1858
+ ? `All ${count} account(s) returned 'model not supported' for the requested model.${entitlementDetail} If this is a GPT-5.5 request during the rollout period, set \`unsupportedCodexPolicy: "fallback"\` (or \`CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=fallback\`) to auto-fallback to gpt-5.4. See \`codex-health\` for per-account details.`
1859
+ : `All ${count} account(s) failed (server errors or auth issues). Check account health with \`codex-health\`.`;
1723
1860
  runtimeMetrics.failedRequests++;
1724
1861
  runtimeMetrics.lastError = message;
1725
- runtimeMetrics.lastErrorCategory = waitMs > 0 ? "rate-limit" : "account-failure";
1862
+ runtimeMetrics.lastErrorCategory =
1863
+ waitMs > 0
1864
+ ? "rate-limit"
1865
+ : wasEntitlementExhaustion
1866
+ ? "unsupported-model"
1867
+ : "account-failure";
1726
1868
  return new Response(JSON.stringify({ error: { message } }), {
1727
1869
  status: waitMs > 0 ? 429 : 503,
1728
1870
  headers: {
@@ -2127,6 +2269,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2127
2269
  account.expiresAt = refreshResult.expires;
2128
2270
  storageChanged = true;
2129
2271
  }
2272
+ if (refreshResult.scope && refreshResult.scope !== account.oauthScope) {
2273
+ account.oauthScope = refreshResult.scope;
2274
+ storageChanged = true;
2275
+ }
2130
2276
  const hydratedEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
2131
2277
  if (hydratedEmail && hydratedEmail !== account.email) {
2132
2278
  account.email = hydratedEmail;