Skip to content

Commit aa3fd72

Browse files
committed
docs: document OpenCode plugin blocking issue (#11)
- Add Known Limitations section to troubleshooting docs explaining that OpenCode blocks this plugin by design (skips plugins with 'opencode-openai-codex-auth' in the name) - Add prominent warning to README about the issue - Include OPENCODE_PR_PROPOSAL.md with upstream fix proposal - Fix: remove debug console.log statements from loader - Fix: check multiAccount flag before handling auth (coexist with built-in) Closes #11
1 parent 9d0878c commit aa3fd72

13 files changed

Lines changed: 274 additions & 55 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ All notable changes to this project are documented here. Dates use the ISO forma
77
### Added
88

99
### Changed
10+
- Fix Node ESM plugin load by importing tool from `@opencode-ai/plugin/tool` and ensuring runtime dependency is installed.
11+
- Correct package metadata (repository links, update-check package name) and add troubleshooting guidance for plugin install/load.
1012

1113
## [4.7.0] - 2026-01-25
1214

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
[![npm version](https://img.shields.io/npm/v/opencode-openai-codex-auth-multi.svg)](https://www.npmjs.com/package/opencode-openai-codex-auth-multi)
44
[![npm downloads](https://img.shields.io/npm/dw/opencode-openai-codex-auth-multi.svg)](https://www.npmjs.com/package/opencode-openai-codex-auth-multi)
5-
[![Tests](https://github.com/ndycode/opencode-openai-codex-auth-multi/actions/workflows/ci.yml/badge.svg)](https://github.com/ndycode/opencode-openai-codex-auth-multi/actions)
5+
[![Tests](https://github.com/ndycode/opencode-chatgpt-multi-auth/actions/workflows/ci.yml/badge.svg)](https://github.com/ndycode/opencode-chatgpt-multi-auth/actions)
66
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
77

88
Enable OpenCode to authenticate against **OpenAI's Codex backend** via OAuth so you can use ChatGPT Plus/Pro rate limits and access models like `gpt-5.2`, `gpt-5.2-codex`, and `gpt-5.1-codex-max` with your ChatGPT credentials.
99

10+
> [!WARNING]
11+
> **Known Issue:** OpenCode currently blocks this plugin by design. The plugin downloads but never loads because OpenCode's plugin loader skips plugins containing `opencode-openai-codex-auth` in the name (to prevent conflicts with the built-in auth). We're working on an upstream fix. See [Issue #11](https://github.com/ndycode/opencode-chatgpt-multi-auth/issues/11) and [Troubleshooting](docs/troubleshooting.md#known-limitations) for details.
12+
1013
## What You Get
1114

1215
- **GPT-5.2, GPT-5.2 Codex, GPT-5.1 Codex Max** and all GPT-5.x variants via ChatGPT OAuth
@@ -48,7 +51,7 @@ Enable OpenCode to authenticate against **OpenAI's Codex backend** via OAuth so
4851
Paste this into any LLM agent (Claude Code, OpenCode, Cursor, etc.):
4952

5053
```
51-
Install the opencode-openai-codex-auth-multi plugin and add the OpenAI model definitions to ~/.config/opencode/opencode.json by following: https://raw.githubusercontent.com/ndycode/opencode-openai-codex-auth-multi/main/README.md
54+
Install the opencode-openai-codex-auth-multi plugin and add the OpenAI model definitions to ~/.config/opencode/opencode.json by following: https://raw.githubusercontent.com/ndycode/opencode-chatgpt-multi-auth/main/README.md
5255
```
5356

5457
**Option B: One-command install**

docs/OPENCODE_PR_PROPOSAL.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# OpenCode PR: Merge Auth Methods from Multiple Plugins
2+
3+
## Problem
4+
5+
When multiple plugins register for the same provider (e.g., `openai`), only the first plugin's auth methods are shown. This is because `auth.ts:310` uses `.find()` which returns the first match:
6+
7+
```typescript
8+
const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
9+
```
10+
11+
Since internal plugins (like `CodexAuthPlugin`) load before external plugins, external plugins can never add auth methods to providers that have internal plugins.
12+
13+
## Use Case
14+
15+
Multi-account authentication plugins need to add their auth methods alongside built-in options. For example:
16+
- Built-in: "ChatGPT Pro/Plus"
17+
- External: "ChatGPT Pro/Plus (Multi-Account)"
18+
19+
Users should see both options when selecting OpenAI provider.
20+
21+
## Proposed Solution
22+
23+
Change `auth.ts` to collect and merge auth methods from ALL plugins that register for the same provider:
24+
25+
### Current Code (auth.ts lines 310-314)
26+
27+
```typescript
28+
const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
29+
if (plugin && plugin.auth) {
30+
const handled = await handlePluginAuth({ auth: plugin.auth }, provider)
31+
if (handled) return
32+
}
33+
```
34+
35+
### Proposed Code
36+
37+
```typescript
38+
// Collect auth methods from ALL plugins that register for this provider
39+
const matchingPlugins = await Plugin.list().then((x) =>
40+
x.filter((x) => x.auth?.provider === provider)
41+
)
42+
43+
if (matchingPlugins.length > 0) {
44+
// Merge methods from all matching plugins
45+
const mergedMethods = matchingPlugins.flatMap((p) => p.auth?.methods ?? [])
46+
47+
// Use the first plugin's loader (internal plugins take precedence)
48+
const primaryPlugin = matchingPlugins[0]
49+
50+
const handled = await handlePluginAuth(
51+
{
52+
auth: {
53+
...primaryPlugin.auth!,
54+
methods: mergedMethods,
55+
}
56+
},
57+
provider
58+
)
59+
if (handled) return
60+
}
61+
```
62+
63+
### Also update lines 326-330 (custom provider handling)
64+
65+
```typescript
66+
// Same pattern for custom providers
67+
const customPlugins = await Plugin.list().then((x) =>
68+
x.filter((x) => x.auth?.provider === provider)
69+
)
70+
if (customPlugins.length > 0) {
71+
const mergedMethods = customPlugins.flatMap((p) => p.auth?.methods ?? [])
72+
const primaryPlugin = customPlugins[0]
73+
const handled = await handlePluginAuth(
74+
{ auth: { ...primaryPlugin.auth!, methods: mergedMethods } },
75+
provider
76+
)
77+
if (handled) return
78+
}
79+
```
80+
81+
## Benefits
82+
83+
1. **Backward Compatible**: Existing behavior unchanged for providers with single plugin
84+
2. **Extensible**: External plugins can add auth methods to any provider
85+
3. **Priority Preserved**: Internal plugins' loaders still take precedence
86+
4. **Minimal Change**: ~10 lines changed, no new dependencies
87+
88+
## Testing
89+
90+
1. Install an external plugin that registers for `openai` provider
91+
2. Run `opencode auth login` → select "OpenAI"
92+
3. Verify both internal and external auth methods appear in the list
93+
94+
## Alternative Considered
95+
96+
Have external plugins use different provider IDs (e.g., `openai-multi`), but this requires users to manually type the provider ID via "Other", which is poor UX.

docs/troubleshooting.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,73 @@ Common issues and debugging techniques for the OpenCode OpenAI Codex Auth Plugin
88
99
---
1010

11+
## Known Limitations
12+
13+
<details open>
14+
<summary><b>⚠️ OpenCode blocks this plugin by design</b></summary>
15+
16+
**Status:** This is a known issue affecting all users.
17+
18+
**What's happening:**
19+
20+
OpenCode's plugin loader explicitly skips plugins with `opencode-openai-codex-auth` in the name (see `packages/opencode/src/plugin/index.ts` line ~53):
21+
22+
```typescript
23+
if (plugin.includes("opencode-openai-codex-auth") || plugin.includes("opencode-copilot-auth")) continue
24+
```
25+
26+
This is intentional — OpenCode has built-in `CodexAuthPlugin` and `CopilotAuthPlugin` that handle OAuth for these providers. External plugins are blocked to prevent conflicts.
27+
28+
**Impact:**
29+
- The plugin downloads successfully
30+
- The plugin is present in `~/.cache/opencode/node_modules/`
31+
- But it never loads or executes
32+
- No logs appear in `~/.opencode/logs/codex-plugin/`
33+
34+
**Workarounds:**
35+
36+
1. **Wait for upstream fix** — We've submitted a [PR proposal](OPENCODE_PR_PROPOSAL.md) to allow multiple auth plugins per provider
37+
2. **Use a patched OpenCode fork** — For advanced users only (requires building from source)
38+
3. **Use the built-in single-account auth** — Run `opencode auth login` without this plugin
39+
40+
**Tracking:** [Issue #11](https://github.com/ndycode/opencode-chatgpt-multi-auth/issues/11)
41+
42+
</details>
43+
44+
---
45+
46+
## Installation & Loading Issues
47+
48+
<details open>
49+
<summary><b>Plugin not downloading / no logs</b></summary>
50+
51+
**Symptoms:**
52+
- Plugin folder missing under `~/.cache/opencode/node_modules/`
53+
- No files in `~/.opencode/logs/codex-plugin/` even with logging enabled
54+
55+
**Checks:**
56+
1. **Verify config path and plugin list**:
57+
- Global: `~/.config/opencode/opencode.json`
58+
- Project: `./.opencode.json`
59+
- Entry should include: `"plugin": ["opencode-openai-codex-auth-multi@latest"]`
60+
2. **Confirm plugin cache location** (npm plugins are cached, not stored in `~/.opencode/plugins/`):
61+
```bash
62+
ls ~/.cache/opencode/node_modules/opencode-openai-codex-auth-multi
63+
```
64+
3. **Remember: request logs only appear after the first OpenAI request**:
65+
```bash
66+
ENABLE_PLUGIN_REQUEST_LOGGING=1 opencode run "test" --model=openai/gpt-5.2
67+
```
68+
4. **Check registry access**:
69+
```bash
70+
npm view opencode-openai-codex-auth-multi version
71+
```
72+
5. **If the plugin is present but still won’t load**, upgrade to v4.8.1+ (fixes Node ESM load issues with `@opencode-ai/plugin`).
73+
74+
</details>
75+
76+
---
77+
1178
## Authentication Issues
1279

1380
<details open>

index.ts

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
2424
*/
2525

26-
import { tool, type Plugin, type PluginInput } from "@opencode-ai/plugin";
26+
import { tool } from "@opencode-ai/plugin/tool";
27+
import type { Plugin, PluginInput } from "@opencode-ai/plugin";
2728
import type { Auth } from "@opencode-ai/sdk";
2829
import {
2930
createAuthorizationFlow,
@@ -108,8 +109,8 @@ import {
108109
* }
109110
* ```
110111
*/
111-
export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
112-
let cachedAccountManager: AccountManager | null = null;
112+
export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
113+
let cachedAccountManager: AccountManager | null = null;
113114

114115
type TokenSuccess = Extract<TokenResult, { type: "success" }>;
115116

@@ -238,9 +239,9 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
238239
}
239240
}
240241

241-
for (const result of results) {
242-
const accountId = extractAccountId(result.access);
243-
const accountEmail = sanitizeEmail(extractAccountEmail(result.access));
242+
for (const result of results) {
243+
const accountId = extractAccountId(result.access);
244+
const accountEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
244245
const existingByEmail =
245246
accountEmail && indexByEmail.has(accountEmail)
246247
? indexByEmail.get(accountEmail)
@@ -383,7 +384,7 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
383384
const refreshed = await queuedRefresh(account.refreshToken);
384385
if (refreshed.type !== "success") return;
385386
const id = extractAccountId(refreshed.access);
386-
const email = sanitizeEmail(extractAccountEmail(refreshed.access));
387+
const email = sanitizeEmail(extractAccountEmail(refreshed.access, refreshed.idToken));
387388
if (id && id !== account.accountId) {
388389
account.accountId = id;
389390
changed = true;
@@ -442,7 +443,14 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
442443
return `resets in ${formatWaitTime(remaining)}`;
443444
};
444445

446+
// Event handler for session recovery (matches antigravity plugin pattern)
447+
const eventHandler = async (_input: { event: { type: string; properties?: unknown } }) => {
448+
// Session recovery is handled inside the loader, but we need to expose the event handler
449+
// to match the antigravity plugin structure that OpenCode expects
450+
};
451+
445452
return {
453+
event: eventHandler,
446454
auth: {
447455
provider: PROVIDER_ID,
448456
/**
@@ -459,13 +467,21 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
459467
* @param provider - Provider configuration from opencode.json
460468
* @returns SDK configuration object or empty object for non-OAuth auth
461469
*/
462-
async loader(getAuth: () => Promise<Auth>, provider: unknown) {
463-
const auth = await getAuth();
464-
465-
// Only handle OAuth auth type, skip API key auth
466-
if (auth.type !== "oauth") {
467-
return {};
468-
}
470+
async loader(getAuth: () => Promise<Auth>, provider: unknown) {
471+
const auth = await getAuth();
472+
473+
// Only handle OAuth auth type, skip API key auth
474+
if (auth.type !== "oauth") {
475+
return {};
476+
}
477+
478+
// Only handle multi-account auth (identified by multiAccount flag)
479+
// If auth was created by built-in plugin, let built-in handle it
480+
const authWithMulti = auth as typeof auth & { multiAccount?: boolean };
481+
if (!authWithMulti.multiAccount) {
482+
logDebug(`[${PLUGIN_NAME}] Auth is not multi-account, skipping loader`);
483+
return {};
484+
}
469485

470486
const accountManager = await AccountManager.loadFromDisk(
471487
auth as OAuthAuthDetails,
@@ -811,7 +827,8 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
811827
* @returns Authorization flow configuration
812828
*/
813829
authorize: async (inputs?: Record<string, string>) => {
814-
if (inputs) {
830+
console.log(`[DEBUG] authorize called, inputs:`, JSON.stringify(inputs));
831+
if (inputs && Object.keys(inputs).length > 0) {
815832
const accounts: TokenSuccess[] = [];
816833
const noBrowser =
817834
inputs.noBrowser === "true" ||
@@ -850,17 +867,17 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
850867
const forceNewLogin = accounts.length > 0;
851868
const result = await runOAuthFlow(useManualMode, forceNewLogin);
852869

853-
if (result.type === "success") {
854-
const email = extractAccountEmail(result.access);
855-
const accountId = extractAccountId(result.access);
856-
const label = email || accountId || "Unknown account";
857-
console.log(`\n✓ Authenticated as: ${label}\n`);
870+
if (result.type === "success") {
871+
const email = extractAccountEmail(result.access, result.idToken);
872+
const accountId = extractAccountId(result.access);
873+
const label = email || accountId || "Unknown account";
874+
console.log(`\n✓ Authenticated as: ${label}\n`);
858875

859-
const isDuplicate = accounts.some(
860-
(acc) =>
861-
(accountId && extractAccountId(acc.access) === accountId) ||
862-
(email && extractAccountEmail(acc.access) === email),
863-
);
876+
const isDuplicate = accounts.some(
877+
(acc) =>
878+
(accountId && extractAccountId(acc.access) === accountId) ||
879+
(email && extractAccountEmail(acc.access, acc.idToken) === email),
880+
);
864881

865882
if (isDuplicate) {
866883
console.warn(
@@ -959,6 +976,23 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
959976
};
960977
}
961978

979+
let startFresh = true;
980+
const existingStorage = await hydrateEmails(await loadAccounts());
981+
if (existingStorage && existingStorage.accounts.length > 0) {
982+
const existingAccounts = existingStorage.accounts.map((account, index) => ({
983+
accountId: account.accountId,
984+
email: account.email,
985+
index,
986+
}));
987+
const loginMode = await promptLoginMode(existingAccounts);
988+
startFresh = loginMode === "fresh";
989+
if (startFresh) {
990+
console.log("\nStarting fresh - existing accounts will be replaced.\n");
991+
} else {
992+
console.log("\nAdding to existing accounts.\n");
993+
}
994+
}
995+
962996
const { pkce, state, url } = await createAuthorizationFlow();
963997
let serverInfo: Awaited<ReturnType<typeof startLocalOAuthServer>> | null =
964998
null;
@@ -974,7 +1008,7 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
9741008
if (!serverInfo || !serverInfo.ready) {
9751009
serverInfo?.close();
9761010
return buildManualOAuthFlow(pkce, url, async (tokens) => {
977-
await persistAccountPool([tokens], false);
1011+
await persistAccountPool([tokens], startFresh);
9781012
});
9791013
}
9801014

@@ -997,7 +1031,7 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
9971031
);
9981032

9991033
if (tokens?.type === "success") {
1000-
await persistAccountPool([tokens], false);
1034+
await persistAccountPool([tokens], startFresh);
10011035
}
10021036

10031037
return tokens?.type === "success"
@@ -1268,4 +1302,6 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
12681302
};
12691303
};
12701304

1271-
export default OpenAIAuthPlugin;
1305+
export const OpenAIAuthPlugin = OpenAIOAuthPlugin;
1306+
1307+
export default OpenAIOAuthPlugin;

0 commit comments

Comments
 (0)