oc-codex-multi-auth 6.11.3 → 6.12.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.
Files changed (41) hide show
  1. package/README.md +9 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +75 -9
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/accounts/persistence.d.ts +21 -0
  6. package/dist/lib/accounts/persistence.d.ts.map +1 -1
  7. package/dist/lib/accounts/persistence.js +59 -1
  8. package/dist/lib/accounts/persistence.js.map +1 -1
  9. package/dist/lib/accounts/rotation.d.ts +39 -4
  10. package/dist/lib/accounts/rotation.d.ts.map +1 -1
  11. package/dist/lib/accounts/rotation.js +111 -17
  12. package/dist/lib/accounts/rotation.js.map +1 -1
  13. package/dist/lib/accounts.d.ts +9 -2
  14. package/dist/lib/accounts.d.ts.map +1 -1
  15. package/dist/lib/accounts.js +15 -6
  16. package/dist/lib/accounts.js.map +1 -1
  17. package/dist/lib/config.d.ts +6 -1
  18. package/dist/lib/config.d.ts.map +1 -1
  19. package/dist/lib/config.js +48 -3
  20. package/dist/lib/config.js.map +1 -1
  21. package/dist/lib/quota-windows.d.ts +59 -0
  22. package/dist/lib/quota-windows.d.ts.map +1 -0
  23. package/dist/lib/quota-windows.js +130 -0
  24. package/dist/lib/quota-windows.js.map +1 -0
  25. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  26. package/dist/lib/request/fetch-helpers.js +30 -5
  27. package/dist/lib/request/fetch-helpers.js.map +1 -1
  28. package/dist/lib/runtime.d.ts +2 -2
  29. package/dist/lib/runtime.d.ts.map +1 -1
  30. package/dist/lib/runtime.js.map +1 -1
  31. package/dist/lib/schemas.d.ts +4 -0
  32. package/dist/lib/schemas.d.ts.map +1 -1
  33. package/dist/lib/schemas.js +1 -0
  34. package/dist/lib/schemas.js.map +1 -1
  35. package/dist/lib/tools/codex-pool.d.ts.map +1 -1
  36. package/dist/lib/tools/codex-pool.js +35 -10
  37. package/dist/lib/tools/codex-pool.js.map +1 -1
  38. package/dist/lib/tui-quota-cache.d.ts.map +1 -1
  39. package/dist/lib/tui-quota-cache.js +5 -51
  40. package/dist/lib/tui-quota-cache.js.map +1 -1
  41. package/package.json +1 -1
package/README.md CHANGED
@@ -307,6 +307,10 @@ reordering accounts does not silently change a model's routing.
307
307
  "gpt-5.6-terra": [
308
308
  "org-another-account-id"
309
309
  ]
310
+ },
311
+ "modelAccountPoolModes": {
312
+ "gpt-5.6-sol": "strict",
313
+ "gpt-5.6-terra": "preferred"
310
314
  }
311
315
  }
312
316
  ```
@@ -323,6 +327,7 @@ codex-pool
323
327
  codex-pool action="set" model="gpt-5.6-sol" accounts=[7,8]
324
328
  codex-pool action="add" model="gpt-5.6-sol" accounts=[9]
325
329
  codex-pool action="remove" model="gpt-5.6-sol" accounts=[7]
330
+ codex-pool action="set-mode" model="gpt-5.6-sol" poolMode="strict"
326
331
  codex-pool action="clear" model="gpt-5.6-sol"
327
332
  ```
328
333
 
@@ -334,11 +339,12 @@ the current project is reported but never automatically deleted.
334
339
 
335
340
  Routing behavior:
336
341
 
337
- - A mapped model uses only healthy, selectable accounts in its preferred pool.
342
+ - A mapped model defaults to `preferred` mode and uses healthy, selectable accounts in its pool.
338
343
  - Existing rotation strategy, quota, cooldown, and token-health rules still apply within the preferred pool.
339
- - If every preferred account is unavailable, disabled, unknown, cooling down, or rate-limited, routing automatically falls back to the healthy general account pool.
344
+ - In `preferred` mode, an unavailable pool falls back to the healthy general account pool.
345
+ - In `strict` mode, routing never leaves the configured pool and immediately returns `strict_pool_unavailable` when no pooled account is selectable.
340
346
  - An unmapped model or an empty account list uses the general account pool directly.
341
- - `codex-status`, `codex-dashboard`, and routing diagnostics report the account-pool mode as `preferred`, `general`, or `general-fallback`.
347
+ - `codex-status`, `codex-dashboard`, and routing diagnostics also report `strict` and `strict-unavailable` modes.
342
348
 
343
349
  Account IDs are local account metadata but should still be treated as private
344
350
  configuration. Do not publish a populated configuration file.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAqN/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAkjH/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAwN/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MA8oH/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ import { queuedRefresh } from "./lib/refresh-queue.js";
32
32
  import { openBrowserUrl } from "./lib/auth/browser.js";
