qwenproxy-cli 1.0.32 → 1.1.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.0.32",
3
+ "version": "1.1.0",
4
4
  "description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "src",
12
12
  "!src/tests",
13
13
  "!src/benchmarks",
14
- "README.md",
14
+ "README*.md",
15
15
  "LICENSE"
16
16
  ],
17
17
  "scripts": {
@@ -26,8 +26,8 @@
26
26
  "reset": "npx tsx src/reset-cooldowns.ts",
27
27
  "purge": "npx tsx src/delete-chats.ts",
28
28
  "test": "npm run test:mock && npm run test:live",
29
- "test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts",
30
- "test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts",
29
+ "test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts",
30
+ "test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts",
31
31
  "test:live": "tsx --test src/tests/concurrentChat.test.ts src/tests/agenticStress.test.ts",
32
32
  "benchmark:proxy": "npx tsx src/benchmarks/proxy-baseline.ts",
33
33
  "typecheck": "tsc --noEmit"
package/src/api/server.ts CHANGED
@@ -3,7 +3,8 @@ import net from "node:net";
3
3
  import { v4 as uuidv4 } from "uuid";
4
4
  import { Hono, type Context } from "hono";
5
5
  import { serve } from "@hono/node-server";
6
- import { config } from "../core/config.js";
6
+ import { config, getRuntimeChatMode, setRuntimeChatMode } from "../core/config.js";
7
+ import { saveTuiSettings } from "../tui/settings.ts";
7
8
  import { metrics } from "../core/metrics.js";
8
9
  import { logger, maskEmail } from "../core/logger.js";
9
10
  import { MemoryCache } from "../cache/memory-cache.js";
@@ -248,7 +249,22 @@ app.post("/v1/upload", uploadFile);
248
249
  app.post("/v1/images/generations", imagesGenerations);
249
250
  app.post("/v1/videos/generations", videosGenerations);
250
251
  app.get("/v1/tasks/status/:taskId", videoTaskStatus);
251
-
252
+ app.get("/v1/chat/mode", (c) => {
253
+ const error = verifyApiKey(c);
254
+ if (error) return error;
255
+ return c.json({ mode: getRuntimeChatMode() });
256
+ });
257
+ app.post("/v1/chat/mode", async (c) => {
258
+ const error = verifyApiKey(c);
259
+ if (error) return error;
260
+ const body = (await c.req.json().catch(() => ({}))) as { mode?: string };
261
+ if (!body.mode) {
262
+ return c.json({ error: { message: "Field 'mode' is required" } }, 400);
263
+ }
264
+ const updated = setRuntimeChatMode(body.mode);
265
+ saveTuiSettings({ chat: { mode: updated } });
266
+ return c.json({ success: true, mode: updated });
267
+ });
252
268
  // OpenAI Responses API compatible routes
253
269
  app.route("", responsesApp);
254
270
  app.route("", anthropicApp);
@@ -122,7 +122,137 @@ export function invalidateAccountsCache(): void {
122
122
  accountsCache = null;
123
123
  accountsCacheTime = 0;
124
124
  }
125
+ export interface BatchAccountEntry {
126
+ email: string;
127
+ password: string;
128
+ }
129
+
130
+ export function parseBatchAccounts(rawInput: string): {
131
+ entries: BatchAccountEntry[];
132
+ invalid: string[];
133
+ } {
134
+ if (!rawInput || typeof rawInput !== "string") {
135
+ return { entries: [], invalid: [] };
136
+ }
137
+
138
+ let text = rawInput.trim();
139
+ // Strip optional QWEN_ACCOUNTS= prefix and surrounding quotes
140
+ text = text.replace(/^QWEN_ACCOUNTS\s*=\s*["']?/i, "").replace(/["']?\s*$/i, "");
141
+
142
+ const lines = text.split(/\r?\n/);
143
+ const entries: BatchAccountEntry[] = [];
144
+ const invalid: string[] = [];
145
+ const seenEmails = new Set<string>();
146
+
147
+ for (const rawLine of lines) {
148
+ const line = rawLine.trim();
149
+ if (!line || line.startsWith("#") || line.startsWith("//")) continue;
150
+
151
+ // Check if line contains multiple accounts separated by comma or semicolon
152
+ const segments =
153
+ (line.includes(",") || line.includes(";")) && (line.match(/@/g) || []).length > 1
154
+ ? line.split(/[,;]/).map((s) => s.trim()).filter(Boolean)
155
+ : [line];
156
+
157
+ for (const seg of segments) {
158
+ let email = "";
159
+ let password = "";
160
+
161
+ if (seg.includes("---")) {
162
+ const parts = seg.split("---");
163
+ email = parts[0].trim();
164
+ password = parts.slice(1).join("---").trim();
165
+ } else if (seg.includes("\t")) {
166
+ const parts = seg.split("\t");
167
+ email = parts[0].trim();
168
+ password = parts.slice(1).join("\t").trim();
169
+ } else if (seg.includes(" | ")) {
170
+ const parts = seg.split(" | ");
171
+ email = parts[0].trim();
172
+ password = parts.slice(1).join(" | ").trim();
173
+ } else if (seg.includes(":")) {
174
+ const colonIdx = seg.indexOf(":");
175
+ email = seg.slice(0, colonIdx).trim();
176
+ password = seg.slice(colonIdx + 1).trim();
177
+ } else if (seg.includes(",")) {
178
+ const commaIdx = seg.indexOf(",");
179
+ email = seg.slice(0, commaIdx).trim();
180
+ password = seg.slice(commaIdx + 1).trim();
181
+ } else {
182
+ invalid.push(seg);
183
+ continue;
184
+ }
185
+
186
+ // Validate email format
187
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
188
+ if (!email || !password || !emailRegex.test(email)) {
189
+ invalid.push(seg);
190
+ continue;
191
+ }
192
+
193
+ const normalizedEmail = email.toLowerCase();
194
+ if (!seenEmails.has(normalizedEmail)) {
195
+ seenEmails.add(normalizedEmail);
196
+ entries.push({ email, password });
197
+ }
198
+ }
199
+ }
200
+
201
+ return { entries, invalid };
202
+ }
125
203
 
204
+ export function addAccountsBatch(
205
+ entries: BatchAccountEntry[],
206
+ ): { added: QwenAccount[]; skipped: string[]; invalid: string[] } {
207
+ const db = getDatabase();
208
+ const added: QwenAccount[] = [];
209
+ const skipped: string[] = [];
210
+ const invalid: string[] = [];
211
+
212
+ const existingEmails = new Set(
213
+ (db.prepare("SELECT email FROM accounts").all() as Array<{ email: string }>).map((r) =>
214
+ r.email.toLowerCase(),
215
+ ),
216
+ );
217
+
218
+ const insertStmt = db.prepare(
219
+ "INSERT INTO accounts (id, email, password) VALUES (?, ?, ?)",
220
+ );
221
+
222
+ const insertBatch = db.transaction((items: BatchAccountEntry[]) => {
223
+ for (const item of items) {
224
+ const email = item.email.trim();
225
+ const password = item.password;
226
+ if (!email || !password) {
227
+ invalid.push(email || "(empty)");
228
+ continue;
229
+ }
230
+
231
+ if (existingEmails.has(email.toLowerCase())) {
232
+ skipped.push(email);
233
+ continue;
234
+ }
235
+
236
+ const newAccount: QwenAccount = {
237
+ id: crypto.randomUUID(),
238
+ email,
239
+ password,
240
+ };
241
+
242
+ insertStmt.run(newAccount.id, newAccount.email, encrypt(newAccount.password));
243
+ existingEmails.add(email.toLowerCase());
244
+ added.push(newAccount);
245
+ }
246
+ });
247
+
248
+ insertBatch(entries);
249
+
250
+ if (added.length > 0) {
251
+ invalidateAccountsCache();
252
+ }
253
+
254
+ return { added, skipped, invalid };
255
+ }
126
256
  export function addAccount(
127
257
  email: string,
128
258
  password: string,
@@ -34,7 +34,18 @@ const envSchema = z
34
34
  // temp chat (chat_mode:"local") for every request and sends the full
35
35
  // history inline (OpenAI standard). Temp chats are ephemeral and never
36
36
  // appear in the account's chat list (live-probed).
37
- QWEN_CHAT_MODE: z.enum(["thread", "temp", "temp-thread"]).default("thread"),
37
+ QWEN_CHAT_MODE: z
38
+ .enum([
39
+ "thread",
40
+ "temp",
41
+ "temp-thread",
42
+ "thread-temp",
43
+ "stateless",
44
+ "stateles",
45
+ "stateless-temp",
46
+ "stateles-temp",
47
+ ])
48
+ .default("thread"),
38
49
  PLAYWRIGHT_HEADLESS: z.string().default("true"),
39
50
  PLAYWRIGHT_BROWSER: z
40
51
  .enum(["chromium", "chrome", "edge"])
@@ -362,7 +373,7 @@ export const config = {
362
373
  /** When true, all requests (personalization, models, media, chat) route exclusively through the browser page (no direct Node fetch). */
363
374
  browserOnlyFetch: env.QWEN_BROWSER_ONLY_FETCH !== "false",
364
375
  mapOpenAiModels: env.QWEN_MAP_OPENAI_MODELS !== "false",
365
- chatMode: env.QWEN_CHAT_MODE,
376
+ chatMode: normalizeChatMode(env.QWEN_CHAT_MODE),
366
377
  maxPromptBytes: Math.max(0, parseInt(env.QWEN_MAX_PROMPT_BYTES)),
367
378
  maxPersonalizationBytes: Math.max(
368
379
  0,
@@ -388,5 +399,85 @@ export const config = {
388
399
 
389
400
  export type Config = typeof config;
390
401
 
391
- /** Conversation mode: thread-native reuse vs ephemeral temp chat per request vs temp-thread (ephemeral continuous session). */
392
- export type ChatMode = "thread" | "temp" | "temp-thread";
402
+ /**
403
+ * Conversation modes:
404
+ * - "thread": thread-native delta (~1KB), saved in Qwen account sidebar (default)
405
+ * - "thread-temp" (alias "temp-thread"): thread-native delta (~1KB), ephemeral local chat (not saved)
406
+ * - "stateless": full history re-sent every turn (OpenAI standard), saved in Qwen account sidebar
407
+ * - "stateless-temp" (alias "temp"): full history re-sent every turn (OpenAI standard), ephemeral local chat (not saved)
408
+ */
409
+ export type ChatMode =
410
+ | "thread"
411
+ | "thread-temp"
412
+ | "temp-thread"
413
+ | "stateless"
414
+ | "stateles"
415
+ | "stateless-temp"
416
+ | "stateles-temp"
417
+ | "temp";
418
+
419
+ export function isStatelessChatMode(mode?: string | null): boolean {
420
+ if (!mode) return false;
421
+ const m = mode.trim().toLowerCase().replace(/_/g, "-");
422
+ return (
423
+ m === "stateless" ||
424
+ m === "stateles" ||
425
+ m === "stateless-temp" ||
426
+ m === "stateles-temp" ||
427
+ m === "temp"
428
+ );
429
+ }
430
+
431
+ export function isLocalChatMode(mode?: string | null): boolean {
432
+ if (!mode) return false;
433
+ const m = mode.trim().toLowerCase().replace(/_/g, "-");
434
+ return (
435
+ m === "stateless-temp" ||
436
+ m === "stateles-temp" ||
437
+ m === "temp" ||
438
+ m === "thread-temp" ||
439
+ m === "temp-thread"
440
+ );
441
+ }
442
+
443
+ export function normalizeChatMode(
444
+ mode?: string | null,
445
+ ): "thread" | "thread-temp" | "stateless" | "stateless-temp" {
446
+ if (!mode) return "thread";
447
+ const m = mode.trim().toLowerCase().replace(/_/g, "-");
448
+ if (m === "stateless-temp" || m === "stateles-temp" || m === "temp") {
449
+ return "stateless-temp";
450
+ }
451
+ if (m === "stateless" || m === "stateles") {
452
+ return "stateless";
453
+ }
454
+ if (m === "thread-temp" || m === "temp-thread") {
455
+ return "thread-temp";
456
+ }
457
+ return "thread";
458
+ }
459
+
460
+ export function setRuntimeChatMode(
461
+ mode?: string | null,
462
+ ): "thread" | "thread-temp" | "stateless" | "stateless-temp" {
463
+ const normalized = normalizeChatMode(mode);
464
+ (config.qwen as { chatMode: "thread" | "thread-temp" | "stateless" | "stateless-temp" }).chatMode = normalized;
465
+ return normalized;
466
+ }
467
+
468
+ export function getRuntimeChatMode(): "thread" | "thread-temp" | "stateless" | "stateless-temp" {
469
+ return config.qwen.chatMode as "thread" | "thread-temp" | "stateless" | "stateless-temp";
470
+ }
471
+
472
+ export function cycleNextChatMode(): "thread" | "thread-temp" | "stateless" | "stateless-temp" {
473
+ const current = getRuntimeChatMode();
474
+ const order: Array<"thread" | "thread-temp" | "stateless-temp" | "stateless"> = [
475
+ "thread",
476
+ "thread-temp",
477
+ "stateless-temp",
478
+ "stateless",
479
+ ];
480
+ const idx = order.indexOf(current);
481
+ const next = order[(idx + 1) % order.length];
482
+ return setRuntimeChatMode(next);
483
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
+ try {
2
+ process.title = "QwenProxy";
3
+ } catch {}
1
4
  process.env.DOTENV_CONFIG_QUIET = 'true'
2
5
  import dotenv from 'dotenv'
3
6
  import fs from 'node:fs'
@@ -1,4 +1,4 @@
1
- import { config, type ChatMode } from "../../core/config.ts";
1
+ import { config, type ChatMode, isStatelessChatMode } from "../../core/config.ts";
2
2
  import { ContextLengthExceededError, ValidationError } from "../../core/errors.ts";
3
3
  import { getModelContextWindow } from "../../core/model-registry.ts";
4
4
  import {
@@ -61,16 +61,16 @@ export async function buildFinalContext(
61
61
 
62
62
  const modelContextWindow = getModelContextWindow(modelId);
63
63
  const useThreadNative = true;
64
- const isTempMode = chatMode === "temp";
64
+ const isStateless = isStatelessChatMode(chatMode);
65
65
  // A continuation is ANY evidence of a prior turn, not just a plain
66
66
  // role:"assistant" message. Tool-loop clients (Zed/Cline) can send history
67
67
  // with tool/function responses or assistant tool_calls but WITHOUT a plain
68
68
  // assistant entry; misclassifying those as a new session forced the FULL
69
69
  // history to be re-sent on every request (and every chat_in_progress retry)
70
70
  // instead of the thread-native delta.
71
- // In temp mode EVERY request is a new (ephemeral) chat, so the whole history
72
- // is always sent and no thread state is ever consulted.
73
- const isNewSession = isTempMode
71
+ // In stateless modes (stateless, stateless-temp, temp) EVERY request is a
72
+ // new chat, so the whole history is always sent and no thread state is consulted.
73
+ const isNewSession = isStateless
74
74
  ? true
75
75
  : !messages.some(isContinuationMessage);
76
76
  const completeInstructions = [systemPrompt.trim(), toolInstructions.trim()]
@@ -83,7 +83,7 @@ export async function buildFinalContext(
83
83
  // OR: this is a continuation (has assistant messages in history)
84
84
  // This prevents new IDE chats from accidentally reusing old Qwen chats
85
85
  // while still allowing continuations without explicit session_id
86
- const allowThreadReuse = isTempMode
86
+ const allowThreadReuse = isStateless
87
87
  ? false
88
88
  : useThreadNative && (hasExplicitConversationKey || !isNewSession); // has assistant messages = continuation of existing chat
89
89
 
@@ -91,7 +91,7 @@ export async function buildFinalContext(
91
91
  // an explicit conversation key. Otherwise, generate an ephemeral ID for
92
92
  // logging/metrics only (not used for thread reuse). Temp mode never persists
93
93
  // a thread, so it has no session id.
94
- const sessionId = isTempMode
94
+ const sessionId = isStateless
95
95
  ? null
96
96
  : (conversationKey || useThreadNative)
97
97
  ? deriveSessionId(
@@ -110,7 +110,7 @@ export async function buildFinalContext(
110
110
  // Thread-native: send full history when Qwen has no context yet, but preserve
111
111
  // tool-result deltas because the upstream parent chain already owns the call.
112
112
  // Temp mode: always send the FULL history (OpenAI standard).
113
- const baseActivePrompt = isTempMode
113
+ const baseActivePrompt = isStateless
114
114
  ? prompt
115
115
  : (!existingThread && !hasTrailingToolResult ? prompt : currentPrompt) ||
116
116
  prompt;
@@ -179,7 +179,7 @@ export async function buildFinalContext(
179
179
  isNewSession,
180
180
  useThreadNative,
181
181
  // Thread state is only persisted in thread mode (temp chats are ephemeral).
182
- updateLogicalThread: isTempMode ? false : useThreadNative,
182
+ updateLogicalThread: isStateless ? false : useThreadNative,
183
183
  chatMode,
184
184
  isThinkingModel,
185
185
  estimatedTokens,
@@ -20,7 +20,7 @@ import {
20
20
  handleChatCompletionsError,
21
21
  type AssistantCompleteEvent,
22
22
  } from "./streaming.ts";
23
- import { config, type ChatMode } from "../../core/config.ts";
23
+ import { config, type ChatMode, normalizeChatMode } from "../../core/config.ts";
24
24
  import { logger } from "../../core/logger.ts";
25
25
  import { metrics } from "../../core/metrics.ts";
26
26
  import { getContextMeterHeaders, type ContextMeterMode } from "../../services/context-meter.ts";
@@ -50,11 +50,20 @@ function formatTimingHeader(timings: Record<string, number>): string {
50
50
  * else silently uses the configured default.
51
51
  */
52
52
  function resolveChatMode(headerValue: string | undefined): ChatMode {
53
- if (headerValue === "thread" || headerValue === "temp" || headerValue === "temp-thread") {
54
- return headerValue;
55
- }
56
- if (headerValue === "temp_thread") {
57
- return "temp-thread";
53
+ if (headerValue) {
54
+ const m = headerValue.trim().toLowerCase().replace(/_/g, "-");
55
+ if (
56
+ m === "thread" ||
57
+ m === "thread-temp" ||
58
+ m === "temp-thread" ||
59
+ m === "stateless" ||
60
+ m === "stateles" ||
61
+ m === "stateless-temp" ||
62
+ m === "stateles-temp" ||
63
+ m === "temp"
64
+ ) {
65
+ return normalizeChatMode(m);
66
+ }
58
67
  }
59
68
  return config.qwen.chatMode;
60
69
  }
@@ -1,6 +1,6 @@
1
1
  import crypto from "crypto";
2
2
  import { getQwenHeaders, isAuthMockEnabled } from "./auth-playwright.ts";
3
- import { config, type ChatMode } from "../core/config.ts";
3
+ import { config, type ChatMode, isLocalChatMode, isStatelessChatMode } from "../core/config.ts";
4
4
  import { logger, isToolcallDebugEnabled } from "../core/logger.ts";
5
5
  import {
6
6
  computeQuotaCooldownMs,
@@ -83,9 +83,8 @@ export function buildChatNewBody(
83
83
  project_id: "",
84
84
  timestamp: Date.now(),
85
85
  chat_type: "t2t",
86
- // thread normal (persisted), temp / temp-thread → local (ephemeral, not listed).
87
- chat_mode:
88
- chatMode === "temp" || chatMode === "temp-thread" ? "local" : "normal",
86
+ // normal: thread & stateless (persisted in Qwen); local: thread-temp & stateless-temp (ephemeral)
87
+ chat_mode: isLocalChatMode(chatMode) ? "local" : "normal",
89
88
  };
90
89
  }
91
90
 
@@ -201,7 +200,7 @@ export async function acquireNewQwenChatSession(
201
200
  accountId?: string,
202
201
  chatMode: ChatMode = "thread",
203
202
  ): Promise<{ chatId: string; leasedFromPool: boolean }> {
204
- if (isQwenChatPoolEnabled() && chatMode !== "temp") {
203
+ if (isQwenChatPoolEnabled() && !isStatelessChatMode(chatMode)) {
205
204
  const key = chatPoolKey(accountId, model);
206
205
  const pooled = precreatedChatSessions.get(key);
207
206
  const chatId = pooled?.shift();
@@ -12,7 +12,7 @@ import {
12
12
  } from "../core/errors.ts";
13
13
  import { buildQwenRequestHeaders } from "./qwen-headers.ts";
14
14
  import { qwenOrigin, qwenUrl } from "./qwen-url.ts";
15
- import { config, type ChatMode } from "../core/config.ts";
15
+ import { config, type ChatMode, isLocalChatMode } from "../core/config.ts";
16
16
  import { logger } from "../core/logger.ts";
17
17
  import { estimateTokenCount } from "../utils/context-truncation.ts";
18
18
  import type {
@@ -2757,10 +2757,7 @@ async function createQwenStreamInternal(
2757
2757
  chatId: chatSessionId || null,
2758
2758
  parentId: actualParentId ?? "",
2759
2759
  chat_id: chatSessionId || null,
2760
- chat_mode:
2761
- options?.chatMode === "temp" || options?.chatMode === "temp-thread"
2762
- ? "local"
2763
- : "normal",
2760
+ chat_mode: isLocalChatMode(options?.chatMode) ? "local" : "normal",
2764
2761
  model: model,
2765
2762
  parent_id: actualParentId,
2766
2763
  messages: [
package/src/tui/app.ts CHANGED
@@ -26,6 +26,7 @@ import { SyncView } from "./views/sync-view.ts";
26
26
  import { StorageView } from "./views/storage-view.ts";
27
27
  import { AccountsView } from "./views/accounts-view.ts";
28
28
  import { LogsView } from "./views/logs-view.ts";
29
+ import { setRuntimeChatMode } from "../core/config.ts";
29
30
  import { loadTuiSettings, saveTuiSettings } from "./settings.ts";
30
31
  export class TuiApp {
31
32
  private screen: Screen;
@@ -39,6 +40,10 @@ export class TuiApp {
39
40
  private renderScheduled = false;
40
41
  constructor(initialTab?: number) {
41
42
  this.screen = new Screen();
43
+ const saved = loadTuiSettings();
44
+ if (saved.chat?.mode) {
45
+ setRuntimeChatMode(saved.chat.mode);
46
+ }
42
47
 
43
48
  this.views = [
44
49
  new StatusView(),
@@ -49,16 +54,10 @@ export class TuiApp {
49
54
  new LogsView(),
50
55
  ];
51
56
 
52
- let resolvedTab = initialTab;
53
- if (!resolvedTab || isNaN(resolvedTab)) {
54
- const saved = loadTuiSettings();
55
- if (saved.lastTab && saved.lastTab >= 1 && saved.lastTab <= 6) {
56
- resolvedTab = saved.lastTab;
57
- } else {
58
- resolvedTab = 1;
59
- }
57
+ let resolvedTab = initialTab ?? (saved.lastTab && saved.lastTab >= 1 && saved.lastTab <= 6 ? saved.lastTab : 1);
58
+ if (isNaN(resolvedTab)) {
59
+ resolvedTab = 1;
60
60
  }
61
-
62
61
  const tabIdx = Math.max(0, Math.min(this.views.length - 1, resolvedTab - 1));
63
62
  this.activeViewIndex = tabIdx;
64
63
  }
package/src/tui/index.ts CHANGED
@@ -23,6 +23,9 @@ function parseInitialTab(): number | undefined {
23
23
 
24
24
  return undefined;
25
25
  }
26
+ try {
27
+ process.title = "QwenProxy";
28
+ } catch {}
26
29
 
27
30
  async function main() {
28
31
  if (!process.stdout.isTTY || !process.stdin.isTTY) {
@@ -190,10 +190,11 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
190
190
  const cacheBytesSaved = lastMetricsData?.cache?.bytesSaved;
191
191
 
192
192
  return {
193
- online,
193
+ online: lastOnlineState,
194
194
  port,
195
195
  host,
196
- overallStatus,
196
+ chatMode: config.qwen.chatMode as any,
197
+ overallStatus: lastOverallStatus,
197
198
  uptimeSeconds,
198
199
  rssMb,
199
200
  systemMemoryPct,
package/src/tui/screen.ts CHANGED
@@ -47,13 +47,20 @@ export class Screen {
47
47
  if (!process.stdout.isTTY || !process.stdin.isTTY) {
48
48
  return false;
49
49
  }
50
-
50
+ try {
51
+ process.title = "QwenProxy";
52
+ } catch {}
51
53
  this.active = true;
52
54
  this.prevRenderedRows = [];
53
- // Switch to alternate screen buffer, clear screen, hide cursor, and enable mouse tracking
55
+ // Switch to alternate screen buffer, clear screen, hide cursor, enable mouse tracking, and set terminal title
54
56
  ServerManager.getInstance().withTuiRendering(() => {
55
57
  process.stdout.write(
56
- ANSI.enterAltScreen + "\x1b[2J" + ANSI.cursorHome + ANSI.hideCursor + ANSI.enableMouse,
58
+ ANSI.enterAltScreen +
59
+ "\x1b[2J" +
60
+ ANSI.cursorHome +
61
+ ANSI.hideCursor +
62
+ ANSI.enableMouse +
63
+ ANSI.setTitle("QwenProxy"),
57
64
  );
58
65
  });
59
66
  // Setup raw keyboard input
@@ -12,6 +12,7 @@ export interface TuiSettings {
12
12
  chat?: {
13
13
  model?: string;
14
14
  effort?: "high" | "medium" | "low";
15
+ mode?: "thread" | "thread-temp" | "stateless" | "stateless-temp";
15
16
  };
16
17
  logs?: {
17
18
  filter?: "all" | "warn" | "error";
@@ -23,6 +24,7 @@ const defaultSettings: TuiSettings = {
23
24
  chat: {
24
25
  model: "qwen3.8-max",
25
26
  effort: "high",
27
+ mode: "thread",
26
28
  },
27
29
  logs: {
28
30
  filter: "all",
package/src/tui/theme.ts CHANGED
@@ -18,6 +18,7 @@ export const ANSI = {
18
18
  exitAltScreen: "\x1b[?1049l",
19
19
  enableMouse: "\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h",
20
20
  disableMouse: "\x1b[?1006l\x1b[?1005l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1015l",
21
+ setTitle: (title: string) => `\x1b]0;${title}\x07`,
21
22
  };
22
23
 
23
24
  export const theme = {
package/src/tui/types.ts CHANGED
@@ -15,6 +15,7 @@ export interface ProxyStatusSnapshot {
15
15
  online: boolean;
16
16
  port: number;
17
17
  host: string;
18
+ chatMode?: "thread" | "thread-temp" | "stateless" | "stateless-temp";
18
19
  overallStatus?: string;
19
20
  uptimeSeconds?: number;
20
21
  rssMb?: number;