opencode-openai-multi-auth 5.0.0 → 5.0.2

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 (43) hide show
  1. package/README.md +153 -81
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +190 -8
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/accounts/manager.d.ts +18 -0
  7. package/dist/lib/accounts/manager.d.ts.map +1 -1
  8. package/dist/lib/accounts/manager.js +149 -16
  9. package/dist/lib/accounts/manager.js.map +1 -1
  10. package/dist/lib/accounts/types.d.ts +1 -0
  11. package/dist/lib/accounts/types.d.ts.map +1 -1
  12. package/dist/lib/accounts/types.js.map +1 -1
  13. package/dist/lib/auth/auth.d.ts.map +1 -1
  14. package/dist/lib/auth/auth.js +8 -0
  15. package/dist/lib/auth/auth.js.map +1 -1
  16. package/dist/lib/codex-status.d.ts +41 -0
  17. package/dist/lib/codex-status.d.ts.map +1 -0
  18. package/dist/lib/codex-status.js +349 -0
  19. package/dist/lib/codex-status.js.map +1 -0
  20. package/dist/lib/constants.d.ts +8 -0
  21. package/dist/lib/constants.d.ts.map +1 -1
  22. package/dist/lib/constants.js +13 -0
  23. package/dist/lib/constants.js.map +1 -1
  24. package/dist/lib/models.d.ts +42 -0
  25. package/dist/lib/models.d.ts.map +1 -0
  26. package/dist/lib/models.js +85 -0
  27. package/dist/lib/models.js.map +1 -0
  28. package/dist/lib/prompts/codex.d.ts.map +1 -1
  29. package/dist/lib/prompts/codex.js +10 -0
  30. package/dist/lib/prompts/codex.js.map +1 -1
  31. package/dist/lib/request/fetch-helpers.d.ts +2 -1
  32. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  33. package/dist/lib/request/fetch-helpers.js +32 -3
  34. package/dist/lib/request/fetch-helpers.js.map +1 -1
  35. package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
  36. package/dist/lib/request/helpers/model-map.js +43 -11
  37. package/dist/lib/request/helpers/model-map.js.map +1 -1
  38. package/dist/lib/request/request-transformer.d.ts.map +1 -1
  39. package/dist/lib/request/request-transformer.js +32 -7
  40. package/dist/lib/request/request-transformer.js.map +1 -1
  41. package/dist/lib/types.d.ts +1 -0
  42. package/dist/lib/types.d.ts.map +1 -1
  43. package/package.json +1 -1
package/README.md CHANGED
@@ -1,70 +1,155 @@
1
1
  ![Image 1: opencode-openai-multi-auth](assets/readme-hero.svg)
2
2
 
