oc-chatgpt-multi-auth 5.4.5 → 5.4.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.
package/README.md CHANGED
@@ -32,6 +32,7 @@ What the installer does:
32
32
 
33
33
  - writes `~/.config/opencode/opencode.json`
34
34
  - backs up an existing config before changing it
35
+ - normalizes the plugin entry to `"oc-chatgpt-multi-auth"`
35
36
  - clears the cached plugin copy so OpenCode reinstalls the latest package
36
37
 
37
38
  ## Example Usage
@@ -120,7 +121,7 @@ Start here if the plugin does not load or authenticate correctly:
120
121
 
121
122
  Common first checks:
122
123
 
123
- - confirm `"plugin": ["oc-chatgpt-multi-auth@latest"]` is present in your OpenCode config
124
+ - confirm `"plugin": ["oc-chatgpt-multi-auth"]` is present in your OpenCode config
124
125
  - rerun `opencode auth login`
125
126
  - inspect `~/.opencode/logs/codex-plugin/` after running one request with `ENABLE_PLUGIN_REQUEST_LOGGING=1`
126
127
 
package/config/README.md CHANGED
@@ -6,8 +6,8 @@ This directory contains the official OpenCode config templates for the ChatGPT C
6
6
 
7
7
  | File | OpenCode version | Description |
8
8
  |------|------------------|-------------|
9
- | [`opencode-modern.json`](./opencode-modern.json) | **v1.0.210+** | Variant-based config: 6 base models with 21 total presets |
10
- | [`opencode-legacy.json`](./opencode-legacy.json) | **v1.0.209 and below** | Legacy explicit entries: 21 individual model definitions |
9
+ | [`opencode-modern.json`](./opencode-modern.json) | **v1.0.210+** | Variant-based config: 7 base models with 26 total presets |
10
+ | [`opencode-legacy.json`](./opencode-legacy.json) | **v1.0.209 and below** | Legacy explicit entries: 26 individual model definitions |
11
11
 
12
12
  ## Quick pick
13
13
 
@@ -34,11 +34,13 @@ opencode --version
34
34
  OpenCode v1.0.210+ added model `variants`, so one model entry can expose multiple reasoning levels. That keeps modern config much smaller while preserving the same effective presets.
35
35
 
36
36
  Both templates include:
37
- - GPT-5.4, GPT-5 Codex, GPT-5.1, GPT-5.1 Codex, GPT-5.1 Codex Max, GPT-5.1 Codex Mini
37
+ - GPT-5.4, GPT-5.4 Mini, GPT-5 Codex, GPT-5.1, GPT-5.1 Codex, GPT-5.1 Codex Max, GPT-5.1 Codex Mini
38
38
  - Reasoning variants per model family
39
39
  - `store: false` and `include: ["reasoning.encrypted_content"]`
40
40
  - Context metadata (`gpt-5.4*`: 1,000,000 context / 128,000 output; other shipped models: 272,000 / 128,000)
41
41
 
42
+ Use `opencode debug config` to verify that these template entries were merged into your effective config. `opencode models openai` currently shows OpenCode's built-in provider catalog and can omit config-defined entries such as `gpt-5.4-mini`.
43
+
42
44
  If your OpenCode runtime supports global compaction tuning, you can also set:
43
45
  - `model_context_window = 1000000`
