qwenproxy-cli 1.0.4 → 1.0.6

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/README.md CHANGED
@@ -284,7 +284,7 @@ qpx
284
284
  | `qpx login` | Abre navegador visível para autenticar novas contas interativamente |
285
285
  | `qpx sync` | Configura e sincroniza clientes (Claude Code, Codex, OpenCode, OMP) |
286
286
  | `qpx clean` | Limpa caches temporários dos perfis Chromium (~4.5MB por conta) |
287
- | `qpx clean:all` | Limpa caches e remove versões antigas de navegadores órfãos no SSD |
287
+ | `qpx clean:all` | Limpa caches e remove versões antigas de navegadores órfãos em disco |
288
288
  | `qpx purge` | Limpa o histórico de conversas remotas no Qwen de todas as contas |
289
289
  | `qpx reset` | Reseta cooldowns e rate limits salvos no banco de dados |
290
290
  ### Opção 2: Execução Instantânea (Zero Instalação)
@@ -845,7 +845,7 @@ QwenProxy/
845
845
  | `npm start` | Iniciar apenas o servidor QwenProxy em modo headless |
846
846
  | `npm run sync` | Sincronizar clientes (Claude Code, Codex, OpenCode, OMP) com backup |
847
847
  | `npm run clean` | Limpar caches temporários dos perfis Chromium (~4.5MB por conta) |
848
- | `npm run clean:all` | Limpar caches + remover navegadores órfãos e versões antigas no SSD |
848
+ | `npm run clean:all` | Limpar caches + remover navegadores órfãos e versões antigas em disco |
849
849
  | `npm run reset` | Zerar cooldowns de contas no banco de dados |
850
850
  | `npm run login` | Adicionar/autenticar novas contas visualmente no navegador |
851
851
  | `npm run purge` | Limpar chats remotos do Qwen nas contas configuradas |
package/bin/qwenproxy.js CHANGED
@@ -115,19 +115,47 @@ try {
115
115
  tsxLoaderArg = pathToFileURL(tsxEntry).href;
116
116
  } catch {}
117
117
 
118
- // Ensure Playwright Chromium is installed for first-time global users
119
- try {
120
- const { chromium } = await import("patchright");
121
- const execPath = chromium.executablePath();
122
- if (!fs.existsSync(execPath)) {
123
- console.log("⏳ [QwenProxy] Instalando o navegador Chromium pela primeira vez...");
124
- spawnSync("npx", ["patchright", "install", "chromium"], {
125
- stdio: "inherit",
126
- shell: true,
127
- });
128
- console.log("✓ [QwenProxy] Navegador instalado com sucesso!\n");
129
- }
130
- } catch {}
118
+ // Ensure Playwright Chromium is installed only for commands that need the browser
119
+ const browserCommands = ["start", "tui", "login"];
120
+ const isBrowserCommand =
121
+ !firstArg ||
122
+ browserCommands.includes(firstArg) ||
123
+ rawArgs.includes("--tui") ||
124
+ rawArgs.includes("--server");
125
+
126
+ if (isBrowserCommand) {
127
+ try {
128
+ const { chromium } = await import("patchright");
129
+ const execPath = chromium.executablePath();
130
+ if (!fs.existsSync(execPath)) {
131
+ console.log("⏳ [QwenProxy] Instalando o navegador Chromium pela primeira vez...");
132
+ let cliPath = "";
133
+ try {
134
+ const patchrightEntry = require.resolve("patchright");
135
+ cliPath = path.join(path.dirname(patchrightEntry), "cli.js");
136
+ } catch {}
137
+ const installEnv = {
138
+ ...process.env,
139
+ PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT:
140
+ process.env.PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT || "300000",
141
+ };
142
+
143
+ if (cliPath && fs.existsSync(cliPath)) {
144
+ spawnSync(process.execPath, [cliPath, "install", "chromium"], {
145
+ stdio: "inherit",
146
+ env: installEnv,
147
+ });
148
+ } else {
149
+ const cmd = process.platform === "win32" ? "npx.cmd" : "npx";
150
+ spawnSync(cmd, ["--yes", "patchright", "install", "chromium"], {
151
+ stdio: "inherit",
152
+ env: installEnv,
153
+ });
154
+ }
155
+ console.log("✓ [QwenProxy] Navegador instalado com sucesso!\n");
156
+ }
157
+ } catch {}
158
+ }
131
159
 