3
- **Maintained by [ZenysTX](https://x.com/zenysTX)**
4
- **Most of the work and original implementation by [Numman Ali](https://x.com/nummanali)**
5
- **Inspired by [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth)**
6
- [![Twitter Follow](https://img.shields.io/twitter/follow/zenysTX?style=social)](https://x.com/zenysTX)
3
+ [![npm version](https://img.shields.io/npm/v/opencode-openai-multi-auth.svg)](https://www.npmjs.com/package/opencode-openai-multi-auth)
4
+ [![Tests](https://github.com/dkraemerwork/opencode-openai-multi-auth/actions/workflows/ci.yml/badge.svg)](https://github.com/dkraemerwork/opencode-openai-multi-auth/actions)
5
+ [![npm downloads](https://img.shields.io/npm/dm/opencode-openai-multi-auth.svg)](https://www.npmjs.com/package/opencode-openai-multi-auth)
7
6
 
7
+ # Multi-Account ChatGPT OAuth for OpenCode
8
8
 
9
+ **Use multiple ChatGPT Plus/Pro personal or organization accounts with OpenCode. Never hit rate limits again.**
9
10
 
11
+ ```
12
+ ┌────────────────────────────────────────────────────────────────┐
13
+ │ │
14
+ │ Account 1 (rate limited) ──┐ │
15
+ │ Account 2 (rate limited) ──┼──► Auto-rotate ──► Keep coding │
16
+ │ Account 3 (available) ─────┘ │
17
+ │ │
18
+ └────────────────────────────────────────────────────────────────┘
19
+ ```
10
20
 
21
+ ## Why Multi-Account?
11
22
 
12
- [![Twitter Follow](https://img.shields.io/twitter/follow/nummanali?style=social)](https://x.com/nummanali)
13
- [![npm version](https://img.shields.io/npm/v/opencode-openai-multi-auth.svg)](https://www.npmjs.com/package/opencode-openai-multi-auth)
14
- [![Tests](https://github.com/dkraemerwork/opencode-openai-multi-auth/actions/workflows/ci.yml/badge.svg)](https://github.com/dkraemerwork/opencode-openai-multi-auth/actions)
15
- [![npm downloads](https://img.shields.io/npm/dm/opencode-openai-multi-auth.svg)](https://www.npmjs.com/package/opencode-openai-multi-auth)
16
- **One install. Every Codex model. Multi-account ready.**
17
- [Install](#-quick-start) · [Models](#-models) · [Configuration](#-configuration) · [Docs](#-docs)
23
+ | Problem | Solution |
24
+ |---------|----------|
25
+ | Hit ChatGPT rate limits while coding | Add multiple accounts, auto-rotate when limited |
26
+ | Team members share one subscription | Each person adds their own account |
27
+ | Different orgs have separate subscriptions | Use accounts from multiple organizations |
28
+ | One account gets throttled | Seamlessly switch to next available account |
18
29
 
19
30
  ---
20
31
 
21
- ## 💡 Philosophy
32
+ ## Quick Start
22
33
 
23
- > **"One config. Every model."**
24
- > OpenCode should feel effortless. This plugin keeps the setup minimal while giving you full GPT‑5.x + Codex access via ChatGPT OAuth across multiple accounts
25
- > from different organizations.
34
+ ```bash
35
+ # Install
36
+ npx -y opencode-openai-multi-auth@latest
26
37
 
27
- ```
28
- ┌─────────────────────────────────────────────────────────┐
29
- │ │
30
- │ ChatGPT OAuth → Codex backend → OpenCode │
31
- │ One command install, full model presets, done. │
32
- │ │
33
- └─────────────────────────────────────────────────────────┘
38
+ # Add your first account
39
+ opencode auth login
40
+ # Select "ChatGPT Plus/Pro (Codex Subscription)"
41
+
42
+ # Add more accounts (optional but recommended)
43
+ opencode auth login
44
+ # Select "Add Another OpenAI Account"
45
+
46
+ # Start coding - accounts rotate automatically on rate limits
47
+ opencode run "write hello world to test.txt" --model=openai/gpt-5.2 --variant=medium
34
48
  ```
35
49
 
36
50
  ---
37
51
 
38
- ## 🚀 Quick Start
52
+ ## How Multi-Account Works
53
+
54
+ ### Adding Accounts
39
55
 
40
56
  ```bash
41
- npx -y opencode-openai-multi-auth@latest
57
+ # First account
58
+ opencode auth login
59
+ # → Select "ChatGPT Plus/Pro (Codex Subscription)"
60
+ # → Browser opens, login with ChatGPT
61
+ # → Account saved
62
+
63
+ # Second account (different email/org)
64
+ opencode auth login
65
+ # → Select "Add Another OpenAI Account"
66
+ # → Login with different ChatGPT account
67
+ # → Account added to rotation pool
68
+
69
+ # Repeat for as many accounts as you have
42
70
  ```
43
71
 
44
- Then:
72
+ ### Automatic Rotation
73
+
74
+ When you hit a rate limit:
75
+
76
+ 1. Plugin detects 429 (rate limited) response
77
+ 2. Marks current account as limited for that model
78
+ 3. Switches to next available account
79
+ 4. Retries your request automatically
80
+ 5. Shows toast notification: `Switched to account2@example.com`
45
81
 
82
+ ### Account Selection Strategies
83
+
84
+ | Strategy | Behavior | Best For |
85
+ |----------|----------|----------|
86
+ | `sticky` (default) | Stay with one account until rate limited | Single user, predictable usage |
87
+ | `round-robin` | Rotate through accounts on each request | Distribute load evenly |
88
+ | `hybrid` | Sticky within session, rotate across sessions | Multiple terminal sessions |
89
+
90
+ Set via environment variable:
46
91
  ```bash
47
- opencode auth login
48
- opencode run "write hello world to test.txt" --model=openai/gpt-5.2 --variant=medium
92
+ OPENCODE_OPENAI_STRATEGY=round-robin opencode run "task"
49
93
  ```
50
94
 
51
- Legacy OpenCode (v1.0.209 and below):
95
+ ### Team Usage
96
+
97
+ Each team member can add their own ChatGPT account:
52
98
 
53
99
  ```bash
54
- npx -y opencode-openai-multi-auth@latest --legacy
55
- opencode run "write hello world to test.txt" --model=openai/gpt-5.2-medium
100
+ # Developer 1 adds their account
101
+ opencode auth login # logs in as dev1@company.com
102
+
103
+ # Developer 2 adds their account
104
+ opencode auth login # → "Add Another OpenAI Account" → dev2@company.com
105
+
106
+ # Developer 3 adds their account
107
+ opencode auth login # → "Add Another OpenAI Account" → dev3@company.com
108
+ ```
109
+
110
+ All accounts are pooled - when one person's account is rate limited, the plugin uses the next available.
111
+
112
+ ---
113
+
114
+ ## Environment Variables
115
+
116
+ | Variable | Description | Default |
117
+ |----------|-------------|---------|
118
+ | `OPENCODE_OPENAI_QUIET=1` | Disable toast notifications | Off |
119
+ | `OPENCODE_OPENAI_DEBUG=1` | Enable debug logging | Off |
120
+ | `OPENCODE_OPENAI_STRATEGY` | Account selection strategy | `sticky` |
121
+ | `OPENCODE_OPENAI_PID_OFFSET=1` | Offset account selection by PID | Off |
122
+
123
+ ---
124
+
125
+ ## Account Management
126
+
127
+ ### View Accounts
128
+ ```bash
129
+ cat ~/.config/opencode/openai-accounts.json | jq '.accounts[] | {email, planType}'
56
130
  ```
57
131
 
58
- Uninstall:
132
+ ### Remove All Accounts
133
+ ```bash
134
+ rm ~/.config/opencode/openai-accounts.json
135
+ ```
59
136
 
137
+ ### Check Rate Limit Status
60
138
  ```bash
61
- npx -y opencode-openai-multi-auth@latest --uninstall
62
- npx -y opencode-openai-multi-auth@latest --uninstall --all
139
+ cat ~/.config/opencode/openai-accounts.json | jq '.accounts[] | {email, rateLimitResets}'
140
+ ```
141
+
142
+ ### Slash Commands (TUI)
143
+ ```text
144
+ /codex-status
63
145
  ```
146
+ Shows usage status for all configured accounts.
64
147
 
65
148
  ---
66
149
 
67
- ## 📦 Models
150
+ ## Models
151
+
152
+ All GPT-5.2 and GPT-5.1 models with reasoning variants:
68
153
 
69
154
  - **gpt-5.2** (none/low/medium/high/xhigh)
70
155
  - **gpt-5.2-codex** (low/medium/high/xhigh)
@@ -73,73 +158,60 @@ npx -y opencode-openai-multi-auth@latest --uninstall --all
73
158
  - **gpt-5.1-codex-mini** (medium/high)
74
159
  - **gpt-5.1** (none/low/medium/high)
75
160
 
161
+ Note: The model selector reflects what the ChatGPT OAuth backend advertises. API-only models (like gpt-5-mini/nano) may not appear until the backend exposes them.
162
+
76
163
  ---
77
164
 
78
- ## 🧩 Configuration
165
+ ## Configuration
79
166
 
80
- - Modern (OpenCode v1.0.210+): `config/opencode-modern.json`
81
- - Legacy (OpenCode v1.0.209 and below): `config/opencode-legacy.json`
167
+ - **Modern** (OpenCode v1.0.210+): `config/opencode-modern.json`
168
+ - **Legacy** (v1.0.209 and below): `config/opencode-legacy.json`
82
169
 
83
- Minimal configs are not supported for GPT‑5.x; use the full configs above.
84
- ---
170
+ ```bash
171
+ # Modern install
172
+ npx -y opencode-openai-multi-auth@latest
85
173
 
86
- ## Features
174
+ # Legacy install
175
+ npx -y opencode-openai-multi-auth@latest --legacy
87
176
 
88
- - **Multi-account support** with automatic rotation on rate limits
89
- - ChatGPT Plus/Pro OAuth authentication (official flow)
90
- - 22 model presets across GPT‑5.2 / GPT‑5.2 Codex / GPT‑5.1 families
91
- - Variant system support (v1.0.210+) + legacy presets
92
- - Multimodal input enabled for all models
93
- - Toast notifications for account switches and rate limits
94
- - Usage‑aware errors + automatic token refresh
177
+ # Uninstall
178
+ npx -y opencode-openai-multi-auth@latest --uninstall
179
+ ```
95
180
 
96
181
  ---
97
182
 
98
- ## 🔄 Multi-Account Support
183
+ ## Features
99
184
 
100
- Add multiple ChatGPT accounts and automatically rotate between them when rate limited:
101
-
102
- ```bash
103
- # Add first account
104
- opencode auth login
105
- # Select "ChatGPT Plus/Pro (Codex Subscription)"
185
+ - **Multi-account rotation** - Add unlimited ChatGPT accounts, auto-rotate on rate limits
186
+ - **Per-model rate tracking** - Each model's limits tracked separately per account
187
+ - **Toast notifications** - Visual feedback when accounts switch
188
+ - **OAuth authentication** - Same secure flow as official Codex CLI
189
+ - **22 model presets** - All GPT-5.2/5.1 variants pre-configured
190
+ - **Automatic token refresh** - Never manually re-authenticate
191
+ - **Multimodal support** - Image input enabled for all models
106
192
 
107
- # Add additional accounts
108
- opencode auth login
109
- # Select "Add Another OpenAI Account"
110
- ```
111
-
112
- **Features:**
113
- - Automatic rotation when an account hits rate limits
114
- - Per-model rate limit tracking
115
- - Toast notifications showing active account
116
- - Seamless failover between accounts
117
- - Imports existing tokens from OpenCode auth
193
+ ---
118
194
 
119
- **Environment Variables:**
120
- | Variable | Description |
121
- |----------|-------------|
122
- | `OPENCODE_OPENAI_QUIET=1` | Disable toast notifications |
123
- | `OPENCODE_OPENAI_DEBUG=1` | Enable debug logging |
124
- | `OPENCODE_OPENAI_STRATEGY` | Account selection: `sticky` (default), `round-robin`, `hybrid` |
195
+ ## Documentation
125
196
 
126
- **Accounts storage:** `~/.config/opencode/openai-accounts.json`
197
+ - [Getting Started](docs/getting-started.md)
198
+ - [Configuration Guide](docs/configuration.md)
199
+ - [Troubleshooting](docs/troubleshooting.md)
200
+ - [Architecture](docs/development/ARCHITECTURE.md)
127
201
 
128
202
  ---
129
203
 
130
- ## 📚 Docs
204
+ ## Credits
131
205
 
132
- - Getting Started: `docs/getting-started.md`
133
- - Configuration: `docs/configuration.md`
134
- - Troubleshooting: `docs/troubleshooting.md`
135
- - Architecture: `docs/development/ARCHITECTURE.md`
206
+ **Maintained by [ZenysTX](https://x.com/zenysTX)**
207
+ **Original implementation by [Numman Ali](https://x.com/nummanali)**
208
+ **Inspired by [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth)**
209
+
210
+ [![Twitter Follow](https://img.shields.io/twitter/follow/zenysTX?style=social)](https://x.com/zenysTX)
211
+ [![Twitter Follow](https://img.shields.io/twitter/follow/nummanali?style=social)](https://x.com/nummanali)
136
212
 
137
213
  ---
138
214
 
139
- ## ⚠️ Usage Notice
215
+ ## Usage Notice
140
216
 
141
217
  This plugin is for **personal development use** with your own ChatGPT Plus/Pro subscriptions.
142
-
143
- **Built for developers who value simplicity.**
144
-
145
- ## Force Build 1
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Plugin } from "@opencode-ai/plugin";
1
+ import { type Plugin } from "@opencode-ai/plugin";
2
2
  export declare const OpenAIAuthPlugin: Plugin;
3
3
  export default OpenAIAuthPlugin;
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAiD/D,eAAO,MAAM,gBAAgB,EAAE,MAua9B,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,MAAM,EAAoB,MAAM,qBAAqB,CAAC;AAuD1E,eAAO,MAAM,gBAAgB,EAAE,MAonB9B,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
1
+ import { tool } from "@opencode-ai/plugin";
1
2
  import { createAuthorizationFlow, decodeJWT, exchangeAuthorizationCode, parseAuthorizationInput, REDIRECT_URI, } from "./lib/auth/auth.js";
2
3
  import { openBrowserUrl } from "./lib/auth/browser.js";
3
4
  import { startLocalOAuthServer } from "./lib/auth/server.js";
4
5
  import { getCodexMode, loadPluginConfig } from "./lib/config.js";
5
- import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, ERROR_MESSAGES, JWT_CLAIM_PATH, LOG_STAGES, PROVIDER_ID, HTTP_STATUS, } from "./lib/constants.js";
6
+ import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, ERROR_MESSAGES, JWT_CLAIM_PATH, LOG_STAGES, PROVIDER_ID, HTTP_STATUS, MODEL_FALLBACKS, } from "./lib/constants.js";
6
7
  import { logRequest, logDebug } from "./lib/logger.js";
7
8
  import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, rewriteUrlForCodex, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
8
9
  import { AccountManager } from "./lib/accounts/index.js";
10
+ import { codexStatus } from "./lib/codex-status.js";
11
+ import { prefetchModels } from "./lib/models.js";
9
12
  function extractModelFromBody(body) {
10
13
  if (!body)
11
14
  return undefined;
@@ -20,6 +23,8 @@ function extractModelFromBody(body) {
20
23
  let lastToastAccountIndex = null;
21
24
  let lastToastTime = 0;
22
25
  const TOAST_DEBOUNCE_MS = 5000;
26
+ /** Track which models we've already shown fallback notifications for */
27
+ const notifiedFallbacks = new Set();
23
28
  export const OpenAIAuthPlugin = async ({ client }) => {
24
29
  const quietMode = process.env.OPENCODE_OPENAI_QUIET === "1";
25
30
  const debugMode = process.env.OPENCODE_OPENAI_DEBUG === "1";
@@ -81,6 +86,40 @@ export const OpenAIAuthPlugin = async ({ client }) => {
81
86
  }
82
87
  catch { }
83
88
  };
89
+ const showModelRetryToast = async (model, failedAccount, nextAccount, triedCount, totalAccounts) => {
90
+ if (quietMode)
91
+ return;
92
+ const failedLabel = failedAccount.email || `Account ${failedAccount.index + 1}`;
93
+ const nextLabel = nextAccount.email || `Account ${nextAccount.index + 1}`;
94
+ const nextPlan = nextAccount.planType ? ` [${nextAccount.planType}]` : "";
95
+ try {
96
+ await client.tui.showToast({
97
+ body: {
98
+ message: `${model} not on ${failedLabel}, trying ${nextLabel}${nextPlan} (${triedCount}/${totalAccounts})`,
99
+ variant: "info",
100
+ },
101
+ });
102
+ }
103
+ catch { }
104
+ };
105
+ const showModelFallbackToast = async (originalModel, fallbackModel) => {
106
+ if (quietMode)
107
+ return;
108
+ // Only show once per model to avoid spam
109
+ const key = `${originalModel}->${fallbackModel}`;
110
+ if (notifiedFallbacks.has(key))
111
+ return;
112
+ notifiedFallbacks.add(key);
113
+ try {
114
+ await client.tui.showToast({
115
+ body: {
116
+ message: `${originalModel} not available yet. Using ${fallbackModel} instead.`,
117
+ variant: "warning",
118
+ },
119
+ });
120
+ }
121
+ catch { }
122
+ };
84
123
  const accountManager = new AccountManager({
85
124
  accountSelectionStrategy: process.env.OPENCODE_OPENAI_STRATEGY || "sticky",
86
125
  debug: process.env.OPENCODE_OPENAI_DEBUG === "1",
@@ -149,13 +188,15 @@ export const OpenAIAuthPlugin = async ({ client }) => {
149
188
  };
150
189
  const pluginConfig = loadPluginConfig();
151
190
  const codexMode = getCodexMode(pluginConfig);
152
- const executeRequest = async (account, input, init, retryCount = 0) => {
191
+ const executeRequest = async (account, input, init, retryCount = 0, triedAccountIndices = new Set()) => {
192
+ // Track this account as tried
193
+ triedAccountIndices.add(account.index);
153
194
  const isTokenValid = await accountManager.ensureValidToken(account);
154
195
  if (!isTokenValid) {
155
- const nextAccount = await accountManager.getNextAvailableAccount();
196
+ const nextAccount = await accountManager.getNextAvailableAccountExcluding(triedAccountIndices);
156
197
  if (nextAccount && nextAccount.index !== account.index) {
157
198
  await showAccountSwitchToast(account, nextAccount);
158
- return executeRequest(nextAccount, input, init, retryCount);
199
+ return executeRequest(nextAccount, input, init, retryCount, triedAccountIndices);
159
200
  }
160
201
  return new Response(JSON.stringify({ error: "All accounts failed token refresh" }), {
161
202
  status: HTTP_STATUS.UNAUTHORIZED,
@@ -183,6 +224,14 @@ export const OpenAIAuthPlugin = async ({ client }) => {
183
224
  headers: { "Content-Type": "application/json" },
184
225
  });
185
226
  }
227
+ // Pre-fetch models to "register" client with backend
228
+ // This may help unlock access to newer models like gpt-5.3-codex
229
+ try {
230
+ await prefetchModels(account.access || "", accountId);
231
+ }
232
+ catch {
233
+ // Ignore errors - this is a best-effort optimization
234
+ }
186
235
  const headers = createCodexHeaders(requestInit, accountId, account.access || "", {
187
236
  model: transformation?.body.model,
188
237
  promptCacheKey: transformation?.body?.prompt_cache_key,
@@ -191,6 +240,18 @@ export const OpenAIAuthPlugin = async ({ client }) => {
191
240
  ...requestInit,
192
241
  headers,
193
242
  });
243
+ try {
244
+ const headersObj = {};
245
+ response.headers.forEach((value, key) => {
246
+ headersObj[key] = value;
247
+ });
248
+ await codexStatus.updateFromHeaders(account, headersObj);
249
+ }
250
+ catch (error) {
251
+ if (debugMode) {
252
+ console.log("[openai-multi-auth] codex-status update failed", error);
253
+ }
254
+ }
194
255
  logRequest(LOG_STAGES.RESPONSE, {
195
256
  status: response.status,
196
257
  ok: response.ok,
@@ -235,19 +296,88 @@ export const OpenAIAuthPlugin = async ({ client }) => {
235
296
  catch { }
236
297
  }
237
298
  if (retryCount < accountManager.getAccountCount() - 1) {
238
- const nextAccount = await accountManager.getNextAvailableAccount(model);
299
+ const nextAccount = await accountManager.getNextAvailableAccountExcluding(triedAccountIndices, model);
239
300
  if (nextAccount && nextAccount.index !== account.index) {
240
301
  await showAccountSwitchToast(account, nextAccount);
241
- return executeRequest(nextAccount, input, init, retryCount + 1);
302
+ return executeRequest(nextAccount, input, init, retryCount + 1, triedAccountIndices);
242
303
  }
243
304
  }
244
305
  }
245
306
  if (response.status === HTTP_STATUS.UNAUTHORIZED && retryCount < 1) {
246
307
  accountManager.markRefreshFailed(account, "401 Unauthorized");
247
- const nextAccount = await accountManager.getNextAvailableAccount(model);
308
+ const nextAccount = await accountManager.getNextAvailableAccountExcluding(triedAccountIndices, model);
248
309
  if (nextAccount && nextAccount.index !== account.index) {
249
310
  await showAccountSwitchToast(account, nextAccount);
250
- return executeRequest(nextAccount, input, init, retryCount + 1);
311
+ return executeRequest(nextAccount, input, init, retryCount + 1, triedAccountIndices);
312
+ }
313
+ }
314
+ // Handle model not supported errors (400 Bad Request with specific message)
315
+ if (response.status === 400) {
316
+ try {
317
+ const cloned = response.clone();
318
+ const errorBody = await cloned.json();
319
+ const detail = errorBody?.detail || errorBody?.error?.message || "";
320
+ // Always log 400 errors to file for debugging
321
+ const fs = await import("node:fs");
322
+ const path = await import("node:path");
323
+ const os = await import("node:os");
324
+ const logDir = path.join(os.homedir(), ".opencode", "logs", "codex-plugin");
325
+ fs.mkdirSync(logDir, { recursive: true });
326
+ fs.writeFileSync(path.join(logDir, "last-400-error.json"), JSON.stringify({
327
+ timestamp: new Date().toISOString(),
328
+ model: transformation?.body.model,
329
+ status: response.status,
330
+ errorBody,
331
+ detail,
332
+ accountIndex: account.index,
333
+ accountEmail: account.email,
334
+ accountPlanType: account.planType,
335
+ triedAccounts: Array.from(triedAccountIndices),
336
+ totalAccounts: accountManager.getAccountCount(),
337
+ }, null, 2));
338
+ // Log the error for debugging
339
+ if (debugMode) {
340
+ console.log(`[openai-multi-auth] 400 error for model ${transformation?.body.model} on account ${account.email || account.index} [${account.planType}]: ${JSON.stringify(errorBody)}`);
341
+ }
342
+ // Check if it's a "model not supported" error
343
+ if (detail.includes("model is not supported") || detail.includes("not supported when using Codex")) {
344
+ const requestedModel = transformation?.body.model || model;
345
+ // STEP 1: Try other accounts first (they might be Plus/Pro/Team and support the model)
346
+ const nextAccount = await accountManager.getNextAvailableAccountExcluding(triedAccountIndices, requestedModel);
347
+ if (nextAccount) {
348
+ if (debugMode) {
349
+ console.log(`[openai-multi-auth] Model ${requestedModel} not supported on ${account.email || account.index} [${account.planType}], trying ${nextAccount.email || nextAccount.index} [${nextAccount.planType}]`);
350
+ }
351
+ await showModelRetryToast(requestedModel, account, nextAccount, triedAccountIndices.size, accountManager.getAccountCount());
352
+ return executeRequest(nextAccount, input, init, retryCount, triedAccountIndices);
353
+ }
354
+ // STEP 2: All accounts tried - fall back to older model
355
+ const fallbackModel = MODEL_FALLBACKS[requestedModel];
356
+ if (fallbackModel) {
357
+ if (debugMode) {
358
+ console.log(`[openai-multi-auth] All ${triedAccountIndices.size} accounts tried for ${requestedModel}, falling back to ${fallbackModel}`);
359
+ }
360
+ await showModelFallbackToast(requestedModel, fallbackModel);
361
+ // Retry with fallback model using first available account (reset tried accounts for new model)
362
+ const modifiedBody = JSON.parse(init?.body || "{}");
363
+ modifiedBody.model = fallbackModel;
364
+ const modifiedInit = {
365
+ ...init,
366
+ body: JSON.stringify(modifiedBody),
367
+ };
368
+ // Get first available account for the fallback model
369
+ const fallbackAccount = await accountManager.getNextAvailableAccount(fallbackModel);
370
+ if (fallbackAccount) {
371
+ // Reset tried accounts for the new model
372
+ return executeRequest(fallbackAccount, input, modifiedInit, 0, new Set());
373
+ }
374
+ // If no account available, use current account
375
+ return executeRequest(account, input, modifiedInit, retryCount + 1, new Set());
376
+ }
377
+ }
378
+ }
379
+ catch {
380
+ // If parsing fails, continue with normal error handling
251
381
  }
252
382
  }
253
383
  if (!response.ok) {
@@ -315,6 +445,58 @@ export const OpenAIAuthPlugin = async ({ client }) => {
315
445
  },
316
446
  ],
317
447
  },
448
+ config: async (cfg) => {
449
+ cfg.command = cfg.command || {};
450
+ cfg.command["codex-status"] = {
451
+ template: "Run the codex-status tool and output the result EXACTLY as returned by the tool, without any additional text or commentary.",
452
+ description: "List all configured OpenAI accounts and their current usage status.",
453
+ };
454
+ cfg.experimental = cfg.experimental || {};
455
+ cfg.experimental.primary_tools = cfg.experimental.primary_tools || [];
456
+ if (!cfg.experimental.primary_tools.includes("codex-status")) {
457
+ cfg.experimental.primary_tools.push("codex-status");
458
+ }
459
+ },
460
+ tool: {
461
+ "codex-status": tool({
462
+ description: "List all configured OpenAI accounts and their current usage status.",
463
+ args: {},
464
+ async execute() {
465
+ const accounts = accountManager.getAllAccounts();
466
+ if (accounts.length === 0) {
467
+ return [
468
+ "OpenAI Codex Status",
469
+ "",
470
+ " Accounts: 0",
471
+ "",
472
+ "Add accounts:",
473
+ " opencode auth login",
474
+ ].join("\n");
475
+ }
476
+ const now = Date.now();
477
+ await Promise.all(accounts.map(async (acc) => {
478
+ if (acc.access && acc.expires && acc.expires > now) {
479
+ await codexStatus.fetchFromBackend(acc, acc.access);
480
+ }
481
+ }));
482
+ const active = accountManager.getActiveAccount();
483
+ const activeIndex = active?.index ?? 0;
484
+ const lines = ["OpenAI Codex Status", ""];
485
+ for (const account of accounts) {
486
+ const status = account.index === activeIndex ? "ACTIVE" : "READY";
487
+ const email = account.email || `Account ${account.index + 1}`;
488
+ const plan = account.planType || "Unknown";
489
+ lines.push(`${account.index + 1}. ${status} ${email} [${plan}]`);
490
+ const statusLines = await codexStatus.renderStatus(account);
491
+ for (const line of statusLines) {
492
+ lines.push(line);
493
+ }
494
+ lines.push("");
495
+ }
496
+ return lines.join("\n");
497
+ },
498
+ }),
499
+ },
318
500
  };
319
501
  };
320
502
  export default OpenAIAuthPlugin;