Bring your own OAuth-protected LLM gateway to OpenCode.
An OpenCode plugin that lets you wire up OpenAI-compatible model providers sitting behind OAuth2 / OIDC — without baking long-lived API keys into your config. Discover models dynamically, refresh tokens automatically, and let OpenCode talk to your gateway as if it were any other provider.
flowchart LR
OC[opencode] -->|chat.headers| Plugin[opencode-oauth2]
Plugin -->|cached token?| Cache[(~/.cache/opencode-oauth2)]
Plugin -->|acquire / refresh| IdP[OAuth server]
Plugin -->|Authorization: Bearer …| Upstream[Provider API]
Most OpenCode providers assume a static bearer key. That works for hosted SaaS, but breaks down the moment you put your models behind:
- a corporate Identity Provider (Keycloak, Auth0, Okta, Azure AD, …)
- a self-hosted gateway with short-lived tokens
- a multi-tenant setup where each user authenticates as themselves
- a CI runner that has no business carrying a long-lived secret
This plugin closes that gap. It handles the OAuth dance for the flow you need, caches tokens, refreshes silently, and feeds OpenCode a normal-looking provider with a fresh Authorization header on every request.
- Five auth flows, pick what matches your runtime:
authorization_code— interactive PKCE login (default)device_code— RFC 8628, for browserless user authclient_credentials— machine-to-machine with aclientSecretjwt_bearer— RFC 7523 federated identity (GitHub Actions OIDC, Kubernetes SA tokens) — no long-lived secret in CItoken_exchange— RFC 8693 federated identity with explicit audience targeting
- Dynamic model discovery from
/v1/models(no hand-maintained model lists) - Display-name normalization so
glm-5shows up asGLM 5 - Persistent token cache with automatic refresh
chat.headershook injects bearer tokens per request- Two configuration styles: per-provider options or a top-level plugin block
Then declare a provider:
{
"plugin": ["@vymalo/opencode-oauth2"],
"provider": {
"example-ai": {
"name": "Example AI",
"options": {
"baseURL": "https://api.example.com/v1",
"oauth2": {
"issuer": "https://auth.example.com",
"clientId": "opencode-client",
"scopes": ["openid", "profile", "offline_access"],
"syncIntervalMinutes": 60
}
}
}
}
}See packages/opencode-oauth2/README.md for the full configuration reference (including the alternative pluginConfig.oauth2ModelSync.servers layout and every optional field).
| Page | When you need it |
|---|---|
docs/architecture.md |
Understand the hooks, token lifecycle per flow, cache layout, sync scheduler, logging |
docs/github-actions.md |
CI without stored secrets — Keycloak/Auth0/Okta setup, reusable workflow, matrix, fork-PR limits |
docs/kubernetes.md |
CronJob / Job / Deployment with projected SA tokens, multi-provider pods, RBAC |
docs/local-development.md |
Sandbox setup, plugin re-export trick, forcing re-auth, dev-only env subject token |
docs/troubleshooting.md |
Symptom-keyed fixes — redirect_uri_mismatch, model discovery 403, invalid_client, projected-token rotation |
For GitHub Actions and Kubernetes workloads, use jwt_bearer (or token_exchange) with the platform's own short-lived OIDC token as the subject. The plugin re-fetches it on every access-token expiry; nothing long-lived gets cached.
End-to-end recipes live in docs/github-actions.md and docs/kubernetes.md. The shipped reusable workflow at .github/workflows/opencode-run.yml covers the common opencode run case.
Refresh tokens are mandatory for the flows that issue them.
authorization_code/device_codeexchanges that don't returnrefresh_tokenare rejected.- Cached tokens missing
refreshTokenare evicted on load (unless they're fromclient_credentials/jwt_bearer/token_exchange, which don't issue one). - Refresh responses that omit a new
refresh_tokenre-use the existing one.
The intent: a user-flow session is either fully renewable or it doesn't get cached. Machine flows re-acquire on every expiry; refresh tokens have no role there.
This is a pnpm monorepo.
| Package | Purpose |
|---|---|
packages/opencode-oauth2 |
The runtime plugin — published as @vymalo/opencode-oauth2 |
packages/plugin-bundle |
Rolldown-based bundling for distribution |
plans/prd.md |
Product requirements and phased roadmap |
pnpm install
pnpm build
pnpm typecheck
pnpm testPlugin-only iteration:
pnpm --filter @vymalo/opencode-oauth2 test
pnpm --filter @vymalo/opencode-oauth2 buildFor end-to-end usage against a local OpenCode install, see GETTING_STARTED.md.
Early but functional. The Phase 1 scaffold and Phase 2 runtime core are in; bundling (Phase 3) has landed. Public API may still shift before 1.0.
Roadmap and phase breakdown live in plans/prd.md.
Issues and PRs are welcome. Please open an issue first for substantial changes so we can align on scope before code review.
MIT © vymalo contributors
{ "$schema": "https://opencode.ai/config.json", "plugin": ["@vymalo/opencode-oauth2"] }