qwenproxy-cli 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +14 -14
  2. package/README.md +906 -906
  3. package/bin/qwenproxy.js +5 -1
  4. package/package.json +77 -78
  5. package/src/api/error-classifier.ts +159 -159
  6. package/src/api/error-helpers.ts +118 -118
  7. package/src/api/models.ts +261 -261
  8. package/src/api/server.ts +860 -859
  9. package/src/cache/memory-cache.ts +385 -385
  10. package/src/clean-cache.ts +204 -204
  11. package/src/core/account-concurrency.ts +671 -671
  12. package/src/core/account-manager.ts +301 -297
  13. package/src/core/account-priority.ts +163 -163
  14. package/src/core/accounts.ts +186 -186
  15. package/src/core/config.ts +383 -383
  16. package/src/core/crypto-utils.ts +79 -79
  17. package/src/core/database.ts +276 -276
  18. package/src/core/errors.ts +118 -118
  19. package/src/core/logger.ts +269 -269
  20. package/src/core/memory-usage.ts +84 -84
  21. package/src/core/metrics.ts +291 -291
  22. package/src/core/model-alias.ts +77 -77
  23. package/src/core/model-registry.ts +544 -544
  24. package/src/core/mutex.ts +119 -119
  25. package/src/core/paths.ts +199 -199
  26. package/src/core/prompt-limits.ts +214 -214
  27. package/src/core/reasoning-effort.ts +102 -102
  28. package/src/core/stream-registry.ts +96 -96
  29. package/src/core/waf-isolation.ts +117 -117
  30. package/src/core/watchdog.ts +195 -195
  31. package/src/delete-chats.ts +23 -23
  32. package/src/index.ts +65 -64
  33. package/src/login.ts +147 -147
  34. package/src/reset-cooldowns.ts +11 -11
  35. package/src/routes/anthropic/index.ts +355 -355
  36. package/src/routes/anthropic/translate.ts +522 -522
  37. package/src/routes/anthropic/types.ts +154 -154
  38. package/src/routes/anthropic/validation.ts +144 -144
  39. package/src/routes/chat/account.ts +1817 -1817
  40. package/src/routes/chat/context.ts +241 -241
  41. package/src/routes/chat/errors.ts +85 -85
  42. package/src/routes/chat/helpers.ts +268 -268
  43. package/src/routes/chat/index.ts +618 -618
  44. package/src/routes/chat/media.ts +285 -285
  45. package/src/routes/chat/retry-policy.ts +754 -754
  46. package/src/routes/chat/stop.ts +98 -98
  47. package/src/routes/chat/streaming.ts +2710 -2710
  48. package/src/routes/chat/validation.ts +526 -526
  49. package/src/routes/chat.ts +2 -2
  50. package/src/routes/completions.ts +290 -290
  51. package/src/routes/images.ts +139 -139
  52. package/src/routes/responses/adapter.ts +503 -503
  53. package/src/routes/responses/index.ts +405 -405
  54. package/src/routes/responses/state.ts +230 -230
  55. package/src/routes/responses/streaming.ts +528 -528
  56. package/src/routes/responses/types.ts +285 -285
  57. package/src/routes/responses/validation.ts +202 -202
  58. package/src/routes/upload.ts +731 -731
  59. package/src/routes/videos.ts +214 -214
  60. package/src/services/auth-playwright.ts +173 -173
  61. package/src/services/captcha-coordinator.ts +161 -161
  62. package/src/services/captcha-solver.ts +553 -553
  63. package/src/services/chat-cleanup.ts +80 -80
  64. package/src/services/context-meter.ts +317 -317
  65. package/src/services/fingerprint.ts +242 -242
  66. package/src/services/human-behavior.ts +173 -173
  67. package/src/services/media-generation.ts +1748 -1748
  68. package/src/services/playwright.ts +2878 -2800
  69. package/src/services/qwen-chat-pool.ts +345 -345
  70. package/src/services/qwen-errors.ts +133 -133
  71. package/src/services/qwen-headers.ts +79 -79
  72. package/src/services/qwen-thread-state.ts +393 -393
  73. package/src/services/qwen-url.ts +19 -19
  74. package/src/services/qwen.ts +3126 -3126
  75. package/src/services/session-keeper.ts +88 -88
  76. package/src/services/token-estimation-metrics.ts +118 -118
  77. package/src/sync/claude-code.ts +75 -75
  78. package/src/sync/codex.ts +123 -123
  79. package/src/sync/index.ts +362 -362
  80. package/src/sync/omp.ts +105 -105
  81. package/src/sync/opencode.ts +214 -214
  82. package/src/sync/types.ts +53 -53
  83. package/src/sync/utils.ts +27 -27
  84. package/src/sync-clients.ts +189 -189
  85. package/src/tools/instructions.ts +137 -137
  86. package/src/tools/manifest.ts +81 -81
  87. package/src/tools/parser.ts +2989 -2989
  88. package/src/tools/toolcall-tags.ts +142 -142
  89. package/src/tui/app.ts +259 -264
  90. package/src/tui/index.ts +61 -61
  91. package/src/tui/markdown.ts +258 -258
  92. package/src/tui/proxy-client.ts +331 -326
  93. package/src/tui/screen.ts +294 -278
  94. package/src/tui/server-manager.ts +270 -270
  95. package/src/tui/theme.ts +432 -432
  96. package/src/tui/types.ts +33 -33
  97. package/src/tui/views/accounts-view.ts +656 -656
  98. package/src/tui/views/chat-view.ts +1018 -823
  99. package/src/tui/views/logs-view.ts +479 -413
  100. package/src/tui/views/status-view.ts +204 -204
  101. package/src/tui/views/storage-view.ts +304 -291
  102. package/src/tui/views/sync-view.ts +409 -409
  103. package/src/types/ali-oss.d.ts +32 -32
  104. package/src/update-cli.ts +121 -0
  105. package/src/utils/context-truncation.ts +84 -84
  106. package/src/utils/json.ts +380 -380
  107. package/src/utils/session-id.ts +37 -37
  108. package/src/utils/tool-call-guard.ts +84 -84
  109. package/src/utils/types.ts +109 -109
