feat: Opencode v2 support - #274
Conversation
OpenCode 2 replaces the auth.loader/fetch-override plugin API with an
Effect-based Integration/Credential/Provider registration, and moves the
Anthropic provider to an AI-SDK-compatible package instead of a builtin
fetch hook. Rework the plugin's entrypoint around that API:
- Register a "claude-code" OAuth method on the built-in Anthropic
integration via ctx.integration.transform, surfaced as "Import Claude
Code subscription" under /connect
- Point the Anthropic provider's model package at a new AI-SDK-compatible
provider (src/provider.ts, built on @ai-sdk/anthropic's createAnthropic)
wired in via ctx.catalog.transform, reusing the existing tool-name
translation, SSE buffering, beta-flag selection, and billing-header logic
- Inject the Claude Code system identity via ctx.session.hook("context")
instead of experimental.chat.system.transform
- Re-sync from the keychain before ever attempting a network refresh: since
OpenCode 2 persists the OAuth credential itself and calls the plugin's
refresh hook on its own schedule, a refresh token that's gone stale
because `claude` rotated credentials independently of this connection
(an interactive re-login, its own periodic refresh, ...) would otherwise
fail with invalid_grant forever. The keychain is the real source of
truth, so check it first and adopt whatever is there when it disagrees
with what OpenCode handed back
This targets OpenCode 2 only and is not backwards compatible with OpenCode
1's plugin API — see the updated README/installation.md for the
`plugin` -> `plugins` config key change and migration notes.
Credential source reading (keychain.ts, credentials.ts), tool-name
translation, SSE handling, beta-flag selection, and billing-header signing
(transforms.ts, signing.ts, betas.ts) are all unchanged.
- Fix the config key: OpenCode 2 uses "plugins" (plural), not OpenCode 1's "plugin" (singular) — the previous instructions would silently no-op on OpenCode 2 - Document the /connect flow (Anthropic -> Import Claude Code subscription) that replaces OpenCode 1's fully-automatic auth - Rework "How it works" and "How it works (technical)" for the new Integration/Credential/Provider registration and the keychain-resync behavior in the refresh hook; drop the auth.json fallback sync and 5-minute background timer, which no longer exist under OpenCode 2's own credential persistence - Add a troubleshooting entry for OpenCode 2 replaying a stale stored refresh token after an out-of-band `claude` re-login
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Pull request overview
This PR updates the plugin to support OpenCode v2’s Effect-based plugin/auth APIs and introduces an AI-SDK-compatible Anthropic provider wrapper to use Claude Code subscription credentials.
Changes:
- Replaces the OpenCode v1 plugin/auth implementation with an OpenCode v2
Plugin.defineintegration + OAuth credential method. - Adds an Anthropic provider wrapper (
src/provider.ts) that injects Bearer auth, required headers, request/stream transforms, and retry behavior. - Updates docs/tests/scripts for OpenCode 2 usage and dependency set (AI SDK + Effect + OpenCode beta plugin API).
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/provider.ts | New AI-SDK Anthropic provider wrapper + Claude subscription transport logic. |
| src/provider.test.ts | Unit tests for provider header/body/stream transformations and failure behavior. |
| src/index.ts | New OpenCode v2 plugin entrypoint using Effect integration/catalog/session hooks and OAuth method registration. |
| src/index.test.ts | Removes legacy v1-focused tests. |
| scripts/test-headless.ts | Updates headless E2E runner to use OpenCode 2 command/config keys and tighter assertions. |
| README.md | Updates documentation for OpenCode 2 setup and /connect flow. |
| pnpm-lock.yaml | Locks new dependencies required for OpenCode 2 + AI SDK integration. |
| package.json | Moves to OpenCode 2 dependency set; updates package description. |
| opencode-claude-auth.js | Adjusts entrypoint exports to re-export dist. |
| installation.md | Updates installation instructions for OpenCode 2 and new connect flow. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| export function createClaudeSubscription(options: Record<string, unknown>) { | ||
| const accessToken = typeof options.apiKey === "string" ? options.apiKey : "" | ||
| const upstream = | ||
| typeof options.fetch === "function" ? (options.fetch as Fetch) : undefined | ||
| return createAnthropic({ | ||
| ...options, | ||
| apiKey: "", | ||
| fetch: claudeSubscriptionFetch(accessToken, upstream), | ||
| }) | ||
| } |
| "dependencies": { | ||
| "@ai-sdk/anthropic": "4.0.39", | ||
| "@opencode-ai/plugin": "0.0.0-beta-17519", | ||
| "effect": "4.0.0-beta.101" | ||
| }, |
| ```bash | ||
| opencode --version | ||
| ``` | ||
|
|
||
| You should see a version number (e.g., `1.2.28`). | ||
| You need an OpenCode 2 build. See the [OpenCode 2 migration guide](https://opencode.ai/v2/docs/migrate-v1) if you're still on an OpenCode 1 build (`1.x`). |
| ```bash | ||
| opencode plugin list | ||
| ``` |
OpenCode 2 beta-17595 bundles effect@4.0.0-beta.107. With our plugin still pinned to effect@4.0.0-beta.101, the plugin failed to load entirely: "failed to load plugin" cause="Cause([Die(TypeError: undefined is not an object (evaluating 'n.base.get'))])" Two mismatched effect runtime instances interacting produced that crash. OpenCode then fell through to an unauthenticated Anthropic connection, surfacing as "API key is invalid" on every request rather than anything that pointed at the plugin. Bumping effect to match, plus @opencode-ai/plugin to the beta-17595 pin, fixes it - verified with a real opencode2 run against the live Anthropic API, not just the unit suite.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
…ugin API
@opencode-ai/plugin publishes two parallel plugin registration APIs: an
Effect-based one (/effect, what we used) and a plain Promise-based one
(the default export). We only ever used Effect as a thin wrapper around
already-async logic, and Effect is pre-1.0 with no cross-version
compatibility guarantees - our plugin's own effect dependency had to match
whatever OpenCode 2 bundled to the exact beta patch, or the plugin crashed
at load time (as it just did against beta-17595). Moving to the Promise
API removes that dependency entirely, leaving @opencode-ai/plugin as the
only OpenCode-coupled dependency to track, pinned to the "beta" dist-tag
so a fresh install picks up whatever OpenCode 2 currently ships without
needing a manual version bump each time.
- Extract all OAuth logic (build credential, authorize, refresh-with-
keychain-resync, label, method descriptor) into src/oauth-method.ts: a
framework-agnostic core with dependency injection (OAuthDeps), shared by
both plugin variants and covered by 29 new unit tests using plain fakes
- Rewrite src/index.ts against @opencode-ai/plugin's Promise API (Plugin
+ setup) instead of @opencode-ai/plugin/effect (Plugin + effect)
- Verified with two separate real `opencode2 run` end-to-end passes
against the live Anthropic API (once before the cutover with the
Promise variant temporarily loaded, once after as the permanent
src/index.ts), not just the unit suite
Also: dropping effect as a direct dependency made TypeScript's
declaration emission stricter, surfacing a pre-existing, unrelated bug -
the multi-account picker (`prompts: [{type: "select", ...}]`) doesn't
match the current Form.Fields schema (form?: Form.Fields, whose field
union has no "select" variant), so multi-account selection during
/connect is likely non-functional against current OpenCode 2 builds. It
only ever type-checked because of a TS excess-property-check loophole.
Documented in place (src/oauth-method.ts) rather than silently fixed here
since it's out of scope for this change; single/no-account paths (what
the e2e tests exercise) are unaffected.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
Summary
Adds support for opencode v2
Related issue
Closes #245
Testing
I have been maintaining a fork and other people have been using that plugin: https://www.npmjs.com/package/opencode-claude-auth-v2 , also can see that I fixed issues with the v2 plugin a couple time in #245
Checklist
feat:,fix:,docs:,chore:, etc.)make allpasses locally (runs lint, build, and test)Everything below is AI generated:
OpenCode 2 replaced the plugin auth API this project was built on (
auth.loader+ a customfetchoverride) with anIntegration/Credential/Providerregistration, and moved the Anthropic provider off a builtin fetch hook onto an AI-SDK-compatible package. This PR reworks the plugin's entrypoint around that new API, using@opencode-ai/plugin's plain Promise-based registration (Plugin.define({ setup: async (ctx) => ... })) rather than its parallel Effect-based one — this plugin has no other use for Effect, and depending on it directly means tracking its exact version against whatever OpenCode 2 itself bundles; the Promise API avoids that dependency entirely.This targets OpenCode 2 only and drops OpenCode 1 support — see "Rollout" below before merging.
Rollout — needs a decision before merging
This repo's commit convention (
feat!:) plus its release-please-style automation (chore(main): release X) means merging this will auto-bump a major version. Existing OpenCode 1 users are pinned toopencode-claude-auth@latestper the current README/installation.md — if the major version publishes under thelatestnpm tag, every OpenCode 1 user's@latestplugin breaks silently on their next OpenCode restart, since this version won't load under OpenCode 1's plugin API at all.Worth deciding explicitly how to roll this out — e.g. gate the initial release behind a
nexttag until OpenCode 1 usage has migrated, or publish with clear release notes calling out that@latestnow requires OpenCode 2.