oc-codex-multi-auth 6.1.2 → 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 +21 -21
  2. package/config/README.md +32 -32
  3. package/config/opencode-legacy.json +5 -80
  4. package/config/opencode-modern.json +1 -34
  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 +43 -11
  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 +37 -25
  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 +28 -25
  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 +46 -22
  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
@@ -14,10 +14,10 @@ Use your ChatGPT Plus/Pro subscription inside OpenCode with OAuth login, GPT-5/C
14
14
 
15
15
  ## What This Project Does
16
16
 
17
- - Adds an OpenCode plugin that authenticates with ChatGPT Plus/Pro through official OAuth
18
- - Ships ready-to-use GPT-5.5 release templates, explicit `gpt-5.5-medium` / `gpt-5.5-high` selectors, `gpt-5-codex`, and related GPT-5 families
19
- - Routes requests through a stateless Codex-compatible request pipeline with automatic token refresh
20
- - Supports multi-account rotation, per-project account storage, and guided onboarding commands
17
+ - Adds an OpenCode plugin that authenticates with ChatGPT Plus/Pro through official OAuth
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
+ - Routes requests through a stateless Codex-compatible request pipeline with automatic token refresh
20
+ - Supports multi-account rotation, per-project account storage, and guided onboarding commands
21
21
 
22
22
  ## Quick Start
23
23
 
@@ -25,12 +25,12 @@ Use your ChatGPT Plus/Pro subscription inside OpenCode with OAuth login, GPT-5/C
25
25
  # 1. Install or refresh the plugin config
26
26
  npx -y oc-codex-multi-auth@latest
27
27
 
28
- # 2. Sign in with ChatGPT Plus/Pro
29
- opencode auth login
30
-
31
- # 3. Run a prompt in OpenCode
32
- opencode run "Explain this repository" --model=openai/gpt-5.5-medium
33
- ```
28
+ # 2. Sign in with ChatGPT Plus/Pro
29
+ opencode auth login
30
+
31
+ # 3. Run a prompt in OpenCode
32
+ opencode run "Explain this repository" --model=openai/gpt-5.5-medium
33
+ ```
34
34
 
35
35
  What the installer does:
36
36
 
@@ -39,20 +39,20 @@ What the installer does:
39
39
  - normalizes the plugin entry to `"oc-codex-multi-auth"`
40
40
  - clears the cached plugin copy so OpenCode reinstalls the latest package
41
41
 
42
- By default, the installer now writes a full catalog config that includes both:
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.
42
+ By default, the installer now writes a full catalog config that includes both:
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.
47
47
 
48
48
  ## Example Usage
49
49
 
50
50
  ```bash
51
- # General GPT-5 workflow
52
- opencode run "Summarize the failing test and suggest a fix" --model=openai/gpt-5.5-medium
53
-
54
- # Codex-focused workflow
55
- opencode run "Refactor the retry logic and update the tests" --model=openai/gpt-5-codex --variant=high
51
+ # General GPT-5 workflow
52
+ opencode run "Summarize the failing test and suggest a fix" --model=openai/gpt-5.5-medium
53
+
54
+ # Codex-focused workflow
55
+ opencode run "Refactor the retry logic and update the tests" --model=openai/gpt-5-codex --variant=high
56
56
  ```
57
57
 
58
58
  ## Usage Notice
@@ -86,7 +86,7 @@ OpenCode users often want the same GPT-5 and Codex model experience they use in
86
86
 
87
87
  ## Common Workflows
88
88
 
89
- - Personal coding sessions in OpenCode using `gpt-5.5-medium`, `gpt-5.5-high`, or `gpt-5-codex`
89
+ - Personal coding sessions in OpenCode using `gpt-5.5-medium`, `gpt-5.5-high`, or `gpt-5-codex`
90
90
  - Switching between personal and workspace-linked ChatGPT accounts
91
91
  - Keeping separate account pools per project or monorepo
92
92
  - Recovering from unsupported-model, auth, or rate-limit issues with guided commands
package/config/README.md CHANGED
@@ -35,13 +35,13 @@ opencode --version
35
35
 
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
- Both templates include:
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
- - Reasoning variants per model family
41
- - `store: false` and `include: ["reasoning.encrypted_content"]`
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
-
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.
38
+ Both templates include:
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
+ - Reasoning variants per model family
41
+ - `store: false` and `include: ["reasoning.encrypted_content"]`
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
+
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,18 @@ If your workspace is entitled, you can add Spark model IDs manually.
55
55
 
56
56
  ## Usage examples
57
57
 
58
- Recommended real-session selectors (tested on OpenCode `1.14.22`):
59
-
60
- ```bash
61
- opencode run "task" --model=openai/gpt-5.5-medium
62
- opencode run "task" --model=openai/gpt-5-codex --variant=high
63
- ```
64
-
65
- If your OpenCode release already exposes bare GPT-5.5 base entries, the compact modern template also supports:
66
-
67
- ```bash
68
- opencode run "task" --model=openai/gpt-5.5 --variant=medium
69
- ```
58
+ Recommended real-session selectors (tested on OpenCode `1.14.22`):
59
+
60
+ ```bash
61
+ opencode run "task" --model=openai/gpt-5.5-medium
62
+ opencode run "task" --model=openai/gpt-5-codex --variant=high
63
+ ```
64
+
65
+ If your OpenCode release already exposes bare GPT-5.5 base entries, the compact modern template also supports:
66
+
67
+ ```bash
68
+ opencode run "task" --model=openai/gpt-5.5 --variant=medium
69
+ ```
70
70
 
71
71
  ## Minimal config (advanced)
72
72
 
@@ -76,19 +76,19 @@ A barebones debug template is available at [`minimal-opencode.json`](./minimal-o
76
76
 
77
77
  Current defaults are strict entitlement handling:
78
78
  - `unsupportedCodexPolicy: "strict"` returns entitlement errors directly
79
- - set `unsupportedCodexPolicy: "fallback"` (or `CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=fallback`) to enable automatic fallback retries
80
- - `fallbackToGpt52OnUnsupportedGpt53: true` keeps the legacy `gpt-5.3-codex -> gpt-5.2-codex` edge inside fallback mode
81
- - `gpt-5.5 -> gpt-5.4` is included by default for current ChatGPT-account entitlement gaps
82
- - `gpt-5.5-pro -> gpt-5.5-20260423` is included by default in fallback mode
83
- - `gpt-5.4-pro -> gpt-5.4` remains available for older manual configs
84
- - `unsupportedCodexFallbackChain` lets you override fallback order per model
85
-
86
- Default fallback chain (when policy is `fallback`):
87
- - `gpt-5.5 -> gpt-5.4`
88
- - `gpt-5.5-pro -> gpt-5.5-20260423`
89
- - `gpt-5.4-pro -> gpt-5.4` (if you manually select `gpt-5.4-pro`)
90
- - `gpt-5.3-codex -> gpt-5-codex -> gpt-5.2-codex`
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)
79
+ - set `unsupportedCodexPolicy: "fallback"` (or `CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=fallback`) to enable automatic fallback retries
80
+ - `fallbackToGpt52OnUnsupportedGpt53: true` keeps the legacy `gpt-5.3-codex -> gpt-5.2-codex` edge inside fallback mode
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
84
+ - `unsupportedCodexFallbackChain` lets you override fallback order per model
85
+
86
+ Default fallback chain (when policy is `fallback`):
87
+ - `gpt-5.5 -> gpt-5.4`
88
+ - `gpt-5.5-pro -> gpt-5.5`
89
+ - `gpt-5.4-pro -> gpt-5.4` (if you manually select `gpt-5.4-pro`)
90
+ - `gpt-5.3-codex -> gpt-5-codex -> gpt-5.2-codex`
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)
92
92
  - `gpt-5.2-codex -> gpt-5-codex`
93
93
  - `gpt-5.1-codex -> gpt-5-codex`
94
94
 
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "models": {
18
18
  "gpt-5.5-none": {
19
- "name": "GPT 5.5 20260423 None (OAuth)",
19
+ "name": "GPT 5.5 None (OAuth)",
20
20
  "limit": {
21
21
  "context": 1050000,
22
22
  "output": 128000
@@ -41,7 +41,7 @@
41
41
  }
42
42
  },
43
43
  "gpt-5.5-low": {
44
- "name": "GPT 5.5 20260423 Low (OAuth)",
44
+ "name": "GPT 5.5 Low (OAuth)",
45
45
  "limit": {
46
46
  "context": 1050000,
47
47
  "output": 128000
@@ -66,7 +66,7 @@
66
66
  }
67
67
  },
68
68
  "gpt-5.5-medium": {
69
- "name": "GPT 5.5 20260423 Medium (OAuth)",
69
+ "name": "GPT 5.5 Medium (OAuth)",
70
70
  "limit": {
71
71
  "context": 1050000,
72
72
  "output": 128000
@@ -91,7 +91,7 @@
91
91
  }
92
92
  },
93
93
  "gpt-5.5-high": {
94
- "name": "GPT 5.5 20260423 High (OAuth)",
94
+ "name": "GPT 5.5 High (OAuth)",
95
95
  "limit": {
96
96
  "context": 1050000,
97
97
  "output": 128000
@@ -116,7 +116,7 @@
116
116
  }
117
117
  },
118
118
  "gpt-5.5-xhigh": {
119
- "name": "GPT 5.5 20260423 Extra High (OAuth)",
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.5-pro-medium": {
269
- "name": "GPT 5.5 Pro 20260423 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.5-pro-high": {
294
- "name": "GPT 5.5 Pro 20260423 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.5-pro-xhigh": {
319
- "name": "GPT 5.5 Pro 20260423 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": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "models": {
18
18
  "gpt-5.5": {
19
- "name": "GPT 5.5 20260423 (OAuth)",
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.5-pro": {
62
- "name": "GPT 5.5 Pro 20260423 (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;