oc-codex-multi-auth 6.1.1 → 6.1.3

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 (61) hide show
  1. package/README.md +8 -6
  2. package/config/README.md +12 -9
  3. package/config/opencode-legacy.json +10 -85
  4. package/config/opencode-modern.json +2 -35
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +124 -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/oauth-constants.d.ts +5 -5
  32. package/dist/lib/oauth-constants.d.ts.map +1 -1
  33. package/dist/lib/oauth-constants.js +5 -5
  34. package/dist/lib/oauth-constants.js.map +1 -1
  35. package/dist/lib/prompts/codex.d.ts +2 -0
  36. package/dist/lib/prompts/codex.d.ts.map +1 -1
  37. package/dist/lib/prompts/codex.js +46 -8
  38. package/dist/lib/prompts/codex.js.map +1 -1
  39. package/dist/lib/request/fetch-helpers.d.ts +2 -2
  40. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  41. package/dist/lib/request/fetch-helpers.js +55 -16
  42. package/dist/lib/request/fetch-helpers.js.map +1 -1
  43. package/dist/lib/request/helpers/model-map.d.ts +1 -0
  44. package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
  45. package/dist/lib/request/helpers/model-map.js +32 -3
  46. package/dist/lib/request/helpers/model-map.js.map +1 -1
  47. package/dist/lib/request/request-transformer.d.ts +1 -0
  48. package/dist/lib/request/request-transformer.d.ts.map +1 -1
  49. package/dist/lib/request/request-transformer.js +71 -29
  50. package/dist/lib/request/request-transformer.js.map +1 -1
  51. package/dist/lib/schemas.d.ts +8 -0
  52. package/dist/lib/schemas.d.ts.map +1 -1
  53. package/dist/lib/schemas.js +3 -0
  54. package/dist/lib/schemas.js.map +1 -1
  55. package/dist/lib/storage/migrations.d.ts +4 -0
  56. package/dist/lib/storage/migrations.d.ts.map +1 -1
  57. package/dist/lib/storage/migrations.js +1 -0
  58. package/dist/lib/storage/migrations.js.map +1 -1
  59. package/dist/lib/types.d.ts +3 -1
  60. package/dist/lib/types.d.ts.map +1 -1
  61. package/package.json +1 -1
package/README.md CHANGED
@@ -15,7 +15,7 @@ Use your ChatGPT Plus/Pro subscription inside OpenCode with OAuth login, GPT-5/C
15
15
  ## What This Project Does
16
16
 
17
17
  - Adds an OpenCode plugin that authenticates with ChatGPT Plus/Pro through official OAuth
18
- - Ships ready-to-use model templates for `gpt-5.4`, `gpt-5-codex`, and related GPT-5 families
18
+ - Ships ready-to-use GPT-5.5 templates, explicit `gpt-5.5-medium` / `gpt-5.5-high` selectors, `gpt-5-codex`, and related GPT-5 families
19
19
  - Routes requests through a stateless Codex-compatible request pipeline with automatic token refresh
20
20
  - Supports multi-account rotation, per-project account storage, and guided onboarding commands
21
21
 
@@ -29,7 +29,7 @@ npx -y oc-codex-multi-auth@latest
29
29
  opencode auth login
30
30
 
31
31
  # 3. Run a prompt in OpenCode
32
- opencode run "Explain this repository" --model=openai/gpt-5.4 --variant=medium
32
+ opencode run "Explain this repository" --model=openai/gpt-5.5-medium
33
33
  ```
34
34
 
35
35
  What the installer does:
@@ -40,14 +40,16 @@ What the installer does:
40
40
  - clears the cached plugin copy so OpenCode reinstalls the latest package
41
41
 
42
42
  By default, the installer now writes a full catalog config that includes both:
43
- - modern base model entries such as `gpt-5.4` for `--variant` workflows
44
- - explicit preset entries such as `gpt-5.4-high` so the shipped catalog is visible directly in model pickers
43
+ - modern base model entries such as `gpt-5.5` for `--variant` workflows
44
+ - explicit preset entries such as `gpt-5.5-medium` / `gpt-5.5-high` so the shipped catalog is visible directly in model pickers
45
+
46
+ Tested live on OpenCode `1.14.22`: explicit GPT-5.5 model IDs like `openai/gpt-5.5-medium` and `openai/gpt-5.5-high` work directly. Some OpenCode releases still reject bare `openai/gpt-5.5` on the CLI even when the base config entry exists in the effective config.
45
47
 
46
48
  ## Example Usage
47
49
 
48
50
  ```bash