44
46
  - `model_auto_compact_token_limit = 900000`
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AA+J/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MA6rL/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;AAGH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AA4M/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAsxL/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -34,8 +34,8 @@ import { initLogger, logRequest, logDebug, logInfo, logWarn, logError, setCorrel
34
34
  import { checkAndNotify } from "./lib/auto-update-checker.js";
35
35
  import { handleContextOverflow } from "./lib/context-overflow.js";
36
36
  import { AccountManager, getAccountIdCandidates, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, } from "./lib/accounts.js";
37
- import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, exportAccounts, importAccounts, previewImportAccounts, createTimestampedBackupPath, loadFlaggedAccounts, saveFlaggedAccounts, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
38
- import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
37
+ import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, exportAccounts, importAccounts, previewImportAccounts, createTimestampedBackupPath, loadFlaggedAccounts, saveFlaggedAccounts, withFlaggedAccountStorageTransaction, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
38
+ import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, isDeactivatedWorkspaceError, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
39
39
  import { applyFastSessionDefaults } from "./lib/request/request-transformer.js";
40
40
  import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
41
41
  import { isEmptyResponse } from "./lib/request/response-handler.js";
@@ -48,6 +48,31 @@ import { buildBeginnerChecklist, buildBeginnerDoctorFindings, recommendBeginnerN
48
48
  import { getModelFamily, getCodexInstructions, MODEL_FAMILIES, prewarmCodexInstructions, } from "./lib/prompts/codex.js";
49
49
  import { prewarmOpenCodeCodexPrompt } from "./lib/prompts/opencode-codex.js";
50
50
  import { createSessionRecoveryHook, isRecoverableError, detectErrorType, getRecoveryToastContent, } from "./lib/recovery.js";
51
+ function getWorkspaceIdentityKey(account) {
52
+ const organizationId = account.organizationId?.trim();
53
+ const accountId = account.accountId?.trim();
54
+ const refreshToken = account.refreshToken.trim();
55
+ if (organizationId) {
56
+ return accountId
57
+ ? `organizationId:${organizationId}|accountId:${accountId}`
58
+ : `organizationId:${organizationId}`;
59
+ }
60
+ if (accountId)
61
+ return `accountId:${accountId}`;
62
+ return `refreshToken:${refreshToken}`;
63
+ }
64
+ function matchesWorkspaceIdentity(account, identityKey) {
65
+ return getWorkspaceIdentityKey(account) === identityKey;
66
+ }
67
+ function upsertFlaggedAccountRecord(accounts, record) {
68
+ const identityKey = getWorkspaceIdentityKey(record);
69
+ const existingIndex = accounts.findIndex((flagged) => matchesWorkspaceIdentity(flagged, identityKey));
70
+ if (existingIndex >= 0) {
71
+ accounts[existingIndex] = record;
72
+ return;
73
+ }
74
+ accounts.push(record);
75
+ }
51
76
  /**
52
77
  * OpenAI Codex OAuth authentication plugin for opencode
53
78
  *
@@ -1737,6 +1762,46 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1737
1762
  requestCorrelationId,
1738
1763
  threadId: threadIdCandidate,
1739
1764
  });
1765
+ const workspaceDeactivated = isDeactivatedWorkspaceError(errorBody, response.status);
1766
+ if (workspaceDeactivated) {
1767
+ const accountLabel = formatAccountLabel(account, account.index);
1768
+ accountManager.refundToken(account, modelFamily, model);
1769
+ accountManager.recordFailure(account, modelFamily, model);
1770
+ account.lastSwitchReason = "rotation";
1771
+ runtimeMetrics.failedRequests++;
1772
+ runtimeMetrics.accountRotations++;
1773
+ runtimeMetrics.lastError = `Deactivated workspace on ${accountLabel}`;
1774
+ runtimeMetrics.lastErrorCategory = "workspace-deactivated";
1775
+ try {
1776
+ const flaggedRecord = {
1777
+ ...account,
1778
+ flaggedAt: Date.now(),
1779
+ flaggedReason: "workspace-deactivated",
1780
+ lastError: "deactivated_workspace",
1781
+ };
1782
+ await withFlaggedAccountStorageTransaction(async (current, persist) => {
1783
+ const nextStorage = {
1784
+ ...current,
1785
+ accounts: current.accounts.map((flagged) => ({ ...flagged })),
1786
+ };
1787
+ upsertFlaggedAccountRecord(nextStorage.accounts, flaggedRecord);
1788
+ await persist(nextStorage);
1789
+ });
1790
+ }
1791
+ catch (flagError) {
1792
+ logWarn(`Failed to persist deactivated workspace flag for ${accountLabel}: ${flagError instanceof Error ? flagError.message : String(flagError)}`);
1793
+ }
1794
+ if (accountManager.removeAccount(account)) {
1795
+ accountManager.saveToDiskDebounced();
1796
+ attempted.clear();
1797
+ accountCount = accountManager.getAccountCount();
1798
+ await showToast(`Workspace deactivated. Removed ${accountLabel} from rotation and switching accounts.`, "warning", { duration: toastDurationMs });
1799
+ break;
1800
+ }
1801
+ accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
1802
+ accountManager.saveToDiskDebounced();
1803
+ break;
1804
+ }
1740
1805
  const unsupportedModelInfo = getUnsupportedCodexModelInfo(errorBody);
1741
1806
  const hasRemainingAccounts = attempted.size < Math.max(1, accountCount);
1742
1807
  // Entitlements can differ by account/workspace, so try remaining
@@ -2204,12 +2269,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2204
2269
  const message = (typeof errorBody?.error?.message === "string"
2205
2270
  ? errorBody.error?.message
2206
2271
  : bodyText) || `HTTP ${response.status}`;
2272
+ if (isDeactivatedWorkspaceError(errorBody, response.status)) {
2273
+ throw new Error("deactivated_workspace");
2274
+ }
2207
2275
  throw new Error(message);
2208
2276
  }
2209
2277
  lastError = new Error("Codex response did not include quota headers");
2210
2278
  }
2211
2279
  catch (error) {
2212
2280
  lastError = error instanceof Error ? error : new Error(String(error));
2281
+ if (lastError.message === "deactivated_workspace") {
2282
+ throw lastError;
2283
+ }
2213
2284
  }
2214
2285
  }
2215
2286
  throw lastError ?? new Error("Failed to fetch quotas");
@@ -2229,9 +2300,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2229
2300
  console.log("\nNo accounts to check.\n");
2230
2301
  return;
2231
2302
  }
2232
- const flaggedStorage = await loadFlaggedAccounts();
2233
2303
  let storageChanged = false;
2234
2304
  let flaggedChanged = false;
2305
+ const flaggedUpdates = new Map();
2235
2306
  const removeFromActive = new Set();
2236
2307
  const total = workingStorage.accounts.length;
2237
2308
  let ok = 0;
@@ -2315,20 +2386,14 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2315
2386
  const message = refreshResult.message ?? refreshResult.reason ?? "refresh failed";
2316
2387
  console.log(`[${i + 1}/${total}] ${label}: ERROR (${message})`);
2317
2388
  if (deepProbe && isFlaggableFailure(refreshResult)) {
2318
- const existingIndex = flaggedStorage.accounts.findIndex((flagged) => flagged.refreshToken === account.refreshToken);
2319
2389
  const flaggedRecord = {
2320
2390
  ...account,
2321
2391
  flaggedAt: Date.now(),
2322
2392
  flaggedReason: "token-invalid",
2323
2393
  lastError: message,
2324
2394
  };
2325
- if (existingIndex >= 0) {
2326
- flaggedStorage.accounts[existingIndex] = flaggedRecord;
2327
- }
2328
- else {
2329
- flaggedStorage.accounts.push(flaggedRecord);
2330
- }
2331
- removeFromActive.add(account.refreshToken);
2395
+ flaggedUpdates.set(getWorkspaceIdentityKey(flaggedRecord), flaggedRecord);
2396
+ removeFromActive.add(getWorkspaceIdentityKey(account));
2332
2397
  flaggedChanged = true;
2333
2398
  }
2334
2399
  continue;
@@ -2391,6 +2456,17 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2391
2456
  catch (error) {
2392
2457
  errors += 1;
2393
2458
  const message = error instanceof Error ? error.message : String(error);
2459
+ if (message === "deactivated_workspace") {
2460
+ const flaggedRecord = {
2461
+ ...account,
2462
+ flaggedAt: Date.now(),
2463
+ flaggedReason: "workspace-deactivated",
2464
+ lastError: message,
2465
+ };
2466
+ flaggedUpdates.set(getWorkspaceIdentityKey(flaggedRecord), flaggedRecord);
2467
+ removeFromActive.add(getWorkspaceIdentityKey(account));
2468
+ flaggedChanged = true;
2469
+ }
2394
2470
  console.log(`[${i + 1}/${total}] ${label}: ERROR (${message.slice(0, 160)})`);
2395
2471
  }
2396
2472
  }
@@ -2401,7 +2477,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2401
2477
  }
2402
2478
  }
2403
2479
  if (removeFromActive.size > 0) {
2404
- workingStorage.accounts = workingStorage.accounts.filter((account) => !removeFromActive.has(account.refreshToken));
2480
+ workingStorage.accounts = workingStorage.accounts.filter((account) => !removeFromActive.has(getWorkspaceIdentityKey(account)));
2405
2481
  clampActiveIndices(workingStorage);
2406
2482
  storageChanged = true;
2407
2483
  }
@@ -2410,12 +2486,21 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2410
2486
  invalidateAccountManagerCache();
2411
2487
  }
2412
2488
  if (flaggedChanged) {
2413
- await saveFlaggedAccounts(flaggedStorage);
2489
+ await withFlaggedAccountStorageTransaction(async (current, persist) => {
2490
+ const nextStorage = {
2491
+ ...current,
2492
+ accounts: current.accounts.map((flagged) => ({ ...flagged })),
2493
+ };
2494
+ for (const flaggedRecord of flaggedUpdates.values()) {
2495
+ upsertFlaggedAccountRecord(nextStorage.accounts, flaggedRecord);
2496
+ }
2497
+ await persist(nextStorage);
2498
+ });
2414
2499
  }
2415
2500
  console.log("");
2416
2501
  console.log(`Results: ${ok} ok, ${errors} error, ${disabled} disabled`);
2417
2502
  if (removeFromActive.size > 0) {
2418
- console.log(`Moved ${removeFromActive.size} account(s) to flagged pool (invalid refresh token).`);
2503
+ console.log(`Moved ${removeFromActive.size} account(s) to flagged pool.`);
2419
2504
  }
2420
2505
  console.log("");
2421
2506
  };
@@ -2433,6 +2518,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2433
2518
  if (!flagged)
2434
2519
  continue;
2435
2520
  const label = flagged.email ?? flagged.accountLabel ?? `Flagged ${i + 1}`;
2521
+ if (flagged.flaggedReason === "workspace-deactivated") {
2522
+ console.log(`[${i + 1}/${flaggedStorage.accounts.length}] ${label}: STILL FLAGGED (workspace deactivated)`);
2523
+ remaining.push(flagged);
2524
+ continue;
2525
+ }
2436
2526
  try {
2437
2527
  const cached = await lookupCodexCliTokensByEmail(flagged.email);
2438
2528
  const now = Date.now();
@@ -2589,7 +2679,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2589
2679
  await saveAccounts(workingStorage);
2590
2680
  await saveFlaggedAccounts({
2591
2681
  version: 1,
2592
- accounts: flaggedStorage.accounts.filter((flagged) => flagged.refreshToken !== target.refreshToken),
2682
+ accounts: flaggedStorage.accounts.filter((flagged) => !matchesWorkspaceIdentity(flagged, getWorkspaceIdentityKey(target))),
2593
2683
  });
2594
2684
  invalidateAccountManagerCache();
2595
2685
  console.log(`\nDeleted ${target.email ?? `Account ${menuResult.deleteAccountIndex + 1}`}.\n`);