33
33
  import { startLocalOAuthServer } from "./lib/auth/server.js";
34
34
  import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
35
- import { getCodexMode, getRequestTransformMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRetryProfile, getRetryBudgetOverrides, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getAutoUpdate, getToastDurationMs, getAccountToastsEnabled, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getRotationStrategy, getModelAccountPool, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, getBeginnerSafeMode, getCodexTuiMaskEmail, loadPluginConfig, } from "./lib/config.js";
35
+ import { getCodexMode, getRequestTransformMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRetryProfile, getRetryBudgetOverrides, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getAutoUpdate, getToastDurationMs, getAccountToastsEnabled, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getRotationStrategy, getModelAccountPool, getModelAccountPoolMode, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, getBeginnerSafeMode, getCodexTuiMaskEmail, loadPluginConfig, } from "./lib/config.js";
36
36
  import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
37
37
  import { initLogger, logRequest, logDebug, logInfo, logWarn, logError, setCorrelationId, clearCorrelationId, } from "./lib/logger.js";
38
38
  import { checkAndNotify } from "./lib/auto-update-checker.js";
@@ -59,6 +59,7 @@ import { createSessionRecoveryHook, isRecoverableError, detectErrorType, getReco
59
59
  import { matchesWorkspaceIdentity, upsertFlaggedAccountRecord, createRetryBudgetUsage, serializeSelectionExplainability, formatRoutingValue, formatExplainabilitySummary, } from "./lib/runtime.js";
60
60
  import { createToolRegistry, } from "./lib/tools/index.js";
61
61
  import { createUsageAccountFingerprint, fetchCodexUsage, formatUsageLimitSummary, formatUsageLimitTitle, hasUsageWindow, parseCodexUsagePayload, } from "./lib/codex-usage.js";
62
+ import { getQuotaExhaustedResetAtMs } from "./lib/quota-windows.js";
62
63
  import { clearTuiQuotaSnapshot, parseTuiQuotaSnapshotFromHeaders, writeTuiQuotaSnapshot, } from "./lib/tui-quota-cache.js";
63
64
  /**
64
65
  * OpenAI Codex OAuth authentication plugin for opencode
@@ -190,7 +191,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
190
191
  lines.push(formatUiKeyValue(ui, "Fallback from", formatRoutingValue(routing.fallbackFrom), "muted"));
191
192
  lines.push(formatUiKeyValue(ui, "Fallback to", formatRoutingValue(routing.fallbackTo), "muted"));
192
193
  lines.push(formatUiKeyValue(ui, "Fallback reason", formatRoutingValue(routing.fallbackReason), routing.fallbackReason ? "warning" : "muted"));
193
- lines.push(formatUiKeyValue(ui, "Account pool", formatRoutingValue(routing.accountPoolMode), routing.accountPoolMode === "general-fallback" ? "warning" : "muted"));
194
+ lines.push(formatUiKeyValue(ui, "Account pool", formatRoutingValue(routing.accountPoolMode), routing.accountPoolMode === "general-fallback" ||
195
+ routing.accountPoolMode === "strict-unavailable"
196
+ ? "warning"
197
+ : "muted"));
194
198
  lines.push(formatUiKeyValue(ui, "Configured pool size", String(routing.configuredAccountPoolSize), "muted"));
195
199
  if (options.includeExplainability) {
196
200
  lines.push("");
@@ -326,6 +330,32 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
326
330
  logDebug(`[${PLUGIN_NAME}] Failed to record TUI quota headers: ${error instanceof Error ? error.message : String(error)}`);
327
331
  }
328
332
  };
333
+ /**
334
+ * Take an account out of rotation whenever the backend reports one of its
335
+ * quota windows as fully spent (issue #218).
336
+ *
337
+ * Every Codex response — success or 429 — carries `x-codex-*-used-percent`
338
+ * and the matching reset time, so the moment an account hits 0% left we
339
+ * can record it instead of rediscovering it with a failed request on every
340
+ * subsequent prompt. The block lands on the persisted `rateLimitResetTimes`
341
+ * map, so it is remembered across restarts and shared with other processes,
342
+ * and it clears itself once the window rolls over.
343
+ */
344
+ const applyQuotaExhaustion = (manager, headers, account, family, model) => {
345
+ try {
346
+ const resetAtMs = getQuotaExhaustedResetAtMs(headers);
347
+ if (resetAtMs === undefined)
348
+ return;
349
+ if (!manager.markQuotaExhausted(account, resetAtMs, family, model))
350
+ return;
351
+ account.lastSwitchReason = "rate-limit";
352
+ manager.saveToDiskDebounced();
353
+ logWarn(`Account ${account.index + 1} (${account.email ?? "unknown"}) has no ${family} quota left; skipping it for ${formatWaitTime(resetAtMs - Date.now())}.`);
354
+ }
355
+ catch (error) {
356
+ logDebug(`[${PLUGIN_NAME}] Failed to apply quota exhaustion: ${error instanceof Error ? error.message : String(error)}`);
357
+ }
358
+ };
329
359
  const resolveActiveIndex = (storage, family = "codex") => {
330
360
  const total = storage.accounts.length;
331
361
  if (total === 0)
@@ -1407,6 +1437,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1407
1437
  let restartAccountTraversalWithFallback = false;
1408
1438
  let restartAccountTraversalAfterWorkspaceDeactivation = false;
1409
1439
  const preferredAccountIds = getModelAccountPool(pluginConfig, model);
1440
+ const accountPoolMode = getModelAccountPoolMode(pluginConfig, model);
1441
+ const strictAccountPool = preferredAccountIds.length > 0 && accountPoolMode === "strict";
1410
1442
  while (attempted.size < Math.max(1, accountCount)) {
1411
1443
  const selectionExplainability = accountManager.getSelectionExplainability(modelFamily, model, Date.now());
1412
1444
  runtimeMetrics.lastSelectionSnapshot = {
@@ -1423,8 +1455,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1423
1455
  fallbackTo,
1424
1456
  fallbackReason,
1425
1457
  configuredAccountPoolSize: preferredAccountIds.length,
1458
+ accountPoolMode: strictAccountPool ? "strict" : undefined,
1426
1459
  };
1427
- const account = accountManager.getAccountForStrategy(rotationStrategy, modelFamily, model, { pidOffsetEnabled }, preferredAccountIds);
1460
+ const account = accountManager.getAccountForStrategy(rotationStrategy, modelFamily, model, { pidOffsetEnabled }, preferredAccountIds, accountPoolMode, attempted);
1428
1461
  if (!account || attempted.has(account.index)) {
1429
1462
  break;
1430
1463
  }
@@ -1442,12 +1475,14 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1442
1475
  fallbackFrom,
1443
1476
  fallbackTo,
1444
1477
  fallbackReason,
1445
- accountPoolMode: preferredAccountIds.length === 0
1446
- ? "general"
1447
- : account.accountId !== undefined &&
1448
- preferredAccountIds.includes(account.accountId)
1449
- ? "preferred"
1450
- : "general-fallback",
1478
+ accountPoolMode: strictAccountPool
1479
+ ? "strict"
1480
+ : preferredAccountIds.length === 0
1481
+ ? "general"
1482
+ : account.accountId !== undefined &&
1483
+ preferredAccountIds.includes(account.accountId)
1484
+ ? "preferred"
1485
+ : "general-fallback",
1451
1486
  configuredAccountPoolSize: preferredAccountIds.length,
1452
1487
  };
1453
1488
  }
@@ -1683,6 +1718,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1683
1718
  headers: Object.fromEntries(response.headers.entries()),
1684
1719
  });