49
51
  # General GPT-5 workflow
50
- opencode run "Summarize the failing test and suggest a fix" --model=openai/gpt-5.4 --variant=medium
52
+ opencode run "Summarize the failing test and suggest a fix" --model=openai/gpt-5.5-medium
51
53
 
52
54
  # Codex-focused workflow
53
55
  opencode run "Refactor the retry logic and update the tests" --model=openai/gpt-5-codex --variant=high
@@ -84,7 +86,7 @@ OpenCode users often want the same GPT-5 and Codex model experience they use in
84
86
 
85
87
  ## Common Workflows
86
88
 
87
- - Personal coding sessions in OpenCode using `gpt-5.4` or `gpt-5-codex`
89
+ - Personal coding sessions in OpenCode using `gpt-5.5-medium`, `gpt-5.5-high`, or `gpt-5-codex`
88
90
  - Switching between personal and workspace-linked ChatGPT accounts
89
91
  - Keeping separate account pools per project or monorepo
90
92
  - Recovering from unsupported-model, auth, or rate-limit issues with guided commands
package/config/README.md CHANGED
@@ -36,12 +36,12 @@ opencode --version
36
36
  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.
37
37
 
38
38
  Both templates include:
39
- - GPT-5.4, GPT-5.4 Pro, GPT-5.4 Mini, GPT-5.4 Nano, GPT-5 Codex, GPT-5.1, GPT-5.1 Codex, GPT-5.1 Codex Max, GPT-5.1 Codex Mini
39
+ - GPT-5.5, GPT-5.5 Pro, GPT-5.4 Mini, GPT-5.4 Nano, GPT-5 Codex, GPT-5.1, GPT-5.1 Codex, GPT-5.1 Codex Max, GPT-5.1 Codex Mini
40
40
  - Reasoning variants per model family
41
41
  - `store: false` and `include: ["reasoning.encrypted_content"]`
42
- - Context metadata (`gpt-5.4`/`gpt-5.4-pro`: 1,050,000; `gpt-5.4-mini`/`gpt-5.4-nano`/Codex models: 400,000; `gpt-5.1`: 272,000; all output: 128,000)
42
+ - Context metadata (`gpt-5.5`/`gpt-5.5-pro`: 1,050,000; `gpt-5.4-mini`/`gpt-5.4-nano`/Codex models: 400,000; `gpt-5.1`: 272,000; all output: 128,000)
43
43
 
44
- 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`.
44
+ Use `opencode debug config` to verify that these template entries were merged into your effective config. On tested OpenCode `1.14.22`, `opencode models openai` exposes explicit GPT-5.5 entries such as `gpt-5.5-medium` / `gpt-5.5-high`, while bare `gpt-5.5` may still be omitted or rejected by provider lookup even when it exists in the effective config.
45
45
 
46
46
  If your OpenCode runtime supports global compaction tuning, you can also set:
47
47
  - `model_context_window = 1050000`
@@ -55,18 +55,17 @@ If your workspace is entitled, you can add Spark model IDs manually.
55
55
 
56
56
  ## Usage examples
57
57
 
58
- Modern template (v1.0.210+):
58
+ Recommended real-session selectors (tested on OpenCode `1.14.22`):
59
59
 
60
60
  ```bash
61
- opencode run "task" --model=openai/gpt-5.4 --variant=medium
61
+ opencode run "task" --model=openai/gpt-5.5-medium
62
62
  opencode run "task" --model=openai/gpt-5-codex --variant=high
63
63
  ```
64
64
 
65
- Legacy template (v1.0.209 and below):
65
+ If your OpenCode release already exposes bare GPT-5.5 base entries, the compact modern template also supports:
66
66
 
67
67
  ```bash