@@ -1,383 +1,383 @@
1
- import { z } from "zod";
2
-
3
- const envSchema = z
4
- .object({
5
- PORT: z
6
- .string()
7
- .regex(/^\d+$/, "PORT must be a number")
8
- .refine((value) => {
9
- const port = Number(value);
10
- return port >= 1 && port <= 65535;
11
- }, "PORT must be between 1 and 65535")
12
- .default("7936"),
13
- HOST: z.string().default("0.0.0.0"),
14
- INTERNAL_HOST: z.string().default("127.0.0.1"),
15
- USER_AGENT: z
16
- .string()
17
- .default(
18
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
19
- ),
20
- QWEN_BX_V: z.string().default("2.5.37"),
21
- // Version header on Qwen API requests = the deployed web bundle version
22
- // (fallback when dynamic DOM detection is not yet completed).
23
- // Override via env when Qwen ships a new bundle.
24
- QWEN_WEB_VERSION: z.string().default("0.2.91"),
25
- // Controls bx-ua/bx-umidtoken injection on the GENERAL API paths
26
- // (chats/new, settings): those work without them (live-probed). The
27
- // completions path is the exception — the 0.2.86 HAR shows the real
28
- // client POSTs completions WITH bx-ua, so it always includes the captured
29
- // tokens regardless of this flag (buildCompletionHeaders). Set true
30
- // to inject them everywhere (legacy behavior).
31
- QWEN_SEND_BX_UA: z.string().default("false"),
32
- // Conversation mode: "thread" (default) reuses the upstream Qwen chat via
33
- // parent_id and sends the thread-native delta; "temp" creates a NEW Qwen
34
- // temp chat (chat_mode:"local") for every request and sends the full
35
- // history inline (OpenAI standard). Temp chats are ephemeral and never
36
- // appear in the account's chat list (live-probed).
37
- QWEN_CHAT_MODE: z.enum(["thread", "temp"]).default("thread"),
38
- PLAYWRIGHT_HEADLESS: z.string().default("true"),
39
- PLAYWRIGHT_BROWSER: z
40
- .enum(["chromium", "chrome", "edge"])
41
- .default("chromium"),
42
- PLAYWRIGHT_INIT_BATCH_SIZE: z.string().default("1"),
43
- PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS: z.string().default("10000"),
44
- PLAYWRIGHT_IDLE_CONTEXT_TTL_MS: z.string().default("60000"),
45
- PLAYWRIGHT_JS_HEAP_MB: z.string().default("256"),
46
- PLAYWRIGHT_LOW_MEMORY_FLAGS: z.string().default("true"),
47
- // Keep 2 warm contexts by default ({main + reserve} covers the common
48
- // failover hop without opening one browser per account): after warmup two
49
- // browsers stay open; any extra context (simultaneous use / failover) is
50
- // closed once idle. The cap only evicts IDLE contexts — busy mutexes and
51
- // active streams are never touched, so concurrent accounts each keep their
52
- // own context while serving. Accounts in cooldown (rate-limited) sit idle,
53
- // drop out of the warm set and get evicted.
54
- PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
55
- PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("true"),
56
- CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
57
- CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
58
- CAPTCHA_SOLVER_TIMEOUT_MS: z.string().default("15000"),
59
- CAPTCHA_SOLVER_RETRY_DELAY_MS: z.string().default("1000"),
60
- CAPTCHA_SOLVER_SETTLE_MS: z.string().default("2000"),
61
- CAPTCHA_ACCOUNT_COOLDOWN_MS: z.string().default("120000"),
62
- // Cap for the escalating quarantine applied when a challenge could NOT be
63
- // solved (hard block). Each consecutive hard block doubles the window
64
- // (base = CAPTCHA_ACCOUNT_COOLDOWN_MS) up to this cap.
65
- CAPTCHA_HARD_BLOCK_MAX_COOLDOWN_MS: z.string().default("3600000"),
66
- OSS_MULTIPART_THRESHOLD_MB: z.string().default("5"),
67
- CHAT_REQUEST_LOG: z.string().default("false"),
68
- HTTP_TIMEOUT: z.string().default("15000"),
69
- CHAT_TIMEOUT: z.string().default("180000"),
70
- NAVIGATION_TIMEOUT: z.string().default("60000"),
71
- PAGE_TIMEOUT: z.string().default("60000"),
72
- HEADERS_TIMEOUT: z.string().default("90000"),
73
- TIME_TO_FIRST_BYTE: z.string().default("60000"),
74
- IDLE_STREAM_TIMEOUT: z.string().default("60000"),
75
- // Deadline for the FIRST upstream chunk on thinking models (the reasoning
76
- // idle of 600s is for gaps AFTER data flows; a stream that produced
77
- // nothing in this window is dead and should fail fast, retryable).
78
- QWEN_FIRST_CHUNK_TIMEOUT: z.string().default("180000"),
79
- TOTAL_REQUEST_TIMEOUT: z.string().default("600000"),
80
- // Mid-stream silence window for thinking models: 3 min with ZERO upstream
81
- // bytes is a dead stream (WAF swallow / dropped connection) — fail fast and
82
- // let the retry policy rotate accounts. Flowing reasoning chunks RESET this
83
- // timer, so legitimate slow thinking is never cut; only total silence is.
84
- REASONING_MODEL_TIMEOUT: z.string().default("180000"),
85
- CACHE_TTL: z.string().default("3600"),
86
- RESPONSE_TTL: z.string().default("1800"),
87
- CACHE_COMPRESSION_ENABLED: z.string().default("true"),
88
- CACHE_COMPRESSION_THRESHOLD: z.string().default("1024"),
89
- CACHE_COMPRESSION_LEVEL: z.string().default("6"),
90
- METRICS_INTERVAL: z.string().default("10000"),
91
- WATCHDOG_INTERVAL: z.string().default("5000"),
92
- WATCHDOG_FAILURES: z.string().default("3"),
93
- RAM_WARNING: z.string().default("80"),
94
- RAM_CRITICAL: z.string().default("95"),
95
- WS_WARNING: z.string().default("50"),
96
- WS_CRITICAL: z.string().default("100"),
97
- RETRY_BASE_DELAY_MS: z.string().default(process.env.TEST_MOCK_QWEN_AUTH === "true" ? "50" : "1000"),
98
- RETRY_MAX_DELAY_MS: z.string().default(process.env.TEST_MOCK_QWEN_AUTH === "true" ? "200" : "10000"),
99
- RETRY_MAX_ATTEMPTS: z.string().default("3"),
100
- RETRY_MAX_ACCOUNT_SWITCHES: z.string().default("2"),
101
- RETRY_ON_UNKNOWN_UPSTREAM: z.string().default("true"),
102
- RETRY_AUTO_MALFORMED_TOOLS: z.string().default("true"),
103
- RETRY_AUTO_MALFORMED_TOOLS_MAX: z.string().default("2"),
104
- MAX_TOOL_CALLS_PER_TURN: z.string().default("24"),
105
- QWEN_REPEATED_TOOL_CALL_WARN: z.string().default("2"),
106
- ACCOUNT_MAX_CONCURRENT_STREAMS: z.string().default("2"),
107
- ACCOUNT_BUSY_WAIT_MS: z.string().default("30000"),
108
- // Cap for the "wait forever" account-lease queue (thread owner / last
109
- // usable account): the queue previously had NO deadline, so a stuck lease
110
- // holder made the next turn wait up to ~600s. A generous finite cap keeps
111
- // the wait far above the normal 30s but still bounded.
112
- ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS: z.string().default("120000"),
113
- // Hard deadline for one stream-acquire attempt. A dead account (upstream
114
- // swallows the completion fetch) otherwise chains metadata/header timeouts
115
- // for minutes; this fails the attempt visibly so the retry loop switches.
116
- ACQUIRE_DEADLINE_MS: z.string().default("120000"),
117
- ACCOUNT_LEASE_MAX_DURATION_MS: z.string().default("600000"),
118
- ACCOUNT_INIT_FAILURE_COOLDOWN_MS: z.string().default("300000"),
119
- // Timeout before a request waiting on the CHAT lock gives up. The chat lock
120
- // is held for the entire stream lifetime, so it must cover the longest
121
- // legitimate generation (reasoning models with huge contexts can spend
122
- // 2-3 min producing the first byte chain). A 60s hard cap turned a normal
123
- // long turn into a 500 for every concurrent request on the same chat
124
- // (2026-08-22 production log: lock held 130s -> acquire_deadline 120s).
125
- CHAT_LOCK_TIMEOUT_MS: z
126
- .string()
127
- .regex(/^\d+$/, "CHAT_LOCK_TIMEOUT_MS must be a number")
128
- .default("180000"),
129
- STREAM_DISCONNECT_GRACE_MS: z
130
- .string()
131
- .regex(/^\d+$/, "STREAM_DISCONNECT_GRACE_MS must be a number")
132
- .default("4000"),
133
- CHAT_IN_PROGRESS_RETRY_DELAY_MS: z.string().default("2000"),
134
- // Temporarily-busy window after a chat_in_progress: long enough to absorb
135
- // the upstream chat settle (measured ~1-2s), short enough that the next
136
- // turn of the sticky owner is not pushed to a cold account with a full
137
- // context replay (8s caused a needless 13.3s hop in the 20:04 session).
138
- CHAT_IN_PROGRESS_BUSY_MS: z.string().default("4000"),
139
- // Same-chat retry budget for chat_in_progress. The upstream chat stays "in
140
- // progress" for 2-16s after a completed turn (grows with turn size); each
141
- // retry waits a jittered window (busyMs-based) and NO retry re-sends the
142
- // full context — the escalation (new chat + full replay on a cold account)
143
- // was the ~1MB re-upload that made tool loops feel like ~40 minutes. After
144
- // this budget the request FAILS (thread binding kept) and the client's own
145
- // retry lands on the settled chat with the delta intact.
146
- CHAT_IN_PROGRESS_MAX_RETRIES: z.string().default("6"),
147
- MID_STREAM_FAILOVER_THRESHOLD: z.string().default("2"),
148
- MID_STREAM_FAILOVER_BUSY_MS: z.string().default("60000"),
149
-
150
-
151
- QWEN_BASE_URL: z.string().default("https://chat.qwen.ai"),
152
- QWEN_CHAT_POOL_SIZE: z.string().default("1"),
153
- QWEN_CHAT_POOL_MODELS: z.string().default("qwen3.7-plus"),
154
- QWEN_PERSONALIZATION_FROM_REQUEST: z.string().default("true"),
155
- QWEN_PERSONALIZATION_VERIFY_GET: z.string().default("true"),
156
- QWEN_BROWSER_ONLY_FETCH: z.string().default("true"),
157
- QWEN_MAX_PROMPT_BYTES: z.string().default("0"),
158
- QWEN_MAX_PERSONALIZATION_BYTES: z.string().default("200000"),
159
- CONTEXT_METER_ENABLED: z.string().default("true"),
160
- CONTEXT_METER_WINDOW_TOKENS: z.string().default("0"),
161
- CONTEXT_METER_REPORT_USAGE: z.string().default("true"),
162
- DELETE_ALL_CHATS_ON_SHUTDOWN: z.string().default("false"),
163
- // Keep idle account pages alive (subtlePageActivity + occasional reload).
164
- // The Baxia WAF scores live page behavior (pointer/scroll events, open
165
- // session) — an account whose page sits frozen for minutes returns a low
166
- // trust score and gets TMD-challenged on the next request. On by default;
167
- // the keeper skips accounts that are mid-stream or mutex-busy.
168
- SESSION_KEEP_ALIVE_ENABLED: z.string().default("true"),
169
- SESSION_KEEP_ALIVE_INTERVAL_MS: z.string().default("30000"),
170
- SESSION_KEEP_ALIVE_IDLE_MS: z.string().default("120000"),
171
- SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS: z.string().default("480000"),
172
- API_KEY: z.string().default(""),
173
- // Static x-ratelimit-* response headers (OpenAI-shaped, doc §5.2). The proxy
174
- // does not enforce a token/request quota; these exist for SDK/tool parsing.
175
- RATE_LIMIT_REQUESTS: z.string().default("5000"),
176
- RATE_LIMIT_TOKENS: z.string().default("200000"),
177
- })
178
- ;
179
-
180
- const env = envSchema.parse(process.env);
181
-
182
- export const config = {
183
- server: {
184
- port: parseInt(env.PORT),
185
- host: env.HOST,
186
- internalHost: env.INTERNAL_HOST,
187
- rateLimit: {
188
- requests: parseInt(env.RATE_LIMIT_REQUESTS),
189
- tokens: parseInt(env.RATE_LIMIT_TOKENS),
190
- },
191
- },
192
- logging: {
193
- chatRequests: env.CHAT_REQUEST_LOG === "true",
194
- },
195
- auth: {
196
- userAgent: env.USER_AGENT,
197
- bxV: env.QWEN_BX_V,
198
- },
199
- playwright: {
200
- headless: env.PLAYWRIGHT_HEADLESS !== "false",
201
- browser: env.PLAYWRIGHT_BROWSER,
202
- initBatchSize: Math.max(1, parseInt(env.PLAYWRIGHT_INIT_BATCH_SIZE)),
203
- contextCloseTimeoutMs: Math.max(
204
- 1_000,
205
- parseInt(env.PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS),
206
- ),
207
- idleContextTtlMs: Math.max(0, parseInt(env.PLAYWRIGHT_IDLE_CONTEXT_TTL_MS)),
208
- jsHeapMb: Math.max(64, parseInt(env.PLAYWRIGHT_JS_HEAP_MB)),
209
- lowMemoryFlags: env.PLAYWRIGHT_LOW_MEMORY_FLAGS !== "false",
210
- maxActiveContexts: Math.max(0, parseInt(env.PLAYWRIGHT_MAX_ACTIVE_CONTEXTS)),
211
- prepareAllOnStartup: env.PLAYWRIGHT_PREPARE_ALL_ON_STARTUP !== "false",
212
- },
213
- captcha: {
214
- enabled: env.CAPTCHA_SOLVER_ENABLED === "true",
215
- maxAttempts: Math.max(1, Math.min(5, parseInt(env.CAPTCHA_SOLVER_MAX_ATTEMPTS))),
216
- timeoutMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_TIMEOUT_MS)),
217
- retryDelayMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_RETRY_DELAY_MS)),
218
- settleMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_SETTLE_MS)),
219
- /** Rest an account whose challenge could not be cleared before reusing it. */
220
- accountCooldownMs: Math.max(0, parseInt(env.CAPTCHA_ACCOUNT_COOLDOWN_MS)),
221
- /** Cap for the escalating hard-block quarantine (×2 per consecutive block). */
222
- hardBlockMaxCooldownMs: Math.max(
223
- 0,
224
- parseInt(env.CAPTCHA_HARD_BLOCK_MAX_COOLDOWN_MS),
225
- ),
226
- },
227
- oss: {
228
- multipartThresholdBytes: Math.max(
229
- 1 * 1024 * 1024,
230
- parseInt(env.OSS_MULTIPART_THRESHOLD_MB) * 1024 * 1024,
231
- ),
232
- },
233
- timeouts: {
234
- http: parseInt(env.HTTP_TIMEOUT),
235
- chat: parseInt(env.CHAT_TIMEOUT),
236
- navigation: parseInt(env.NAVIGATION_TIMEOUT),
237
- page: parseInt(env.PAGE_TIMEOUT),
238
- headers: parseInt(env.HEADERS_TIMEOUT),
239
- timeToFirstByte: parseInt(env.TIME_TO_FIRST_BYTE),
240
- idleStreamTimeout: parseInt(env.IDLE_STREAM_TIMEOUT),
241
- totalRequestTimeout: parseInt(env.TOTAL_REQUEST_TIMEOUT),
242
- reasoningModelTimeout: parseInt(env.REASONING_MODEL_TIMEOUT),
243
- firstChunkTimeout: parseInt(env.QWEN_FIRST_CHUNK_TIMEOUT),
244
- },
245
- cache: {
246
- defaultTTL: parseInt(env.CACHE_TTL),
247
- responseTTL: parseInt(env.RESPONSE_TTL),
248
- compression: {
249
- enabled: env.CACHE_COMPRESSION_ENABLED !== "false",
250
- threshold: parseInt(env.CACHE_COMPRESSION_THRESHOLD),
251
- level: parseInt(env.CACHE_COMPRESSION_LEVEL),
252
- },
253
- },
254
-
255
- metrics: {
256
- interval: parseInt(env.METRICS_INTERVAL),
257
- },
258
- watchdog: {
259
- checkInterval: parseInt(env.WATCHDOG_INTERVAL),
260
- consecutiveFailuresThreshold: parseInt(env.WATCHDOG_FAILURES),
261
- ram: {
262
- warningThreshold: parseInt(env.RAM_WARNING),
263
- criticalThreshold: parseInt(env.RAM_CRITICAL),
264
- },
265
- streams: {
266
- warningThreshold: parseInt(env.WS_WARNING),
267
- criticalThreshold: parseInt(env.WS_CRITICAL),
268
- },
269
- },
270
- retry: {
271
- baseDelayMs: parseInt(env.RETRY_BASE_DELAY_MS),
272
- maxDelayMs: parseInt(env.RETRY_MAX_DELAY_MS),
273
- maxAttempts: Math.max(1, parseInt(env.RETRY_MAX_ATTEMPTS)),
274
- maxAccountSwitches: Math.max(0, parseInt(env.RETRY_MAX_ACCOUNT_SWITCHES)),
275
- onUnknownUpstream: env.RETRY_ON_UNKNOWN_UPSTREAM !== "false",
276
- chatInProgressDelayMs: Math.max(0, parseInt(env.CHAT_IN_PROGRESS_RETRY_DELAY_MS)),
277
- chatInProgressBusyMs: Math.max(0, parseInt(env.CHAT_IN_PROGRESS_BUSY_MS)),
278
- chatInProgressMaxAttempts: Math.max(1, parseInt(env.CHAT_IN_PROGRESS_MAX_RETRIES)),
279
- midStreamFailoverThreshold: Math.max(
280
- 0,
281
- parseInt(env.MID_STREAM_FAILOVER_THRESHOLD),
282
- ),
283
- midStreamFailoverBusyMs: Math.max(
284
- 0,
285
- parseInt(env.MID_STREAM_FAILOVER_BUSY_MS),
286
- ),
287
- autoRetryMalformedTools: env.RETRY_AUTO_MALFORMED_TOOLS !== "false",
288
- autoRetryMalformedToolsMax: Math.max(1, parseInt(env.RETRY_AUTO_MALFORMED_TOOLS_MAX)),
289
- maxToolCallsPerTurn: Math.max(0, parseInt(env.MAX_TOOL_CALLS_PER_TURN)),
290
- repeatedToolCallWarnThreshold: Math.max(
291
- 1,
292
- parseInt(env.QWEN_REPEATED_TOOL_CALL_WARN),
293
- ),
294
- },
295
- concurrency: {
296
- maxStreamsPerAccount: Math.max(1, parseInt(env.ACCOUNT_MAX_CONCURRENT_STREAMS)),
297
- busyWaitMs: Math.max(
298
- 0,
299
- Number.isFinite(parseInt(env.ACCOUNT_BUSY_WAIT_MS as string))
300
- ? parseInt(env.ACCOUNT_BUSY_WAIT_MS as string)
301
- : 30_000,
302
- ),
303
- /** Bound for the "wait forever" lease queue (default 2 min). */
304
- queueWaitForeverCapMs: Math.max(
305
- 0,
306
- Number.isFinite(parseInt(env.ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS as string))
307
- ? parseInt(env.ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS as string)
308
- : 120_000,
309
- ),
310
- /** Hard deadline for one stream-acquire attempt (default 2 min). */
311
- acquireDeadlineMs: Math.max(
312
- 0,
313
- Number.isFinite(parseInt(env.ACQUIRE_DEADLINE_MS as string))
314
- ? parseInt(env.ACQUIRE_DEADLINE_MS as string)
315
- : 120_000,
316
- ),
317
- /** Safety net: force-release leases held longer than this (default 10 min). */
318
- leaseMaxDurationMs: Math.max(
319
- 0,
320
- parseInt(env.ACCOUNT_LEASE_MAX_DURATION_MS),
321
- ),
322
- initFailureCooldownMs: Math.max(
323
- 30_000,
324
- parseInt(env.ACCOUNT_INIT_FAILURE_COOLDOWN_MS),
325
- ),
326
- /** Max time a request waits on the per-chat lock (default 3 min). */
327
- chatLockTimeoutMs: Math.max(
328
- 0,
329
- parseInt(env.CHAT_LOCK_TIMEOUT_MS),
330
- ),
331
- },
332
- stream: {
333
- disconnectGraceMs: Math.max(
334
- 0,
335
- parseInt(env.STREAM_DISCONNECT_GRACE_MS),
336
- ),
337
- },
338
-
339
-
340
- sessionKeeper: {
341
- enabled: env.SESSION_KEEP_ALIVE_ENABLED !== "false",
342
- intervalMs: parseInt(env.SESSION_KEEP_ALIVE_INTERVAL_MS),
343
- idleMs: parseInt(env.SESSION_KEEP_ALIVE_IDLE_MS),
344
- navigationIntervalMs: parseInt(
345
- env.SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS,
346
- ),
347
- },
348
- apiKey: env.API_KEY,
349
- qwen: {
350
- baseUrl: env.QWEN_BASE_URL,
351
- chatPoolSize: Math.max(0, parseInt(env.QWEN_CHAT_POOL_SIZE)),
352
- chatPoolModels: env.QWEN_CHAT_POOL_MODELS.split(",")
353
- .map((model) => model.trim())
354
- .filter(Boolean),
355
- personalizationFromRequest:
356
- env.QWEN_PERSONALIZATION_FROM_REQUEST === "true",
357
- personalizationVerifyGet: env.QWEN_PERSONALIZATION_VERIFY_GET !== "false",
358
- /** "thread" (reuse upstream chat) or "temp" (new ephemeral chat per request). */
359
- /** When true, all requests (personalization, models, media, chat) route exclusively through the browser page (no direct Node fetch). */
360
- browserOnlyFetch: env.QWEN_BROWSER_ONLY_FETCH !== "false",
361
- chatMode: env.QWEN_CHAT_MODE,
362
- maxPromptBytes: Math.max(0, parseInt(env.QWEN_MAX_PROMPT_BYTES)),
363
- maxPersonalizationBytes: Math.max(
364
- 0,
365
- parseInt(env.QWEN_MAX_PERSONALIZATION_BYTES),
366
- ),
367
- deleteAllChatsOnShutdown: env.DELETE_ALL_CHATS_ON_SHUTDOWN === "true",
368
- /** Send the captured bx-ua/bx-umidtoken headers (real client does NOT). */
369
- sendBxUa: env.QWEN_SEND_BX_UA === "true",
370
- /** Deployed web bundle version sent as the `version` API header. */
371
- webVersion: env.QWEN_WEB_VERSION,
372
- },
373
- contextMeter: {
374
- enabled: env.CONTEXT_METER_ENABLED === "true",
375
- windowTokens: Math.max(0, parseInt(env.CONTEXT_METER_WINDOW_TOKENS)),
376
- reportUsage: env.CONTEXT_METER_REPORT_USAGE === "true",
377
- },
378
- };
379
-
380
- export type Config = typeof config;
381
-
382
- /** Conversation mode: thread-native reuse vs ephemeral temp chat per request. */
383
- export type ChatMode = "thread" | "temp";
1
+ import { z } from "zod";
2
+
3
+ const envSchema = z
4
+ .object({
5
+ PORT: z
6
+ .string()
7
+ .regex(/^\d+$/, "PORT must be a number")
8
+ .refine((value) => {
9
+ const port = Number(value);
10
+ return port >= 1 && port <= 65535;
11
+ }, "PORT must be between 1 and 65535")
12
+ .default("7936"),
13
+ HOST: z.string().default("0.0.0.0"),
14
+ INTERNAL_HOST: z.string().default("127.0.0.1"),
15
+ USER_AGENT: z
16
+ .string()
17
+ .default(
18
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
19
+ ),
20
+ QWEN_BX_V: z.string().default("2.5.37"),
21
+ // Version header on Qwen API requests = the deployed web bundle version
22
+ // (fallback when dynamic DOM detection is not yet completed).
23
+ // Override via env when Qwen ships a new bundle.
24
+ QWEN_WEB_VERSION: z.string().default("0.2.91"),
25
+ // Controls bx-ua/bx-umidtoken injection on the GENERAL API paths
26
+ // (chats/new, settings): those work without them (live-probed). The
27
+ // completions path is the exception — the 0.2.86 HAR shows the real
28
+ // client POSTs completions WITH bx-ua, so it always includes the captured
29
+ // tokens regardless of this flag (buildCompletionHeaders). Set true
30
+ // to inject them everywhere (legacy behavior).
31
+ QWEN_SEND_BX_UA: z.string().default("false"),
32
+ // Conversation mode: "thread" (default) reuses the upstream Qwen chat via
33
+ // parent_id and sends the thread-native delta; "temp" creates a NEW Qwen
34
+ // temp chat (chat_mode:"local") for every request and sends the full
35
+ // history inline (OpenAI standard). Temp chats are ephemeral and never
36
+ // appear in the account's chat list (live-probed).
37
+ QWEN_CHAT_MODE: z.enum(["thread", "temp"]).default("thread"),
38
+ PLAYWRIGHT_HEADLESS: z.string().default("true"),
39
+ PLAYWRIGHT_BROWSER: z
40
+ .enum(["chromium", "chrome", "edge"])
41
+ .default("chromium"),
42
+ PLAYWRIGHT_INIT_BATCH_SIZE: z.string().default("1"),
43
+ PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS: z.string().default("10000"),
44
+ PLAYWRIGHT_IDLE_CONTEXT_TTL_MS: z.string().default("60000"),
45
+ PLAYWRIGHT_JS_HEAP_MB: z.string().default("256"),
46
+ PLAYWRIGHT_LOW_MEMORY_FLAGS: z.string().default("true"),
47
+ // Keep 2 warm contexts by default ({main + reserve} covers the common
48
+ // failover hop without opening one browser per account): after warmup two
49
+ // browsers stay open; any extra context (simultaneous use / failover) is
50
+ // closed once idle. The cap only evicts IDLE contexts — busy mutexes and
51
+ // active streams are never touched, so concurrent accounts each keep their
52
+ // own context while serving. Accounts in cooldown (rate-limited) sit idle,
53
+ // drop out of the warm set and get evicted.
54
+ PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
55
+ PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("false"),
56
+ CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
57
+ CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
58
+ CAPTCHA_SOLVER_TIMEOUT_MS: z.string().default("15000"),
59
+ CAPTCHA_SOLVER_RETRY_DELAY_MS: z.string().default("1000"),
60
+ CAPTCHA_SOLVER_SETTLE_MS: z.string().default("2000"),
61
+ CAPTCHA_ACCOUNT_COOLDOWN_MS: z.string().default("120000"),
62
+ // Cap for the escalating quarantine applied when a challenge could NOT be
63
+ // solved (hard block). Each consecutive hard block doubles the window
64
+ // (base = CAPTCHA_ACCOUNT_COOLDOWN_MS) up to this cap.
65
+ CAPTCHA_HARD_BLOCK_MAX_COOLDOWN_MS: z.string().default("3600000"),
66
+ OSS_MULTIPART_THRESHOLD_MB: z.string().default("5"),
67
+ CHAT_REQUEST_LOG: z.string().default("false"),
68
+ HTTP_TIMEOUT: z.string().default("15000"),
69
+ CHAT_TIMEOUT: z.string().default("180000"),
70
+ NAVIGATION_TIMEOUT: z.string().default("60000"),
71
+ PAGE_TIMEOUT: z.string().default("60000"),
72
+ HEADERS_TIMEOUT: z.string().default("90000"),
73
+ TIME_TO_FIRST_BYTE: z.string().default("60000"),
74
+ IDLE_STREAM_TIMEOUT: z.string().default("60000"),
75
+ // Deadline for the FIRST upstream chunk on thinking models (the reasoning
76
+ // idle of 600s is for gaps AFTER data flows; a stream that produced
77
+ // nothing in this window is dead and should fail fast, retryable).
78
+ QWEN_FIRST_CHUNK_TIMEOUT: z.string().default("180000"),
79
+ TOTAL_REQUEST_TIMEOUT: z.string().default("600000"),
80
+ // Mid-stream silence window for thinking models: 3 min with ZERO upstream
81
+ // bytes is a dead stream (WAF swallow / dropped connection) — fail fast and
82
+ // let the retry policy rotate accounts. Flowing reasoning chunks RESET this
83
+ // timer, so legitimate slow thinking is never cut; only total silence is.
84
+ REASONING_MODEL_TIMEOUT: z.string().default("180000"),
85
+ CACHE_TTL: z.string().default("3600"),
86
+ RESPONSE_TTL: z.string().default("1800"),
87
+ CACHE_COMPRESSION_ENABLED: z.string().default("true"),
88
+ CACHE_COMPRESSION_THRESHOLD: z.string().default("1024"),
89
+ CACHE_COMPRESSION_LEVEL: z.string().default("6"),
90
+ METRICS_INTERVAL: z.string().default("10000"),
91
+ WATCHDOG_INTERVAL: z.string().default("5000"),
92
+ WATCHDOG_FAILURES: z.string().default("3"),
93
+ RAM_WARNING: z.string().default("80"),
94
+ RAM_CRITICAL: z.string().default("95"),
95
+ WS_WARNING: z.string().default("50"),
96
+ WS_CRITICAL: z.string().default("100"),
97
+ RETRY_BASE_DELAY_MS: z.string().default(process.env.TEST_MOCK_QWEN_AUTH === "true" ? "50" : "1000"),
98
+ RETRY_MAX_DELAY_MS: z.string().default(process.env.TEST_MOCK_QWEN_AUTH === "true" ? "200" : "10000"),
99
+ RETRY_MAX_ATTEMPTS: z.string().default("3"),
100
+ RETRY_MAX_ACCOUNT_SWITCHES: z.string().default("2"),
101
+ RETRY_ON_UNKNOWN_UPSTREAM: z.string().default("true"),
102
+ RETRY_AUTO_MALFORMED_TOOLS: z.string().default("true"),
103
+ RETRY_AUTO_MALFORMED_TOOLS_MAX: z.string().default("2"),
104
+ MAX_TOOL_CALLS_PER_TURN: z.string().default("24"),
105
+ QWEN_REPEATED_TOOL_CALL_WARN: z.string().default("2"),
106
+ ACCOUNT_MAX_CONCURRENT_STREAMS: z.string().default("2"),
107
+ ACCOUNT_BUSY_WAIT_MS: z.string().default("30000"),
108
+ // Cap for the "wait forever" account-lease queue (thread owner / last
109
+ // usable account): the queue previously had NO deadline, so a stuck lease
110
+ // holder made the next turn wait up to ~600s. A generous finite cap keeps
111
+ // the wait far above the normal 30s but still bounded.
112
+ ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS: z.string().default("120000"),
113
+ // Hard deadline for one stream-acquire attempt. A dead account (upstream
114
+ // swallows the completion fetch) otherwise chains metadata/header timeouts
115
+ // for minutes; this fails the attempt visibly so the retry loop switches.
116
+ ACQUIRE_DEADLINE_MS: z.string().default("120000"),
117
+ ACCOUNT_LEASE_MAX_DURATION_MS: z.string().default("600000"),
118
+ ACCOUNT_INIT_FAILURE_COOLDOWN_MS: z.string().default("300000"),
119
+ // Timeout before a request waiting on the CHAT lock gives up. The chat lock
120
+ // is held for the entire stream lifetime, so it must cover the longest
121
+ // legitimate generation (reasoning models with huge contexts can spend
122
+ // 2-3 min producing the first byte chain). A 60s hard cap turned a normal
123
+ // long turn into a 500 for every concurrent request on the same chat
124
+ // (2026-08-22 production log: lock held 130s -> acquire_deadline 120s).
125
+ CHAT_LOCK_TIMEOUT_MS: z
126
+ .string()
127
+ .regex(/^\d+$/, "CHAT_LOCK_TIMEOUT_MS must be a number")
128
+ .default("180000"),
129
+ STREAM_DISCONNECT_GRACE_MS: z
130
+ .string()
131
+ .regex(/^\d+$/, "STREAM_DISCONNECT_GRACE_MS must be a number")
132
+ .default("4000"),
133
+ CHAT_IN_PROGRESS_RETRY_DELAY_MS: z.string().default("2000"),
134
+ // Temporarily-busy window after a chat_in_progress: long enough to absorb
135
+ // the upstream chat settle (measured ~1-2s), short enough that the next
136
+ // turn of the sticky owner is not pushed to a cold account with a full
137
+ // context replay (8s caused a needless 13.3s hop in the 20:04 session).
138
+ CHAT_IN_PROGRESS_BUSY_MS: z.string().default("4000"),
139
+ // Same-chat retry budget for chat_in_progress. The upstream chat stays "in
140
+ // progress" for 2-16s after a completed turn (grows with turn size); each
141
+ // retry waits a jittered window (busyMs-based) and NO retry re-sends the
142
+ // full context — the escalation (new chat + full replay on a cold account)
143
+ // was the ~1MB re-upload that made tool loops feel like ~40 minutes. After
144
+ // this budget the request FAILS (thread binding kept) and the client's own
145
+ // retry lands on the settled chat with the delta intact.
146
+ CHAT_IN_PROGRESS_MAX_RETRIES: z.string().default("6"),
147
+ MID_STREAM_FAILOVER_THRESHOLD: z.string().default("2"),
148
+ MID_STREAM_FAILOVER_BUSY_MS: z.string().default("60000"),
149
+
150
+
151
+ QWEN_BASE_URL: z.string().default("https://chat.qwen.ai"),
152
+ QWEN_CHAT_POOL_SIZE: z.string().default("1"),
153
+ QWEN_CHAT_POOL_MODELS: z.string().default("qwen3.7-plus"),
154
+ QWEN_PERSONALIZATION_FROM_REQUEST: z.string().default("true"),
155
+ QWEN_PERSONALIZATION_VERIFY_GET: z.string().default("true"),
156
+ QWEN_BROWSER_ONLY_FETCH: z.string().default("true"),
157
+ QWEN_MAX_PROMPT_BYTES: z.string().default("0"),
158
+ QWEN_MAX_PERSONALIZATION_BYTES: z.string().default("200000"),
159
+ CONTEXT_METER_ENABLED: z.string().default("true"),
160
+ CONTEXT_METER_WINDOW_TOKENS: z.string().default("0"),
161
+ CONTEXT_METER_REPORT_USAGE: z.string().default("true"),
162
+ DELETE_ALL_CHATS_ON_SHUTDOWN: z.string().default("false"),
163
+ // Keep idle account pages alive (subtlePageActivity + occasional reload).
164
+ // The Baxia WAF scores live page behavior (pointer/scroll events, open
165
+ // session) — an account whose page sits frozen for minutes returns a low
166
+ // trust score and gets TMD-challenged on the next request. On by default;
167
+ // the keeper skips accounts that are mid-stream or mutex-busy.
168
+ SESSION_KEEP_ALIVE_ENABLED: z.string().default("true"),
169
+ SESSION_KEEP_ALIVE_INTERVAL_MS: z.string().default("180000"),
170
+ SESSION_KEEP_ALIVE_IDLE_MS: z.string().default("120000"),
171
+ SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS: z.string().default("480000"),
172
+ API_KEY: z.string().default(""),
173
+ // Static x-ratelimit-* response headers (OpenAI-shaped, doc §5.2). The proxy
174
+ // does not enforce a token/request quota; these exist for SDK/tool parsing.
175
+ RATE_LIMIT_REQUESTS: z.string().default("5000"),
176
+ RATE_LIMIT_TOKENS: z.string().default("200000"),
177
+ })
178
+ ;
179
+
180
+ const env = envSchema.parse(process.env);
181
+
182
+ export const config = {
183
+ server: {
184
+ port: parseInt(env.PORT),
185
+ host: env.HOST,
186
+ internalHost: env.INTERNAL_HOST,
187
+ rateLimit: {
188
+ requests: parseInt(env.RATE_LIMIT_REQUESTS),
189
+ tokens: parseInt(env.RATE_LIMIT_TOKENS),
190
+ },
191
+ },
192
+ logging: {
193
+ chatRequests: env.CHAT_REQUEST_LOG === "true",
194
+ },
195
+ auth: {
196
+ userAgent: env.USER_AGENT,
197
+ bxV: env.QWEN_BX_V,
198
+ },
199
+ playwright: {
200
+ headless: env.PLAYWRIGHT_HEADLESS !== "false",
201
+ browser: env.PLAYWRIGHT_BROWSER,
202
+ initBatchSize: Math.max(1, parseInt(env.PLAYWRIGHT_INIT_BATCH_SIZE)),
203
+ contextCloseTimeoutMs: Math.max(
204
+ 1_000,
205
+ parseInt(env.PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS),
206
+ ),
207
+ idleContextTtlMs: Math.max(0, parseInt(env.PLAYWRIGHT_IDLE_CONTEXT_TTL_MS)),
208
+ jsHeapMb: Math.max(64, parseInt(env.PLAYWRIGHT_JS_HEAP_MB)),
209
+ lowMemoryFlags: env.PLAYWRIGHT_LOW_MEMORY_FLAGS !== "false",
210
+ maxActiveContexts: Math.max(0, parseInt(env.PLAYWRIGHT_MAX_ACTIVE_CONTEXTS)),
211
+ prepareAllOnStartup: env.PLAYWRIGHT_PREPARE_ALL_ON_STARTUP !== "false",
212
+ },
213
+ captcha: {
214
+ enabled: env.CAPTCHA_SOLVER_ENABLED === "true",
215
+ maxAttempts: Math.max(1, Math.min(5, parseInt(env.CAPTCHA_SOLVER_MAX_ATTEMPTS))),
216
+ timeoutMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_TIMEOUT_MS)),
217
+ retryDelayMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_RETRY_DELAY_MS)),
218
+ settleMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_SETTLE_MS)),
219
+ /** Rest an account whose challenge could not be cleared before reusing it. */
220
+ accountCooldownMs: Math.max(0, parseInt(env.CAPTCHA_ACCOUNT_COOLDOWN_MS)),
221
+ /** Cap for the escalating hard-block quarantine (×2 per consecutive block). */
222
+ hardBlockMaxCooldownMs: Math.max(
223
+ 0,
224
+ parseInt(env.CAPTCHA_HARD_BLOCK_MAX_COOLDOWN_MS),
225
+ ),
226
+ },
227
+ oss: {
228
+ multipartThresholdBytes: Math.max(
229
+ 1 * 1024 * 1024,
230
+ parseInt(env.OSS_MULTIPART_THRESHOLD_MB) * 1024 * 1024,
231
+ ),
232
+ },
233
+ timeouts: {
234
+ http: parseInt(env.HTTP_TIMEOUT),
235
+ chat: parseInt(env.CHAT_TIMEOUT),
236
+ navigation: parseInt(env.NAVIGATION_TIMEOUT),
237
+ page: parseInt(env.PAGE_TIMEOUT),
238
+ headers: parseInt(env.HEADERS_TIMEOUT),
239
+ timeToFirstByte: parseInt(env.TIME_TO_FIRST_BYTE),
240
+ idleStreamTimeout: parseInt(env.IDLE_STREAM_TIMEOUT),
241
+ totalRequestTimeout: parseInt(env.TOTAL_REQUEST_TIMEOUT),
242
+ reasoningModelTimeout: parseInt(env.REASONING_MODEL_TIMEOUT),
243
+ firstChunkTimeout: parseInt(env.QWEN_FIRST_CHUNK_TIMEOUT),
244
+ },
245
+ cache: {
246
+ defaultTTL: parseInt(env.CACHE_TTL),
247
+ responseTTL: parseInt(env.RESPONSE_TTL),
248
+ compression: {
249
+ enabled: env.CACHE_COMPRESSION_ENABLED !== "false",
250
+ threshold: parseInt(env.CACHE_COMPRESSION_THRESHOLD),
251
+ level: parseInt(env.CACHE_COMPRESSION_LEVEL),
252
+ },
253
+ },
254
+
255
+ metrics: {
256
+ interval: parseInt(env.METRICS_INTERVAL),
257
+ },
258
+ watchdog: {
259
+ checkInterval: parseInt(env.WATCHDOG_INTERVAL),
260
+ consecutiveFailuresThreshold: parseInt(env.WATCHDOG_FAILURES),
261
+ ram: {
262
+ warningThreshold: parseInt(env.RAM_WARNING),
263
+ criticalThreshold: parseInt(env.RAM_CRITICAL),
264
+ },
265
+ streams: {
266
+ warningThreshold: parseInt(env.WS_WARNING),
267
+ criticalThreshold: parseInt(env.WS_CRITICAL),
268
+ },
269
+ },
270
+ retry: {
271
+ baseDelayMs: parseInt(env.RETRY_BASE_DELAY_MS),
272
+ maxDelayMs: parseInt(env.RETRY_MAX_DELAY_MS),
273
+ maxAttempts: Math.max(1, parseInt(env.RETRY_MAX_ATTEMPTS)),
274
+ maxAccountSwitches: Math.max(0, parseInt(env.RETRY_MAX_ACCOUNT_SWITCHES)),
275
+ onUnknownUpstream: env.RETRY_ON_UNKNOWN_UPSTREAM !== "false",
276
+ chatInProgressDelayMs: Math.max(0, parseInt(env.CHAT_IN_PROGRESS_RETRY_DELAY_MS)),
277
+ chatInProgressBusyMs: Math.max(0, parseInt(env.CHAT_IN_PROGRESS_BUSY_MS)),
278
+ chatInProgressMaxAttempts: Math.max(1, parseInt(env.CHAT_IN_PROGRESS_MAX_RETRIES)),
279
+ midStreamFailoverThreshold: Math.max(
280
+ 0,
281
+ parseInt(env.MID_STREAM_FAILOVER_THRESHOLD),
282
+ ),
283
+ midStreamFailoverBusyMs: Math.max(
284
+ 0,
285
+ parseInt(env.MID_STREAM_FAILOVER_BUSY_MS),
286
+ ),
287
+ autoRetryMalformedTools: env.RETRY_AUTO_MALFORMED_TOOLS !== "false",
288
+ autoRetryMalformedToolsMax: Math.max(1, parseInt(env.RETRY_AUTO_MALFORMED_TOOLS_MAX)),
289
+ maxToolCallsPerTurn: Math.max(0, parseInt(env.MAX_TOOL_CALLS_PER_TURN)),
290
+ repeatedToolCallWarnThreshold: Math.max(
291
+ 1,
292
+ parseInt(env.QWEN_REPEATED_TOOL_CALL_WARN),
293
+ ),
294
+ },
295
+ concurrency: {
296
+ maxStreamsPerAccount: Math.max(1, parseInt(env.ACCOUNT_MAX_CONCURRENT_STREAMS)),
297
+ busyWaitMs: Math.max(
298
+ 0,
299
+ Number.isFinite(parseInt(env.ACCOUNT_BUSY_WAIT_MS as string))
300
+ ? parseInt(env.ACCOUNT_BUSY_WAIT_MS as string)
301
+ : 30_000,
302
+ ),
303
+ /** Bound for the "wait forever" lease queue (default 2 min). */
304
+ queueWaitForeverCapMs: Math.max(
305
+ 0,
306
+ Number.isFinite(parseInt(env.ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS as string))
307
+ ? parseInt(env.ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS as string)
308
+ : 120_000,
309
+ ),
310
+ /** Hard deadline for one stream-acquire attempt (default 2 min). */
311
+ acquireDeadlineMs: Math.max(
312
+ 0,
313
+ Number.isFinite(parseInt(env.ACQUIRE_DEADLINE_MS as string))
314
+ ? parseInt(env.ACQUIRE_DEADLINE_MS as string)
315
+ : 120_000,
316
+ ),
317
+ /** Safety net: force-release leases held longer than this (default 10 min). */
318
+ leaseMaxDurationMs: Math.max(
319
+ 0,
320
+ parseInt(env.ACCOUNT_LEASE_MAX_DURATION_MS),
321
+ ),
322
+ initFailureCooldownMs: Math.max(
323
+ 30_000,
324
+ parseInt(env.ACCOUNT_INIT_FAILURE_COOLDOWN_MS),
325
+ ),
326
+ /** Max time a request waits on the per-chat lock (default 3 min). */
327
+ chatLockTimeoutMs: Math.max(
328
+ 0,
329
+ parseInt(env.CHAT_LOCK_TIMEOUT_MS),
330
+ ),
331
+ },
332
+ stream: {
333
+ disconnectGraceMs: Math.max(
334
+ 0,
335
+ parseInt(env.STREAM_DISCONNECT_GRACE_MS),
336
+ ),
337
+ },
338
+
339
+
340
+ sessionKeeper: {
341
+ enabled: env.SESSION_KEEP_ALIVE_ENABLED !== "false",
342
+ intervalMs: parseInt(env.SESSION_KEEP_ALIVE_INTERVAL_MS),
343
+ idleMs: parseInt(env.SESSION_KEEP_ALIVE_IDLE_MS),
344
+ navigationIntervalMs: parseInt(
345
+ env.SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS,
346
+ ),
347
+ },
348
+ apiKey: env.API_KEY,
349
+ qwen: {
350
+ baseUrl: env.QWEN_BASE_URL,
351
+ chatPoolSize: Math.max(0, parseInt(env.QWEN_CHAT_POOL_SIZE)),
352
+ chatPoolModels: env.QWEN_CHAT_POOL_MODELS.split(",")
353
+ .map((model) => model.trim())
354
+ .filter(Boolean),
355
+ personalizationFromRequest:
356
+ env.QWEN_PERSONALIZATION_FROM_REQUEST === "true",
357
+ personalizationVerifyGet: env.QWEN_PERSONALIZATION_VERIFY_GET !== "false",
358
+ /** "thread" (reuse upstream chat) or "temp" (new ephemeral chat per request). */
359
+ /** When true, all requests (personalization, models, media, chat) route exclusively through the browser page (no direct Node fetch). */
360
+ browserOnlyFetch: env.QWEN_BROWSER_ONLY_FETCH !== "false",
361
+ chatMode: env.QWEN_CHAT_MODE,
362
+ maxPromptBytes: Math.max(0, parseInt(env.QWEN_MAX_PROMPT_BYTES)),
363
+ maxPersonalizationBytes: Math.max(
364
+ 0,
365
+ parseInt(env.QWEN_MAX_PERSONALIZATION_BYTES),
366
+ ),
367
+ deleteAllChatsOnShutdown: env.DELETE_ALL_CHATS_ON_SHUTDOWN === "true",
368
+ /** Send the captured bx-ua/bx-umidtoken headers (real client does NOT). */
369
+ sendBxUa: env.QWEN_SEND_BX_UA === "true",
370
+ /** Deployed web bundle version sent as the `version` API header. */
371
+ webVersion: env.QWEN_WEB_VERSION,
372
+ },
373
+ contextMeter: {
374
+ enabled: env.CONTEXT_METER_ENABLED === "true",
375
+ windowTokens: Math.max(0, parseInt(env.CONTEXT_METER_WINDOW_TOKENS)),
376
+ reportUsage: env.CONTEXT_METER_REPORT_USAGE === "true",
377
+ },
378
+ };
379
+
380
+ export type Config = typeof config;
381
+
382
+ /** Conversation mode: thread-native reuse vs ephemeral temp chat per request. */
383
+ export type ChatMode = "thread" | "temp";