Skip to content

feat: OpenCode V2 support (dual V1/V2 package, 2.0.0) - #45

Merged
marcusolsson merged 7 commits into
mainfrom
feature/opencode-v2-support
Oct 3, 2026
Merged

marcusolsson merged 7 commits into
mainfrom
feature/opencode-v2-support

Conversation

@marcusolsson

@marcusolsson marcusolsson commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds OpenCode V2 support to @bergetai/opencode-auth while keeping V1 working from the same package. Ships as 2.0.0.

How it works

  • Dual default export (official recommended shape): V2 reads id + setup(); V1 (>= 1.3.4) calls server(). V2 code loads lazily via dynamic import, so no V2 SDK code ever executes under V1.
  • Type-only SDK: @opencode/[email protected] is a devDependency; all src/v2/ imports are import type (CI-enforced). User installs stay qrcode-weight.
  • Integration registration upserts onto the CLI's native models.dev berget integration: PKCE (id: "oauth", matches V2's legacy-import mapping), device flow (id: "device"), API key. That is the plugin's core value on V2: V2 natively supports only key/env auth for Berget.
  • Provider transform composes with the native definition: binds integrationID, keeps V1's always-on activation, honors BERGET_INFERENCE_URL, and fills in models the live /v1/models/chat catalog knows that models.dev doesn't (never clobbers native metadata).
  • V1 credential import (src/v2/migrate.ts): one-time, idempotent import of the V1 auth.json berget login — feature-detected (see findings below), silent no-op when unavailable, never blocks startup.
  • V1 code paths unchanged (server() delegates to the existing BergetAuthPlugin); only additive change is an optional expiresAt on the device flow result.

Notable findings during validation

  1. The CLI's built-in V1 auth.json import migration does not run on fresh V2 databases (@opencode/[email protected]: fresh-db bootstrap journals all 48 migrations without executing their up-functions), and the plugin context does not expose the client credential API yet. The plugin-side import covers both automatically once either lands upstream.
  2. The V2 CLI ships as @opencode/cli (npm), not opencode-ai@beta.

Test plan

  • Unit: 96 tests pass (59 pre-existing V1 tests untouched; 37 new: entrypoint shape, lazy setup, type-only SDK guard, method-id consistency incl. the oauth/device registration-vs-credential regression guard, refresh mapping/rejection, all import outcomes, provider upsert/fill-in/env-override). Lint, typecheck, format clean.
  • E2E on installed packages (tarball prepopulated into each CLI's plugin cache; real exports resolution):
    • V1 1.3.4 (floor leg — passes, floor stays >= 1.3.4), V1 1.18.34, V2 2.0.22: plugin loads, models + auth register, seeded API-key request reaches Berget's real API (upstream 401 for the seeded fake key, verified as Berget's own response body).
    • V2: integration shows all 3 methods + display name; provider enabled + integration-bound; 6 native models.
  • Manual (user): PKCE browser login via /connect on the V2 TUI with this package — verified working end to end.
  • Known user-gated leftovers: device-flow login + TUI QR rendering visual check; device/PKCE authorize starts and the whole refresh/re-persistence pipeline were verified headlessly in spike validation.
  • V1 CLIs < 1.3.4 cannot load object-form default exports — README and release notes point users to pin @1. No behavior change for V1 >= 1.3.4 users.

Docs: README (V1/V2 compatibility table, config keys, transition summary), docs/auth.md (V2 credential storage + provider registration sections), docs/release-2.0.0.md (release notes draft).

After merge: release via publish.yml using docs/release-2.0.0.md as the release body.

…ke findings

- MIGRATE_V2.md: researched dual-support strategy for OpenCode V2
- IMPLEMENTATION_PLAN_V2.md: phased execution plan, amended with phase 0 outcomes
- PHASE0_FINDINGS.md: spike validation results (credential pipeline, SDK-free
  entrypoint, broken fresh-db V1 import in @opencode/cli 2.0.22 + proven fallback)
Default export becomes the officially recommended dual shape: V2 CLIs read
id + setup() (V2 implementation loads lazily via dynamic import, so no V2
SDK code executes under V1); V1 CLIs >= 1.3.4 call server(), which
delegates to the unchanged BergetAuthPlugin. All named exports preserved.

- add @opencode/[email protected] as exact devDependency (type-only usage,
  enforced by test; user installs stay qrcode-weight)
- promote @opencode-ai/[email protected] to explicit dependency (typecheck
  hygiene; imports are all type-only)