132
160
  const child = spawn(process.execPath, ["--import", tsxLoaderArg, targetPath, ...scriptArgs], {
133
161
  stdio: "inherit",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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": {
package/src/api/server.ts CHANGED
@@ -597,6 +597,7 @@ export async function stopServer(): Promise<void> {
597
597
 
598
598
  export async function startServer(options?: {
599
599
  installSignalHandlers?: boolean;
600
+ showBanner?: boolean;
600
601
  }): Promise<StartedServerInfo> {
601
602
  if (server) {
602
603
  if (options?.installSignalHandlers !== false) installSignalHandlers();
@@ -829,7 +830,8 @@ export async function startServer(options?: {
829
830
 
830
831
  const endpoint = `${started.url}/v1`;
831
832
 
832
- console.log(`
833
+ if (options?.showBanner !== false) {
834
+ console.log(`
833
835
  +${"-".repeat(W)}+
834
836
  |${blank()}|
835
837
  |${center("QwenProxy")}|
@@ -845,6 +847,7 @@ export async function startServer(options?: {
845
847
  |${blank()}|
846
848
  +${"-".repeat(W)}+
847
849
  `);
850
+ }
848
851
  return started;
849
852
  })();
850
853
 
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
5
  import { chromium } from "patchright";
6
- import { pruneAllPlaywrightProfiles } from "./services/playwright.ts";
6
+ import { pruneAllPlaywrightProfiles, cleanupOrphanProfiles } from "./services/playwright.ts";
7
7
 
8
8
  export function formatBytes(bytes: number): string {
9
9
  if (bytes < 1024) return `${bytes} B`;
@@ -141,13 +141,19 @@ async function main() {
141
141
  // 1. Profile Transient Cache Pruning (V8 Code Cache, GPU Cache)
142
142
  console.log("1. Limpando caches transitórios dos perfis (data/qwen_profiles/)...");
143
143
  const profileResult = pruneAllPlaywrightProfiles();
144
- if (profileResult.totalFreedFiles > 0) {
145
- console.log(
146
- ` [OK] Perfis limpos com sucesso!`,
147
- );
148
- console.log(
149
- ` Espaço liberado: ${formatBytes(profileResult.totalFreedBytes)} em ${profileResult.totalFreedFiles} arquivos (${profileResult.profilesCleaned} perfil(is)).`,
150
- );
144
+ const orphanResult = cleanupOrphanProfiles();
145
+ if (profileResult.totalFreedFiles > 0 || orphanResult.removedCount > 0) {
146
+ console.log(` [OK] Perfis limpos com sucesso!`);
147
+ if (profileResult.totalFreedFiles > 0) {
148
+ console.log(
149
+ ` Espaço liberado: ${formatBytes(profileResult.totalFreedBytes)} em ${profileResult.totalFreedFiles} arquivos (${profileResult.profilesCleaned} perfil(is)).`,
150
+ );
151
+ }
152
+ if (orphanResult.removedCount > 0) {
153
+ console.log(
154
+ ` Perfis órfãos removidos: ${orphanResult.removedCount} (${formatBytes(orphanResult.freedBytes)} liberados).`,
155
+ );
156
+ }
151
157
  console.log(
152
158
  ` (Todos os cookies, sessões e logins foram 100% preservados!)`,
153
159
  );
@@ -172,13 +178,13 @@ async function main() {
172
178
  for (const d of browserResult.unusedDirs) {
173
179
  console.log(` - ${d.name} (${d.size})`);
174
180
  }
175
- console.log(` Total recuperado no SSD: ${formatBytes(browserResult.freedBytes)}!`);
181
+ console.log(` Total recuperado em disco: ${formatBytes(browserResult.freedBytes)}!`);
176
182
  } else {
177
- console.log(` [INFO] Encontrados ${browserResult.unusedDirs.length} navegador(es) legados/não utilizados no seu SSD:`);
183
+ console.log(` [INFO] Encontrados ${browserResult.unusedDirs.length} navegador(es) legados/não utilizados em disco:`);
178
184
  for (const d of browserResult.unusedDirs) {
179
185
  console.log(` - ${d.name} (${d.size})`);
180
186
  }
181
- console.log(` Espaço recuperável no SSD: ${formatBytes(totalReclaimable)}.`);
187
+ console.log(` Espaço recuperável em disco: ${formatBytes(totalReclaimable)}.`);
182
188
  console.log(` Para liberar esse espaço automaticamente, execute:`);
183
189
  console.log(` npm run clean:all\n`);
184
190
  }
@@ -154,6 +154,7 @@ const envSchema = z
154
154
  QWEN_PERSONALIZATION_FROM_REQUEST: z.string().default("true"),
155
155
  QWEN_PERSONALIZATION_VERIFY_GET: z.string().default("true"),
156
156
  QWEN_BROWSER_ONLY_FETCH: z.string().default("true"),
157
+ QWEN_MAP_OPENAI_MODELS: z.string().default("true"),
157
158
  QWEN_MAX_PROMPT_BYTES: z.string().default("0"),
158
159
  QWEN_MAX_PERSONALIZATION_BYTES: z.string().default("200000"),
159
160
  CONTEXT_METER_ENABLED: z.string().default("true"),
@@ -358,6 +359,7 @@ export const config = {
358
359
  /** "thread" (reuse upstream chat) or "temp" (new ephemeral chat per request). */
359
360
  /** When true, all requests (personalization, models, media, chat) route exclusively through the browser page (no direct Node fetch). */
360
361
  browserOnlyFetch: env.QWEN_BROWSER_ONLY_FETCH !== "false",
362
+ mapOpenAiModels: env.QWEN_MAP_OPENAI_MODELS !== "false",
361
363
  chatMode: env.QWEN_CHAT_MODE,
362
364
  maxPromptBytes: Math.max(0, parseInt(env.QWEN_MAX_PROMPT_BYTES)),
363
365
  maxPersonalizationBytes: Math.max(
@@ -63,15 +63,55 @@ export function stripThinkingSuffix(model: string): {
63
63
  return { baseModel: normalizedModel, enableThinking: true, reasoningMode: "auto" };
64
64
  }
65
65
 
66
+ /**
67
+ * Mapeia modelos populares de terceiros (OpenAI / Anthropic) para o tier Qwen equivalente:
68
+ * - mini / haiku / 3.5 -> qwen3.7-plus
69
+ * - gpt-4* / o1* / o3* / opus / sonnet / claude-* -> qwen3.8-max
70
+ * Modelos que já começam com "qwen" passam intactos.
71
+ */
72
+ export function mapKnownModelAlias(model: string): string {
73
+ if (!model) return model;
74
+ const lower = model.toLowerCase();
75
+ if (lower.startsWith("qwen")) return model;
76
+
77
+ // Lightweight / mini models
78
+ if (
79
+ lower.includes("mini") ||
80
+ lower.includes("haiku") ||
81
+ lower.includes("3.5-turbo")
82
+ ) {
83
+ return "qwen3.7-plus";
84
+ }
85
+
86
+ // Flagship reasoning / chat models
87
+ if (
88
+ lower.startsWith("gpt-") ||
89
+ lower.startsWith("chatgpt-") ||
90
+ lower.startsWith("o1") ||
91
+ lower.startsWith("o3") ||
92
+ lower.includes("sonnet") ||
93
+ lower.includes("opus") ||
94
+ lower.startsWith("claude")
95
+ ) {
96
+ return "qwen3.8-max";
97
+ }
98
+
99
+ return model;
100
+ }
101
+
66
102
  /**
67
103
  * Mapeia o id de modelo para o Qwen upstream.
68
- * Ids `qwen*` passam direto (após remover o sufixo de raciocínio); ids de
69
- * outros provedores (gpt-*, grok-*, etc.) também passam “as-is” o Codex/Custom
70
- * provider envia o id Qwen correto, e qualquer id desconhecido deve chegar ao
71
- * upstream para que este responda um erro claro de modelo, em vez de ser
72
- * silenciosamente roteado para um tier qualquer.
104
+ * Quando enableAliases é verdadeiro (padrão via QWEN_MAP_OPENAI_MODELS),
105
+ * converte gpt-* / o1* / claude-* para os tiers correspondentes do Qwen.
73
106
  */
74
- export function mapClientModelToQwen(model: string): string {
107
+ export function mapClientModelToQwen(
108
+ model: string,
109
+ enableAliases = true,
110
+ ): string {
75
111
  if (!model) return model;
76
- return stripThinkingSuffix(model.trim()).baseModel;
112
+ const base = stripThinkingSuffix(model.trim()).baseModel;
113
+ if (enableAliases) {
114
+ return mapKnownModelAlias(base);
115
+ }
116
+ return base;
77
117
  }
package/src/login.ts CHANGED
@@ -130,6 +130,11 @@ async function removeAccountFlow() {
130
130
  const confirm = await askQuestion(`\nRemove ${account.email}? (y/N): `);
131
131
  if (confirm.toLowerCase() === "y") {
132
132
  if (removeAccount(account.id)) {
133
+ try {
134
+ const { removePlaywrightProfile } = await import("./services/playwright.ts");
135
+ const { getAccountProfilePath } = await import("./core/paths.ts");
136
+ removePlaywrightProfile(getAccountProfilePath(account.id));
137
+ } catch {}
133
138
  console.log(`Account ${maskEmail(account.email)} removed.`);
134
139
  } else {
135
140
  console.log("Failed to remove account.");
@@ -70,7 +70,7 @@ export async function parseRequestBody(c: Context): Promise<ParsedRequest> {
70
70
 
71
71
  // Thinking suffixes → base model + reasoning mode
72
72
  const { baseModel, enableThinking, reasoningMode } = stripThinkingSuffix(body.model);
73
- const modelId = mapClientModelToQwen(baseModel);
73
+ const modelId = mapClientModelToQwen(baseModel, config.qwen.mapOpenAiModels);
74
74
 
75
75
  // OpenAI `reasoning_effort` (none|minimal|low|medium|high|xhigh|max).
76
76
  // Precedence: an explicit model suffix wins — effort only acts on unsuffixed
@@ -23,9 +23,48 @@ function pick<T>(rng: () => number, values: readonly T[]): T {
23
23
  function randInt(rng: () => number, min: number, max: number): number {
24
24
  return Math.floor(rng() * (max - min + 1)) + min;
25
25
  }
26
+ let dynamicChromeMajor = 151;
26
27
 
27
- const CHROME_MAJOR = 149;
28
+ export function updateChromeMajor(major: number): void {
29
+ if (typeof major === "number" && major >= 100) {
30
+ dynamicChromeMajor = major;
31
+ }
32
+ }
33
+
34
+ export function getChromeMajor(): number {
35
+ return dynamicChromeMajor;
36
+ }
37
+
38
+ export function getSystemLocale(): string {
39
+ return (
40
+ process.env.QWEN_LOCALE ||
41
+ (typeof Intl !== "undefined" && Intl.DateTimeFormat
42
+ ? Intl.DateTimeFormat().resolvedOptions().locale
43
+ : "") ||
44
+ "pt-BR"
45
+ );
46
+ }
47
+
48
+ function getLanguageProfiles(locale = getSystemLocale()): readonly (readonly string[])[] {
49
+ const primary = locale.split("-")[0];
50
+ if (primary === "en") {
51
+ return [
52
+ ["en-US", "en"],
53
+ ["en-US", "en", "es"],
54
+ ["en", "en-US"],
55
+ ["en-US", "en;q=0.9"],
56
+ ] as const;
57
+ }
58
+ return [
59
+ [locale, primary, "en-US", "en"],
60
+ [locale, primary, "en-US", "en", "es"],
61
+ [locale, "en-US", "en", primary],
62
+ [locale, primary, "en"],
63
+ [locale, `${primary};q=0.9`, "en-US;q=0.8", "en;q=0.7"],
64
+ ] as const;
65
+ }
28
66
 
67
+ const CHROME_MAJOR = 151;
29
68
  const VIEWPORTS = [
30
69
  { width: 1366, height: 768 },
31
70
  { width: 1440, height: 900 },
@@ -176,20 +215,21 @@ export function getFingerprintProfile(accountId: string): FingerprintProfile {
176
215
  const rng = mulberry32(seed);
177
216
  const viewport = pick(rng, VIEWPORTS);
178
217
  const webgl = pick(rng, WEBGL_PROFILES);
179
- const languages = [...pick(rng, LANGUAGE_PROFILES)];
218
+ const languages = [...pick(rng, getLanguageProfiles())];
180
219
  const hardwareConcurrency = pick(rng, HARDWARE_CONCURRENCIES);
181
220
  const deviceMemory = pick(rng, DEVICE_MEMORIES);
182
221
  const platformInfo = pick(rng, PLATFORM_VERSIONS);
183
222
  const notABrand = pick(rng, NOT_A_BRAND_VARIANTS);
184
223
 
224
+ const major = dynamicChromeMajor;
185
225
  const build = randInt(rng, 7300, 7600);
186
226
  const patch = randInt(rng, 0, 160);
187
- const chromeVersion = `${CHROME_MAJOR}.0.${build}.${patch}`;
227
+ const chromeVersion = `${major}.0.${build}.${patch}`;
188
228
 
189
229
  const brands = [
190
230
  { brand: notABrand.brand, version: notABrand.version },
191
- { brand: "Google Chrome", version: String(CHROME_MAJOR) },
192
- { brand: "Chromium", version: String(CHROME_MAJOR) },
231
+ { brand: "Google Chrome", version: String(major) },
232
+ { brand: "Chromium", version: String(major) },
193
233
  ];
194
234
  const fullVersionList = [
195
235
  { brand: notABrand.brand, version: chromeVersion },
@@ -7,7 +7,38 @@ import { chromium, type Browser, type BrowserContext, type Page } from "patchrig
7
7
  import path from "path";
8
8
  import fs from "fs";
9
9
  import crypto from "crypto";
10
- import type { QwenAccount } from "../core/accounts.ts";
10
+ import { spawnSync } from "child_process";
11
+ import { createRequire } from "module";
12
+
13
+ const requireLocal = createRequire(import.meta.url);
14
+
15
+ function autoInstallPlaywrightChromium(): void {
16
+ const installEnv = {
17
+ ...process.env,
18
+ PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT:
19
+ process.env.PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT || "300000",
20
+ };
21
+ try {
22
+ const patchrightEntry = requireLocal.resolve("patchright");
23
+ const cliPath = path.join(path.dirname(patchrightEntry), "cli.js");
24
+ if (fs.existsSync(cliPath)) {
25
+ console.log("⏳ [Playwright] Navegador Chromium não encontrado. Instalando automaticamente...");
26
+ spawnSync(process.execPath, [cliPath, "install", "chromium"], {
27
+ stdio: "inherit",
28
+ env: installEnv,
29
+ });
30
+ return;
31
+ }
32
+ } catch {}
33
+
34
+ console.log("⏳ [Playwright] Navegador Chromium não encontrado. Instalando via npx...");
35
+ const cmd = process.platform === "win32" ? "npx.cmd" : "npx";
36
+ spawnSync(cmd, ["--yes", "patchright", "install", "chromium"], {
37
+ stdio: "inherit",
38
+ env: installEnv,
39
+ });
40
+ }
41
+ import { loadAccounts, type QwenAccount } from "../core/accounts.ts";
11
42
  // Imported here rather than injected from session-keeper.ts: account-concurrency
12
43
  // only depends on config/logger, so playwright -> account-concurrency stays
13
44
  // acyclic, while the reverse direction would drag the browser layer into core.
@@ -23,6 +54,7 @@ import { getAccountsByPriority } from "../core/account-priority.ts";
23
54
  import {
24
55
  clearFingerprintCache,
25
56
  getFingerprintProfile,
57
+ updateChromeMajor,
26
58
  type FingerprintProfile,
27
59
  } from "./fingerprint.ts";
28
60
  import { subtlePageActivity } from "./human-behavior.ts";
@@ -260,16 +292,38 @@ export async function getOrLaunchSharedBrowser(
260
292
  const launchArgs = buildChromiumLaunchArgs(defaultViewport);
261
293
 
262
294
  console.log(
263
- `[Playwright] Launching single shared ${browserType} browser...`,
295
+ `🌐 [Playwright] Launching single shared ${browserType} browser...`,
264
296
  );
265
297
 
266
- const browser = await engine.launch({
267
- headless,
268
- channel,
269
- ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
270
- args: launchArgs,
271
- });
298
+ let browser: Browser;
299
+ try {
300
+ browser = await engine.launch({
301
+ headless,
302
+ channel,
303
+ ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
304
+ args: launchArgs,
305
+ });
306
+ } catch (launchErr: any) {
307
+ if (launchErr?.message?.includes("Executable doesn't exist")) {
308
+ autoInstallPlaywrightChromium();
309
+ browser = await engine.launch({
310
+ headless,
311
+ channel,
312
+ ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
313
+ args: launchArgs,
314
+ });
315
+ } else {
316
+ throw launchErr;
317
+ }
318
+ }
272
319
 
320
+ try {
321
+ const v = browser.version();
322
+ const major = parseInt(v.split(".")[0], 10);
323
+ if (major >= 100) {
324
+ updateChromeMajor(major);
325
+ }
326
+ } catch {}
273
327
  browser.on("disconnected", () => {
274
328
  console.warn("[Playwright] Shared browser disconnected");
275
329
  sharedBrowser = null;
@@ -2338,6 +2392,64 @@ export function pruneAllPlaywrightProfiles(baseDir = getProfilesDir()): {
2338
2392
 
2339
2393
  return { totalFreedBytes, totalFreedFiles, profilesCleaned };
2340
2394
  }
2395
+ /**
2396
+ * Removes profile directories in data/qwen_profiles that do not belong to any
2397
+ * active account configured in the database or environment, plus any lingering
2398
+ * .stale-* directories from previous lock renames.
2399
+ */
2400
+ export function cleanupOrphanProfiles(
2401
+ baseDir = getProfilesDir(),
2402
+ activeAccountIds?: Set<string>,
2403
+ ): {
2404
+ removedCount: number;
2405
+ freedBytes: number;
2406
+ } {
2407
+ let removedCount = 0;
2408
+ let freedBytes = 0;
2409
+
2410
+ try {
2411
+ if (!fs.existsSync(baseDir)) {
2412
+ return { removedCount, freedBytes };
2413
+ }
2414
+
2415
+ const activeIds =
2416
+ activeAccountIds ?? new Set(loadAccounts().map((a) => a.id));
2417
+
2418
+ const entries = fs.readdirSync(baseDir, { withFileTypes: true });
2419
+ for (const entry of entries) {
2420
+ if (!entry.isDirectory()) continue;
2421
+ const isStale = entry.name.includes(".stale-");
2422
+ const isOrphan = !isStale && !activeIds.has(entry.name);
2423
+
2424
+ if (isStale || isOrphan) {
2425
+ const targetPath = path.join(baseDir, entry.name);
2426
+ try {
2427
+ let bytes = 0;
2428
+ const countSize = (d: string) => {
2429
+ try {
2430
+ const subEntries = fs.readdirSync(d, { withFileTypes: true });
2431
+ for (const e of subEntries) {
2432
+ const full = path.join(d, e.name);
2433
+ if (e.isDirectory()) countSize(full);
2434
+ else if (e.isFile()) {
2435
+ try { bytes += fs.statSync(full).size; } catch {}
2436
+ }
2437
+ }
2438
+ } catch {}
2439
+ };
2440
+ countSize(targetPath);
2441
+ removePlaywrightProfile(targetPath);
2442
+ if (!fs.existsSync(targetPath)) {
2443
+ removedCount++;
2444
+ freedBytes += bytes;
2445
+ }
2446
+ } catch {}
2447
+ }
2448
+ }
2449
+ } catch {}
2450
+
2451
+ return { removedCount, freedBytes };
2452
+ }
2341
2453
 
2342
2454
  const PROFILE_RESET_TIMEOUT_MS = Math.max(90_000, config.timeouts.headers);
2343
2455
 
@@ -153,10 +153,10 @@ const WARM_POOL_LOW_WATER = 3;
153
153
 
154
154
  function warmChatKey(
155
155
  accountId: string | undefined,
156
- model: string,
156
+ _model: string,
157
157
  chatId: string,
158
158
  ) {
159
- return `${accountId || "global"}:${model}:${chatId}`;
159
+ return `${accountId || "global"}:${chatId}`;
160
160
  }
161
161
 
162
162
  function markWarmChatInFlight(
@@ -183,8 +183,8 @@ function isWarmChatInFlight(
183
183
  return inFlightWarmChats.has(warmChatKey(accountId, model, chatId));
184
184
  }
185
185
 
186
- function chatPoolKey(accountId: string | undefined, model: string): string {
187
- return `${accountId || "global"}:${model}`;
186
+ function chatPoolKey(accountId: string | undefined, _model?: string): string {
187
+ return accountId || "global";
188
188
  }
189
189
 
190
190
  function isQwenChatPoolEnabled(): boolean {
@@ -1,6 +1,24 @@
1
1
  import { v4 as uuidv4 } from "uuid";
2
2
  import { qwenUrl, qwenOrigin } from "./qwen-url.ts";
3
3
  import { config } from "../core/config.js";
4
+ import { getChromeMajor, getSystemLocale } from "./fingerprint.ts";
5
+
6
+ export function getSystemAcceptLanguage(customLocale?: string): string {
7
+ const locale = customLocale || getSystemLocale();
8
+ const primary = locale.split("-")[0];
9
+ if (locale.toLowerCase() === "en-us" || locale.toLowerCase() === "en") {
10
+ return "en-US,en;q=0.9";
11
+ }
12
+ if (locale.toLowerCase().startsWith("pt")) {
13
+ return "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7";
14
+ }
15
+ return `${locale},${primary};q=0.9,en-US;q=0.8,en;q=0.7`;
16
+ }
17
+
18
+ export function getDefaultQwenUserAgent(major = getChromeMajor()): string {
19
+ return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${major}.0.0.0 Safari/537.36`;
20
+ }
21
+
4
22
  let dynamicWebVersion: string | null = null;
5
23
 
6
24
  export function updateQwenWebVersion(version?: string | null): void {
@@ -14,13 +32,12 @@ export function getQwenWebVersion(): string {
14
32
  }
15
33
 
16
34
  export const QWEN_WEB_VERSION = config.qwen.webVersion;
17
- export const DEFAULT_QWEN_USER_AGENT =
18
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36";
35
+ export const DEFAULT_QWEN_USER_AGENT = getDefaultQwenUserAgent(151);
19
36
  const QWEN_TIMEZONE_HEADER = new Date().toString().split(" (")[0];
20
-
21
37
  export interface BuildQwenHeadersOptions {
22
38
  cookie: string;
23
39
  userAgent?: string;
40
+ acceptLanguage?: string;
24
41
  bxUa?: string;
25
42
  bxUmidtoken?: string;
26
43
  bxV?: string;
@@ -40,7 +57,10 @@ export function buildQwenRequestHeaders(
40
57
  const headers: Record<string, string> = {
41
58
  ...(opts.extra ?? {}),
42
59
  Accept: "application/json",
43
- "Accept-Language": "pt-BR,pt;q=0.9",
60
+ "Accept-Language":
61
+ opts.extra?.["Accept-Language"] ||
62
+ opts.acceptLanguage ||
63
+ getSystemAcceptLanguage(),
44
64
  "Accept-Encoding": "gzip, deflate, br, zstd",
45
65
  "Content-Type": "application/json",
46
66
  Cookie: opts.cookie,
@@ -54,7 +74,7 @@ export function buildQwenRequestHeaders(
54
74
  "Sec-Fetch-Mode": "cors",
55
75
  "Sec-Fetch-Site": "same-origin",
56
76
  Connection: "keep-alive",
57
- "User-Agent": opts.userAgent || DEFAULT_QWEN_USER_AGENT,
77
+ "User-Agent": opts.userAgent || getDefaultQwenUserAgent(),
58
78
  "X-Request-Id": uuidv4(),
59
79
  "bx-v": opts.bxV || "2.5.37",
60
80
  source: "web",
@@ -62,7 +82,9 @@ export function buildQwenRequestHeaders(
62
82
  timezone: opts.extra?.timezone || new Date().toString().split(" (")[0],
63
83
  // Use the real browser client-hints when captured (anti-hardcoded); fall
64
84
  // back to the static fingerprint otherwise.
65
- "sec-ch-ua": opts.secChUa || '"Google Chrome";v="150", "Chromium";v="150", "Not.A/Brand";v="99"',
85
+ "sec-ch-ua":
86
+ opts.secChUa ||
87
+ `"Google Chrome";v="${getChromeMajor()}", "Chromium";v="${getChromeMajor()}", "Not.A/Brand";v="99"`,
66
88
  "sec-ch-ua-mobile": opts.secChUaMobile || "?0",
67
89
  "sec-ch-ua-platform": opts.secChUaPlatform || '"Windows"',
68
90
  };