68
- opencode run "task" --model=openai/gpt-5.4-medium
69
- opencode run "task" --model=openai/gpt-5-codex-high
68
+ opencode run "task" --model=openai/gpt-5.5 --variant=medium
70
69
  ```
71
70
 
72
71
  ## Minimal config (advanced)
@@ -79,10 +78,14 @@ Current defaults are strict entitlement handling:
79
78
  - `unsupportedCodexPolicy: "strict"` returns entitlement errors directly
80
79
  - set `unsupportedCodexPolicy: "fallback"` (or `CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=fallback`) to enable automatic fallback retries
81
80
  - `fallbackToGpt52OnUnsupportedGpt53: true` keeps the legacy `gpt-5.3-codex -> gpt-5.2-codex` edge inside fallback mode
82
- - `gpt-5.4-pro -> gpt-5.4` is included by default in fallback mode (relevant only if you add `gpt-5.4-pro` manually)
81
+ - `gpt-5.5 -> gpt-5.4` is included by default for accounts/workspaces that do not yet expose GPT-5.5
82
+ - `gpt-5.5-pro -> gpt-5.5` is included by default in fallback mode
83
+ - `gpt-5.4-pro -> gpt-5.4` remains available for older manual configs
83
84
  - `unsupportedCodexFallbackChain` lets you override fallback order per model
84
85
 
85
86
  Default fallback chain (when policy is `fallback`):
87
+ - `gpt-5.5 -> gpt-5.4`
88
+ - `gpt-5.5-pro -> gpt-5.5`
86
89
  - `gpt-5.4-pro -> gpt-5.4` (if you manually select `gpt-5.4-pro`)
87
90
  - `gpt-5.3-codex -> gpt-5-codex -> gpt-5.2-codex`
88
91
  - `gpt-5.3-codex-spark -> gpt-5-codex -> gpt-5.3-codex -> gpt-5.2-codex` (only relevant if Spark IDs are added manually)
@@ -15,8 +15,8 @@
15
15
  "store": false
16
16
  },
17
17
  "models": {
18
- "gpt-5.4-none": {
19
- "name": "GPT 5.4 None (OAuth)",
18
+ "gpt-5.5-none": {
19
+ "name": "GPT 5.5 None (OAuth)",
20
20
  "limit": {
21
21
  "context": 1050000,
22
22
  "output": 128000
@@ -40,8 +40,8 @@
40
40
  "store": false
41
41
  }
42
42
  },
43
- "gpt-5.4-low": {
44
- "name": "GPT 5.4 Low (OAuth)",
43
+ "gpt-5.5-low": {
44
+ "name": "GPT 5.5 Low (OAuth)",
45
45
  "limit": {
46
46
  "context": 1050000,
47
47
  "output": 128000
@@ -65,8 +65,8 @@
65
65
  "store": false
66
66
  }
67
67
  },
68
- "gpt-5.4-medium": {
69
- "name": "GPT 5.4 Medium (OAuth)",
68
+ "gpt-5.5-medium": {
69
+ "name": "GPT 5.5 Medium (OAuth)",
70
70
  "limit": {
71
71
  "context": 1050000,
72
72
  "output": 128000
@@ -90,8 +90,8 @@
90
90
  "store": false
91
91
  }
92
92
  },
93
- "gpt-5.4-high": {
94
- "name": "GPT 5.4 High (OAuth)",
93
+ "gpt-5.5-high": {
94
+ "name": "GPT 5.5 High (OAuth)",
95
95
  "limit": {
96
96
  "context": 1050000,
97
97
  "output": 128000
@@ -115,8 +115,8 @@
115
115
  "store": false
116
116
  }
117
117
  },
118
- "gpt-5.4-xhigh": {
119
- "name": "GPT 5.4 Extra High (OAuth)",
118
+ "gpt-5.5-xhigh": {
119
+ "name": "GPT 5.5 Extra High (OAuth)",
120
120
  "limit": {
121
121
  "context": 1050000,
122
122
  "output": 128000
@@ -265,81 +265,6 @@
265
265
  "store": false
266
266
  }
267
267
  },
268
- "gpt-5.4-pro-medium": {
269
- "name": "GPT 5.4 Pro Medium (OAuth)",
270
- "limit": {
271
- "context": 1050000,
272
- "output": 128000
273
- },
274
- "modalities": {
275
- "input": [
276
- "text",
277
- "image"
278
- ],
279
- "output": [
280
- "text"
281
- ]
282
- },
283
- "options": {
284
- "reasoningEffort": "medium",
285
- "reasoningSummary": "detailed",
286
- "textVerbosity": "medium",
287
- "include": [
288
- "reasoning.encrypted_content"
289
- ],
290
- "store": false
291
- }
292
- },
293
- "gpt-5.4-pro-high": {
294
- "name": "GPT 5.4 Pro High (OAuth)",
295
- "limit": {
296
- "context": 1050000,
297
- "output": 128000
298
- },
299
- "modalities": {
300
- "input": [
301
- "text",
302
- "image"
303
- ],
304
- "output": [
305
- "text"
306
- ]
307
- },
308
- "options": {
309
- "reasoningEffort": "high",
310
- "reasoningSummary": "detailed",
311
- "textVerbosity": "medium",
312
- "include": [
313
- "reasoning.encrypted_content"
314
- ],
315
- "store": false
316
- }
317
- },
318
- "gpt-5.4-pro-xhigh": {
319
- "name": "GPT 5.4 Pro Extra High (OAuth)",
320
- "limit": {
321
- "context": 1050000,
322
- "output": 128000
323
- },
324
- "modalities": {
325
- "input": [
326
- "text",
327
- "image"
328
- ],
329
- "output": [
330
- "text"
331
- ]
332
- },
333
- "options": {
334
- "reasoningEffort": "xhigh",
335
- "reasoningSummary": "detailed",
336
- "textVerbosity": "medium",
337
- "include": [
338
- "reasoning.encrypted_content"
339
- ],
340
- "store": false
341
- }
342
- },
343
268
  "gpt-5.4-nano-none": {
344
269
  "name": "GPT 5.4 Nano None (OAuth)",
345
270
  "limit": {
@@ -15,8 +15,8 @@
15
15
  "store": false
16
16
  },
17
17
  "models": {
18
- "gpt-5.4": {
19
- "name": "GPT 5.4 (OAuth)",
18
+ "gpt-5.5": {
19
+ "name": "GPT 5.5 (OAuth)",
20
20
  "limit": {
21
21
  "context": 1050000,
22
22
  "output": 128000
@@ -58,39 +58,6 @@
58
58
  }
59
59
  }
60
60
  },
61
- "gpt-5.4-pro": {
62
- "name": "GPT 5.4 Pro (OAuth)",
63
- "limit": {
64
- "context": 1050000,
65
- "output": 128000
66
- },
67
- "modalities": {
68
- "input": [
69
- "text",
70
- "image"
71
- ],
72
- "output": [
73
- "text"
74
- ]
75
- },
76
- "variants": {
77
- "medium": {
78
- "reasoningEffort": "medium",
79
- "reasoningSummary": "detailed",
80
- "textVerbosity": "medium"
81
- },
82
- "high": {
83
- "reasoningEffort": "high",
84
- "reasoningSummary": "detailed",
85
- "textVerbosity": "medium"
86
- },
87
- "xhigh": {
88
- "reasoningEffort": "xhigh",
89
- "reasoningSummary": "detailed",
90
- "textVerbosity": "medium"
91
- }
92
- }
93
- },
94
61
  "gpt-5.4-mini": {
95
62
  "name": "GPT 5.4 Mini (OAuth)",
96
63
  "limit": {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAwL/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAmrG/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;AA2L/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MA60G/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
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";
@@ -68,7 +71,6 @@ import { createToolRegistry, } from "./lib/tools/index.js";
68
71
  * }
69
72
  * ```
