Use Windsurf's 130+ AI models (Claude Sonnet/Opus 4.x, GPT-5.x, Gemini 3, Kimi, GLM, Grok, …) inside OpenCode through your own WindsurfAPI proxy.
Upstream pin: this plugin is E2E-verified against
dwgx/WindsurfAPI@v2.0.96. Other versions usually work, but the plugin emits a warning on connect if the proxy is older thanv2.0.96or a major version ahead. See Compatibility below.
A thin companion plugin — it does not embed or supervise WindsurfAPI. You run the proxy however you like (node src/index.js, Docker, PM2, …) and the plugin talks to it over HTTP, adding:
- Per-model routing — Claude family →
/v1/messages(native Anthropic semantics, best tool-call fidelity), everything else →/v1/chat/completions. - OpenCode-tuned system prompt overrides — pushed once into the proxy so models don't narrate
/tmp/windsurf-workspace/paths or refuse to use tools. - Defence-in-depth output sanitiser — streams residual Cascade/Windsurf mentions out of the response.
- Anthropic prompt-cache markers + system-message dedup — small but real savings on long sessions (where supported by upstream).
This plugin reuses your Windsurf account to access models through the WindsurfAPI proxy. The plan dictates which models are actually usable through any API surface (including this one):
| Plan | What you get | Verdict |
|---|---|---|
| Free | 25 prompts/week, ~4 small models on paper (gpt-4o-mini, gpt-4.1-mini, gpt-5-mini, gemini-2.5-flash, minimax-m2.5) — but most are deprecated or de-entitled after the first call when accessed outside the Windsurf desktop client. | |
| Pro ($15/mo) | The full ~130-model catalogue (Claude Sonnet 4.6, Opus 4.7, GPT-5.x, Gemini 3, …) with generous quotas. | ✅ This is the recommended plan. |
| Pro Ultimate / Teams | Higher quotas + 1M-context Claude variants. | ✅ Best, but Pro is enough for most use. |
If you don't have a Pro Windsurf subscription, install this plugin only to test the pipeline — none of the high-end models you actually want will respond. Get Pro first, then come back here.
Manual install, three steps. The plugin is registered in OpenCode via the standard <name>@file://<dir> pattern that OpenCode supports for local plugins — no npm publish required.
# Pick any directory; ~/.opencode/plugins/ keeps things tidy.
mkdir -p ~/.opencode/plugins
git clone https://github.com/ilkinnabiev/opencode-windsurf-auth.git \
~/.opencode/plugins/opencode-windsurf-auth
cd ~/.opencode/plugins/opencode-windsurf-auth
npm install
npm run buildOpen ~/.config/opencode/opencode.json and add to the plugin array:
Replace /Users/YOU/ with your real home path (echo $HOME).
Copy one of the presets from this repo into the provider section of your opencode.json:
config/opencode-modern.json— curated 25-model list (Claude 4.5/4.6/4.7, GPT-5.x, Gemini 2.5/3.0, Kimi, GLM, Grok, SWE).config/opencode-claude-only.json— Claude-only preset (Sonnet 4.6, Opus 4.6/4.7, Haiku 4.5). Best tool-call stability.
Minimum viable provider block:
"provider": {
"windsurf": {
"npm": "@ai-sdk/openai-compatible",
"name": "Windsurf",
"options": { "baseURL": "http://localhost:3003/v1" },
"models": {
"claude-sonnet-4.6": { "name": "Claude Sonnet 4.6" }
}
}
}Restart OpenCode after editing the config. The plugin will autodetect the proxy on first request.
The plugin does not start the proxy for you — that's intentional (the proxy can live anywhere: localhost, Docker, a VPS). The proxy is a separate project: dwgx/WindsurfAPI. Clone it once, then pick a run mode:
git clone https://github.com/dwgx/WindsurfAPI.git ~/.windsurfapi
cd ~/.windsurfapi
# Pin to the version this plugin was last E2E-verified against.
# (Skip to use upstream HEAD — the plugin will log a one-line compat
# warning on connect if it detects a version it hasn't been tested on.)
git checkout v2.0.96
# Install the Language Server binary it needs (one-time, ~250MB):
bash install-ls.sh
# Configure (see ".env" template below)
cp .env.example .env && $EDITOR .envcd ~/.windsurfapi
node src/index.jsLeave it running in one terminal, use OpenCode in another.
cd ~/.windsurfapi
nohup node src/index.js > /tmp/windsurfapi.log 2>&1 &
disown
# logs: tail -f /tmp/windsurfapi.log
# stop: lsof -ti:3003 | xargs killThe WindsurfAPI repo ships a docker-compose.yml:
cd ~/.windsurfapi
docker compose up -dHOST=127.0.0.1 # loopback only — required for open mode
PORT=3003
API_KEY= # empty = open mode (no proxy auth)
DASHBOARD_PASSWORD=<your-choice> # required only if you want the dashboard
# Language Server binary (installed by `bash install-ls.sh`). Path varies by OS:
# macOS Apple Silicon: ~/.windsurf/language_server_macos_arm
# macOS Intel: ~/.windsurf/language_server_macos_x64
# Linux x64: ~/.windsurf/language_server_linux_x64
# Windows x64: ~/.windsurf/language_server_windows_x64.exe
LS_BINARY_PATH=~/.windsurf/language_server_macos_armThe empty API_KEY is safe because HOST=127.0.0.1 makes the proxy listen on loopback only — no remote process can reach it. The plugin auto-detects this and runs prompt-free during sign-in.
If you want to expose the proxy beyond your machine (team-shared VPS), set API_KEY=<secret> and HOST=0.0.0.0. The plugin then picks up the bearer either from WINDSURFAPI_KEY env or via the "advanced" sign-in method.
Once the proxy is running and the plugin is installed:
opencode auth login
# → Windsurf
# → Sign in with Windsurf (import desktop login) ← zero-click in the best case| Method | What it does | When to pick it |
|---|---|---|
| Sign in with Windsurf (import desktop login) | Reads the Windsurf desktop app's stored apiKey from state.vscdb and registers it with your proxy. |
You have the Windsurf desktop app installed and signed in. Zero clicks, zero prompts. |
| Sign in with Windsurf (paste auth token) | Opens https://windsurf.com/show-auth-token in your browser; you paste the token shown there. |
No desktop app, or you want a one-off account. |
| WindsurfAPI proxy API key (advanced) | Just records the proxy's API_KEY. Does not add a Windsurf account on its own. |
Your proxy already has accounts (added on another machine, in CI, via the dashboard, etc). |
| Open WindsurfAPI dashboard (advanced) | Opens http://localhost:3003/dashboard; you add the account there; plugin polls until it appears. |
Fine-grained control or the other methods don't work for you. |
The first two methods never ask you for the proxy's API_KEY. They figure it out:
- Try open mode —
GET /auth/accountswith noAuthorizationheader. If it returns 200, the proxy doesn't need auth, done. - Fallback to
WINDSURFAPI_KEYenv var (legacy alias:WINDSURF_API_KEY). - Fail with a friendly hint pointing at the env var or the "advanced" method.
opencode
# /models → Windsurf → Claude Sonnet 4.6
# ask anything; the plugin transparently routes Claude → /v1/messages
# and everything else → /v1/chat/completions| Goal | Pick | Why |
|---|---|---|
| Daily driver | claude-sonnet-4.6 |
Best tool stability, 200K ctx, sane price. |
| Heavy reasoning | claude-opus-4-7-medium or -high |
Top-tier coding model; pick -medium unless you really need -high. |
| Cheap & fast | claude-4.5-haiku |
Same protocol as Sonnet, ⅛ the credit cost. |
| Massive context | claude-sonnet-4.6-1m |
1M-token window for whole-repo questions. |
| GPT taste | gpt-5.2 / gpt-5.2-codex-medium |
Works, but tool-call reliability is lower than Claude. |
| Reasoning experiments | gpt-5.2-xhigh, claude-opus-4-7-xhigh |
When you want the model to actually think. |
Avoid in agent loops (e.g. opencode run): glm-4.7 (occasional empty responses break the loop), gpt-4o-mini / gpt-4.1-mini (deprecated upstream; only listed in Windsurf Free entitlements but no longer routable), and anything tagged legacy in GET /v1/models.
Everything lives under provider.windsurf.options in ~/.config/opencode/opencode.json:
"windsurf": {
"npm": "@ai-sdk/openai-compatible",
"name": "Windsurf",
"options": {
"baseURL": "http://localhost:3003/v1",
"dashboardPassword": "...", // only if you want prompt-overrides to push
"shrinkBody": true, // default
"injectCacheControl": true, // default
"postSanitize": true // default
},
"models": { /* … */ }
}| Key | Default | Purpose |
|---|---|---|
baseURL |
autodetect | Proxy URL. Stripped of the trailing /v1 for routing internally. |
candidates |
[] |
Extra URLs to probe before falling back to defaults (localhost:3003, 127.0.0.1:3003, localhost:3000). |
routeClaudeToAnthropic |
true |
Send claude-* requests to /v1/messages. Disable only if the proxy doesn't expose that endpoint. |
installPromptOverrides |
true |
PUT OpenCode-tuned prompts into the proxy on first init (debounced to 12h). Soft-skips with an info log if the dashboard refuses (no password / 401 / 429). |
dashboardPassword |
from WINDSURFAPI_DASHBOARD_PASSWORD or DASHBOARD_PASSWORD env |
The dashboard admin password. Only needed if you want installPromptOverrides to succeed. |
shrinkBody |
true |
Dedupe byte-identical / empty system messages before sending. Conservative — never touches user content or tool specs. |
injectCacheControl |
true |
Mark Anthropic /v1/messages bodies with cache_control: ephemeral on the system + tools prefix. Currently a no-op against Cascade (proxy doesn't propagate yet) but correct against direct Anthropic. |
postSanitize |
true |
Stream-level scrub of residual Cascade / Windsurf / /tmp/windsurf-workspace/ mentions in model output. |
silent |
false |
Suppress plugin log lines on stderr. |
| Var | Effect |
|---|---|
WINDSURF_API_URL |
Force a specific proxy URL (overrides autodetect, beaten by explicit baseURL). |
WINDSURFAPI_KEY |
Proxy API_KEY used during sign-in when the proxy is not in open mode. Lets the "Sign in with Windsurf" methods stay prompt-free. (Alias: WINDSURF_API_KEY.) |
WINDSURFAPI_DASHBOARD_PASSWORD |
Proxy DASHBOARD_PASSWORD. Required only for the optional system-prompt push. Falls back to bare DASHBOARD_PASSWORD so you can reuse the proxy's .env value directly. |
WINDSURF_AUTH_DEBUG=1 |
Enable debug log lines on stderr (request URLs, shrink stats, cache_control diagnostics, etc.). |
This plugin is a thin adapter over the upstream dwgx/WindsurfAPI proxy, so a proxy update can break it if the upstream changes the endpoint contract. To make breakage loud and recoverable, the plugin:
- Hardcodes the last verified upstream version as a constant in
lib/constants.ts(SUPPORTED_PROXY.LAST_VERIFIED). Currentlyv2.0.96. - On every
locateProxy()call, reads/health.versionfrom the proxy and compares it against that constant. - Emits exactly one log line per session when it detects a version it hasn't been verified against:
| Detected proxy version | Plugin reaction | Log level |
|---|---|---|
Equal to LAST_VERIFIED |
Silent — only the regular "connected to WindsurfAPI at … (vX.Y.Z)" line | info |
Newer minor / patch (e.g. v2.0.97, v2.1.0) |
"Newer than verified — should be fine, file an issue if anything breaks" | info |
Older than MIN (e.g. v2.0.85) |
"Older than verified — some endpoints may behave differently. Upgrade to vX.Y.Z." | warn |
Newer major (e.g. v3.0.0) |
"MAJOR version ahead — breaking changes likely. Pin to vX.Y.Z." | warn |
| Missing / unparseable | Silent (already validated as WindsurfAPI via the provider marker on /health) |
— |
The plugin never blocks on a version mismatch — upstream may legitimately fix something in a minor that we haven't re-verified yet, and hard-blocking would force users to wait for a plugin release just to use a proxy patch.
- Check the plugin's startup log — look for the
(vX.Y.Z)line and the compat warning. Most likely cause: the proxy was bumped past what this plugin understands. - Pin the proxy to the last verified version (no plugin reinstall needed):
cd ~/.windsurfapi
git fetch --tags
git checkout v2.0.96
npm install # only if package.json moved
# restart the proxy:
lsof -ti:3003 | xargs kill 2>/dev/null
node src/index.js- If the failure is on a minor bump and looks specific (e.g. a single model started returning empty responses, or a header was renamed), please open an issue with the proxy version and the failing request. Bumping
SUPPORTED_PROXY.LAST_VERIFIEDin the plugin is then a 1-line PR after the contract is re-verified.
Anyone forking the upstream proxy needs to preserve these endpoints / payload shapes:
| Endpoint | Method | What we read / send |
|---|---|---|
/health |
GET | { provider, version, accounts.active } — used for locate + compat check |
/v1/models |
GET | OpenAI-style models list — used by @ai-sdk/openai-compatible |
/v1/chat/completions |
POST | OpenAI chat semantics (streaming) — non-Claude requests |
/v1/messages |
POST | Anthropic semantics with tool_use blocks (streaming) — Claude requests |
/auth/login |
POST | Accepts either { apiKey } or { token } payloads |
/auth/accounts |
GET | { accounts: [...] } — used during sign-in flows and for open-mode detection |
/dashboard/api/system-prompts |
PUT | X-Dashboard-Password header + { communicationWithTools, communicationNoTools } body — optional, soft-skipped on 401 |
Per request, the plugin's fetch hook does this:
opencode (TUI)
→ @ai-sdk/openai-compatible
→ THIS PLUGIN
1. shrink body (drop dup/empty system messages)
2. pick endpoint: claude-* → /v1/messages (Anthropic body)
other → /v1/chat/completions
3. inject cache_control on system + tools prefix (Anthropic only)
4. set Authorization: Bearer + headers
5. fire request to proxy
6. stream response, scrubbing Cascade/Windsurf mentions
→ WindsurfAPI proxy
├─ tool emulation (handlers/tool-emulation.js)
└─ Cascade Language Server (gRPC)
→ Windsurf cloud
→ upstream model (Anthropic / OpenAI / Google / …)
On loader init (once per session) the plugin additionally:
- Locates the proxy — autodetects on common ports, overridable via
options.baseURLorWINDSURF_API_URL. - Verifies the stored bearer actually works (a quick
GET /auth/accountsround-trip). - Pushes OpenCode-tuned prompts into the proxy's runtime config via
PUT /dashboard/api/system-prompts— debounced to once per 12h, soft-skipped if no dashboard password is available.
Two slots get written to the proxy's runtime-config.json, overriding Cascade's defaults at proto-level (via SectionOverrideConfig on communication_section):
communicationWithTools— "You are running as the backing model for OpenCode. The OpenCode client executes tools locally. Call the function instead of describing what you would do; don't reference/tmp/windsurf-workspace/; match the user's language."communicationNoTools— "You are running as the backing model for OpenCode (no tools attached). No file/shell/web access; don't claim to have viewed anything; match the user's language."
Deliberately omitted:
- Identity manipulation ("don't call yourself Cascade") — Cascade's anti-prompt-injection layer rejects requests that contain such instructions. We do identity cleanup on the outbound stream instead (
postSanitize). - Tool-call format reinforcement — the proxy already inserts the correct markup per model (
openai_json_xml/glm47/kimi_k2…) viahandlers/tool-emulation.js. Pushing our own would duplicate or contradict it.
Per WindsurfAPI's own notes:
Claude family
<tool_use>protocol is the most reliably trained. GPT in cascade DEFAULT planner mode is unstable for tools because cascade doesn't transmit the OpenAItools[]schema natively. GLM-5.1 has frequent empty-response regressions.
The Anthropic path keeps tool_use / tool_result blocks native end-to-end and noticeably improves tool-call fidelity. For non-Claude models, OpenAI chat/completions is what they were trained on, so we use that.
The Windsurf desktop app isn't installed or you're signed out. The plugin checks these paths for state.vscdb:
- macOS:
~/Library/Application Support/Windsurf/User/globalStorage/state.vscdb - Linux:
~/.config/Windsurf/User/globalStorage/state.vscdb - Windows:
%APPDATA%\Windsurf\User\globalStorage\state.vscdb
Fix: install Windsurf desktop and sign in once, or use "paste auth token".
You're on the Free Windsurf plan. See the warning at the top — Free is effectively non-functional through any API surface. Upgrade to Pro.
The proxy enforces a dashboard password and you haven't given it to the plugin. Either:
export WINDSURFAPI_DASHBOARD_PASSWORD=<your DASHBOARD_PASSWORD>…or set it in the plugin options:
"windsurf": { "options": { "dashboardPassword": "..." } }…or just ignore it — the plugin's own stream sanitiser still scrubs Cascade mentions client-side. The push is a "make it cleaner on the wire" optimisation, not a hard requirement.
The proxy's per-IP bruteforce counter triggered because something hit the dashboard with the wrong password too many times (often: repeated OpenCode restarts before the dashboard password was set up). Restart the proxy to clear the in-memory ban table; the plugin will then back off for 12h and not retry. This shouldn't happen with the current plugin — it skips the push entirely when no dashboardPassword is configured.
You're probably using a non-Claude model. Switch to claude-sonnet-4.6 and the difference is night and day. If you must use a GPT or Gemini model, accept that tool-call reliability is lower at the API layer — it's a property of how Cascade transmits those tool specs upstream, not the plugin.
The proxy keeps the Cascade Language Server warm but Cascade itself spins up a session per inactive period. First request after >5 min idle = ~1-2s overhead, subsequent requests are fast.
Q: Does this expose my Windsurf credentials?
No. The token never leaves your machine — sign-in posts it to localhost:3003/auth/login, which stores it in the proxy's own data/accounts.json. The plugin stores nothing in OpenCode's auth.json beyond what's needed to talk to the proxy (in open mode that's literally an empty string).
Q: Can I add multiple Windsurf accounts to the same proxy?
Yes — open http://localhost:3003/dashboard (password from DASHBOARD_PASSWORD) and add accounts there. The proxy load-balances between active accounts automatically. The plugin's sign-in flow only adds one account at a time though.
Q: Is this legal / sanctioned by Windsurf? Same answer as the underlying WindsurfAPI project: it's reverse-engineered, not officially supported. Windsurf could theoretically close the loophole in any update. Use accordingly — fine for personal use, not recommended as a hard production dependency.
Q: Why not embed the proxy directly in the plugin? Because then this plugin would dictate one specific deployment shape (localhost? Docker? VPS?). WindsurfAPI already owns its own lifecycle excellently (LS supervision, account pool, dashboard, self-update). The plugin is just the OpenCode-specific glue.
Q: Will this work with opencode run (non-interactive mode)?
Yes for most models. Avoid glm-4.7 and the deprecated gpt-4o-mini family in agent loops — they sometimes return empty responses that break the loop.
git clone https://github.com/ilkinnabiev/opencode-windsurf-auth.git
cd opencode-windsurf-auth
npm install
npm run typecheck # tsc --noEmit
npm run build # tsc → dist/
# Smoke-test against a running proxy. Note: helpers are at dist/helpers.js
# (index.ts deliberately exports only the plugin factory — see note below).
WINDSURF_API_URL=http://localhost:3003 node -e \
"import('./dist/helpers.js').then(({ locateProxy }) => locateProxy().then(console.log))"Helpers (probe / login functions usable from your own scripts) are exposed via opencode-windsurf-auth/helpers:
import {
locateProxy, verifyProxyApiKey, listAccounts,
loginWithWindsurfToken, applyOpenCodePrompts,
} from "opencode-windsurf-auth/helpers";index.ts exports only the plugin factory — OpenCode iterates every named export from a plugin module and tries to call it as a factory, so helpers must live behind the /helpers subpath to avoid loader crashes.
This plugin would not exist without the work of the WindsurfAPI project by @dwgx and contributors. WindsurfAPI does all the genuinely hard work — reverse-engineering the Windsurf Cascade gRPC protocol, supervising the local Language Server, normalising tool-call dialects across model families, managing the multi-account pool, exposing the dashboard, and sanitising Cascade fingerprints from upstream responses.
This plugin is purely the OpenCode-side adapter: it speaks HTTP to a WindsurfAPI proxy, picks the right endpoint per model, and threads OpenCode's auth flow through to the proxy's /auth/login. All credit for the heavy lifting belongs upstream — please star dwgx/WindsurfAPI if you find this useful.
MIT — see LICENSE.
Depends on WindsurfAPI (MIT), which is not bundled and runs as a separate process.
{ "$schema": "https://opencode.ai/config.json", "plugin": [ "opencode-windsurf-auth@file:///Users/YOU/.opencode/plugins/opencode-windsurf-auth" ], "provider": { /* see step 3 */ } }