Releases: dwgx/WindsurfAPI
Release list
v3.2.0
v3.2.0 — Statistics dashboard overhaul
2026-07-11 (UTC+9)
A feature release focused on the statistics dashboard: it closes a real
data-collection gap, adds a per-model dimension to the time series, enriches the
rankings, and fixes two chart-rendering bugs found by an adversarial pre-commit
review. Every change was verified against a real headless-Chrome render via CDP,
and the whole batch passed a multi-reviewer adversarial pass before commit. Full
suite green (2531), i18n green.
Added — statistics
- Token usage breakdown now populates on DEVIN_CONNECT deployments. The
dashboard's fresh-input / cache-read / cache-write / output breakdown had been
fed only from the Cascade paths, so a connect-only deployment (e.g. homecloud)
showed all-zero token totals despite hundreds of requests.recordTokenUsage
is now fed from the DEVIN_CONNECT request path too — the non-stream choke point
plus all three streaming sites (primary, transient-replay, re-login-replay).
Safe no-op when a turn carries no usage. - Request time series shows which models were requested. Hovering an hour in
the request-volume chart now lists the top models used in that hour (aggregated
client-side from the recent-requests ring buffer — no backend change). - Rankings gained dimensions. The overview leaderboard rows now carry a
second line: a success-rate dot (green / amber / red), lifetime credits, and
p95 latency — so it answers "which model, how reliable, how expensive, how
fast", not just raw request count. - Credits are readable. The credits total compacts large numbers with a
k
suffix (e.g.41.2k) and keeps the exact value in a hover tooltip. No USD
conversion — Windsurf retired the credit system (2026-03 → quota tiers), so
there is no authoritative credit→$ rate; credits stay an internal accounting
unit. - Cache-efficiency indicator. The token card shows what share of input was
served from cache (a real credit-saving signal), filling what was previously
dead whitespace.
Fixed — chart rendering
- High-error bars no longer render gray. The bar color mixed brand-blue →
amber → red linearly by error ratio, which passed through a dead gray (~#a09f9a)
around an 18% error rate — a high-error hour looked disabled, not errored.
Bars now use discrete semantic buckets (healthy blue / warning amber / high-error
red), matching the rankings dots and the model table's red/green. - Trend error line no longer dives off-canvas (regression fix). The new
converged Y-axis (which lifts the floor for steady high-volume data so small
variation stays visible) mapped the low-magnitude error overlay below the plot,
silently hiding real errors. Anchor points are now clamped to the plot band, so
a sub-floor error series sits truthfully on the baseline instead of vanishing.
Spiky/low data keeps its zero baseline unchanged.
Fixed — tooling
- check-i18n gate #8 no longer false-positives on JS expression fragments. A
nested-${}template (inline IIFE) could leak arithmetic/member-access
fragments into the hardcoded-English scan. The scanner now skips
operator/paren arithmetic, optional chaining, method calls, and code-shaped
member access — while tightening the member-access guard so it does not
skip real English copy that contains a dot (v2.0 released,95.5% donestill
flag correctly).
Housekeeping
- Repo reorg: all AI / handoff / dev notes moved under a git-ignored
docs-internal/(root keeps onlyREADME*.md,LICENSE,CONTRIBUTING.md,
SECURITY.md,CLAUDE.md); source/script comment path references synced.
Verification
npm test→ 2531/0 (+B1 token-wiring tests over v3.1.5).node src/dashboard/check-i18n.jsgreen (new copy double-written en/zh).- CDP against real headless Chrome: token breakdown populates, credits compact +
tooltip, rankings sublines with colored dots, per-model hover, converged-Y trend
with error line on-plot, high-error bars red (chroma-asserted, no gray). - Adversarial multi-reviewer pre-commit pass: 1 real regression (error line) + 3
edge findings caught and fixed before commit.
v3.1.5
v3.1.5 — Dashboard a11y + JS-region i18n gate + CORS & proxy SSRF fixes
2026-07-11 (UTC+9)
An accessibility and i18n-hardening release. Every change landed test-first
(a failing/asserting test first, then the change) and the dashboard work was
verified against a real headless-Chrome accessibility tree via CDP, not just
static assertions. Also closes a dashboard CORS-preflight allowlist bypass and a
DNS-rebinding TOCTOU in proxy connections. Full suite green (2527), i18n green.
Added — accessibility
- Segmented controls are now real radio groups. The four
.seg-group
single-select toggles (pool view, overview trend range, stats chart type,
stats range) declaredrole="tablist"while behaving as radio groups — a
screen reader announced a tablist whose "tabs" had no selected state. They are
nowrole="radiogroup"withrole="radio"+aria-checkedbuttons and a
localized grouparia-label. Selection state was CSS-class-only; a new
App._syncRadioGrouptoggles.activeandaria-checkedtogether, and all
six toggle handlers route through it so the a11y state always tracks the visual
one. The.activeCSS is unchanged (selectors key off the class, not the role). - Chart canvases are labelled images. The three chart canvases
(overview-trend, stats, model-pie) now carryrole="img"+ a dynamic
aria-label. The overview trend also gets ansr-onlydata-table fallback and
a spoken summary (_updateTrendA11y, refreshed on every render) so a
screen-reader user gets the same numbers a sighted user reads off the chart. - Modals are dialogs with focus management.
Utils.confirm/Utils.prompt
now renderrole="dialog"+aria-modal="true"+aria-labelledby. A new
App._trapFocuskeeps Tab focus cycling inside the open dialog, and the
previously-focused element is captured on open and restored on close. The
existing Esc/Enter handling and the custom-select Esc deferral are preserved. data-i18n-aria-labelruntime.I18n.applygained anaria-label
translation handler (mirroring the existingtitlehandler: store original,
restore under zh). Newaria.*andoverview.trend.a11y*strings are written
in bothenandzh-CN.
Fixed — security
- Dashboard CORS preflight bypassed its own allowlist (#9). The dashboard API
had an allowlist-gated OPTIONS handler, but it was dead code:server.js
answered every OPTIONS — including/dashboard/api/*— with a blanket
Access-Control-Allow-Origin: *before the dashboard dispatch, so preflights
never consultedDASHBOARD_CORS_ORIGINS. Not a data-read hole (actual dashboard
responses stayed allowlist-gated, and the global preflight omits
X-Dashboard-PasswordfromAllow-Headers, so authenticated cross-origin calls
were blocked at preflight regardless), but the documented "preflight shares the
allowlist decision" never happened, and inDASHBOARD_ALLOW_NO_AUTH=1mode it
weakened CSRF protection for cross-origin JSON POSTs. The global OPTIONS
short-circuit now excludes/dashboard/api/, so dashboard preflights fall
through to the allowlist handler (allowed origin echoed +Vary: Origin,
disallowed origin gets no ACAO)./v1/*preflight is unchanged (blanket*,
no credentials — correct for the open, key-authenticated API). - DNS-rebinding TOCTOU in proxy connections (#11 / W6).
validateProxyHost
resolved a configured proxy host once for its private-IP check, but the actual
net.connect(host)(SOCKS) and HTTPCONNECTtunnels re-resolved the hostname
at dial time — a second DNS lookup an attacker's server can answer with a
private address (169.254.169.254,127.0.0.1, …), turning a "public" proxy
into an SSRF pivot into the host's internal network. New
net-safety.resolveProxyConnectHostresolves the proxy host once, rejects
if any returned address is private (mixed public+private rebinding answers
included), and returns a vetted IP literal to dial — so the socket performs
no second resolution and the address we validate is exactly the address we
connect to. Wired into all five proxy connect paths (SOCKS tunnel + the four
HTTP CONNECT tunnels, including dashboard proxy-test). Honors
ALLOW_PRIVATE_PROXY_HOSTS(still pins to a literal; skips the private
rejection). The destination reached through the proxy is left as a name on
purpose — the proxy resolves it, not us.
Added — i18n gate
- check-i18n gate #8: hardcoded English in the App
<script>region. Checks
#6/#7 stop at the<script>tag, leaving the entire JS half — where most
runtime UI copy is generated — unguarded:el.textContent = 'Save'or an
innerHTMLtemplate with English text nodes could ship untranslated with a
green gate. The new scan flags English string literals assigned to DOM copy
sinks (textContent/placeholder/title/…) and text nodes insideinnerHTML
templates that bypassI18n.t(). It is calibrated to 0 findings on the
currentindex.html(multi-word phrases are copy; single tokens must clear
identifier / CONSTANT / dotted-key / URL / unit exclusions plus a
universal-technical-term allowlist; metadata rows are split on separators). The
gate is warn-first — it reports findings but does not touch the exit code,
so a future regression is visible without breaking the build; it can be
promoted to a hard error once the script region stays clean.
Verification
npm test→ 2527/0 (+23 over v3.1.4: 12 a11y + 2 i18n + 7 proxy SSRF + 2 CORS).node src/dashboard/check-i18n.jsgreen (including the new #8 = 0 findings).- CDP against real headless Chrome: live-DOM radiogroup/radio roles +
aria-checked
toggling, canvasimagerole, sr-only trend table,role="dialog"modals with
focus-return on Escape, andAccessibility.getFullAXTreeconfirming
radiogroup/radio/image roles (no tablist) andaria-labelswitching between
en/zh live.
v3.1.4
v3.1.4 — Self-update restart, in-flight leak, fire lifecycle, safer update.sh
2026-07-10 (UTC+9)
A hardening release from a full adversarial review of the v3.1.0→v3.1.3 work.
Every fix landed test-first (a failing regression test first, then the change),
and the whole set was independently re-reviewed for release-blocking regressions
before shipping. Full suite green (2504), i18n green.
Fixed
- Self-update would stop the service on systemd. After a successful dashboard
self-update the process didprocess.exit(0), whose comment assumed PM2
autorestart. On asystemd Restart=on-failurehost (e.g. homecloud) exit 0 is
a success and systemd does not relaunch — so "Update" bricked the service
until a manual restart. The self-update path now exits with a non-zero
restart-requested code (75 / EX_TEMPFAIL) so systemdRestart=on-failureand
PM2 autorestart both relaunch. Normal SIGINT/SIGTERM shutdown keeps its separate
exit(0)path inindex.js(unchanged). Only fires when the update actually
changed the commit, so no restart loop. - In-flight slot leak on mid-request re-login.
chat.jsreleased the account
by its mutableapiKey(releaseAccount(acct.apiKey)) in two attempt paths. A
background re-login rekeysapiKeyin place, so releasing by the stale key
leaked the in-flight slot forever (the same class as REF-1/#165). Both paths now
release by the immutablereleaseAccountById(acct.id), matching the already-correct
finalize path. Regression tests drive the real non-stream and stream handlers
through an in-place rekey and assert the slot returns to 0. - WebGL pool fire didn't recover after leaving the overview, and a late poll
could remount hidden fires. Returning to the overview hit the_poolSig
short-circuit and never re-mounted the fire; an in-flight overview request that
resolved after navigating away could mount WebGL contexts on a hidden panel.
_teardownFires()now invalidates_poolSig, removes the canvases, and restores
the emoji fallback;navigate()/loadOverview()carry an overview-load
generation + active-panel guard so a stale async load can't remount. Verified via
CDP: 5 saturated rows → 3 contexts; leave → 0; return → 3; late render while on
another panel → 0. - Trend chart could still clip a pure-nice double peak. niceMax + padding
didn't fully cover data like[0,100,100,0](peak on the top row, Catmull-Rom
overshoot above it) or a 100% success-rate plateau. The trend spline now clamps
its bezier control points to the plot bounds (_smoothPath/_trendSampletake an
optionalyBounds, passed only by the overview trend); the main stats chart is
unchanged (defaultnull).
Changed
- Docker image defaults
DEVIN_CONNECT=1. v3.1.1 set it in docker-compose, but
a baredocker run/ K8s pod running the image directly still fell back to
Cascade+emulation (#210). The Dockerfile now also setsENV DEVIN_CONNECT=1.
Overridable:docker run -e DEVIN_CONNECT=0, compose env, and K8s env all win
over the image default. update.shno longer hard-resets on any pull failure. It used to
git reset --hardwheneverpull --ff-onlyfailed, discarding local changes/
commits. Now a dirty tree or local-ahead commits fail closed (asks you to
review first); a destructive reset requiresWINDSURFAPI_UPDATE_FORCE_RESET=1,
which stashes (--include-untracked) before resetting. Clean deploys still
fast-forward normally (runtime files are gitignored, so they don't trip it).
Ops notes
- If your systemd monitoring alerts on "main process exited non-zero", a dashboard
self-update now exits 75 once per successful update — addSuccessExitStatus=75
to the unit to silence it. (The repo ships no systemd unit; this is deployment-side.) update.shis stricter than the dashboard updater (baregit status --porcelain
vs-uno): an untracked, non-ignored stray file makes it fail closed by design —
review it or setWINDSURFAPI_UPDATE_FORCE_RESET=1.
Notes
- Verified: full suite green (2504/0, +10 new regression tests), i18n green,
bash -n update.shclean, fire lifecycle + trend clamp verified via CDP.
v3.1.3
v3.1.3 — Harden the WebGL pool fire (context leaks fixed)
2026-07-10 (UTC+9)
A follow-up to v3.1.2. An adversarial code review of the new WebGL pool fire
found three ways it could leak or thrash WebGL contexts; all three are fixed.
No user-visible behaviour change beyond "the fire no longer wastes GPU / breaks
under a full-pool rate-limit event".
Fixed
- Unbounded fire contexts under pool-wide saturation. A row got its own
WebGL2 context whenever the account wassaturated, with no upper bound. A
pool-wide rate-limit event (the exact state this dashboard exists to show)
marks many accounts saturated at once, so a 20-account deployment would spawn
20+ contexts — past the browser's ~16-context cap, which silently drops the
oldest and thrashes the GPU. Fires are now capped to the 3 hottest.hot
rows (by intensity,_FIRE_MAX); the rest keep the emoji/CSS highlight. - Orphaned fires on a transient empty poll. When a
/connect-metricspoll
came back empty (cm===null, a common transient blip),_renderPoolHealth
rebuilt the panel to the empty state and returned without disposing the
running fires — their RAF loops kept rendering the full 3-pass pipeline against
detached canvases until a later non-empty poll. Empty-state now disposes first. - Fires running forever after leaving the overview.
display:nonedoes not
pauserequestAnimationFrame(only a hidden tab does), so navigating to
another panel left every fire burning its 3-pass GL in the background.navigate()
now disposes all fires (re-mounted on return to overview).
All three funnel through a new _teardownFires() that force-loses each WebGL
context. Verified via CDP: 5 saturated accounts → 3 fires; leaving overview and
a cm===null poll → 0 fires (contexts reclaimed).
Notes
- Verified: full test suite green (2494), i18n check green, dashboard syntax green.
v3.1.2
v3.1.2 — Self-update fix, trend-chart clipping fix, WebGL pool fire
2026-07-10 (UTC+9)
A small fix + polish release: the dashboard Update button works again, the
request-trend chart no longer clips its peak, and saturated accounts now burn a
real WebGL flame in the pool-health view.
Fixed
- Dashboard "Check Update" printed
✗ Invalid JSON. An empty-body dashboard
POST (App.api('POST', path)with no payload →Content-Length: 0) hit
JSON.parse(''), which throws, and the route surfaced it asInvalid JSON.
The Update button (POST /self-update) sends no body, so it always failed.
Empty bodies are now treated as{}. This fixes every no-payload dashboard
POST (self-update, probe-all, langserver restart, …). Route-level regression
test added. - Request-trend chart clipped its peak (
最高点的线超出图外). The smoothed
(Catmull-Rom) request line overshoots above a peak when both neighbours are
lower; the peak data point sat on the very top row (12px padding), so the
overshoot drew above the canvas. The left-axis max is now rounded up to a
"nice" number (niceMax, e.g. 74 → 100) and the top padding is larger, giving
the overshoot headroom. Axis labels also come out cleaner (80/4=20 not 74/4).
Added
- WebGL fire on saturated pool rows (ported from KiroStudio FireCanvas). In
the StatusBars pool-health view, an account that is RPM-saturated now burns a
real flame across its row: a WebGL2 three-pass pipeline (flame sim → gaussian
blur → glow composite) with 7-stop intensity coloring — the harder the account
is working (RPM / inflight), the hotter the color (cyan → green → gold →
orange → violet → ice-white → ruby-red at full tilt).- Scales to hundreds of accounts: the flame is mounted only on saturated
(.hot) rows — usually 1-2 at a time — so there are never more than a couple
of live WebGL contexts. Unmounting (or switching to the grid view) force-loses
the context, so contexts never leak. Verified viatools/verify-fire.mjs
(CDP): fires dispose to 0 on view switch, contexts stay alive while mounted. - Graceful fallback: no WebGL2 (or
prefers-reduced-motion) → the row keeps
its existing emoji / CSS-glow highlight.
- Scales to hundreds of accounts: the flame is mounted only on saturated
Notes
- Verified: full test suite green (2494), i18n check green, dashboard syntax
green; fire + trend verified via CDP DOM inspection + screenshot.
v3.1.1
v3.1.1 — Docker defaults to native tool calling, fable env-lift fix
2026-07-10 (UTC+9)
A small follow-up to v3.1.0. Two changes, both aimed at agent clients (Claude
Code / Cline / Codex) that declare tools: Docker now opts into the native
tool-call path out of the box, and the fable family no longer idles to an empty
completion when a caller <env> block is present.
Fixed
claude-5-fable-*empty completion with tools +<env>(#209). On the
prompt-emulation path, lifting the caller's<env>block (working directory /
git status / platform) into the prototool_calling_sectionalongside tools
made the fable planner return 0 text / 0 thinking / 0 tool_calls. The env-lift
is now skipped for the weak-model family (shouldLiftCallerEnv), and a global
escape hatchWINDSURFAPI_ENV_LIFT=0disables it for every model. Non-fable
models are byte-identical — they still get the env-lift.
Changed
- Docker defaults
DEVIN_CONNECT=1. A freshdocker compose upnow uses the
pure-HTTP cloud egress + native tool-call path out of the box. Native is what
lets weaker models (glm-5.2 etc.) follow the tool protocol reliably in agentic
clients — withDEVIN_CONNECTOFF, requests fall back to Cascade +
prompt-emulation, where those models intermittently narrate instead of
emitting a tool_call (root cause of #210). Overridable: setDEVIN_CONNECT=0
in your.envto force the legacy Cascade path. Bare-source / systemd deploys
are unchanged (the code default stays OFF; only the Docker compose env opts in).
Notes
- #210 ("无法在 claude code 持续运行", glm-5.2) was reproduced live: on the
emulation path glm-5.2 dropped ~1/3 of turns to narration-instead-of-tool_call;
on the native path (DEVIN_CONNECT=1) it ran 50+ consecutive tool calls with
zero stalls. The Docker default change is the fix. tools/model-probe.mjsgained a--tool-follow/--repeatmode that
quantifies tool-call adherence per model (the glm-vs-fable comparison).- Verified: full test suite green (2493), i18n check green.
v3.1.0
v3.1.0 — Native tool calling restored, 429-lockout mitigation, calmer dashboard
2026-07-10 (UTC+9)
A fix + reliability release. The headline is restoring native tool calling for
Claude-family models through the DEVIN_CONNECT path (agent clients like Claude
Code and OpenCode work again), plus a set of small-pool rate-limit mitigations
and dashboard polish. Every root cause was confirmed live against real
devin.exe wire captures before a fix landed.
Fixed
- Native tool calling for Claude-family models (opus / sonnet / fable).
Requests that declared tools were being rejected upstream (internal error occurred/content policy) for Claude-family selectors while gpt/swe worked
with byte-identical encoding. Five independent causes, all fixed:- Competitor-identity content-policy block. The client system prompt's
self-identification (You are Claude Code…/…Claude Agent SDK), the
x-anthropic-billing-headerline, and the interactive Environment "brand
block" (Claude Code product blurb + Claude model-ID catalogue) tripped the
upstream content policy.neutralizeClientIdentitynow rewrites all of
these to a generic assistant identity. It moved to a standalone
handlers/identity-neutralize.jsso the DEVIN_CONNECT egress can apply it
without a circular import — neutralization now also covers Codex
/v1/responsesand direct/v1/chat/completions, not just/v1/messages. permission_deniedmisclassified as a dead token. A content-policy
rejection used to be read as a dead session token, benching a perfectly
healthy account and cascading the pool to "all accounts exhausted". It is
now classifiedCONTENT_BLOCKED→400 invalid_request_errorwith no
account penalty.- Empty/absent system prompt with tools. The upstream rejects a
Claude-family request that declares tools but carries an empty system
prompt. A minimal system prompt is injected when tools are present and no
system text was supplied. - Over-long tool descriptions. A long tool description pushes the request
past an upstream content threshold. Tool descriptions are capped
(WINDSURFAPI_TOOL_DESC_MAX, default 500) — a model hint only, never the
tool name or schema.
- Competitor-identity content-policy block. The client system prompt's
- Circuit-breaker help tooltip clipping. The "熔断与限流" section's
?
tooltip grew leftward off a left-edge icon and was clipped by the section's
overflow:hidden. It now drops down-and-right and stays fully inside the card. - Config files written 0600.
writeJsonAtomicnow creates config files
(accounts, runtime-config, credential stores — which can carry the runtime API
key, dashboard password hash, and upstream tokens) as owner-only, so they are
not world-readable on a shared host.
Added
- 429-lockout mitigation for small account pools. A single throttled
account could black out the whole pool (hard filter → empty → 429 → the
client's auto-retry re-extends the cooldown). Modelled on KiroStudio's
cooldown design:- Tier-aware last-account exemption. A pro account whose only healthy peer
is free is still treated as "last usable" for paid selectors, so it is not
quarantined into a pool-wide blackout. - Degraded-serve fallback (opt-in,
WINDSURFAPI_DEGRADED_SERVE). When the
whole entitled pool is transiently throttled, serve the least-cooled account
instead of returning 429. Default off = byte-identical to prior behaviour. - Client-replay backoff clamp. The advertised
Retry-Afteron a 429 is
clamped to[floor, ceil](WINDSURFAPI_RL_CLIENT_BACKOFF_FLOOR_MS/
_CEIL_MS) so an agent client's auto-retry backs off usefully instead of
hot-looping. Default floor 0 = unchanged. - Shorter internal-error quarantine. Default cut from 5 min to 2 min — an
upstream internal error is transient and self-heals in seconds. - Tunable bare-429 cooldown (
WINDSURFAPI_RL_BURST_MS, default 5 min).
- Tier-aware last-account exemption. A pro account whose only healthy peer
- Full-chain request tracing (
WINDSURFAPI_TRACE=1, default off). Stitches
client request → routing decision → raw Devin wire bytes → client response
under one trace id for offline debugging / RE.tools/model-probe.mjsfires
one prompt at N models;tools/trace-view.mjsinspects and hexdumps a trace.
Never a served endpoint; secrets redacted.
Changed
- Dashboard pool health defaults to the calm StatusBars view instead of the
glowing GlowGrid. The grid is still one click away (choice is persisted), and
its glow was toned down (narrower breathe, slower cadence, a smooth ember
instead of a fast stepped flicker) so opt-in grid is pleasant too.
Notes
- All new tunables resolve env → runtime override → historical default, and
every default is byte-identical to prior behaviour — upgrading changes nothing
until you opt in. - Verified: full test suite green (2488), i18n check green, dashboard syntax
green; UI fixes verified via CDP DOM inspection.
v3.0.2
v3.0.2 — Foolproof deploy, public onboarding, global settings
2026-07-09 (UTC+9)
A feature + hardening release focused on making WindsurfAPI easy and safe for
anyone to deploy and operate, with a much stronger account-onboarding surface.
Added
- Global Settings page. New dashboard panel for shared preferences and
security thresholds (stored server-side, shared across browsers). Adjust the
email / dashboard-IP lockout thresholds and durations, or disable a lockout
entirely by setting its threshold to0. - Public remote onboarding. Add accounts from a public deployment, not just
loopback: email/password login can optionally encrypt-and-store the password
for auto-relogin (gated byDEVIN_CONNECT_ALLOW_REMOTE_CRED_STORE=1+ an
explicit per-request opt-in), plus an in-memory OAuth session flow
(/oauth/start·/oauth/callback·/oauth/status) that lets you finish
Google/GitHub/Devin sign-in in your own browser and paste the callback URL
back — no localhost callback needed. - OAuth login chooser. Clicking Google/GitHub offers "open login page" vs
"copy login URL", with a "don't ask again" preference (re-enable it from
Settings).
Fixed
- Deploy foolproofing (both Docker and bare source).
DEVIN_CONNECT/DEVIN_ONLYdeployments skip the language-server startup
and its auto-install entirely — no more downloading a ~100 MB binary a
binary-less Devin deploy never uses, and no spawn/ENOENT noise on boot.docker compose upon a fresh clone no longer aborts on a missing.env
(env_fileis now optional); the bundled nginx LB setsTRUST_PROXY_*so
per-caller lockout works behind it.install-ls.shruns under a timeout so a slow network can't hang boot;
DATA_DIRmkdir failures are surfaced instead of silently swallowed.
- Dashboard lockout false-positives. Opening the dashboard fired ~a dozen
authenticated API calls with an empty password before you'd typed anything,
and each empty-password401counted as a failed attempt — banning your IP
the instant the page loaded. Empty-password preloads no longer count; only a
submitted wrong password does. The frontend now halts polling and shows a
countdown on429instead of stacking error toasts. - Lockouts are configurable and releasable. Setting a lockout threshold to
0now releases any active ban immediately (not just at natural expiry). A
no-password / OAuth-only account no longer counts a wrong-method attempt
toward the email lockout. - Account onboarding token routing. A
devin-session-token$…pasted into
the OAuth flow or smart-import is now added via the api-key path, not the
Firebase-only RegisterUser path, so session-token onboarding succeeds. - Credential-store gate hardened for reverse proxies. The "store my
password" gate now requires the request to come from a loopback peer (not
just a loopback bind host), so a reverse proxy in front can't let a remote
user bypass the operator opt-in.
Notes
- No breaking API changes. Docker users:
docker compose pull && docker compose up -d. - All new UI is fully bilingual (English + 简体中文). Plaintext passwords and
tokens are never written to logs.
v3.0.1
v3.0.1 — Docker boot fix
2026-07-09 (UTC+9)
A patch release that fixes a critical startup bug in the v3.0.0 Docker image.
Fixed
- Docker image failed to boot. Runtime code (
src/devin-connect-models.js)
loaded the Devin catalog snapshot fromtest/fixtures/, but the Docker image
only shipssrc/— sodocker run/docker compose upcrashed on startup
withENOENT: .../test/fixtures/devin-catalog-snapshot.json. The snapshot now
ships undersrc/data/devin-catalog-snapshot.json, so the image boots cleanly.
Source (systemd /node src/index.js) deployments were unaffected.
Notes
- No config or API changes. If you run v3.0.0 via Docker, pull
v3.0.1
(orlatest) and recreate the container:
docker compose pull && docker compose up -d. - The catalog-drift test now reads the same shipped
src/data/snapshot, so the
guard also protects the file that actually ships.
v3.0.0
v3.0.0 — A New Chapter
2026-07-09 (UTC+9)
Today isn't just another release.
It marks the beginning of a new chapter for this project.
When I first started WindsurfAPI, the goal was surprisingly simple: I just wanted a stable solution for my own daily use. I never expected it to become an open-source project that so many people would actually use.
At that time, there were very few open-source projects exploring this direction publicly. Even while the project was still incomplete, it received far more stars, issues, discussions, and pull requests than I had imagined. That support became the motivation to continue improving it release after release.
Over the past months, the project has grown far beyond its original scope.
Today it is capable of:
- Reverse proxying the latest Devin cloud models
- Native Tool Call support
- Vision / image understanding
- Fable 5 coding and code reading
- OpenAI / Anthropic / Gemini compatible APIs
- Running entirely on Node.js with zero npm runtime dependencies
Keeping the project dependency-free has always been intentional. I wanted every line of code to be understandable, auditable, and easy to deploy without bringing in an entire ecosystem of packages.
Community
Thank you to everyone who opened issues, submitted pull requests, tested edge cases, and patiently reported bugs.
Some issues took me much longer to solve than they should have. Some answers were delayed. Some problems remained unresolved for longer than anyone wanted.
For that, I sincerely apologize.
Every report helped make the project better, even if I wasn't able to respond as quickly or as perfectly as I hoped.
Thank you for staying with this project.
What's New in v3.0.0
This release represents the largest architectural update since the project began.
Highlights include:
- Major Devin Connect improvements
- OAuth workflow refinements
- Dashboard redesign and new management features
- Stronger internationalization (i18n) system and validation
- Improved model catalog synchronization
- Better compatibility with OpenAI / Anthropic / Gemini clients
- Expanded automated testing and release validation
- Numerous internal optimizations and stability improvements
This version also introduces a much stronger localization audit, helping prevent untranslated UI text from slipping into future releases.
Demo
A real-world demonstration is available on Bilibili:
https://www.bilibili.com/video/BV1AfM56BE5t
The video isn't professionally produced, but it shows the project running in real-world scenarios, which I believe is more valuable than polished marketing.
About the Name
One funny problem remains.
The backend now talks to Devin.
The frontend still carries the history of Windsurf.
Both ecosystems coexist.
So... is this project called DevinAPI?
Or is it still WindsurfAPI?
Maybe one day I'll end up calling it WinDeSurfingAPI.
Whatever the name becomes in the future, the goal stays the same:
Build a simple, transparent, dependency-free AI gateway that anyone can understand, deploy, and improve.
Thank you for being part of this journey.
See you in the next release.