1685
1720
  void recordPromptQuotaHeaders(response, account, accountCount);
1721
+ applyQuotaExhaustion(accountManager, response.headers, account, modelFamily, model);
1686
1722
  if (!response.ok) {
1687
1723
  const contextOverflowResult = await handleContextOverflow(response, model);
1688
1724
  if (contextOverflowResult.handled) {
@@ -2104,6 +2140,36 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2104
2140
  if (restartAccountTraversalAfterWorkspaceDeactivation) {
2105
2141
  continue;
2106
2142
  }
2143
+ if (strictAccountPool) {
2144
+ const poolWaitMs = accountManager.getMinWaitTimeForFamily(modelFamily, model, preferredAccountIds);
2145
+ const effectiveModel = model ?? requestedModel ?? "requested model";
2146
+ const waitDetail = poolWaitMs > 0
2147
+ ? ` Try again in ${formatWaitTime(poolWaitMs)}.`
2148
+ : " Check the pooled accounts with `codex-health`.";
2149
+ const message = `Strict account pool unavailable for ${effectiveModel}. ` +
2150
+ `None of its ${preferredAccountIds.length} configured account(s) is selectable.` +
2151
+ waitDetail;
2152
+ if (runtimeMetrics.lastSelectionSnapshot) {
2153
+ runtimeMetrics.lastSelectionSnapshot = {
2154
+ ...runtimeMetrics.lastSelectionSnapshot,
2155
+ accountPoolMode: "strict-unavailable",
2156
+ };
2157
+ }
2158
+ runtimeMetrics.failedRequests++;
2159
+ runtimeMetrics.lastError = message;
2160
+ runtimeMetrics.lastErrorCategory = "strict-pool-unavailable";
2161
+ return new Response(JSON.stringify({
2162
+ error: {
2163
+ code: "strict_pool_unavailable",
2164
+ message,
2165
+ },
2166
+ }), {
2167
+ status: poolWaitMs > 0 ? 429 : 503,
2168
+ headers: {
2169
+ "content-type": "application/json; charset=utf-8",
2170
+ },
2171
+ });
2172
+ }
2107
2173
  const waitMs = accountManager.getMinWaitTimeForFamily(modelFamily, model);
2108
2174
  const count = accountManager.getAccountCount();
2109
2175
  if (retryAllAccountsRateLimited &&