Skip to content
Projects

dwgx/WindsurfAPI has 3,123 GitHub stars and 635 forks. GitHot measured +1 stars in 24 hours. Last checked 2026-11-04 15:33 UTC. Last push 2026-11-01.

How these counts are measured

Turn Windsurf / Devin Desktop's 100+ AI models (Claude, GPT, Gemini, DeepSeek, Kimi, GLM, SWE) into OpenAI-, Anthropic- & Gemini-compatible APIs. Zero-dependency self-hosted reverse proxy for Claude Code, Cline & Cursor. 把 Windsurf/Devin 云端 100+ 模型变成三套兼容 API。

Measured star growth

in 24h
+1
in 7 days
Update delayed
in 15 days
Update delayed
in 1 month
Update delayed

Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.

README.en.md

Original

WindsurfAPI

WindsurfAPI · DevinAPI

Turn Windsurf / Devin's 100+ AI models (Claude, GPT, Gemini, DeepSeek, Kimi, GLM, SWE…) into OpenAI / Anthropic / Gemini standard APIs. Zero npm runtime dependencies.

History Ledger · Every commit laid out: 1387 commits, 196 releases, 82 PRs, 180 issues (the ledger's own frozen scope — its numbers stop at v3.9.34 / 2026-09-15) — timeline, contributor analysis, Git tree (vertical / horizontal / ring), and a full account of every mistake along the way. Open the interactive ledger (zero-dependency, pure vanilla).

Stars  License  Release  CI  Docs  Follow  ·  中文/简体中文

Notice

If you haven't starred and followed: commercial use, resale, paid deployment, hosting as a backend for public services, or reselling as a relay service is strictly prohibited. If you have starred and followed: go ahead, I'll look the other way.

The code itself is MIT-licensed (see LICENSE); the above is the author's personal stance.


Turns Windsurf (formerly Codeium, now Devin Desktop)'s AI models into four standard, compatible APIs:

  • POST /v1/chat/completions — OpenAI Compatible for any OpenAI SDK.
  • POST /v1/completions — OpenAI legacy Completions (non-stream; prompt becomes one user turn — stream via chat).
  • POST /v1/responses — OpenAI Responses Compatible (plus GET / DELETE /v1/responses/{id} to retrieve or drop a stored response — these need an identity header, see below).
  • POST /v1/messages — Anthropic Compatible for direct connection with Claude Code / Cline / Cursor.
  • POST /v1beta/models/* — Gemini Compatible for direct Gemini SDK use.

Auth transport: prefer the Authorization: Bearer <API_KEY> header, or Gemini's x-goog-api-key header. ?key= is accepted for Gemini-native clients, but it puts the key in the URL, where reverse proxies, access logs, browser history and packet captures can retain it. The bundled nginx.conf logs only $uri (the query string is dropped); proxies you add are yours to configure. ?key= is supported but deprecated — see docs/QUERY-KEY-DEPRECATION.md for the removal plan and the header migration.

100+ Models: Claude 4.5/4.6/Opus 4.7/5 · GPT-5/5.1/5.2/5.4/5.5/5.6-Luna series · Gemini 2.5/3.0/3.1 · Grok · Qwen · Kimi K2.x · GLM 4.7/5/5.1/5.2 · MiniMax · SWE 1.5/1.6/1.7/2 · Arena, etc. Zero npm dependencies, pure Node.js.

Keywords: Windsurf reverse proxy · Devin API · Claude Code proxy · Cursor mirror · free Claude/GPT/Gemini · Codeium API · OpenAI-compatible endpoint · self-hosted LLM gateway

How it works · Quick start · Client setup · Env switches · All docs · 中文

Project Scope and Usage Boundaries

WindsurfAPI is an independent, open-source, self-hosted protocol-compatibility gateway for connecting Windsurf / Devin account capabilities that the deployer owns or is authorized to use to OpenAI-, Anthropic-, and Gemini-compatible clients.

Usage Model

  • Bring Your Own Account (BYO-account): the deployer supplies and controls their own upstream accounts and credentials.
  • The project itself does not provide, sell, rent, share, or bundle any Windsurf / Devin accounts, subscriptions, or credentials.
  • Multi-account support is for account sets owned or authorized for management by the same deployer — for example failover, health checks, and rate-limit isolation; it is not a public shared account pool.
  • The project aims at protocol interoperability, self-hosted client compatibility, and engineering research; it is not intended for subscription resale, cross-user account sharing, bypassing upstream quotas, or circumventing access controls.
  • Deployers are responsible for confirming that their accounts, subscriptions, and specific usage comply with the applicable upstream terms of service and applicable law.

Independent Project Statement

WindsurfAPI is an independent open-source project and is not affiliated with, authorized by, or endorsed by Cognition, Windsurf, Devin, OpenAI, Anthropic, Google, or any other model provider, unless explicitly stated otherwise.

The project code is released under the MIT License. The MIT License grants rights to this project's code; it does not grant access to any third-party service, model, account, or subscription.

This section only sets the boundaries; the rest of this document covers what it actually does.

What is it doing?

flowchart LR
    subgraph clients["Your clients"]
        A["OpenAI SDK<br/>curl / frontend"]
        B["Claude Code<br/>Cline · Cursor"]
        C["Gemini SDK"]
    end

    subgraph gw["WindsurfAPI (this service · port 3003)"]
        direction TB
        R["Protocol translation<br/>OpenAI ↔ Anthropic ↔ Gemini"]
        P["Account pool<br/>round-robin · rate-limit isolation · failover · breaker"]
        N["Identity neutralisation<br/>strips the upstream Windsurf identity"]
        R --- P
        R --- N
    end

    LS["Language Server<br/>(Windsurf binary)"]
    UP["Windsurf cloud<br/>server.self-serve.windsurf.com"]
    DC["Devin cloud<br/>(DEVIN_CONNECT path)"]

    A -- "/v1/chat/completions" --> R
    B -- "/v1/messages" --> R
    C -- "/v1beta/models/*" --> R
    R -- "gRPC" --> LS
    LS -- "HTTPS" --> UP
    R -. "HTTPS (optional direct)" .-> DC

    classDef gwStyle fill:#1f6feb22,stroke:#1f6feb,stroke-width:2px
    classDef upStyle fill:#8957e522,stroke:#8957e5
    class gw gwStyle
    class UP,DC upStyle

What it does:

  1. An HTTP service (port 3003) exposing both OpenAI and Anthropic APIs simultaneously.
  2. Translates requests into Windsurf's internal gRPC protocol and sends them to the Windsurf cloud via a local Language Server.
  3. Manages an account pool with automatic round-robin, rate limiting, and failover.
  4. Strips the upstream Windsurf identity before returning, making the model identify as "I am Claude Opus 4.6, developed by Anthropic."

How to use with Claude Code / Cline / Cursor

The model itself does not operate on files — file operations are executed locally by the IDE Agent client (Claude Code, Cline, etc.):

sequenceDiagram
    actor U as You
    participant CC as Claude Code
    participant WA as WindsurfAPI
    participant WC as Windsurf Cloud

    U->>CC: "Help me fix a bug"
    CC->>WA: POST /v1/messages<br>messages + tools + system
    WA->>WC: Package into Cascade request
    WC-->>WA: Model thinks → returns<br>tool_use(edit_file)
    WA-->>CC: Anthropic SSE<br>content_block=tool_use
    CC->>CC: Execute edit_file() locally<br>(Read/write local files)
    CC->>WA: Send tool_result
    WA->>WC: Continue conversation...
    loop Conversation Loop
        WC-->>WA: Response
        WA-->>CC: SSE stream
    end
    CC-->>U: Final answer

Key Point: WindsurfAPI is only responsible for passing tool_use / tool_result. The client CLI is what actually modifies the files.

Quick Start

Runtime: Node.js 22.13+ (24.x is the release gate). Node 20 reached EOL on 2026-04-30 and is unsupported; local Windsurf account import needs the unflagged node:sqlite from 22.13, and earlier 22.x returns sqlite_unavailable.

One-Click Deployment

git clone https://github.com/dwgx/WindsurfAPI.git
cd WindsurfAPI
bash setup.sh          # Create directories · Set permissions · Generate .env
node src/index.js

Dashboard: http://YOUR_IP:3003/dashboard

Docker Deployment

cp .env.example .env
# Empty API_KEY / DASHBOARD_PASSWORD is fail-closed (compose binds 0.0.0.0).
# Compose defaults to DEVIN_CONNECT=1 — no Language Server, no LS auto-download.
# LS install on first boot only runs if you turn DEVIN_CONNECT off (Cascade).

docker compose up -d --build
docker compose logs -f

Default mounts:

  • ./.docker-data/data: persisted accounts.json, proxy.json, stats.json, runtime-config.json, model-access.json, and logs/
  • ./.docker-data/opt/windsurf: Language Server binary and its data directory
  • ./.docker-data/tmp/windsurf-workspace: temporary workspace

If you want a different persistence location, set DATA_DIR in .env. The Docker setup defaults it to /data.

One-Command Docker Update

Docker deployments use deploy/update.sh: pull the new image → recreate only windsurf-api → wait for /health → roll back to the previous image automatically on failure. Data volumes are never touched, and no docker.sock mount or Watchtower is required.

bash deploy/update.sh              # pull :latest and update
bash deploy/update.sh --tag 3.9.52 # pin a release
bash deploy/update.sh --build      # rebuild from this checkout (local/fork images)
bash deploy/update.sh --dry-run    # print the plan; change nothing
bash deploy/update.sh --rollback   # restore the image from before the last update

Full walkthrough, health gate, rollback and troubleshooting: docs/DOCKER-UPDATE.md.

The update.sh below is for git/PM2 deployments. Docker deployments use deploy/update.sh.

One-Click Update

To pull the latest fixes after deployment, just run one command:

cd ~/WindsurfAPI && bash update.sh

update.sh does: git pull → updates the LS binary via install-ls.sh → stops PM2 → kills any residual process on port 3003 → restarts → health check.

If you are using our public instances (skiapi.dev, etc.), you don't need to do anything; we've already pushed the updates.

Manual Installation

git clone https://github.com/dwgx/WindsurfAPI.git
cd WindsurfAPI

# Language Server binary — auto-detects Linux/macOS, one-click download + chmod
bash install-ls.sh

# Download chain (manifest-driven, fail-closed):
#   1. Read https://windsurf-stable.codeium.com/api/versions
#   2. Pick the newest pinned stable release in ls-pins.json (currently 3.10.67)
#      and require the manifest's commit + archive sha256 to match the pin
#   3. Download the desktop archive → verify archive sha256 → extract the LS →
#      verify the inner binary sha256
# Any mismatch exits non-zero; there is no silent fallback to the old mirror.
#
# Print the plan without downloading (recommended before the first install):
#   bash install-ls.sh --dry-run
#
# Pin a version / override the source:
#   bash install-ls.sh --version 3.10.67
#   WINDSURFAPI_LS_VERSION=3.10.67 bash install-ls.sh
#   WINDSURFAPI_LS_MANIFEST_URL=<url|local path> bash install-ls.sh
#   WINDSURFAPI_LS_PINS=/path/to/ls-pins.json bash install-ls.sh
#
# Roll back to the pinned 3.5.17 (explicit dwgx/windsurf-ls-release v3.5.17 tag):
#   bash install-ls.sh --rollback
#   same as: bash install-ls.sh --version 3.5.17 --allow-mirror
#
# Default install paths:
#   Linux x64:           /opt/windsurf/language_server_linux_x64
#   macOS Apple Silicon: $HOME/.windsurf/language_server_macos_arm
#   macOS Intel:         $HOME/.windsurf/language_server_macos_x64
#   Linux arm64:         no upstream archive; install a desktop binary with --file.

# Or use a local binary you already have:
#   bash install-ls.sh /path/to/language_server_linux_x64
# Or specify a custom URL (prefer --sha256; without it the installer warns):
#   bash install-ls.sh --url https://example.com/language_server_linux_x64 --sha256 <64-hex>

# ⚠️ Platform not covered by the upstream archive / want a different source?
# Upstream only ships linux-x64, darwin-arm64 and darwin-x64 desktop archives.
# For other platforms, copy the LS binary out of the Windsurf desktop app:
#
#   macOS:   "$HOME/Library/Application Support/Windsurf/resources/app/extensions/windsurf/bin/language_server_macos_arm"
#   Linux:   "$HOME/.windsurf/bin/language_server_linux_x64"
#            or /opt/Windsurf/resources/app/extensions/windsurf/bin/language_server_linux_x64
#   Windows: %APPDATA%\Windsurf\bin\language_server_windows_x64.exe
#
#   # Install from the local desktop copy:
#   bash install-ls.sh /path/to/language_server_linux_x64
#
# Note: swapping the LS binary does not change /v1/models.
# The catalog is fetched by the proxy over HTTPS (GetCascadeModelConfigs /
# GetCliModelConfigs). ideVersion is hardcoded in src/windsurf-api.js and is
# not read from the binary — so the listing depends on what upstream grants
# this account. A missing new model is an entitlement gap, not a stale file.

cat > .env << 'EOF'
PORT=3003
# Empty API_KEY is fail-closed (401) even on localhost.
# Local open access: WINDSURFAPI_ALLOW_UNAUTHENTICATED=1 and HOST=127.0.0.1
API_KEY=
DEFAULT_MODEL=claude-sonnet-4.6
MAX_TOKENS=8192
LOG_LEVEL=info
LS_BINARY_PATH=/opt/windsurf/language_server_linux_x64
LS_DATA_DIR=/opt/windsurf/data
LS_PORT=42100
# Empty DASHBOARD_PASSWORD is fail-closed. Local open panel: DASHBOARD_ALLOW_NO_AUTH=1
DASHBOARD_PASSWORD=
EOF

# For a local macOS run, use the LS_BINARY_PATH printed by install-ls.sh
# and set LS_DATA_DIR to a user-writable path such as /Users/you/.windsurf/data.

# Note: Inline comments are supported in .env for unquoted values:
#   PORT=3003  # Service port
# Quoted values preserve everything inside the quotes.

node src/index.js

Add Accounts

After the service is running, you need to add Windsurf accounts. There are three ways:

Method 1: Dashboard One-Click Login (Recommended)

Open http://YOUR_IP:3003/dashboard → Login to get token → Click Sign in with Google or Sign in with GitHub (OAuth popup) or fill in email/password directly. All methods will automatically add the account to the pool.

Method 2: Token (Works with any login method)

Go to windsurf.com/show-auth-token to copy your token:

curl -X POST http://localhost:3003/auth/login \
  -H "Content-Type: application/json" \
  -d '{"token": "YOUR_TOKEN"}'

Method 3: Batch

curl -X POST http://localhost:3003/auth/login \
  -H "Content-Type: application/json" \
  -d '{"accounts": [{"token": "t1"}, {"token": "t2"}]}'

Usage Examples

OpenAI Format (Python / JS / curl)

from openai import OpenAI
client = OpenAI(base_url="http://YOUR_IP:3003/v1", api_key="YOUR_API_KEY")
r = client.chat.completions.create(
    model="claude-sonnet-4.6",
    messages=[{"role": "user", "content": "Hello"}]
)
print(r.choices[0].message.content)

Anthropic Format (Directly with Claude Code)

export ANTHROPIC_BASE_URL=http://YOUR_IP:3003
export ANTHROPIC_API_KEY=YOUR_API_KEY
claude                # Use Claude Code as usual
# Raw curl test
curl http://localhost:3003/v1/messages \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -d '{"model":"claude-opus-4.6","max_tokens":100,"messages":[{"role":"user","content":"Hello"}]}'

Cline / Cursor / Aider

In your client's settings for Custom OpenAI Compatible:

  • Base URL: http://YOUR_IP:3003/v1
  • API Key: YOUR_API_KEY
  • Model: Choose any supported model.

Cursor users: Cursor's client-side whitelist blocks model names containing claude (the request never reaches the backend). Use these aliases instead:

Type in Cursor Actual model
opus-4.6 claude-opus-4.6
sonnet-4.6 claude-sonnet-4.6
opus-4.7 claude-opus-4-7-medium
ws-opus claude-opus-4.6
ws-sonnet claude-sonnet-4.6

GPT / Gemini / DeepSeek models are not affected by Cursor's filter — use their original names.

Environment Variables

Variable Default Description
PORT 3003 Service port
API_KEY empty Caller key. Empty is fail-closed (401) even on localhost. Local open access requires WINDSURFAPI_ALLOW_UNAUTHENTICATED=1 on a local bind.
WINDSURFAPI_ALLOW_UNAUTHENTICATED off Allow empty API_KEY on a local bind. Default off. Ignored on a public bind.
DATA_DIR project root Directory for persisted JSON state and logs/. Docker deployments should usually use /data.
CODEIUM_API_KEY empty Direct API key from Windsurf (alternative to token-based auth).
CODEIUM_AUTH_TOKEN empty Token from windsurf.com/show-auth-token.
CODEIUM_EMAIL empty Email for Windsurf account authentication.
CODEIUM_PASSWORD empty Password for Windsurf account authentication.
CODEIUM_API_URL https://server.self-serve.windsurf.com Windsurf cloud API endpoint.
DEFAULT_MODEL claude-sonnet-4.6 The model to use if model is not specified. Must be a name the active backend can resolve. Unmapped Connect names return 400 model_not_found unless WINDSURFAPI_STRICT_MODEL=0, which degrades to a free selector the live catalog actually exposes — and still returns 400 when there is none, instead of falling back to swe-1-6-slow, which the catalog no longer carries.
MAX_TOKENS 8192 Default maximum number of response tokens.
LOG_LEVEL info Console level: debug / info / warn / error. JSONL files and Dashboard still receive all levels.
LOG_RETENTION_DAYS 0 (off) Application JSONL retention in UTC calendar days including today; e.g. 14. Cleanup at startup and hourly. Restart after changes.
LOG_MAX_FILE_MB 0 (off) MiB limit for newly written log files; e.g. 20. Rotate between complete JSONL records.
LOG_MAX_TOTAL_MB 0 (off) Combined app/error log MiB budget; e.g. 500. Evict oldest dated segments, including today's if necessary. Oversized records skip disk persistence. See disk-log policy.
WAPI_ACCOUNTS_ONLY empty (all) Per-instance account isolation: comma-separated emails or account ids; only matching accounts are loaded from accounts.json, case-insensitively. A non-blank value with no valid tokens fails closed (loads no accounts). Use it to pin each port of a multi-port deployment to its own account(s) when they share one accounts.json
WINDSURFAPI_IGNORE_CLOUD_FILTER 0 On the Cascade transport, after per-account cloud catalogs sync, pool listings show their union and routing enforces each selected account's catalog. Set to 1 to restore the full static catalog. Missing, empty, or failed catalog syncs fail open. DEVIN_CONNECT uses its separate selector catalog.
LS_BINARY_PATH /opt/windsurf/language_server_linux_x64 Path to the LS binary.
LS_PORT 42100 LS gRPC port.
LS_DATA_DIR Linux: /opt/windsurf/data; macOS: ~/.windsurf/data Per-proxy LS data directory root.
LS_MAX_INSTANCES adaptive, max 20 Maximum LS pool size. The adaptive default reserves at least one non-default proxy slot on small hosts.
LS_SPAWN_MIN_AVAILABLE_BYTES 700MB Minimum available memory required before starting a new non-default LS.
LS_PREWARM_DEFAULT 1 Set to 0 to skip startup default-LS prewarm and start LS lazily on first request. Useful for low-memory, all-proxy pools.
LS_PREWARM_PROXIES 0 Set to 1 to prewarm all proxy LS instances on startup. Scheduled probes and predictive prewarm only reuse idle resident LS instances.
LS_PREWARM_ON_ACCOUNT_ADD 0 Set to 1 to prewarm LS immediately after dashboard/import/OAuth account add. Default avoids memory spikes during bulk import.
WINDSURFAPI_NATIVE_TOOL_BRIDGE empty Lab/remote-execution opt-in only. all_mapped enables native bridge only when every declared function tool is allowlisted and mappable; 1 enables partition mode for mapped subsets. Do not treat this as a general fix for local IDE tools.
WINDSURFAPI_NATIVE_TOOL_BRIDGE_TOOLS Bash/shell_command/run_command families Tool allowlist for native bridge. The default intentionally contains only the command path. Read / Grep / Glob / WebSearch / WebFetch require explicit allowlisting plus model/account/API-key gates and are still protocol-lab scope, not production defaults.
WINDSURFAPI_NATIVE_TOOL_BRIDGE_MODELS / PROVIDERS / ROUTES / CALLERS / ACCOUNTS / API_KEYS empty Optional native-bridge gray gates. Empty means unrestricted; when set, the request must match. ACCOUNTS accepts upstream account id/email. API_KEYS matches caller API keys without passing plaintext tokens into chat logic.
WINDSURFAPI_NATIVE_TOOL_BRIDGE_OFF empty Set to 1 to force native bridge off.
WINDSURFAPI_SPECIAL_AGENT_BACKEND empty Optional lab-only special-agent backend. Set devin-cli to test swe-1.6, swe-1.6-fast, adaptive, and arena-* through Devin CLI instead of direct Cascade. This is not a normal catalog-model fix.
DEVIN_CLI_PATH devin Devin CLI executable path. Docker/macOS deployments must install or mount it themselves.
DEVIN_CLI_MODE print print uses conservative devin -p; acp is an experimental ACP stdio backend using upstream Windsurf account-pool apiKeys.
DEVIN_MAX_PROCS 1 Maximum concurrent Devin CLI processes.
DASHBOARD_PASSWORD empty Dashboard password. Empty is fail-closed even on localhost. Local open panel requires DASHBOARD_ALLOW_NO_AUTH=1.
ALLOW_PRIVATE_PROXY_HOSTS empty Set to 1 to allow private/internal IPs (e.g., 192.168.x.x, 10.x.x.x) in proxy tests and login. Leave empty to only allow public addresses (default).
CASCADE_REUSE_STRICT 0 Set to 1 for strict conversation reuse mode (waits for same fingerprint).
CASCADE_REUSE_STRICT_RETRY_MS 60000 Retry delay in ms for strict reuse mode.
CASCADE_REUSE_HASH_SYSTEM 1 System messages are hashed into the conversation reuse fingerprint by default. Set to 0 to opt out — raises the reuse hit rate for callers whose system prompt drifts every turn (Claude Code with cwd snapshots), at the cost of reuse isolation: two requests with different system prompts can then share one pooled conversation.
CASCADE_REUSE_BY_CALLER 0 Set to 1 to enable caller-based fallback reuse. When fingerprint misses, falls back to the latest cascade for the same caller+model. Best for single-user Claude Code setups.
CASCADE_POOL_MAX 500 Max conversation pool entries. Set to 1–5 for single-user setups to minimize resource usage.
STICKY_SESSION_ENABLED 0 Set to 1 to pin each conversation to one upstream account. Strongly recommended on DEVIN_CONNECT: upstream prompt caches are per-account and a cache write costs ~5.6× a read (devin-connect.js 17.8%-of-miss calibration), so without pinning every turn rotates accounts and re-writes the whole context. Requires a per-user signal on the caller (user / safety_identifier / prompt_cache_key / Claude Code metadata.user_id); single-user self-hosts without one set WINDSURFAPI_SINGLE_TENANT_CACHE=1. Observe via the sticky field of /dashboard/api/connect-metrics.
STICKY_SESSION_TTL_MS 1800000 Binding TTL (30 min); active conversations auto-renew each turn.
STICKY_SESSION_MAX 10000 Binding table cap, LRU-evicted.
RESPONSE_STORE_ENABLED 1 Responses API server-side conversation state. With it on, previous_response_id continues a conversation (the client sends only the new turn); set 0 and such requests get a 400, as do GET/DELETE /v1/responses/{id}. Scoped by callerKey. Retrieval and deletion use the same scope as chaining: another caller's id always 404s, without revealing whether it exists. This row used to say "tenants cannot read each other's conversations", which overstated where the guarantee comes from — the scope is not a secret. Behind one shared API key the callerKey is api:{hash(apiKey)}:user:{hash(body.user)}, and user is typically an email or account id, i.e. guessable. What actually prevents a cross-read is the 90 bits of entropy in the response id (resp_ + a dash-stripped UUIDv4 truncated to 24 hex chars — 96 bits of width less the version/variant bits that fall inside the slice). Measured: with the scope entirely correct but the id wrong, the lookup still returns not_found. So the isolation does hold, but it holds because you have to hit a 90-bit id — not because the scope separates tenants. Do not treat user as an access control. Retrieval/deletion carry no request body, so the identity signal rides a header: GET /v1/responses/{id} with x-response-prompt-cache-key: <the value you sent on POST>. All six scope signals work: user / prompt_cache_key / safety_identifier / conversation / conversation_id / session_id. Header names replace underscores with hyphens (x-response-conversation-id); the query fallback accepts either spelling (?conversation_id= and ?conversation-id=, and likewise for the other multi-word signals). It must match the value used when the response was created, otherwise 404. Send ONE scope signal and send it consistently — supplying several different scope signals folds them all into a single identity, so adding one changes the derived key (pre-existing behaviour, not specific to the query channel). Query strings are logged by reverse proxies, CDNs and browser history and user often contains PII — prefer the header.
RESPONSE_STORE_TTL_MS 3600000 An idle timeout (1 hour), not a retention ceiling. It measures time since last access, and every successful GET refreshes that — so on its own, periodic reads keep an entry alive indefinitely. The absolute bound is the next row.
RESPONSE_STORE_MAX_AGE_MS 86400000 The absolute retention bound (24 hours), measured from when the entry was created and never refreshed by reads. The default sits well above a long agent session rather than close to the idle timeout: dropping a running loop's context mid-flight is a worse failure than retaining it longer, and total memory is already bounded by the byte and count caps.
RESPONSE_STORE_MAX 2000 Max stored conversations, LRU-evicted with a per-tenant fair share.
RESPONSE_STORE_MAX_BYTES 128m Total byte budget for stored conversations (b/k/kb/m/mb/g/gb). The count caps bound cardinality, not memory — a realistic agent conversation measures ~167KB, so 2000 entries is ~327MB. Eviction triggers on whichever limit binds first.
DEVIN_CONNECT_IMAGE_TAG 10 DEVIN_CONNECT image-field tag. Defaults to repeated field #10, independently verified by the extracted Devin schema and a native SWE-1.7 capture; set 0 to stop emitting images.
DEVIN_CONNECT_COLLAPSE_SYSTEM 0 Set 1 to wrap system content in <system>...</system> and merge it, in order, into the next user message, avoiding the stricter upstream policy path on field #2. Default off; when tools require a non-empty field #2, only the existing benign placeholder remains there.
DEVIN_CONNECT_REPLAY_SIGNED_THINKING 0 Set 1 to replay signed Claude thinking from /v1/messages history natively as #11 thinking + #12 signature (tool-call and plain assistant turns), so the model can read its earlier reasoning inside a tool loop and after a new user message. Needs DEVIN_CONNECT_SIGNATURE_TAG=10 so clients receive the signature at all. The upstream verifies the signature; only an untouched block is replayed. Details in docs/ENV-SWITCHES.md.
DEVIN_CONNECT_CATALOG_TTL_MS 300000 Successful DEVIN_CONNECT live-catalog TTL (5 minutes by default, 10-second minimum). Accounts sync independently; failed or empty responses keep the last-known-good catalog.

The full list lives in .env.example; the table above covers the common ones.

Enabling images / vision

The DEVIN_CONNECT backend now emits inline images using the native Devin wire by default:

# 10 is already the default; use only for an emergency rollback
DEVIN_CONNECT_IMAGE_TAG=0

Tag 10 has two independent sources of truth: the extracted Devin schema declares ChatMessagePrompt.images as repeated #10 and ImageData as base64_data #1 / mime_type #2; a native SWE-1.7 request attaches the image directly to a source=USER message, receives modelUid=swe-1-7, and correctly identifies the macOS Dock, Sketch, QQ, and WPS. The gateway therefore no longer invents an assistant read tool call, a synthetic tool result, or a top-level read ToolDef, and it no longer rejects vision from a model merely because its name starts with swe-.

Ordinary user images remain on their original user message; multiple images are repeated on that same message. Images in a native tool result remain source=TOOL_RESULT and preserve the caller's tool_call_id #7. Live catalog synchronization also decodes ClientModelConfig.supports_images #5 and exposes supports_images through /v1/models when upstream explicitly supplies true or false. A missing field remains unknown rather than being invented as false. Models carrying upstream disabled #4 do not enter the live catalog.

DEVIN_CONNECT_IMAGE_INNER_TAGS can still override the inner base64,mime tags (default 1,2) as an emergency calibration surface for a future wire change. Remote http(s):// image URLs are now downloaded by streamChat before the proto is assembled, through the same bounded fetchImageUrl the Cascade path uses (8 s total budget, 5 MB cap, image/* allowlist, and a private/loopback rejection on every redirect hop), then inlined as data URLs in #10. A failed, oversized, or wrong-type fetch logs a warning (host only — never a possibly signed full URL) and drops just that image; the text still goes upstream. With DEVIN_CONNECT_IMAGE_TAG=0 nothing is downloaded. Alternatively, explicitly enable DEVIN_ACP_VISION=1 to route vision through a locally installed Devin CLI ACP transport.

Remote images are https-only by default: plaintext http:// bytes can be replaced by any middlebox on the path, and those bytes are inlined verbatim into the model's context (#10 / Cascade image blocks). A rejected fetch drops just that image and still sends the text, logging a warning with the host only (never the full URL, which may carry a signed query). WINDSURFAPI_ALLOW_HTTP_IMAGES=1 re-allows plaintext http for public hosts only (both http and https); the default or any other value accepts https only. Private/internal/loopback targets stay blocked regardless of the switch — the SSRF policy is separate and not relaxable (including per-hop DNS resolutions), so it cannot be used for a trusted LAN mirror. The check runs on every fetchImageUrl hop, so an https → http redirect is rejected too.

Dashboard Features

Open http://YOUR_IP:3003/dashboard:

Panel Features
Overview Runtime status · Account pool · LS health · Success rate
Login/Get Token Google / GitHub OAuth one-click login · Email/password login · Test Proxy button (tests egress IP)
Account Management Add / Delete / Disable · Detect subscription level · Check balance · Ban models via blacklist
Model Control Global model whitelist/blacklist
Proxy Config Global or per-account HTTP / SOCKS5 proxy
Logs Real-time SSE streaming · Filter by level · turns=N chars=M diagnostics per turn
Stats & Analytics Time range 6h / 24h / 72h · Per-account dimensions · p50 / p95 latency
ACU & Usage Org-scoped ACU limit · cycle usage · daily burn-down · per-product breakdown · overage balance in USD
Experimental Cascade conversation reuse · Model Identity Injection (custom prompt per vendor)

The ACU & Usage panel reads app.devin.ai/{org}/billing/* once per organization, so members sharing an org render one ledger instead of one per account. Truth rules: max_acu_limit: null means unlimited (never zero), an absent cycle total is unknown (—), and overage_credits is a signed USD balance. A negative balance is an overdrawn on-demand balance: it renders as -$0.10 (overdrawn) and is never clamped to zero or hidden.

Supported Models

100+ static models in the main catalog plus dynamic cloud-side models added at startup via mergeCloudModels. On the Cascade transport, after per-account cloud catalogs sync, GET /v1/models and the Dashboard show the union available across active accounts, while routing applies the selected account's own catalog. DEVIN_CONNECT remains governed by its separate selector catalog. The full static catalog remains available on the GitHub Pages model catalog (auto-generated from src/models.js).

Claude (Anthropic) — 80 models

claude-3.5-sonnet / 3.7-sonnet / thinking · claude-4-sonnet / opus / thinking · claude-4.1-opus · claude-4.5-haiku / sonnet / opus · claude-sonnet-4.6 (incl. 1m / thinking / thinking-1m) · claude-opus-4.6 / thinking · claude-opus-4.7-medium · claude-opus-4.8 series (low / medium / high / xhigh / max + fast) · claude-5-fable / claude-sonnet-5 / claude-opus-5 series (low / medium / high / xhigh / max; opus-5 incl. fast) · claude-fable-5.1 / claude-sonnet-5.5 / claude-opus-5.5 series (low / medium / high / xhigh / max)

GPT (OpenAI) — 118 models

gpt-4o · gpt-4.1 · gpt-5 series (incl. medium / high / codex) · gpt-5.1 series (base / low / medium / high + fast + codex, all 6 variants) · gpt-5.2 series (none / low / medium / high / xhigh + fast + codex) · gpt-5.4 series (base / mini × low/medium/high/xhigh) · gpt-5.5 series (none / low / medium / high / xhigh + fast) · gpt-5.6-luna series (none / low / medium / high / xhigh / max) · gpt-5.6-sol / terra series (none / low / medium / high / xhigh / max) · gpt-6 family (sol / luna: none / low / medium / high / xhigh / max; astra and gpt-6.1-sol: low / medium / high / xhigh / max) · o3 series (base / mini / pro) · o4-mini

Gemini (Google) — 26 models

gemini-2.5-pro / flash · gemini-3.0-pro / flash (minimal / low / medium / high — 4 reasoning levels) · gemini-3.1-pro (low / high) · gemini-3.6-flash (minimal / low / medium / high) · gemini-3.7 / 3.8-flash (low / medium / high) · gemini-4-argon (low / medium / high)

Open source / Chinese providers

Kimi: kimi-k2 / k2.5 / k2-6 / k2-7 / k3 · GLM: glm-4.7 / 5 / 5.1 / 5.2 / 5.3 · Qwen: qwen-3 · DeepSeek: deepseek-v4 / v4.1-flash · Grok: grok-3 / grok-3-mini-thinking / grok-code-fast-1 / grok-4.5 / 4.6 / 4.7 · MiniMax: minimax-m2.5 · Nemotron: nemotron-3-ultra · Inkling: inkling

Windsurf in-house + Arena

swe-1.5 / 1.5-fast / 1.6 / 1.6-fast / 1.7 / 1.7-lightning · arena-fast · arena-smart

DEVIN_CONNECT model spellings: the dotted claude-sonnet-4.6 is a deliberate alias that resolves to the upstream claude-sonnet-4-6-thinking selector (thinking variant, credit 6). For the non-thinking base, ask for the catalog selector claude-sonnet-4-6 explicitly (credit 4). Both spellings are mapped:true — the alias is a curated default, not a downgrade.

Free-account entitlements typically include gemini-2.5-flash, glm-4.7 / glm-5 / 5.1, kimi-k2 / k2.5 / k2-6, qwen-3 and similar open-source models; Claude family, GPT family, and Opus / thinking variants require Pro. Each account's exact list shows up in the dashboard.

Tool-calling reliability (measured v2.0.82+): Claude family is the most reliable (their training covered prompt-level tool protocols); GLM-4.7 / Kimi-K2.5 work for most cases via NLU fallback + optional retry-with-correction; GLM-5.1 is unreliable on the cascade backend (it often returns empty responses, no narration to recover from); GPT family is also limited because the cascade upstream doesn't carry tools[] schema. For Claude Code / Cline / Codex doing local tool calls, prefer claude-haiku-4.5 or claude-sonnet-4.6.

Language-Following for CJK Users

The service automatically detects Chinese, Japanese, or Korean characters in your messages and injects a language-following hint to ensure the model responds in the same language. This fixes the issue where Claude Code's large English system prompt would override the communication language.

Architecture Highlights

  • Zero npm dependencies Everything uses node:* built-ins · Protobuf is handcrafted (src/proto.js) · image codecs vendored (src/vendor/, BSD-3 jpeg-js + an original pure-Node PNG decoder) · download and run.
  • Account Pool + LS Pool Each independent proxy gets its own LS instance, no mixing.
  • NO_TOOL Mode planner_mode=3 disables Cascade's built-in tool loop to prevent /tmp/windsurf-workspace/ path leakage.
  • Three-layer sanitization LS built-in tool result filtering · <tool_call> text parsing · Output path cleaning.
  • Real token counting Fetches real inputTokens / outputTokens / cacheRead / cacheWrite from CortexStepMetadata.model_usage. On DEVIN_CONNECT, prompt_tokens is the full input (fresh + cache_read + cache_write), with the split on prompt_tokens_details.cached_tokens and cache_creation_input_tokens; on Cascade, prompt_tokens carries fresh + cache_read while cache_write lands in total_tokens and cache_creation_input_tokens.

PM2 Deployment

npm install -g pm2
pm2 start src/index.js --name windsurf-api
pm2 save && pm2 startup

Do not use pm2 restart (it can create zombie processes). Use the one-click update script bash update.sh.

Firewall

# Ubuntu
ufw allow 3003/tcp

# CentOS
firewall-cmd --add-port=3003/tcp --permanent && firewall-cmd --reload

Remember to open port 3003 in your cloud provider's security group.

Having Issues? Start Here

Search by symptom — no need to read the whole FAQ. This is a protocol-conversion gateway, so the first step of any investigation is always to figure out which layer is broken: the client, this gateway, or the upstream.

flowchart TD
    S{"Symptom?"} --> A["Request never arrives<br/>connection refused / timeout"]
    S --> B["401 / 403 returned"]
    S --> C["Replies, but tools not called"]
    S --> D["Replies, but content is wrong<br/>lost context / thinking leaked in"]
    S --> E["All accounts down<br/>rate-limited / unavailable"]

    A --> A1["1. Is the service alive?<br/>curl :3003/v1/models"]
    A1 --> A2["2. Is port 3003 open in the firewall?<br/>see the Firewall section"]
    A2 --> A3["3. Raising a timeout in .env won't help<br/>see the context deadline entry"]

    B --> B1["Don't mix up the two key layers:<br/>caller key ≠ upstream account"]
    B1 --> B2["Gemini clients use<br/>x-goog-api-key or ?key="]

    C --> C1["Check ToolRoute[...] in the logs first<br/>it lists why tools were filtered/downgraded"]
    C1 --> C2["Then check for server-side tools<br/>the translation layer drops unimplemented kinds"]

    D --> D1["Lost context → should you use<br/>the /v1/responses chain?"]
    D1 --> D2["Thinking leaked in → enable LEAK_TRACE<br/>and capture boundary logs"]

    E --> E1["First separate: account-limited<br/>or IP-level cooldown?"]
    E1 --> E2["See the 'All accounts<br/>temporarily rate-limited' entry"]

    classDef sym fill:#8957e522,stroke:#8957e5
    classDef act fill:#1f6feb22,stroke:#1f6feb
    class A,B,C,D,E sym
    class A1,A2,A3,B1,B2,C1,C2,D1,D2,E1,E2 act

The two easiest to trip over:

Symptom Real cause
Raised the timeout in .env but context deadline exceeded is still there That timeout lives at a different layer. See the FAQ entry with the same name below
"All accounts rate-limited" right after start, suspecting the proxy is broken Most likely an IP-level cooldown — not an account problem, not a proxy problem

When tracing a request chain, send a real request and read the response — don't just read the code. This is a protocol-conversion gateway with two key layers and four egress paths; reasoning from the source alone leads you astray.

FAQ

Q: Login fails with "Invalid email or password" A: You probably signed up for Windsurf using Google/GitHub, which means your account doesn't have a password. The Dashboard's login panel now directly supports one-click login via Google / GitHub OAuth.

Q: The model says "I cannot operate on the file system" A: This is a chat API, not an IDE agent. To have the model actually modify files, use a client CLI like Claude Code / Cline / Cursor / Aider and point their API base URL to this service. The model will produce tool_use, the client executes it locally, and sends the tool_result back. The diagram above shows the detailed flow.

Q: Context is lost / The model forgets previous parts of the conversation A: Multi-account round-robin will not lose context — every request repackages the full history and sends it to Cascade. The real reason is usually a relay layer (like new-api) not passing the full messages[] array. Check turns=N in the Dashboard logs: if it's a multi-turn conversation but turns=1, then a layer before you has already dropped the history.

Q: Long prompts are timing out A: This has been fixed. Cold stall detection is adaptive to input length (30s base, +5s per 1500 characters) and is capped by CASCADE_MAX_WAIT_MS (default 600s) — the old fixed 90s cap was removed on 2026-04-20.

Q: Can I use Claude Code? A: Yes. export ANTHROPIC_BASE_URL=http://YOUR_API + export ANTHROPIC_API_KEY=YOUR_KEY. /v1/messages supports the full suite: system, tools, tool_use, tool_result, stream, and multi-turn, all tested and working.

Q: What models can free accounts use? A: Mostly gemini-2.5-flash, glm-4.7 / 5 / 5.1, kimi-k2 / k2.5 / k2-6, qwen-3 (open-source series). Claude family, GPT family, and Opus / Max / -thinking variants need Pro entitlement. The dashboard shows each account's entitled list, and model_not_entitled error responses include an available_in_pool field with the names you can switch to. The static cold-start floor is gemini-2.5-flash only; everything else is discovered per account (GetUserStatus allowlist + a successful call), so a fresh account showing one model is expected — run Probe in the dashboard to fill it in.

Q: Are tool calls reliable on free accounts? A: Depends on the model. Claude family is rock-solid (also free-account-entitled when available). GLM-4.7 / Kimi-K2.5 work in most cases via NLU recovery + WINDSURFAPI_NLU_RETRY=1 retry-with-correction. GLM-5.1 is unreliable on the cascade backend (frequent empty responses) — proxy can't fix this. GPT family is similarly limited by the cascade protocol layer not passing tools[] schema. For Claude Code / Cline / Codex doing local file/shell ops prefer claude-haiku-4.5 or claude-sonnet-4.6.

Q: The client shows "tools not being called" — how do I debug this? A: First look for ToolRoute[...] in the logs. It lists the tools your client declared, the effective tools after tool_choice filtering, mapped/unmapped tools for the native bridge, the preamble downgrade tier, and the reason (tool_choice_none / forced_tool_not_declared / preamble_compacted / native_bridge_*). Server-side tools on /v1/messages and /v1/responses (Anthropic advisor/code_execution, OpenAI file_search/mcp/computer_use) are dropped at the translation layer when the proxy doesn't implement them; those aren't ordinary function tools, and their absence doesn't mean WindsurfAPI can execute them on the client's behalf. The native bridge is also not a "local IDE tool fix" switch: the default safe path is prompt/tool emulation with local client-side execution; the native bridge makes the remote Windsurf workspace execute Cascade built-in tools and is only meant for low-traffic experiments behind a model/account/API-key gate.

Q: 31 trial accounts go unavailable after a few hundred calls A: Likely the model is a weekly-quota variant — claude-opus-4-7-max / gpt-5.5-xhigh / claude-opus-4-8-xhigh etc. cap at 5 calls per week per account, so 31 accounts × 5 ≈ 150 calls hit the wall fast. Switch to claude-sonnet-4.6 / claude-haiku-4.5 (daily quotas are much wider). Verify with docker logs windsurfapi-windsurf-api-1 | grep rate_limit — the per-account cooldown reason is in the log.

Q: "All accounts temporarily rate-limited" / IP-level cooldown — is the proxy broken? A: Usually not. Windsurf's upstream applies cooldowns to dense bursts from the same egress IP + same model, so accounts sharing one egress IP get limited together. WindsurfAPI stops burning accounts and returns 429 + Retry-After; since v2.0.140 the wait time reflects the upstream's real value (e.g. Resets in: 27m12s) instead of a fixed 30-second hint. The fixes are lowering concurrency, switching to gentler models, binding accounts to different egress IPs, or waiting for the upstream cooldown to expire.

Q: Is the free tier locally limited to 1 request per minute? A: No. The local free-tier RPM defaults to 10/min. The 1/min you're seeing (or recovery after a while) is usually Windsurf upstream's dynamic free-tier throttling or a model entitlement limit. Check account status and the available-model list in the Dashboard; when you request a model you're not entitled to, the error's available_in_pool field lists what the current pool can use.

Q: Can context deadline exceeded / Client.Timeout be fixed by raising the .env timeout? A: No. Long thinking / long output is cut at roughly 236-243 seconds — that's the Windsurf provider/Cascade single-stream window. WindsurfAPI marks it upstream_deadline_exceeded / windsurf_provider_deadline and discards the half-finished Cascade reuse trajectory so the next round's context doesn't get corrupted. The only real mitigations are splitting the task, lowering reasoning/max output, or switching to a faster model.

Contributors

Huge thanks to the following folks who sent pull requests or systematically audited the code:

  • @dd373156 — PR #1 Fixed the Pro tier model-merge logic: the hardcoded table wasn't picking up dynamically-fetched cloud models, so Pro accounts couldn't see newly-released models in Cursor / Cherry Studio.
  • @colin1112a — PR #13 A single-shot audit that flagged 15 security / concurrency / resource bugs: XSS escaping, shell injection, OOM guards, auth route placement, gRPC double-callback, LS pool race, HTTP/2 frame size caps, and more. On top of this we later added a JS-level escJsAttr, coalesced concurrent ensureLs calls via _pending, released pooled sessions on LS exit, and fixed 6 more issues surfaced by a follow-up Antigravity audit.
  • @baily-zhang — PR #36 + PR #45 Core Cascade reuse fixes: stableTurns fingerprinting (#36) solved 0% hit rate; trajectory offset tracking (#45) eliminated context bloat during multi-turn reuse.
  • @aict666 — PR #44 Fixed inferTier demoting Pro/Trial accounts to free after every chat call, preserving the authoritative tier from GetUserStatus.
  • @smeinecke — PR #43 Full Dashboard i18n: 14 commits covering Chinese/English translations, I18n system, and check-i18n.js validation tool.
  • @you922 — PR #162 + PR #163 Built sticky session from scratch (callerKey + modelKey → accountId binding) + LS crash auto-restart with exponential backoff. Also provided root-cause analysis of SectionOverrideConfig tool-call suppression in #164.
  • @Fermiz — PR #181 Cascade reuse optimization for single-user setups (skip unnecessary rotation) + HTTPS proxy layer + configurable conversation-pool size.
  • @linqichenggg — PR #175 Cross-platform LS path alignment: unified binary paths, data dirs, and install scripts across Windows / macOS / Linux.
  • @lauvww — PR #182 Rewrote dashboard batch import parser: supports JSON / CSV / plain text mixed paste with auto-detection.
  • @ucloudnb666 — PR #184 Added Astraflow as a third-party provider.
  • @datfooldive — PR #173 Dashboard UI overhaul: unified component styles, improved card layouts and responsive design.
  • @The-five-stooges — PR #188 Sticky session streaming-path fix + body.user multi-user isolation + stickyNoFallback / stickyBindByUserOnly toggles.
  • @andya1lan — PR #192 Routed update.sh LS binary updates through install-ls.sh, aligned the WindsurfAPI release / public LS mirror / Exafunction source chain, and fixed macOS grep -P compatibility.
  • @MatrixNeoKozak — PR #195 Made malformed JSON sent to the dashboard API return HTTP 400 instead of a 200 response with ok:false, so UI and automation callers can rely on status-code semantics for bad request bodies.
  • @brandonedley — PR #201 Added GLM 5.2 and Kimi K2.7 catalog entries while keeping README, README.en, package metadata, generated model docs checks, and catalog tests in sync.
  • @forrinzhao — PR #219 Traced the Devin content-policy block that killed every request from a Feishu codex bot to one sentence in codex's apply_patch tool description: FREEFORM tool, so do not wrap the patch in JSON. Live-bisected 7/7 deterministic, with the decisive two-fragment A/B — rewriting only one fragment still blocks, both must change. The finding exposed a deeper structural gap: the tool-description preamble is injected after neutralizeClientIdentity, so any trigger phrase arriving via a tool description bypassed the entire a1-a6 defense line on the native path. The landed fix moves neutralization after preamble injection and adds the two rewrites as rule (a7) in the existing sequence (reusing the main switch and test harness), so it now covers every client and every trigger. Also adopted his responses.js fix: Codex sends bare {role, content} input items without type:'message', which were silently dropped and left upstream with an empty messages array → UPSTREAM_INTERNAL.
  • @warelik — PR #224 #225 #226 #227 #228 #229 Six PRs in one round, across the account pool, rate limiting, identity neutralization, and protocol conformance. The 429 reset-window bug was two links of one chain: the upstream's 3h reset window was dropped in transport (both error paths destructured only {code, message}), and a model-scoped cooldown is structurally invisible to pool selection via getApiKey(modelKey=null) — fixing either alone has no observable effect, so a just-429'd account got re-picked within seconds and hammered upstream into a hard lockout (#224). Client disconnects are no longer treated as account faults: abort is now recognized uniformly (undici and AbortController disagree on shape), and no exit path penalizes the error budget, burns quota on failover, or writes to a dead socket (#225). Grok CLI's You are Grok … released by xAI self-ID trips the content policy — added to the a1-a5 rule family, plus whole-block stripping of <executing_actions_with_care> (#227). Thinking blocks now omit signature instead of sending "", which strict clients reject (#228). Responses usage gained input/output_tokens_details, which pairs exactly with the DEVIN_CONNECT cache wire-tag calibration (#229). Also a zero-dependency pair-chain session-continuity module (600 lines when it landed, 858 today) deriving a stable session_id from the conversation's own completed request/response pairs — opt-in, byte-identical when off (#226).
  • @warelik — PR #216 + PR #215 Neutralized the upstream MCP-gate: server.codeium.com fingerprints tool descriptions and rejected 8 of Cursor's 21 tools with permission_denied. The fix replaces the native #10 ToolDef top-level description with the tool name and recursively strips description annotations from the parameter schema (preserving structure and parameters literally named description), then injects a description-only preamble into the system prompt to restore tool-selection context — 21/21 tools pass (#216). Also fixed Node 20+ IPv6 Happy Eyeballs ETIMEDOUT by disabling autoSelectFamily and switching local HTTP/2 to 127.0.0.1 (#215).

Want to be on this list? Open an issue or a pull request. The dashboard's Credits panel on the left is the fuller list: it renders from contributors.json — currently 36 contributors across 68 entries, each with a weight tier and a mechanism note — while the list above is a hand-picked subset of it.

License

MIT License. See LICENSE.

Release and Secret Boundary

Release automation may publish Docker images and GitHub Releases. The current release is v3.141.592658; the repository carries 228 git tags and 218 release-note files (2026-10-30). Every release from v3.10.0 attaches a SHA256SUMS manifest covering the four published assets (Windows exe/zip, macOS arm64 binary/zip) and signs the four assets and the manifest with cosign keyless signing (GitHub OIDC; no private key or signing secret in the repository). From v3.141.592657 the Docker image digest (image-digest.txt) and the source-OTA provenance statement (release-attestation.json) are also each signed with a .sigstore.json bundle. Each signed blob ships a single .sigstore.json bundle (signature, Fulcio certificate and Rekor inclusion proof in one file). The signature proves the bytes were produced by this repository's release workflow for a v* tag; it does not prove human review or a reproducible build, and anyone who can rewrite the repository/tag can re-sign. Verify before running a download: node scripts/verify-release-assets.mjs <download-dir> (requires cosign), or follow docs/RELEASE-SIGNING.md to verify SHA256SUMS.sigstore.json with cosign verify-blob and then run sha256sum -c SHA256SUMS (shasum -a 256 -c on macOS; Get-FileHash on Windows). The manifest uses the flat asset names, so all files can sit in one directory; v3.9.53 recorded dist-windows/ / dist-macos/ paths, and releases before v3.10.0 are unsigned. The Docker image is signed by digest and its digest is published as image-digest.txt; the dashboard's Docker update resolves the tag through the daemon and refuses to pull or deploy unless it equals that signed digest. The git self-update path does not download binaries, but it verifies the release's release-attestation.json (tag→commit/tree and every asset's SHA-256, cosign-signed); WINDSURFAPI_UPDATE_ATTESTATION=warn is the default (missing material logs a warning), and required refuses. See docs/RELEASE-SIGNING.md for the trust boundary and the OTA checks. Keep tokens, API keys, cookies, and provider credentials out of issues, pull requests, logs, and committed config. If a report needs authentication details, share only redacted metadata and reproduction steps.


Star History

Star History · 点击查看完整星图

Explore related projects