- src/v2/setup.ts stub establishes the lazy import target
- index.test.ts: entrypoint shape, lazy setup(), server() delegation,
  public API surface, and a type-only SDK import guard for src/v2/
- tsconfig: include index.test.ts

Verified: npm test (66 pass), npm run lint, npm run typecheck,
npm run format:check
src/v2/integration.ts registers the berget integration via
ctx.integration.transform as an upsert onto the CLI's native (models.dev)
integration: PKCE (id 'oauth', mode auto), device flow (id 'device', mode
auto, device-code expiry passed through as expiresAt), and an API key
method. Flow logic is shared with V1 (createPkceAuthorizeMethod /
createDeviceAuthorizeMethod unchanged except an additive optional
expiresAt field); toV2Authorization adapts V1 results to the V2
authorization shape, mapping failed results to rejections.

src/v2/credential.ts adapts V1 token fields to the V2 Credential.OAuth
shape (integer epoch-ms expires) and implements the shared refresh
callback on top of refreshAccessTokenDirect. Method ids are shared
constants: PKCE must register as 'oauth' to match V2's legacy-import
mapping; refresh echoes the method's own id.

src/v2/migrate.ts performs a one-time, idempotent import of the V1
auth.json berget OAuth credential via the client credential API. It is
feature-detected (the plugin context does not expose the credential
domain on @opencode/[email protected]) and never blocks startup.

Also: vitest now runs index.test.ts (root-level tests were previously
not executed by the runner — typecheck was the only gate covering it).

Verified: npm test (88 pass), lint, typecheck, format:check.
src/v2/provider.ts composes the provider transform with the CLI's native
models.dev definition instead of replacing it: updates in place to bind
integrationID 'berget', keep V1's always-on activation, and honor
BERGET_INFERENCE_URL as a baseURL override; fills in models the live
/v1/models/chat catalog lists that models.dev does not yet know (native
metadata and costs preserved). A missing provider is added from scratch.
Models are fetched once in setup and captured by the synchronous,
repeatable transform; fetch failures degrade to the native catalog.

VISION_MODELS is exported from src/plugin/models.ts (additive) for reuse
in the V2 mapping.

setup() now fetches models and registers the provider. Verified end to
end with the actual package loaded by @opencode/cli: integration shows
the three methods, provider shows activation enabled + integrationID
binding, and the model catalog stays native (no fill-ins needed today).

Verified: npm test (96 pass), lint, typecheck, format:check.
E2E matrix (installed package prepopulated into the CLI plugin cache so
the loader exercises the real exports map; config spec pinned to
@2.0.0 which short-circuits the installer on all three CLIs):

- V1 1.3.4 (floor leg): PASS — object-form default export loads via
  exports["./server"], provider + live-fetched models register, auth
  reaches Berget's API (upstream 401 'Invalid token format' is the real
  API body for the seeded fake key)
- V1 1.18.34 (latest): PASS — same results
- V2 @opencode/cli 2.0.22: PASS — 3 methods registered on the
  integration, provider enabled + integration-bound, 6 native models,
  key connect + agent loop hits the real API
- PKCE/device client login, TUI QR rendering, and full CLI versions of
  the expiry/transition legs remain user-gated; the credential pipeline
  (refresh near expiry, re-persistence), device/PKCE authorize starts,
  and the SDK-free install were verified headlessly in phase 0
  (PHASE0_FINDINGS.md)

Changes:
- README: V1/V2 compatibility table (foot note: < 1.3.4 must pin @1),
  V2 'plugins' config example, V1->V2 credential-transition summary
- docs/release-2.0.0.md: release notes draft
- version: 2.0.0

Verified: npm test (96 pass), lint, typecheck, format:check.
MIGRATE_V2.md, IMPLEMENTATION_PLAN_V2.md and PHASE0_FINDINGS.md are
working documents for this change, not package documentation; they stay
local. Also drop the dangling PHASE0_FINDINGS.md reference from
docs/auth.md.
- Move @opencode-ai/sdk to devDependencies (type-only usage; it already
  reaches installs transitively via @opencode-ai/plugin)
- Guard device-flow expiresAt with Number.isFinite so a malformed
  expires_in can never hand NaN to the V2 framework
- Exclude catalog models without a numeric contextWindow from the V2
  fill-in instead of registering them with limit.context: 0
- Read V1's pre-XDG ~/.opencode/auth.json as a fallback in the V1
  credential import, matching V1's own read order
@marcusolsson
marcusolsson merged commit 97ef234 into main Oct 3, 2026
1 check passed
@marcusolsson
marcusolsson deleted the feature/opencode-v2-support branch October 3, 2026 13:35
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.

1 participant