-
Notifications
You must be signed in to change notification settings - Fork 618
Expand file tree
/
Copy path.env.example
More file actions
673 lines (643 loc) · 39.3 KB
/
Copy path.env.example
File metadata and controls
673 lines (643 loc) · 39.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# ========== Server ==========
PORT=3003
# Bind host. Default 0.0.0.0 so Docker/LAN work; set 127.0.0.1 for loopback.
# Empty API_KEY is FAIL-CLOSED (401) even on a local bind — "local bind" is
# not "no proxy". A non-local bind also needs DASHBOARD_PASSWORD for panel
# writes. Open chat access is opt-in only:
# WINDSURFAPI_ALLOW_UNAUTHENTICATED=1 AND a local bind. Windows exe
# auto-generates a key on first run so it never needs that flag.
# HOST=0.0.0.0
# Chat/REST caller key. Leave empty only if you also set the opt-in above.
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 "<prompt>"
# 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=<path; default <DATA_DIR>/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.
# Default map is cache_read_tokens=5,cache_write_tokens=4 only. Fractional ACU
# (#239) is opt-in until a live frame pins top-level #22: add committed_acu_cost=^22.
# DEVIN_CONNECT_BILLING_TAGS=cache_read_tokens=5,cache_write_tokens=4,committed_acu_cost=^22
# 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 <system> 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: `<varint>=<openai reason>`
# 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/<owner>/<repo>/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.
# FAILS CLOSED by default: missing DASHBOARD_PASSWORD is 401 even on localhost.
# API_KEY does NOT double as the dashboard secret unless you also set
# DASHBOARD_ALLOW_API_KEY_AS_PASSWORD=1 (verified-local convenience, default OFF).
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 能解析的名字。旧 Cascade 别名
# claude-4.5-sonnet-thinking 在 connect 上 mapped:false —— 默认
# WINDSURFAPI_STRICT_MODEL=1 会 400 model_not_found;设 0 才静默降到
# 免费 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
# Hash system prompt into the reuse fingerprint. Default ON; set =0 to opt
# out (higher hit rate when Claude Code's system prompt drifts every turn,
# weaker isolation across different system prompts).
# 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
# (FREE_REACHABLE_SELECTORS, 目前 swe-1-6-slow)。另有 DROUGHT_SAFE_SELECTORS
# (swe-1-7 / swe-1-7-medium / glm-5-2):付费号旱灾时仍放行,因为它们不吃周配额。
# 那份 Set 不是免费账号 entitlement,不要和 FREE_REACHABLE 混。
# 阈值本身在 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> / </thinking> / ◁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 并重启即可关闭。