# ========== Server ========== PORT=3003 # Bind host. Default binds beyond localhost (0.0.0.0) so Docker/LAN work; set to # 127.0.0.1 to restrict to loopback. On a non-local bind the server FAILS CLOSED # unless API_KEY (REST) and DASHBOARD_PASSWORD (dashboard writes) are set. # HOST=0.0.0.0 # API key for incoming requests (leave empty for open access) API_KEY= # Directory for persisted JSON state and logs (Docker 推荐用 /data). # Leave empty and it defaults to the repo/app root — set a writable path on # bare-source installs so accounts.json / stats.json / logs don't scatter there. DATA_DIR= # On the Cascade transport, after GetCascadeModelConfigs returns usable # per-account catalogs, pool model lists show their union while routing enforces # the selected account's catalog. DEVIN_CONNECT uses its separate selector # catalog and is not filtered by this switch. # Missing, empty, or failed catalog fetches fail open. Set this to 1 only if you # need the legacy behavior that exposes and routes the full static catalog. # WINDSURFAPI_IGNORE_CLOUD_FILTER=1 # ========== Reverse proxy / load balancer ========== # Only honour X-Forwarded-For when a trusted proxy sits in front (e.g. the # bundled docker-compose nginx LB, or openresty/Caddy). Without this every # caller looks like the proxy's IP and the per-caller brute-force lockout # collapses to one global bucket. TRUST_PROXY_HOPS = number of trusted proxies # appended to XFF (default 1). The docker-compose file sets both automatically. # TRUST_PROXY_X_FORWARDED_FOR=1 # TRUST_PROXY_HOPS=1 # In-memory duplicate response cache. Max bytes accepts b/k/kb/kib/m/mb/mib/g/gb/gib. # RESPONSE_CACHE_ENABLED=1 # RESPONSE_CACHE_MAX_BYTES=16m # ========== Codeium Auth ========== # Option 1: Direct API key from Windsurf CODEIUM_API_KEY= # Option 2: Token from windsurf.com/show-auth-token CODEIUM_AUTH_TOKEN= # ========== Language Server ========== # Path to the language server binary. # Linux x64 default: /opt/windsurf/language_server_linux_x64 # Linux arm64 default: /opt/windsurf/language_server_linux_arm # macOS Apple Silicon default: ~/.windsurf/language_server_macos_arm # macOS Intel default: ~/.windsurf/language_server_macos_x64 LS_BINARY_PATH=/opt/windsurf/language_server_linux_x64 # Per-proxy language server data root. # macOS local runs should use a user-writable directory, e.g. ~/.windsurf/data. LS_DATA_DIR=/opt/windsurf/data # gRPC port for language server LS_PORT=42100 # Max live LS instances. Empty = memory-aware default (about 700MB per LS). # Set low on small VPSes, especially when using per-proxy/per-user LS isolation. # LS_MAX_INSTANCES=2 # Prewarm the no-proxy default LS on startup. Set 0 on low-memory hosts when # all real traffic uses per-account proxies; LS will start lazily on demand. # LS_PREWARM_DEFAULT=1 # How long a request waits for an active non-default LS to go idle before # returning LS_POOL_EXHAUSTED / LS_MEMORY_GUARD. # LS_POOL_WAIT_MS=30000 # Minimum available memory required before spawning another non-default LS. If # unset, WindsurfAPI uses observed live LS RSS + margin after the first LS is # ready, falling back to 700MB before any sample exists. Set this to pin a # fixed operator budget. Accepts 700m / 1g / bytes. # LS_SPAWN_MIN_AVAILABLE_BYTES=700m # Dynamic RSS estimate floor/margin used only when LS_SPAWN_MIN_AVAILABLE_BYTES # is unset. # LS_OBSERVED_RSS_MIN_BYTES=384m # LS_OBSERVED_RSS_MARGIN_PERCENT=35 # Disable memory guard only on hosts with external memory control/monitoring. # LS_MEMORY_GUARD=1 # Stop idle non-default LS instances after this many milliseconds. 0 disables. # LS_IDLE_TTL_MS=1200000 # Idle reaper interval. Empty = derived from LS_IDLE_TTL_MS. # LS_IDLE_SWEEP_MS=300000 # Prewarm every configured proxy LS on startup. Default off to avoid memory spikes. # LS_PREWARM_PROXIES=0 # Also prewarm/probe LS when adding accounts from Dashboard/batch/OAuth. # Default off so bulk account import cannot spawn many heavy LSPs at once. # LS_PREWARM_ON_ACCOUNT_ADD=0 # Background credit/token refresh skips accounts currently serving chat, # account maintenance, or LS maintenance by default. Set 0 only if you want # scheduled maintenance to run even when an account is busy. # WINDSURFAPI_BACKGROUND_MAINTENANCE_SKIP_BUSY=1 # Native Cascade tool bridge. Default off because Cascade executes native # Read/Bash-style tools in the remote Windsurf workspace, while most clients # expect local execution. # v2.0.139 production canary scope is intentionally narrow: without an # explicit tool allowlist, only Bash / shell_command / run_command can route # through the native bridge. Read/Grep/Glob/WebSearch/WebFetch are protocol # matrix work, not production defaults. For local IDE tools (Claude Code, # Cline, Codex, opencode), keep the default emulation path unless you are # explicitly testing remote execution semantics. # WINDSURFAPI_NATIVE_TOOL_BRIDGE=all_mapped # Optional gray gates for real testing: # WINDSURFAPI_NATIVE_TOOL_BRIDGE_TOOLS=Bash # WINDSURFAPI_NATIVE_TOOL_BRIDGE_MODELS=claude-4.5-haiku # WINDSURFAPI_NATIVE_TOOL_BRIDGE_API_KEYS=your-test-api-key # Force bridge mode for mapped subsets only after the canary is proven: # WINDSURFAPI_NATIVE_TOOL_BRIDGE=1 # Emergency hard-off: # WINDSURFAPI_NATIVE_TOOL_BRIDGE_OFF=1 # Smoke an already-running native bridge deployment with: # API_KEY=... BASE_URL=http://127.0.0.1:3003 npm run smoke:native-bridge # Protocol trace is lab-only. For Read wrapper reverse engineering, prefer the # dedicated child summary over global raw string dumps: # WINDSURFAPI_PROTO_TRACE=1 # WINDSURFAPI_PROTO_TRACE_READ_WRAPPER_STRINGS=0 # Optional special-agent backend for models that do not work through direct # Cascade chat (currently swe-1.6 / swe-1.6-fast / adaptive / arena-*). # Default off. When enabled, the first PoC backend uses Devin CLI print mode: # devin -p "" # Install/configure Devin CLI separately on the host/container and point this # path at it. Do not bake it into the base image unless you want that runtime # dependency for every deployment. # WINDSURFAPI_SPECIAL_AGENT_BACKEND=devin-cli # DEVIN_CLI_PATH=devin # DEVIN_CLI_MODE=print # DEVIN_MAX_PROCS=1 # DEVIN_QUEUE_TIMEOUT_MS=30000 # DEVIN_TIMEOUT_MS=600000 # Pass the checked-out Windsurf account apiKey to Devin CLI as WINDSURF_API_KEY. # Set to 0 only when the CLI has its own login/session and you understand that # the HTTP server still expects a normal WindsurfAPI account pool. # DEVIN_CLI_USE_ACCOUNT_POOL=1 # Print mode refuses caller-local tools/media by default; ACP bridging should # be used before enabling these in production. # DEVIN_CLI_ALLOW_CLIENT_TOOLS=0 # DEVIN_CLI_ALLOW_MEDIA=0 # After configuring Devin CLI/ACP, validate the route with: # API_KEY=... BASE_URL=http://127.0.0.1:3003 npm run smoke:special-agent # ========== DEVIN_CONNECT (pure-HTTP Devin backend — POST-CASCADE SURVIVAL) ========== # Cascade (the legacy LS-driven chat backend) is retiring. DEVIN_CONNECT is the # pure-HTTP replacement: it talks directly to the Devin cloud GetChatMessage # endpoint using the pooled session tokens (accounts.json) — NO language_server # binary and NO local `devin` CLI required. This is the correct switch for a # headless box (e.g. homecloud) that has neither binary installed. # # NOTE: DEVIN_ONLY is a DIFFERENT switch — it forces traffic onto the LOCAL # Devin CLI subprocess and returns 503 if the `devin` binary is absent. For a # binary-less host use DEVIN_CONNECT, not DEVIN_ONLY. # # Minimal cutover config: set DEVIN_CONNECT=1 and keep your existing API_KEY + # account pool. Free-tier accounts resolve only `swe-1-6-slow`; paid selectors # (claude-*/gpt-*/gemini-*) require a paid entitlement on the pooled account. # DEVIN_CONNECT=1 # # Single-token deploy (no account pool): provide the session token in env. # DEVIN_CONNECT_TOKEN= # # Cross-account failover: max pooled-account hops after a dead session token. # Default 2; set 0 to disable failover (same-account re-login still applies). # DEVIN_CONNECT_FAILOVER_MAX=2 # # --- Speculative re-issues: empty completions and thinking-only turns --- # These decide how often the proxy re-sends a request the upstream already answered, # so they cost real tokens on a real account. Worst case per client request is # 1 + RETRY_ON_EMPTY_MAX + RESCUE_MAX upstream calls, and that ceiling IS the account # protection: a paid E2E (2026-07-08, 27/27) showed the weak (fable) family returns # DETERMINISTIC empties, so retrying never heals them and only triples upstream load # until the account is burned into a 3h rate limit. # # Weak models are therefore exempt from BOTH mechanisms regardless of what you set here. # Note the asymmetry in why: for empty-retry that exemption is what the E2E measured; # for the rescue it is an extrapolation — whether fable produces reasoning-only finishes # at all is unproven either way, and the code errs toward the account protection. # # Master off switch for BOTH mechanisms. Default ON: an empty completion is always a # degenerate result, and the retry only fires when the turn yielded nothing at all. # Set 0 to stop all speculative re-issues (e.g. on an account you are nursing back # from a rate limit). # DEVIN_CONNECT_RETRY_ON_EMPTY=1 # Max retries for a turn that produced ZERO output (no content, no reasoning, no tool # call) with a clean stop. Default 2. # DEVIN_CONNECT_RETRY_ON_EMPTY_MAX=2 # Backoff base; the Nth retry waits base×N. Default 350. # DEVIN_CONNECT_RETRY_ON_EMPTY_MS=350 # Max thinking-only rescues. swe-1-7 (Kimi K2 fine-tune) intermittently spends a whole # turn in reasoning declaring tool intent without emitting the call; a corrective nudge # forces emission (24/24 on a live probe). Default 2, hard ceiling 5 — values above it # are clamped, and non-numeric input falls back to the default rather than silently # disabling the feature. Set 0 to disable just the rescue, leaving empty-retry on. # Since v3.9.14 this budget is independent of RETRY_ON_EMPTY_MAX (#240): a rescue chain # no longer consumes the empty-retry budget. The combined ceiling above is unchanged. # DEVIN_CONNECT_RESCUE_MAX=2 # # --- Session continuity (experimental, opt-in, default OFF) --- # By default every DEVIN_CONNECT call mints a fresh protobuf session_id (#16), # so the upstream velocity limiter sees each turn of one dialog as a brand-new # session. With reuse ON, a stable session_id is derived from the conversation's # own completed request/response pair chain (never from requestId/IP/timestamp), # so a multi-turn dialog rides ONE session_id. Identity survives tool_call_id # regeneration, system-prompt drift, and response truncation; parallel dialogs on # the same key stay independent. In-memory only (no persistence across restarts). # DEVIN_CONNECT_SESSION_REUSE=0 # Stable ModelConfig (#15.1/#15.2) keyed on the session from DEVIN_CONNECT_SESSION_REUSE. # Genuine devin.exe keeps #15.1 constant within a session and increments #15.2 per turn # (calibrated on capture 9501aa2c). Requires DEVIN_CONNECT_SESSION_REUSE. Default 0 # (stateless: fresh uuid + turn 1, byte-identical to pre-feature wire). # DEVIN_CONNECT_MODEL_CONFIG_STABLE=0 # --- Reasoning continuity (Thinking-core T1) --- # Keep per-session reasoning tails and re-inject them as a system-prompt # checkpoint on the next turn (text channel; A3 matrix proved #11/#9 are # accepted but not consumed upstream). Requires DEVIN_CONNECT_SESSION_REUSE. # Injection gate, default off (store still fills when MAX_CHARS > 0). # DEVIN_CONNECT_SESSION_REASONING_INJECT=0 # Total injection budget AND per-turn digest cap (tail kept). 0 disables. # Clamped at 32000 (ceiling) so a runaway value cannot bloat the system prompt. # DEVIN_CONNECT_SESSION_REASONING_MAX_CHARS=4000 # Queue length in turns kept per session (capped 32). # DEVIN_CONNECT_SESSION_REASONING_COUNT=5 # --- Think-text reroute (Thinking-core item 1, loop break) --- # When the model emits a LEADING think-tagged span on the CONTENT channel, reroute it # to the thinking channel so clients do not store it as visible assistant text and # resend it into a self-reinforcing loop. Anthropic Messages egress. Default 0 (off). # DEVIN_CONNECT_THINKTEXT_REROUTE=0 # State TTL before a dormant dialog's session_id is forgotten. Default 1800000 (30 min). # DEVIN_CONNECT_SESSION_TTL_MS=1800000 # Max tracked dialogs (LRU-evicted). Default 500. # DEVIN_CONNECT_SESSION_MAX_STATES=500 # # --- Timeouts --- # Idle (no-activity) socket timeout — resets on every byte. Default 120000. # DEVIN_CONNECT_IDLE_TIMEOUT_MS=120000 # Absolute wall-clock cap from request start — the real backstop against a # hung upstream that trickles bytes forever and defeats the idle timer. # Default 600000 (10 min). Lower it for tighter SLAs. # DEVIN_CONNECT_TIMEOUT_MS=600000 # # --- Auto-recovery (recommended for unattended prod) --- # Session tokens are opaque session_ids with no expiry/refresh — when one dies # the ONLY recovery is a fresh email/password Auth1 login. Enable auto re-login # + an encrypted credential store so a dead token self-heals. Accounts added via # the email/password login path auto-store their password in this store. # DEVIN_CONNECT_AUTO_RELOGIN=1 # DEVIN_CONNECT_CRED_KEY=<32+ char secret — required for auto-relogin to work> # DEVIN_CONNECT_CRED_FILE=/accounts.creds.json> # # --- Liveness probe (recommended) --- # Zero-billable GetUserStatus sweep that detects + pre-emptively recovers dead # tokens before a real request hits them. Off by default. # DEVIN_CONNECT_LIVENESS_PROBE=1 # DEVIN_CONNECT_LIVENESS_INTERVAL_MS=600000 # Successful live model-catalog cache TTL, per account. Default 300000 (5 min), # minimum 10000. Empty/failed refreshes keep the last-known-good catalog and do # not advance the success timestamp. # DEVIN_CONNECT_CATALOG_TTL_MS=300000 # # --- Optional wire-tag decoders (frame-verified 2026-07-05; default off) --- # actual_model_uid: report the concrete model that served a request (differs # from the requested selector for router models). Frame-verified at metadata #7.9. # DEVIN_CONNECT_ACTUAL_MODEL_TAG=9 # Native tool_call decode (repeated ChatToolCall). Off = prompt emulation owns # tool calls. Only set once calibrated against a paid tool-using capture. # DEVIN_CONNECT_TOOL_CALL_TAGS=outer=6,id=1,name=2,arguments_json=3 # Billing / cache-usage decode (metadata #7 varints). DEFAULT: cache_read_tokens=5 # — CONFIRMED 2026-07-23 on a paid teams account (miss dumped {2,3,6}, hit # {2,3,5,6}; tag5=3840 + tag2=436 == 4276 == the miss request's prompt_tokens), # so cached input shows up as prompt_tokens_details.cached_tokens instead of being # billed as fresh in the dashboard (#220). Harmless on free accounts: the counter # is zero there and protobuf omits zero-valued scalars, so nothing is decoded. # Set this to override the map, or to `off` to decode nothing. Calibrate further # tags (credit_cost / committed_*) on a PAID token with DEVIN_CONNECT_DEBUG_META=1. # DEVIN_CONNECT_BILLING_TAGS=cache_read_tokens=5 # Send the role-priming chunk eagerly (pre-2.0.146 behavior). Default defers it # until the first real delta so first-connect transient errors keep recovery armed. # DEVIN_CONNECT_EAGER_PRIME=1 # Collapse system-role content into the next user turn as a block. # Default off. This avoids the stricter upstream content-policy path on request # field #2 while preserving message order. With native tools and no remaining # field-#2 content, the existing benign tools placeholder is still emitted. # DEVIN_CONNECT_COLLAPSE_SYSTEM=1 # # --- Vision / images over DEVIN_CONNECT (native wire; default tag = 10) --- # Field carrying repeated images inside each ChatMessage. The extracted Devin # schema and a native SWE-1.7 request/response independently confirm #10, with # ImageData {base64_data #1, mime_type #2}. Images stay on their original source # message; no synthetic read tool call/tool result/ToolDef is invented. # Unset = 10 (enabled). Set 0 for an emergency rollback that suppresses images. # DEVIN_CONNECT_IMAGE_TAG=0 # Override the inner ImageData field tags. POSITIONAL, exactly `base64,mime` # (anything else is ignored and the default kept). Default `1,2` matches the # schema, the native SWE-1.7 capture, and the Cascade path. Only change this # against a fresh capture showing an upstream wire migration. # DEVIN_CONNECT_IMAGE_INNER_TAGS=1,2 # # --- Router models (adaptive / arena-*) — built, gated OFF, tags uncalibrated --- # Resolve a router name to a concrete model via AssignModel before dispatch. # Default off because the wire tags below are INFERRED from frame shape, not # confirmed on a paid round-trip. See docs/DEVIN-CONNECT-CUTOVER.md §8. # DEVIN_CONNECT_ASSIGN_MODEL=1 # Tag overrides for that round-trip. Inferred defaults: req_model_uid=2, # resp_assignment=1, asg_model_uid=1, asg_jwt=2, asg_harness=3. # DEVIN_CONNECT_ASSIGN_TAGS=req_model_uid=2,resp_assignment=1,asg_model_uid=1 # Extra names/prefixes to treat as router models, beyond the built-in list. # DEVIN_CONNECT_ROUTER_MODELS=auto,smart-* # # --- Upstream finish-reason map (calibrate before trusting) --- # The upstream sends finish as an enum varint. Unmapped values fall back to a # clean stop, so a truncation or a refusal can read as a normal completion until # the real numbers are pinned from a capture. Format: `=` # where the reason is one of stop / length / tool_calls / content_filter. # DEVIN_CONNECT_STOP_REASON_MAP=3=length,8=content_filter # # --- Native tool definitions on the connect path (calibration surface) --- # Field tags for sending ToolDefs natively instead of via prompt emulation. # Unset → the built-in default map when native tool mode is on, disabled otherwise. # Accepted keys: outer, name, description, parameters (alias: schema), strict — # unknown keys are ignored rather than failing the whole map. A bare positional # form is back-compat only and must be EXACTLY four numbers # (outer,name,description,parameters); a 3- or 5-tuple fails closed to disabled. # DEVIN_CONNECT_TOOL_DEF_TAGS=outer=10,name=1,description=2,parameters=3 # Probe: send ToolDefs WITHOUT the emulation preamble, to test whether upstream # honours them alone. Diagnostic only — leave unset in production. # DEVIN_CONNECT_TOOL_DEF_SOLO=1 # # --- Diagnostics (verbose; none of these are for steady-state production) --- # Dump the metadata varint map of every response. This is the tool the cutover # runbook points at for calibrating billing / ACU tags on a PAID token (#239). # DEVIN_CONNECT_DEBUG_META=1 # Write raw request/response protobuf bytes to disk for offline RE. WIRE_DUMP # needs a writable directory; DUMP_RAW logs raw frames inline. Both are noisy and # can capture request content — do not enable on a shared deployment. # DEVIN_CONNECT_WIRE_DUMP=1 # DEVIN_CONNECT_WIRE_DUMP_DIR=/tmp/windsurfapi-wire # DEVIN_CONNECT_DUMP_RAW=1 # # --- Response-store absolute retention bound --- # RESPONSE_STORE_TTL_MS above is an IDLE timeout: it measures time since the last # access and every successful GET refreshes it, so a polling client could keep one # entry alive forever. This is the bound that does not depend on read traffic — # measured from the entry's creation and never refreshed. Default 86400000 (24h). # Chosen 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 a while longer, and the byte/count caps already bound total memory. # RESPONSE_STORE_MAX_AGE_MS=86400000 # # --- Narrate-instead-of-calling retry (referenced in the README FAQ) --- # Some models answer "I will read that file" in prose instead of emitting a tool # call. When that happens the narration is folded back into history with a # correction prompt asking for a protocol-conformant emit (#125). # Tri-state, and the default is NOT a plain on/off: # unset → ON for GLM / Kimi / Zhipu / Moonshot only (they narrate most), OFF elsewhere # 1 → force ON for every model, including Claude / GPT # 0 → disable globally # Claude and GPT are left out by default because their first-pass compliance is # good and the retry costs an extra upstream call. # WINDSURFAPI_NLU_RETRY=1 # # --- Language Server download mirror (read by install-ls.sh, not by the server) --- # Base URL that install-ls.sh pulls the LS binary from. Default is the public # mirror `https://github.com/dwgx/windsurf-ls-release/releases/latest/download`. # Point it at your own release repo for an air-gapped install or to pin an older # binary. This one is consumed by the shell installer — setting it in .env has no # effect on a running server; pass it on the install-ls.sh command line. # WINDSURFAPI_LS_RELEASE=https://github.com///releases/latest/download # # --- Misc bounds --- # Cap the number of distinct model names the dashboard stats map tracks # (overflow folds into an "(other)" bucket). Default 500; 0 = unbounded. # STATS_MAX_MODELS=500 # # Thinking-only rescue digest: when a turn finishes reasoning-only (swe-1-7 # intermittently declares tool intent without emitting the call), the corrective # nudge quotes the END of that reasoning so the model resumes where it stopped. # Caps how many trailing reasoning chars are kept for the digest. Default 2000; # 0 = plain nudge without any reasoning quote. Hard ceiling 32000 — values above it # are clamped (without it `1e9` passes the finite check and ships the ENTIRE reasoning # in every rescue nudge; measured 50089 bytes from 50000 chars of reasoning). # NOTE: leaving this blank (`…MAX_CHARS=` with nothing after the `=`) reads as 0 and # turns the digest OFF, because Number('') === 0. The same is true of the three sibling # knobs — DEVIN_CONNECT_RETRY_ON_EMPTY_MAX, DEVIN_CONNECT_RETRY_ON_EMPTY_MS and # DEVIN_CONNECT_RESCUE_MAX — which are named here rather than called "the three above" # because they live ~75 lines up in the "Speculative re-issues" block, not adjacent. # To get a default back, comment the line out rather than blanking it. # Values are floored to an integer: a fraction below 1 would otherwise mean "no cap", # since the consumer is slice(-n) and slice(-0.5) === slice(0) === the whole string. # DEVIN_CONNECT_RESCUE_REASONING_MAX_CHARS=2000 # # Debug/probe only — replay assistant reasoning back upstream as a native # reasoning tag on request build. '1' = write as tag #11 (the genuine-history # candidate), '9' = write as tag #9 (negative control; #9 is the inbound tag). # Unset = byte-identical traffic (default). Live matrix (2026-08) showed # upstream ACCEPTS but does NOT CONSUME either field; kept for future re-probing. # DEVIN_CONNECT_REPLAY_REASONING= # # Validate the whole path post-deploy with: # API_KEY=... BASE_URL=http://127.0.0.1:3003 npm run smoke:devin-connect # (set CONNECT_SMOKE_REAL_CALLS=0 to run preflight only, zero billable calls) # ========== Dashboard ========== # Dashboard password — protects /dashboard and all /dashboard/api/* endpoints. # The dashboard now FAILS CLOSED by default: with no DASHBOARD_PASSWORD and no # API_KEY set, unauthenticated requests are denied even on a localhost bind. # Set DASHBOARD_PASSWORD (or API_KEY, which is accepted as the dashboard secret # on localhost) for any real deployment. DASHBOARD_PASSWORD= # Opt back into the old open-on-localhost convenience (single-user dev only). # When =1, a localhost bind with no secret configured is treated as authed. # Discouraged; leave unset for fail-closed. Never enable on an internet bind. DASHBOARD_ALLOW_NO_AUTH= # CSV allowlist of origins reflected in Access-Control-Allow-Origin for the # dashboard API. Unset = no cross-origin (same-origin only). Do NOT use "*". DASHBOARD_CORS_ORIGINS= # When =1, /dashboard/api/self-update may hard-reset (git reset --hard) even if # it would drop unpushed local commits. Default: refuse and report. Opt-in only. DASHBOARD_ALLOW_HARD_RESET= # ========== Astraflow (OpenAI-compatible, 200+ models — https://astraflow.ucloud-global.com) ========== # Global endpoint (env: ASTRAFLOW_API_KEY) — base URL: https://api-us-ca.umodelverse.ai/v1 ASTRAFLOW_API_KEY= # China endpoint (env: ASTRAFLOW_CN_API_KEY) — base URL: https://api.modelverse.cn/v1 ASTRAFLOW_CN_API_KEY= # ========== Advanced ========== CODEIUM_API_URL=https://server.self-serve.windsurf.com # 不传 model 时用哪个。默认(不设此项)是 claude-sonnet-4.6。 # 注意命名空间:此处必须写一个能被 DEVIN_CONNECT 解析的名字。生产默认走 # DEVIN_CONNECT,而 claude-4.5-sonnet-thinking 这类旧 Cascade 名在 connect # 上 mapped:false —— 会被静默降级成免费 selector swe-1-6-slow(输出与计费 # 都不是你要的模型)。改动后用 GET /v1/models 确认这个名字在列表里。 # DEFAULT_MODEL=claude-sonnet-4.6 MAX_TOKENS=8192 LOG_LEVEL=info # ========== 429 缓解 / 限流调优 (2026-07-12: 默认已开启) ========== # 这三个旋钮治理 Claude Code / OpenCode 的 "429 死循环":单号被限流 → 网关秒拒 # 429 → 客户端立即重试 → 冷却越叠越长 → 打穿账号池。都是运行时 tunable(面板 # 设置页可热调,不重启),也可用下面的环境变量固定。2026-07-12 起默认值已从 # "字节等价关闭" 翻为 "开启";把值改回括号里的旧默认即可回退。 # # F1 降级放行:整个可用池仅被"瞬态限流"(非配额耗尽)时,不甩 429,而是选一个 # "最不糟、只是短暂冷却"的号继续服务。配额干井(quotaResetAt)仍严格排除。 # 默认 true(旧默认 false=硬 429)。设 0 关闭。 # WINDSURFAPI_DEGRADED_SERVE=1 # # F2 裸 429 短冷却:上游给裸 429(无 Retry-After reset)时账号冷却时长(毫秒)。 # 默认 15000(15s);旧默认 300000(5min)。KiroStudio 生产数据显示裸 burst 几秒 # 自愈,冻 5min 是小池雪崩源。breaker 指数退避仍会拉长重复触发的号。 # WINDSURFAPI_RL_BURST_MS=15000 # # F3 客户端退避地板:给客户端的 Retry-After 响应头设最小值(毫秒),掐断"1s 提示 # → 立即重试"的热循环。默认 30000(30s);旧默认 0(无地板)。注意:影响所有 429 # 响应(不止 CC),所有客户端至少等这么久。ceil 默认 600000(10min)只削超长值。 # WINDSURFAPI_RL_CLIENT_BACKOFF_FLOOR_MS=30000 # WINDSURFAPI_RL_CLIENT_BACKOFF_CEIL_MS=600000 # ========== Cascade Reuse (single-user Claude Code optimization) ========== # Caller-based fallback: when fingerprint misses, reuse the latest cascade # for the same caller+model. Set to 1 for single-user Claude Code setups. CASCADE_REUSE_BY_CALLER=0 # Max pool entries. Single-user setups can set to 1-5. CASCADE_POOL_MAX=500 # Don't hash system prompt (reduces fingerprint drift from Claude Code's # dynamic system prompt). Already defaults to 0. # CASCADE_REUSE_HASH_SYSTEM=0 # ========== Sticky session (account affinity / prompt-cache economics) ========== # Pin each conversation to ONE upstream account across turns. Off by default. # Why it matters on DEVIN_CONNECT: upstream prompt caches are per-account, and # a cache WRITE costs several times a read (~5.6x, from the 17.8%-of-miss calibration in # devin-connect.js; the older "~10x" figure in this repo has no derivation anywhere and is # superseded) — without pinning, the pool's LRU rotation # hands every turn to a different account and re-writes the whole accumulated # context each turn. With pinning, each turn writes only its increment and # reads the rest back. On Cascade it also keeps a resumed cascade_id valid. # STICKY_SESSION_ENABLED=1 # Binding TTL / max concurrent bindings (defaults: 30 min / 10000). # STICKY_SESSION_TTL_MS=1800000 # STICKY_SESSION_MAX=10000 # Binding requires a per-user scope on the caller (body.user, safety_identifier, # prompt_cache_key, Claude Code metadata.user_id, or an x-session-id header) so # a shared API key can't funnel N users onto one account. A genuine single-user # self-host without those signals opts in via: # WINDSURFAPI_SINGLE_TENANT_CACHE=1 # Watch it work: GET /dashboard/api/connect-metrics → sticky.{hits,creates,...}. # Related experimental flags (dashboard → 实验性): stickyBindByUserOnly, # stickyNoFallback. # # queue-on-pin:当被绑定的账号暂时不可用(RPM 触顶 / 冷却)时,**短等它自己恢复**, # 而不是立刻换到替补号。0 = 关(默认),现有部署行为不变。2000 是有用的起点。 # # 为什么值得等:换号要在替补上付一次完整前缀写(约为读的 5.6 倍 —— 这个比例来自 # devin-connect.js 的 "hit cost measured at 17.8% of miss",1/0.178;**不是**来自那组 # tag4 token 计数,那组数只确定了哪个 tag 承载 cache-write),而且绑定不会回来 # —— 成功路径会把替补重新钉上,没有回家机制。实测 8 轮(前 2 轮 warmup + 6 轮受阻): # 关 → A,A,sub,sub,sub,sub,sub,sub 触达 2 个账号 = 2 次完整前缀写 # 开 → A,A,A,A,A,A,A,A 触达 1 个账号 = 1 次 # # 代价:每个受阻轮约 +1 秒延迟。下界由 getAccountAvailability 的 retryAfterMs 地板 # (1000ms)决定,即使窗口实际 400ms 就恢复也会等满 1 秒。所以这是"省 token 还是 # 抢延迟"的选择:省成本开它,抢延迟别开。 # # 只在账号自己声明的恢复时间放得进预算时才等 —— 配额干涸(小时级)立即换号,权限类 # 不可用永不等待(它不会过期)。可在面板设置页热调,无需重启。 # WINDSURFAPI_STICKY_QUEUE_ON_PIN_MS=2000 # ========== Responses API server-side state ========== # The Responses API is built around the server holding the conversation: a client # sends only the new turn plus previous_response_id. On by default. Turn it off and # requests carrying previous_response_id get a 400 telling them to send the full # `input` array instead (never a silent context reset). # Conversations are scoped by callerKey, so one tenant can never chain from # another's response id. Bounded by TTL + LRU + a per-tenant fair share. # RESPONSE_STORE_ENABLED=0 # RESPONSE_STORE_TTL_MS=3600000 # RESPONSE_STORE_MAX=2000 # RESPONSE_STORE_MAX_MESSAGES=400 # Total byte budget. The count caps above bound cardinality, NOT memory: a # realistic agent-loop conversation measures ~167KB once stored, so the default # 2000 entries is ~327MB (a text-heavy shape measured ~518MB). Lower this on a # small VPS — eviction triggers on whichever limit binds first. # RESPONSE_STORE_MAX_BYTES=128m # Watch it: GET /dashboard/api/connect-metrics -> responseStore.{hits,misses,size} # ========== Security ========== # Allow private/internal hosts (e.g., 192.168.x.x, 10.x.x.x, localhost) in proxy tests. # Set to 1 for local deployments where you need to test proxies on private networks. # Leave empty or set to 0 for public-facing deployments (default: only public hosts allowed). ALLOW_PRIVATE_PROXY_HOSTS= # Run the REST API with NO API_KEY (fail-open). Default OFF = fail closed: a # request without a matching API_KEY is rejected even on a local bind, because # "local bind" is not "no proxy" (a same-host reverse proxy makes every request # look loopback). Set to 1 ONLY for genuine local/dev use on a local bind; never # on a public bind. The Windows exe auto-generates an API_KEY so it never needs this. # WINDSURFAPI_ALLOW_UNAUTHENTICATED= # Let the chat API_KEY double as the dashboard password on a verified-local # client (single-secret convenience). Default OFF = the dashboard requires a real # DASHBOARD_PASSWORD. A shared chat key granting full operator access is a footgun, # and a reverse-proxy misconfig can make a remote client look local — so this is # opt-in. If you enable it, also set TRUST_PROXY_X_FORWARDED_FOR=1 behind a proxy. # DASHBOARD_ALLOW_API_KEY_AS_PASSWORD= # Open the dashboard with NO secret at all, from a verified-local client only. # Default OFF = fail closed. Legacy single-user open-local convenience. # DASHBOARD_ALLOW_NO_AUTH= # ========== 之前漏记的运维项 ========== # 这一节补的是"src/ 会读、运维确实会去设、不知道会出事"的项。 # # 刻意不在本文件列出的三类(它们不是遗漏): # 1. 线上校准 tag(DEVIN_CONNECT_*_TAG / *_TAGS / STOP_REASON_MAP 等)—— # 取值只能从真实付费抓包得到,流程见 docs/DEVIN-CONNECT-CUTOVER.md §8; # 2. 调试转储(DEVIN_CONNECT_WIRE_DUMP* / WINDSURFAPI_PROTO_TRACE_* / # DEBUG_REQUEST_BODIES)—— 只在排障时临时开,会写大量数据到磁盘; # 3. 已由 Dashboard 实验性面板暴露的内部旋钮(断路器/配额/错误窗口等)—— # 用面板改会持久化到 runtime-config,比环境变量更合适。 # ---- 干旱模式(低配额保护)---- # 所有活跃账号本周配额都低于阈值时,对 premium 模型直接返 503,而不是让请求 # 一路打到上游 rate-limit 把最后一点配额烧掉。**默认开启**。 # 免费层模型不受限制 —— 注意这份免费白名单按后端解析:Cascade 上是静态免费 # 模型表,DEVIN_CONNECT 上是 connect 命名空间真正免费可达的 selector。 # 阈值本身在 Dashboard 实验性面板配(GET /dashboard/api/drought 看实时值)。 # DROUGHT_RESTRICT_PREMIUM=0 # ---- 上游反竞品指纹的防线(两个默认开的关,关掉是静默失效)---- # 这两个都**默认开**,列在这里是因为关掉之后的失败形态是静默的:请求被上游挡下, # 而报错不会提到这两个开关,所以运维很难把症状连回来。 # # Devin 上游对竞品 coding-agent 流量有两道闸:529 竞品指纹闸,以及 2026-07-10 实测 # 确认的 content-policy permission_denied(踩 Claude Code Agent-SDK 那行自我标识)。 # 默认会把 system prompt 正文里的这类指纹改写成通用助手身份,请求才发得出去。 # 设 0 关掉改写 —— 只在你确认某条改写规则误伤了自己的提示词时才用。 # WINDSURFAPI_NEUTRALIZE_CLIENT_ID=0 # # Cascade 路径上把长 system prompt 压缩成事实清单(标题生成那类短指令除外)。 # 设 0 只做身份改写、不压缩。长 prompt 在 Cascade 上更容易触发上游限制,所以关掉它 # 之后看到的失败同样不会指向这个开关。 # CASCADE_COMPACT_CLAUDE_SYSTEM=0 # ---- 未知模型名的处理(footgun,建议别关)---- # 默认 1 = 目录里没有的模型名直接 400 model_not_found。 # 设 0 会恢复"静默降级到免费 selector swe-1-6-slow"的旧行为 —— 这不是一个 # 兼容开关而是个陷阱:客户端明确要了付费模型,实际跑的是免费模型,**输出与 # 计费都变了**,而响应里回显的仍是客户端请求的那个名字,客户端无法察觉。 # 只有在确信快照过期、且某个确实存活的 selector 被误拒时才临时用。 # WINDSURFAPI_STRICT_MODEL=0 # ---- total_tokens 的算术恒等式 ---- # OpenAI 规定 total_tokens == prompt_tokens + completion_tokens。本代理默认**刻意 # 打破**这条恒等式:cache_write > 0 时 total 会多出 cache_write,因为它承担的是 # "完整可计费成本"这个职责,而分桶字段严格遵守 OpenAI/Anthropic 语义 # (cache_write 走 cache_creation_input_tokens,不进 prompt_tokens)。 # # #118 是刻意这么选的:另一个方案(cache_write 进 prompt_tokens)会让下游中转 # (one-api / new-api / sub2api)把它当普通输入计费,几小时烧穿试用额度。 # # 设 1 恢复恒等式,给会**校验**这条等式的客户端用。默认 0,因为两种失效不对称: # 恒等式被破对绝大多数消费者只是观感问题,而把 cache_write 从 total 里拿掉会 # **少报真实开销** —— 按 total_tokens 计费的中转会悄悄少收钱。观感问题比钱的问题 # 轻,所以"严格合规"是 opt-in 而不是默认。 # # 开了也不影响你自己的账:成本统计**不只**读 total_tokens —— 它取 # max(total_tokens, prompt + completion + cache_creation_input_tokens),所以 flag 从 total # 里拿掉的那部分会从 cache_creation_input_tokens 补回来。(此处原文写的是"不读 # total_tokens",那句字面上是错的:它确实读,只是不**只**读。行为承诺不变,有断言钉住 # "开关前后账单不变"。) # # 三个 usage builder 全部生效(Cascade / DEVIN_CONNECT / Devin CLI-ACP)。这一点是 # 刻意的:一个只在部分协议前端成立的 flag 比没有这个 flag 更糟 —— 客户端无法判断 # 手上这个响应属于哪一种。有断言钉住三者在开启时必须**同时**满足恒等式。 # WINDSURFAPI_STRICT_USAGE_TOTAL=1 # ---- 绑定地址 ---- # BIND_HOST 是 HOST 的别名(HOST 优先)。打包版(exe)在两者都未设时会把 # HOST 兜底成 127.0.0.1,以免首次运行就把无鉴权网关暴露到局域网 —— 而只设 # BIND_HOST 会满足那个条件判断却不提供 loopback 值,绑定仍是 0.0.0.0。 # 想限制在本机请设 HOST,不要用 BIND_HOST。 # BIND_HOST=127.0.0.1 # ---- 图片解码上限(资源护栏)---- # 单张图片字节上限、长边像素上限、重编码 JPEG 质量。 # WINDSURFAPI_IMAGE_MAX_BYTES=10m # WINDSURFAPI_IMAGE_MAX_LONG_SIDE=1568 # WINDSURFAPI_IMAGE_JPEG_QUALITY=82 # ---- Language Server 自动重启 ---- # LS 进程异常退出后是否自动重启,以及重试次数与首次退避。仅 Cascade 路径用到 # LS;DEVIN_CONNECT 是纯 HTTP 出口,不需要 LS。 # LS_AUTO_RESTART=1 # LS_AUTO_RESTART_MAX_RETRIES=5 # LS_AUTO_RESTART_BASE_DELAY_MS=1000 # ---- 登录失败锁定表上限 ---- # 暴力破解防护的锁定表最多记多少个来源。上限存在的意义是防止伪造 XFF 的请求 # 把这张表撑成内存放大器。 # LOCKOUT_MAX_ENTRIES=10000 # ---- ACP 视觉路径 ---- # 带图片的请求改走 Devin CLI 的 ACP 通道,而不是上面的原生 Connect #10 # 直连路径。适合需要完整 Devin CLI 多模态会话语义的部署,需要本机装好 devin CLI。 # 注意:这条重路由会在 connect 分支的前置校验**之前**离开该分支。 # DEVIN_ACP_VISION=1 # ---- 配额干涸判定 ---- # 本周配额百分比低到多少算"干涸"(会给账号加一段自愈冷却),以及冷却时长。 # WINDSURFAPI_QUOTA_DRY_THRESHOLD=0 # WINDSURFAPI_QUOTA_COOLDOWN_MS=1800000 # ---- 启动行为 ---- # 跳过 .env 加载(容器里用真实环境变量注入时更干净)。 # WINDSURFAPI_SKIP_DOTENV=1 # 启动后不自动打开浏览器(打包版默认会开)。 # WINDSURFAPI_NO_OPEN=1 # gRPC 传输协议;'connect' 走 Connect 协议,默认走 gRPC。仅影响 Cascade 路径。 # GRPC_PROTOCOL=connect # --- Reasoning/content 边界泄漏追踪 (实验性,默认 OFF) --- # WINDSURFAPI_LEAK_TRACE=1 # 输出 LEAK_TRACE 前缀的结构化日志,用于在线抓取"模型推理(thinking)泄漏进 # content 通道"的活体问题(#238/#241/#243 只做了 rescue 与 ` thinking` 重排, # 从未在强制复现中抓到泄漏本身)。每个流事件记:channel(content/reasoning)、 # think(命中的标记,如 / / ◁think▷)、sample(截断样本,非全文)、 # len、reqId/account;messages 路径记 block-start/classify(blockType/channel/ # msgId/reqId);settle 时记 outcome/contentChars/reasoningChars/rerouted(outcome # 标记 ok / client-abort / upstream-error —— 错误出口上其余字段为空是预期, # outcome 说明为什么)。默认 OFF:关闭时热路径只读一次 env 标志,不改任何行为。 # 量级(实测):逐事件记录 —— 一个 2000 token 的回答(每 token 一个事件) # 产生 2000 行、约 215 KiB(约 110 B/行)。生产上打开抓现场时按"每个长回答 # 几百到几千行"预估,抓到后改回 0 并重启即可关闭。