70
73
  */
71
- // eslint-disable-next-line @typescript-eslint/require-await
72
74
  export const OpenAIOAuthPlugin = async ({ client }) => {
73
75
  initLogger(client);
74
76
  let cachedAccountManager = null;
@@ -252,7 +254,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
252
254
  openBrowserUrl(url);
253
255
  if (!serverInfo || !serverInfo.ready) {
254
256
  serverInfo?.close();
255
- const message = `OAuth callback server failed to start on http://127.0.0.1:1455. ` +
257
+ const message = `OAuth callback server failed to start on localhost loopback port 1455. ` +
256
258
  `Retry with "${AUTH_LABELS.OAUTH_DEVICE_CODE}" or "${AUTH_LABELS.OAUTH_MANUAL}".`;
257
259
  logWarn(`\n[${PLUGIN_NAME}] ${message}\n`);
258
260
  return {
@@ -296,6 +298,67 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
296
298
  const raw = Number.isFinite(rawCandidate) ? rawCandidate : 0;
297
299
  return Math.max(0, Math.min(raw, total - 1));
298
300
  };
301
+ const backfillHostOpenAIAuthFromPool = async () => {
302
+ const authPath = join(homedir(), ".local", "share", "opencode", "auth.json");
303
+ let authStore = {};
304
+ try {
305
+ const authRaw = await readFile(authPath, "utf8");
306
+ const parsed = JSON.parse(authRaw);
307
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
308
+ authStore = parsed;
309
+ }
310
+ }
311
+ catch (error) {
312
+ const errorCode = error?.code;
313
+ if (errorCode !== "ENOENT") {
314
+ logWarn(`[${PLUGIN_NAME}] Failed to read host auth store for OpenAI backfill: ${error?.message ?? String(error)}`);
315
+ return;
316
+ }
317
+ }
318
+ const existing = authStore[PROVIDER_ID];
319
+ const hasExistingOAuth = existing?.type === "oauth" &&
320
+ typeof existing.access === "string" &&
321
+ existing.access.trim().length > 0 &&
322
+ typeof existing.refresh === "string" &&
323
+ existing.refresh.trim().length > 0 &&
324
+ typeof existing.expires === "number" &&
325
+ Number.isFinite(existing.expires);
326
+ if (hasExistingOAuth) {
327
+ return;
328
+ }
329
+ const storage = await loadAccounts();
330
+ if (!storage || storage.accounts.length === 0) {
331
+ return;
332
+ }
333
+ const hasUsableTokens = (account) => typeof account?.accessToken === "string" &&
334
+ account.accessToken.trim().length > 0 &&
335
+ typeof account?.refreshToken === "string" &&
336
+ account.refreshToken.trim().length > 0 &&
337
+ typeof account?.expiresAt === "number" &&
338
+ Number.isFinite(account.expiresAt);
339
+ const activeIndex = resolveActiveIndex(storage, "codex");
340
+ const activeAccount = storage.accounts[activeIndex];
341
+ const candidate = hasUsableTokens(activeAccount)
342
+ ? activeAccount
343
+ : storage.accounts.find(hasUsableTokens);
344
+ if (!candidate || !hasUsableTokens(candidate)) {
345
+ return;
346
+ }
347
+ authStore[PROVIDER_ID] = {
348
+ type: "oauth",
349
+ access: candidate.accessToken,
350
+ refresh: candidate.refreshToken,
351
+ expires: candidate.expiresAt,
352
+ };
353
+ try {
354
+ await mkdir(join(homedir(), ".local", "share", "opencode"), { recursive: true });
355
+ await writeFile(authPath, `${JSON.stringify(authStore, null, 2)}\n`, "utf8");
356
+ logInfo(`[${PLUGIN_NAME}] Restored missing host OpenAI auth entry from stored account pool`);
357
+ }
358
+ catch (error) {
359
+ logWarn(`[${PLUGIN_NAME}] Failed to backfill host OpenAI auth entry: ${error?.message ?? String(error)}`);
360
+ }
361
+ };
299
362
  const hydrateEmails = async (storage) => {
300
363
  if (!storage)
301
364
  return storage;
@@ -339,6 +402,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
339
402
  account.expiresAt = refreshed.expires;
340
403
  changed = true;
341
404
  }
405
+ if (refreshed.scope && refreshed.scope !== account.oauthScope) {
406
+ account.oauthScope = refreshed.scope;
407
+ changed = true;
408
+ }
342
409
  if (refreshed.refresh && refreshed.refresh !== account.refreshToken) {
343
410
  account.refreshToken = refreshed.refresh;
344
411
  changed = true;
@@ -844,6 +911,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
844
911
  invalidateAccountManagerCache,
845
912
  upsertFlaggedAccountRecord,
846
913
  };
914
+ const startupPluginConfig = loadPluginConfig();
915
+ const startupPerProjectAccounts = getPerProjectAccounts(startupPluginConfig);
916
+ setStoragePath(startupPerProjectAccounts ? process.cwd() : null);
917
+ await backfillHostOpenAIAuthFromPool();
847
918
  return {
848
919
  event: eventHandler,
849
920
  auth: {
@@ -900,9 +971,17 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
900
971
  if (needsPersist) {
901
972
  await accountManager.saveToDisk();
902
973
  }
903
- if (accountManager.getAccountCount() === 0) {
904
- logDebug(`[${PLUGIN_NAME}] No Codex accounts available (run opencode auth login)`);
905
- return {};
974
+ const accountCount = accountManager.getAccountCount();
975
+ const storagePath = getStoragePath();
976
+ logDebug(`[${PLUGIN_NAME}] Loader auth bootstrap`, {
977
+ authType: auth.type,
978
+ authHasRefresh: !!authFallback?.refresh,
979
+ authHasMultiAccount: !!authWithMulti?.multiAccount,
980
+ accountCount,
981
+ storagePath,
982
+ });
983
+ if (accountCount === 0) {
984
+ logWarn(`[${PLUGIN_NAME}] No Codex accounts available (run opencode auth login)`);
906
985
  }
907
986
  // Extract user configuration (global + per-model options)
908
987
  const providerConfig = provider;
@@ -1500,15 +1579,31 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1500
1579
  fallbackFrom = previousModel;
1501
1580
  fallbackTo = model;
1502
1581
  fallbackReason = "fallback-unsupported-model-entitlement";
1582
+ const fallbackInstructions = await getCodexInstructions(model);
1503
1583
  if (transformedBody && typeof transformedBody === "object") {
1504
- transformedBody = { ...transformedBody, model };
1584
+ transformedBody = {
1585
+ ...transformedBody,
1586
+ model,
1587
+ instructions: fallbackInstructions,
1588
+ input: upsertBackendModelIdentityMessage(transformedBody.input, model),
1589
+ };
1505
1590
  }
1506
1591
  else {
1507
- let fallbackBody = { model };
1592
+ let fallbackBody = {
1593
+ model,
1594
+ instructions: fallbackInstructions,
1595
+ };
1508
1596
  if (requestInit?.body && typeof requestInit.body === "string") {
1509
1597
  try {
1510
1598
  const parsed = JSON.parse(requestInit.body);
1511
- fallbackBody = { ...parsed, model };
1599
+ fallbackBody = {
1600
+ ...parsed,
1601
+ model,
1602
+ instructions: fallbackInstructions,
1603
+ };
1604
+ if (Array.isArray(fallbackBody.input)) {
1605
+ fallbackBody.input = upsertBackendModelIdentityMessage(fallbackBody.input, model);
1606
+ }
1512
1607
  }
1513
1608
  catch {
1514
1609
  // Keep minimal fallback body if parsing fails.
@@ -1715,14 +1810,28 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1715
1810
  continue;
1716
1811
  }
1717
1812
  const waitLabel = waitMs > 0 ? formatWaitTime(waitMs) : "a bit";
1813
+ const wasEntitlementExhaustion = runtimeMetrics.lastErrorCategory === "unsupported-model";
1814
+ const entitlementModel = typeof runtimeMetrics.lastError === "string"
1815
+ ? runtimeMetrics.lastError.replace(/^Unsupported model.*?:\s*/i, "").trim()
1816
+ : "";
1817
+ const entitlementDetail = entitlementModel.length > 0
1818
+ ? ` The backend rejected '${entitlementModel}' as not entitled for Codex OAuth on every pooled account.`
1819
+ : "";
1718
1820
  const message = count === 0
1719
1821
  ? "No Codex accounts configured. Run `opencode auth login`."
1720
1822
  : waitMs > 0
1721
1823
  ? `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\`.`;
1824
+ : wasEntitlementExhaustion
1825
+ ? `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.`
1826
+ : `All ${count} account(s) failed (server errors or auth issues). Check account health with \`codex-health\`.`;
1723
1827
  runtimeMetrics.failedRequests++;
1724
1828
  runtimeMetrics.lastError = message;
1725
- runtimeMetrics.lastErrorCategory = waitMs > 0 ? "rate-limit" : "account-failure";
1829
+ runtimeMetrics.lastErrorCategory =
1830
+ waitMs > 0
1831
+ ? "rate-limit"
1832
+ : wasEntitlementExhaustion
1833
+ ? "unsupported-model"
1834
+ : "account-failure";
1726
1835
  return new Response(JSON.stringify({ error: { message } }), {
1727
1836
  status: waitMs > 0 ? 429 : 503,
1728
1837
  headers: {
@@ -2127,6 +2236,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2127
2236
  account.expiresAt = refreshResult.expires;
2128
2237
  storageChanged = true;
2129
2238
  }
2239
+ if (refreshResult.scope && refreshResult.scope !== account.oauthScope) {
2240
+ account.oauthScope = refreshResult.scope;
2241
+ storageChanged = true;
2242
+ }
2130
2243
  const hydratedEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
2131
2244
  if (hydratedEmail && hydratedEmail !== account.email) {
2132
2245
  account.email = hydratedEmail;