Skip to content

fix: OpenCode v2 compatibility and 401 failover handling - #5

Open
abolfazl-moeini wants to merge 10 commits into
bulutmuf:mainfrom
abolfazl-moeini:opencode-v2-compat
Open

abolfazl-moeini wants to merge 10 commits into
bulutmuf:mainfrom
abolfazl-moeini:opencode-v2-compat

Conversation

@abolfazl-moeini

@abolfazl-moeini abolfazl-moeini commented Oct 18, 2026 •

Copy link
Copy Markdown

Hey! Ran into a few issues trying to run this on OpenCode v2 and put together a fix:

• v2 schema compatibility: Updated default exports for both server and TUI to { id, setup }, and adapted keymap/dialog calls to match v2's API so it doesn't throw Invalid V2 TUI plugin module.
• Tool execution return format: Wrapped tool returns into { content: string }. In v2/CodeMode, returning raw strings was throwing a TypeError (o is not an Object (evaluating '"output" in o')).
• 401/402/403 failover: When an invalid or exhausted key hits 401/402, it now immediately disables the bad key and rotates to the next one instead of bailing out early.
• Native v2 session hooks & auto-retry: Registered http.request, http.response, and retry hooks on input.session.hook. OpenCode v2 routes model calls via Effect's HTTP client which bypassed globalThis.fetch. Native hooks directly intercept requests, rotate keys on 429 / FreeUsageLimitError, sync the SQLite credential table in opencode.db, and override the retry decision so OpenCode automatically re-attempts failed steps with the new key.
• Environment & startup sync: Added loadAllEnv at server startup to load keys from project, ~/.config/opencode/.env, and ~/.env. Merged env-defined keys (<PROVIDER>_API_KEYS) with keychain.json so existing keychain keys don't shadow env variables.
• Registered commands: Added /keychain and /failover commands via input.command.transform.
• Tests: Added comprehensive unit & integration tests (bun test passes cleanly, 52/52).

Tested locally with OpenCode v2 on multiple providers and working smoothly.

…lover bypass

- Export default plugin schema { id, setup } for both server and TUI
- Register keychain and failover commands via input.command.transform
- Support OpenCode v2 keymap.layer callback syntax and slash command definitions
- Add fallback dialog compatibility for OpenCode v2 alert/select
- Fix early return in fetch-patch to disable invalid/exhausted keys on 401/402/403 and rotate
- Add integration tests for 429 rotation and 401 key disable/failover
… in o' TypeError

- OpenCode v2 tool dispatcher evaluates ('output' in o) expecting an object
- Returning a string causes JavaScriptCore to throw: TypeError: o is not an Object. (evaluating '"output"in o')
- If 'output' property is present without an outputSchema, OpenCode v2 rejects with schema error
- Normalizing tool results to { content: string } guarantees compliance with OpenCode v2 and Code Mode
- Added tests verifying { content } format and error safety
…status

- Call loadAllEnv() at server startup across project, config dir, and home .env files
- Trigger res.config() hook inside plugin.setup() for OpenCode v2 compatibility
- Dynamically re-scan environment and discover newly added providers inside keychain-status
- Automatically import and sync providers from ~/.local/share/opencode/auth.json
@bulutmuf

Copy link
Copy Markdown
Owner

Thanks a lot for the PR and the comprehensive test coverage, @abolfazl-moeini! Looks like a massive improvement for OpenCode v2 compatibility.

I haven't reviewed the changes in detail yet, but it's on my radar. I'll approve the CI workflow and take a closer look over the next few days.

@bulutmuf bulutmuf left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this fixes the actual v2 blocker, and the native input.session.hook + retry-override approach is the right one. I pulled the branch locally: bun test is 71/71 green and bun run typecheck is clean. I also checked the v2.1.4 binary: the TUI loader validates the default export as exactly { id: string, setup: function }, so that part of the fix is correct. No key exfiltration in the diff — the only new outbound call is opencode.ai/console/api/v2/config (org id lookup for the provider's own key).

A few changes before merge.

Blocking

  1. src/lib/hooks.ts:296-299 — the retry hook sets event.decision = { retry: true, delay: 100 } whenever any key is active, without checking the error class or the attempt count. A non-retryable error (400, invalid request, context length) gets retried for as long as the pool is non-empty. Please gate the override on the classification (Rotate/Overload only) and/or an event.attempt cap, and otherwise keep the original decision.

  2. src/lib/hooks.ts:169 and src/lib/hooks.ts:288-293 — writeAuthKey() (rewrites auth.json) and syncCredentialDb() (opens opencode.db) run on every model request. On this machine opencode.db is 4.2 GB; a synchronous open+update per request is a latency and SQLITE_BUSY problem. Please write only when the active key actually changed (track the last-written key per provider).

  3. src/lib/config.ts:202-221 — loadAllEnv() copies every entry from ~/.env, ~/.config/opencode/.env and <project>/.env into Bun.env unconditionally, including PATH and NODE_OPTIONS. Please restrict it to provider/plugin keys (e.g. *_API_KEYS, OPENCODE_FAILOVER_*) and do not overwrite variables that are already set.

Non-blocking

  1. src/lib/hooks.ts:60-78 — resolveProviderFromUrl uses host.includes("openai.com"), so evil-openai.com also matches. Use host === d || host.endsWith("." + d).

  2. src/lib/auth.ts:40 — when no credential row matches the new key, the existing row's value is overwritten, so the user's original key is lost from opencode.db after the first rotation. It may be intended (same behaviour as auth.json), but it needs a comment and a test, or it should insert a new row instead.

Compatibility

  1. src/index.ts:413 — changing the default export from the server function to { id, setup } drops OpenCode 1.x support, while the README still advertises "OpenCode 1.17+". If 1.x is still supported this needs a function-compatible default, otherwise a major version bump plus README/package.json peerDependency update (@opencode-ai/plugin: ^1.0.0).

Note: CI has never run on this PR (CI is action_required because it comes from a fork), so the green result above is from my local run only. I have approved the workflow run so CI can verify the branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants