qwenproxy-cli 1.6.5 → 1.6.7

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.
@@ -3588,6 +3588,15 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
3588
3588
  await existingInit;
3589
3589
  return;
3590
3590
  }
3591
+ let resolveInit;
3592
+ let rejectInit;
3593
+ const inFlightDeferred = new Promise((res, rej) => {
3594
+ resolveInit = res;
3595
+ rejectInit = rej;
3596
+ });
3597
+ inFlightDeferred.catch(() => {
3598
+ });
3599
+ inFlightAccountInits.set(account.id, inFlightDeferred);
3591
3600
  const initPromise = (async () => {
3592
3601
  const release = await acquireAccountMutex(
3593
3602
  account.id,
@@ -3872,9 +3881,12 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
3872
3881
  release();
3873
3882
  }
3874
3883
  })();
3875
- inFlightAccountInits.set(account.id, initPromise);
3876
3884
  try {
3877
3885
  await initPromise;
3886
+ resolveInit();
3887
+ } catch (error) {
3888
+ rejectInit(error);
3889
+ throw error;
3878
3890
  } finally {
3879
3891
  inFlightAccountInits.delete(account.id);
3880
3892
  }
@@ -5481,7 +5493,10 @@ function isPlaywrightAlreadyClosedMessage(message) {
5481
5493
  return message.includes("Target page, context or browser has been closed") || message.includes("Browser has been closed") || message.includes("Target closed") || message.includes("Target crashed") || message.includes("Page crashed") || message.includes("Assertion error") || message.includes("Cannot find parent object") || message.includes("Connection closed") || message.includes("session closed") || message.includes("Session closed") || message.includes("Network.setCacheDisabled") || message.includes("Protocol error") || message.includes("storageState timed out");
5482
5494
  }
5483
5495
  function isBenignTeardownError(error) {
5484
- if (!error) return false;
5496
+ if (!error) return true;
5497
+ if (typeof error === "object" && Object.keys(error).length === 0 && !(error instanceof Error)) {
5498
+ return true;
5499
+ }
5485
5500
  const message = error instanceof Error ? error.message : typeof error === "object" && "message" in error ? String(error.message) : String(error);
5486
5501
  return isPlaywrightAlreadyClosedMessage(message);
5487
5502
  }
@@ -19,7 +19,10 @@ function isPlaywrightAlreadyClosedMessage(message) {
19
19
  return message.includes("Target page, context or browser has been closed") || message.includes("Browser has been closed") || message.includes("Target closed") || message.includes("Target crashed") || message.includes("Page crashed") || message.includes("Assertion error") || message.includes("Cannot find parent object") || message.includes("Connection closed") || message.includes("session closed") || message.includes("Session closed") || message.includes("Network.setCacheDisabled") || message.includes("Protocol error") || message.includes("storageState timed out");
20
20
  }
21
21
  function isBenignTeardownError(error) {
22
- if (!error) return false;
22
+ if (!error) return true;
23
+ if (typeof error === "object" && Object.keys(error).length === 0 && !(error instanceof Error)) {
24
+ return true;
25
+ }
23
26
  const message = error instanceof Error ? error.message : typeof error === "object" && "message" in error ? String(error.message) : String(error);
24
27
  return isPlaywrightAlreadyClosedMessage(message);
25
28
  }
@@ -4447,6 +4450,15 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
4447
4450
  await existingInit;
4448
4451
  return;
4449
4452
  }
4453
+ let resolveInit;
4454
+ let rejectInit;
4455
+ const inFlightDeferred = new Promise((res, rej) => {
4456
+ resolveInit = res;
4457
+ rejectInit = rej;
4458
+ });
4459
+ inFlightDeferred.catch(() => {
4460
+ });
4461
+ inFlightAccountInits.set(account.id, inFlightDeferred);
4450
4462
  const initPromise = (async () => {
4451
4463
  const release = await acquireAccountMutex(
4452
4464
  account.id,
@@ -4731,9 +4743,12 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
4731
4743
  release();
4732
4744
  }
4733
4745
  })();
4734
- inFlightAccountInits.set(account.id, initPromise);
4735
4746
  try {
4736
4747
  await initPromise;
4748
+ resolveInit();
4749
+ } catch (error) {
4750
+ rejectInit(error);
4751
+ throw error;
4737
4752
  } finally {
4738
4753
  inFlightAccountInits.delete(account.id);
4739
4754
  }
@@ -7370,6 +7385,10 @@ function stripThinkingSuffix(model) {
7370
7385
  reasoningMode: "thinking"
7371
7386
  };
7372
7387
  }
7388
+ const lower = normalizedModel.toLowerCase();
7389
+ if (lower.includes("omni-flash") || lower.includes("turbo")) {
7390
+ return { baseModel: normalizedModel, enableThinking: false, reasoningMode: "fast" };
7391
+ }
7373
7392
  return { baseModel: normalizedModel, enableThinking: true, reasoningMode: "auto" };
7374
7393
  }
7375
7394
  function mapKnownModelAlias(model) {
@@ -7737,6 +7756,14 @@ async function recoverBaxiaCaptcha(accountId, label, options = {}) {
7737
7756
  );
7738
7757
  const challengeUrl = options.challengeBody ? extractBaxiaChallengeUrl(options.challengeBody, config.qwen.baseUrl) : null;
7739
7758
  activeCaptchaRecoveries.add(accountId);
7759
+ try {
7760
+ const { abortActiveBrowserStreamsForAccount: abortActiveBrowserStreamsForAccount2 } = await Promise.resolve().then(() => (init_qwen(), qwen_exports));
7761
+ abortActiveBrowserStreamsForAccount2(
7762
+ accountId,
7763
+ "Account stream interrupted by captcha challenge recovery"
7764
+ );
7765
+ } catch {
7766
+ }
7740
7767
  try {
7741
7768
  const solved = await withAccountPage(
7742
7769
  accountId,
@@ -8513,6 +8540,7 @@ __export(qwen_exports, {
8513
8540
  QwenUpstreamUnavailableError: () => QwenUpstreamUnavailableError,
8514
8541
  RetryableQwenStreamError: () => RetryableQwenStreamError,
8515
8542
  STREAM_SLOT_MAX_HOLD_MS: () => STREAM_SLOT_MAX_HOLD_MS,
8543
+ abortActiveBrowserStreamsForAccount: () => abortActiveBrowserStreamsForAccount,
8516
8544
  acquireAccountStreamLock: () => acquireAccountStreamLock,
8517
8545
  acquireNewQwenChatSession: () => acquireNewQwenChatSession,
8518
8546
  buildCapturedQwenHeaders: () => buildCapturedQwenHeaders,
@@ -8559,6 +8587,27 @@ __export(qwen_exports, {
8559
8587
  });
8560
8588
  import crypto5 from "crypto";
8561
8589
  import { v4 as uuidv43 } from "uuid";
8590
+ function abortActiveBrowserStreamsForAccount(accountId, reason = "Account browser stream interrupted (page context unloaded)") {
8591
+ let count = 0;
8592
+ for (const [requestId, state] of browserStreamStates) {
8593
+ if (state.accountId === accountId && !state.done) {
8594
+ count++;
8595
+ handleBrowserStreamEvent(requestId, {
8596
+ type: "error",
8597
+ message: reason,
8598
+ errorName: "AbortError"
8599
+ });
8600
+ }
8601
+ }
8602
+ if (count > 0) {
8603
+ logger.debug("[Qwen] Aborted active browser streams for account", {
8604
+ accountId,
8605
+ count,
8606
+ reason
8607
+ });
8608
+ }
8609
+ return count;
8610
+ }
8562
8611
  async function registerBrowserContextStreamBinding(context) {
8563
8612
  if (browserStreamBindingContexts.has(context)) return;
8564
8613
  browserStreamBindingContexts.add(context);
@@ -9253,6 +9302,7 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
9253
9302
  }
9254
9303
  const requestId = uuidv43();
9255
9304
  const state = {
9305
+ accountId,
9256
9306
  chunks: [],
9257
9307
  done: false,
9258
9308
  error: null,
@@ -9269,10 +9319,15 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
9269
9319
  let settled = false;
9270
9320
  let abortListener;
9271
9321
  let cancelPromise;
9322
+ let pageCleanup;
9272
9323
  const cleanup = () => {
9273
9324
  if (settled) return;
9274
9325
  settled = true;
9275
9326
  if (abortListener) signal.removeEventListener("abort", abortListener);
9327
+ try {
9328
+ pageCleanup?.();
9329
+ } catch {
9330
+ }
9276
9331
  browserStreamStates.delete(requestId);
9277
9332
  };
9278
9333
  const cancel = () => {
@@ -9306,6 +9361,35 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
9306
9361
  accountId,
9307
9362
  async (page) => {
9308
9363
  await ensureBrowserStreamBinding(page);
9364
+ if (typeof page.on === "function") {
9365
+ const onFrameNavigated = (frame) => {
9366
+ if (!settled && typeof page.mainFrame === "function" && frame === page.mainFrame()) {
9367
+ handleBrowserStreamEvent(requestId, {
9368
+ type: "error",
9369
+ message: "Qwen browser page navigated during stream (session interrupted)",
9370
+ errorName: "AbortError"
9371
+ });
9372
+ }
9373
+ };
9374
+ const onPageClosed = () => {
9375
+ if (!settled) {
9376
+ handleBrowserStreamEvent(requestId, {
9377
+ type: "error",
9378
+ message: "Qwen browser page closed during stream",
9379
+ errorName: "AbortError"
9380
+ });
9381
+ }
9382
+ };
9383
+ page.on("framenavigated", onFrameNavigated);
9384
+ page.on("close", onPageClosed);
9385
+ pageCleanup = () => {
9386
+ try {
9387
+ page.off?.("framenavigated", onFrameNavigated);
9388
+ page.off?.("close", onPageClosed);
9389
+ } catch {
9390
+ }
9391
+ };
9392
+ }
9309
9393
  if (config.captcha.enabled) {
9310
9394
  captchaWatcher = startBaxiaCaptchaWatcher(
9311
9395
  page,
@@ -10490,7 +10574,7 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
10490
10574
  }
10491
10575
  });
10492
10576
  }
10493
- const baseTimeoutMs = enableThinking ? config.timeouts.reasoningModelTimeout : config.timeouts.idleStreamTimeout;
10577
+ const baseTimeoutMs = config.timeouts.idleStreamTimeout;
10494
10578
  const dynamicIdleTimeoutMs = computeDynamicIdleTimeout({
10495
10579
  enableThinking,
10496
10580
  parallelEscape: options?.parallelEscape,
@@ -10506,9 +10590,10 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
10506
10590
  dynamicTimeout: dynamicIdleTimeoutMs
10507
10591
  });
10508
10592
  const firstChunkDeadlineMs = enableThinking ? Math.max(
10593
+ config.timeouts.reasoningModelTimeout,
10509
10594
  config.timeouts.firstChunkTimeout,
10510
10595
  config.timeouts.timeToFirstByte
10511
- ) : void 0;
10596
+ ) : Math.max(config.timeouts.firstChunkTimeout, config.timeouts.timeToFirstByte);
10512
10597
  return addIdleTimeoutToStream(
10513
10598
  stream,
10514
10599
  controller2,
package/dist/index.js CHANGED
@@ -156,7 +156,10 @@ function isPlaywrightAlreadyClosedMessage(message) {
156
156
  return message.includes("Target page, context or browser has been closed") || message.includes("Browser has been closed") || message.includes("Target closed") || message.includes("Target crashed") || message.includes("Page crashed") || message.includes("Assertion error") || message.includes("Cannot find parent object") || message.includes("Connection closed") || message.includes("session closed") || message.includes("Session closed") || message.includes("Network.setCacheDisabled") || message.includes("Protocol error") || message.includes("storageState timed out");
157
157
  }
158
158
  function isBenignTeardownError(error) {
159
- if (!error) return false;
159
+ if (!error) return true;
160
+ if (typeof error === "object" && Object.keys(error).length === 0 && !(error instanceof Error)) {
161
+ return true;
162
+ }
160
163
  const message = error instanceof Error ? error.message : typeof error === "object" && "message" in error ? String(error.message) : String(error);
161
164
  return isPlaywrightAlreadyClosedMessage(message);
162
165
  }
@@ -5557,6 +5560,15 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
5557
5560
  await existingInit;
5558
5561
  return;
5559
5562
  }
5563
+ let resolveInit;
5564
+ let rejectInit;
5565
+ const inFlightDeferred = new Promise((res, rej) => {
5566
+ resolveInit = res;
5567
+ rejectInit = rej;
5568
+ });
5569
+ inFlightDeferred.catch(() => {
5570
+ });
5571
+ inFlightAccountInits.set(account.id, inFlightDeferred);
5560
5572
  const initPromise = (async () => {
5561
5573
  const release = await acquireAccountMutex(
5562
5574
  account.id,
@@ -5841,9 +5853,12 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
5841
5853
  release();
5842
5854
  }
5843
5855
  })();
5844
- inFlightAccountInits.set(account.id, initPromise);
5845
5856
  try {
5846
5857
  await initPromise;
5858
+ resolveInit();
5859
+ } catch (error) {
5860
+ rejectInit(error);
5861
+ throw error;
5847
5862
  } finally {
5848
5863
  inFlightAccountInits.delete(account.id);
5849
5864
  }
@@ -8718,6 +8733,10 @@ function stripThinkingSuffix(model) {
8718
8733
  reasoningMode: "thinking"
8719
8734
  };
8720
8735
  }
8736
+ const lower = normalizedModel.toLowerCase();
8737
+ if (lower.includes("omni-flash") || lower.includes("turbo")) {
8738
+ return { baseModel: normalizedModel, enableThinking: false, reasoningMode: "fast" };
8739
+ }
8721
8740
  return { baseModel: normalizedModel, enableThinking: true, reasoningMode: "auto" };
8722
8741
  }
8723
8742
  function mapKnownModelAlias(model) {
@@ -9127,6 +9146,14 @@ async function recoverBaxiaCaptcha(accountId, label, options = {}) {
9127
9146
  );
9128
9147
  const challengeUrl = options.challengeBody ? extractBaxiaChallengeUrl(options.challengeBody, config.qwen.baseUrl) : null;
9129
9148
  activeCaptchaRecoveries.add(accountId);
9149
+ try {
9150
+ const { abortActiveBrowserStreamsForAccount: abortActiveBrowserStreamsForAccount2 } = await Promise.resolve().then(() => (init_qwen(), qwen_exports));
9151
+ abortActiveBrowserStreamsForAccount2(
9152
+ accountId,
9153
+ "Account stream interrupted by captcha challenge recovery"
9154
+ );
9155
+ } catch {
9156
+ }
9130
9157
  try {
9131
9158
  const solved = await withAccountPage(
9132
9159
  accountId,
@@ -11703,6 +11730,7 @@ __export(qwen_exports, {
11703
11730
  QwenUpstreamUnavailableError: () => QwenUpstreamUnavailableError,
11704
11731
  RetryableQwenStreamError: () => RetryableQwenStreamError,
11705
11732
  STREAM_SLOT_MAX_HOLD_MS: () => STREAM_SLOT_MAX_HOLD_MS,
11733
+ abortActiveBrowserStreamsForAccount: () => abortActiveBrowserStreamsForAccount,
11706
11734
  acquireAccountStreamLock: () => acquireAccountStreamLock,
11707
11735
  acquireNewQwenChatSession: () => acquireNewQwenChatSession,
11708
11736
  buildCapturedQwenHeaders: () => buildCapturedQwenHeaders,
@@ -11749,6 +11777,27 @@ __export(qwen_exports, {
11749
11777
  });
11750
11778
  import crypto7 from "crypto";
11751
11779
  import { v4 as uuidv43 } from "uuid";
11780
+ function abortActiveBrowserStreamsForAccount(accountId, reason = "Account browser stream interrupted (page context unloaded)") {
11781
+ let count = 0;
11782
+ for (const [requestId, state] of browserStreamStates) {
11783
+ if (state.accountId === accountId && !state.done) {
11784
+ count++;
11785
+ handleBrowserStreamEvent(requestId, {
11786
+ type: "error",
11787
+ message: reason,
11788
+ errorName: "AbortError"
11789
+ });
11790
+ }
11791
+ }
11792
+ if (count > 0) {
11793
+ logger.debug("[Qwen] Aborted active browser streams for account", {
11794
+ accountId,
11795
+ count,
11796
+ reason
11797
+ });
11798
+ }
11799
+ return count;
11800
+ }
11752
11801
  async function registerBrowserContextStreamBinding(context) {
11753
11802
  if (browserStreamBindingContexts.has(context)) return;
11754
11803
  browserStreamBindingContexts.add(context);
@@ -12443,6 +12492,7 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
12443
12492
  }
12444
12493
  const requestId = uuidv43();
12445
12494
  const state = {
12495
+ accountId,
12446
12496
  chunks: [],
12447
12497
  done: false,
12448
12498
  error: null,
@@ -12459,10 +12509,15 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
12459
12509
  let settled = false;
12460
12510
  let abortListener;
12461
12511
  let cancelPromise;
12512
+ let pageCleanup;
12462
12513
  const cleanup = () => {
12463
12514
  if (settled) return;
12464
12515
  settled = true;
12465
12516
  if (abortListener) signal.removeEventListener("abort", abortListener);
12517
+ try {
12518
+ pageCleanup?.();
12519
+ } catch {
12520
+ }
12466
12521
  browserStreamStates.delete(requestId);
12467
12522
  };
12468
12523
  const cancel = () => {
@@ -12496,6 +12551,35 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
12496
12551
  accountId,
12497
12552
  async (page) => {
12498
12553
  await ensureBrowserStreamBinding(page);
12554
+ if (typeof page.on === "function") {
12555
+ const onFrameNavigated = (frame) => {
12556
+ if (!settled && typeof page.mainFrame === "function" && frame === page.mainFrame()) {
12557
+ handleBrowserStreamEvent(requestId, {
12558
+ type: "error",
12559
+ message: "Qwen browser page navigated during stream (session interrupted)",
12560
+ errorName: "AbortError"
12561
+ });
12562
+ }
12563
+ };
12564
+ const onPageClosed = () => {
12565
+ if (!settled) {
12566
+ handleBrowserStreamEvent(requestId, {
12567
+ type: "error",
12568
+ message: "Qwen browser page closed during stream",
12569
+ errorName: "AbortError"
12570
+ });
12571
+ }
12572
+ };
12573
+ page.on("framenavigated", onFrameNavigated);
12574
+ page.on("close", onPageClosed);
12575
+ pageCleanup = () => {
12576
+ try {
12577
+ page.off?.("framenavigated", onFrameNavigated);
12578
+ page.off?.("close", onPageClosed);
12579
+ } catch {
12580
+ }
12581
+ };
12582
+ }
12499
12583
  if (config.captcha.enabled) {
12500
12584
  captchaWatcher = startBaxiaCaptchaWatcher(
12501
12585
  page,
@@ -13680,7 +13764,7 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
13680
13764
  }
13681
13765
  });
13682
13766
  }
13683
- const baseTimeoutMs = enableThinking ? config.timeouts.reasoningModelTimeout : config.timeouts.idleStreamTimeout;
13767
+ const baseTimeoutMs = config.timeouts.idleStreamTimeout;
13684
13768
  const dynamicIdleTimeoutMs = computeDynamicIdleTimeout({
13685
13769
  enableThinking,
13686
13770
  parallelEscape: options?.parallelEscape,
@@ -13696,9 +13780,10 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
13696
13780
  dynamicTimeout: dynamicIdleTimeoutMs
13697
13781
  });
13698
13782
  const firstChunkDeadlineMs = enableThinking ? Math.max(
13783
+ config.timeouts.reasoningModelTimeout,
13699
13784
  config.timeouts.firstChunkTimeout,
13700
13785
  config.timeouts.timeToFirstByte
13701
- ) : void 0;
13786
+ ) : Math.max(config.timeouts.firstChunkTimeout, config.timeouts.timeToFirstByte);
13702
13787
  return addIdleTimeoutToStream(
13703
13788
  stream,
13704
13789
  controller2,
@@ -21513,7 +21598,7 @@ async function processStreamingResponse(params) {
21513
21598
  let chatInProgressSameAccountRetries = 0;
21514
21599
  let antiBotSameAccountRetries = 0;
21515
21600
  const totalConfiguredAccounts = loadAccounts().length;
21516
- let quotaRotationsLeft = Math.min(Math.max(1, totalConfiguredAccounts - 1), 6);
21601
+ let quotaRotationsLeft = Math.max(1, totalConfiguredAccounts - 1);
21517
21602
  let streamCompletedOk = false;
21518
21603
  let stoppedByToolCap = false;
21519
21604
  const runDisconnectTeardown = () => {
@@ -22938,7 +23023,7 @@ Please retry your tool call(s) with correct JSON and valid tool names from the a
22938
23023
  stack: err.stack?.split("\n").slice(0, 3).join(" | "),
22939
23024
  completionId
22940
23025
  };
22941
- if (retryable) {
23026
+ if (retryable || isNetworkLikeError(err) || isAbortError(err)) {
22942
23027
  if (activeAccountId && isNetworkLikeError(err)) {
22943
23028
  noteMidStreamNetworkFailure(activeAccountId);
22944
23029
  }
@@ -22950,28 +23035,32 @@ Please retry your tool call(s) with correct JSON and valid tool names from the a
22950
23035
  logger.error("[Chat] Stream callback error", errorDetails);
22951
23036
  }
22952
23037
  try {
22953
- const userFriendlyNotice = `
23038
+ const isNetwork = isNetworkLikeError(err);
23039
+ const isModeration = normalizedErrorCode === "data_inspection_failed" || normalizedErrorCode === "fail_sys_user_validate" || err.message?.toLowerCase().includes("content moderation") || err.message?.toLowerCase().includes("security verification");
23040
+ if (!isNetwork && isModeration) {
23041
+ const userFriendlyNotice = `
22954
23042
 
22955
- \u26A0\uFE0F **[Qwen Security / Erro]** ${err.message || "A resposta foi interrompida pelo Qwen."}
23043
+ \u26A0\uFE0F **[Qwen Modera\xE7\xE3o / Seguran\xE7a]** ${err.message || "A resposta foi interrompida pelas pol\xEDticas de seguran\xE7a do Qwen."}
22956
23044
 
22957
23045
  `;
22958
- const errorDelta = {
22959
- id: completionId,
22960
- object: "chat.completion.chunk",
22961
- created: Math.floor(Date.now() / 1e3),
22962
- model: body.model,
22963
- choices: [
22964
- {
22965
- index: 0,
22966
- delta: { content: userFriendlyNotice },
22967
- logprobs: null,
22968
- finish_reason: "stop"
22969
- }
22970
- ]
22971
- };
22972
- await errorStream.write(`data: ${JSON.stringify(errorDelta)}
23046
+ const errorDelta = {
23047
+ id: completionId,
23048
+ object: "chat.completion.chunk",
23049
+ created: Math.floor(Date.now() / 1e3),
23050
+ model: body.model,
23051
+ choices: [
23052
+ {
23053
+ index: 0,
23054
+ delta: { content: userFriendlyNotice },
23055
+ logprobs: null,
23056
+ finish_reason: "content_filter"
23057
+ }
23058
+ ]
23059
+ };
23060
+ await errorStream.write(`data: ${JSON.stringify(errorDelta)}
22973
23061
 
22974
23062
  `);
23063
+ }
22975
23064
  await errorStream.write(
22976
23065
  `data: ${JSON.stringify({
22977
23066
  error: {
@@ -27004,7 +27093,7 @@ function getAppVersion() {
27004
27093
  }
27005
27094
  } catch {
27006
27095
  }
27007
- cachedVersion = "v1.6.5";
27096
+ cachedVersion = "v1.6.7";
27008
27097
  return cachedVersion;
27009
27098
  }
27010
27099
  var cachedVersion;
package/dist/login.js CHANGED
@@ -1108,7 +1108,10 @@ function isPlaywrightAlreadyClosedMessage(message) {
1108
1108
  return message.includes("Target page, context or browser has been closed") || message.includes("Browser has been closed") || message.includes("Target closed") || message.includes("Target crashed") || message.includes("Page crashed") || message.includes("Assertion error") || message.includes("Cannot find parent object") || message.includes("Connection closed") || message.includes("session closed") || message.includes("Session closed") || message.includes("Network.setCacheDisabled") || message.includes("Protocol error") || message.includes("storageState timed out");
1109
1109
  }
1110
1110
  function isBenignTeardownError(error) {
1111
- if (!error) return false;
1111
+ if (!error) return true;
1112
+ if (typeof error === "object" && Object.keys(error).length === 0 && !(error instanceof Error)) {
1113
+ return true;
1114
+ }
1112
1115
  const message = error instanceof Error ? error.message : typeof error === "object" && "message" in error ? String(error.message) : String(error);
1113
1116
  return isPlaywrightAlreadyClosedMessage(message);
1114
1117
  }
@@ -4420,6 +4423,15 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
4420
4423
  await existingInit;
4421
4424
  return;
4422
4425
  }
4426
+ let resolveInit;
4427
+ let rejectInit;
4428
+ const inFlightDeferred = new Promise((res, rej) => {
4429
+ resolveInit = res;
4430
+ rejectInit = rej;
4431
+ });
4432
+ inFlightDeferred.catch(() => {
4433
+ });
4434
+ inFlightAccountInits.set(account.id, inFlightDeferred);
4423
4435
  const initPromise = (async () => {
4424
4436
  const release = await acquireAccountMutex(
4425
4437
  account.id,
@@ -4704,9 +4716,12 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
4704
4716
  release();
4705
4717
  }
4706
4718
  })();
4707
- inFlightAccountInits.set(account.id, initPromise);
4708
4719
  try {
4709
4720
  await initPromise;
4721
+ resolveInit();
4722
+ } catch (error) {
4723
+ rejectInit(error);
4724
+ throw error;
4710
4725
  } finally {
4711
4726
  inFlightAccountInits.delete(account.id);
4712
4727
  }
@@ -2825,7 +2825,10 @@ function isPlaywrightAlreadyClosedMessage(message) {
2825
2825
  return message.includes("Target page, context or browser has been closed") || message.includes("Browser has been closed") || message.includes("Target closed") || message.includes("Target crashed") || message.includes("Page crashed") || message.includes("Assertion error") || message.includes("Cannot find parent object") || message.includes("Connection closed") || message.includes("session closed") || message.includes("Session closed") || message.includes("Network.setCacheDisabled") || message.includes("Protocol error") || message.includes("storageState timed out");
2826
2826
  }
2827
2827
  function isBenignTeardownError(error) {
2828
- if (!error) return false;
2828
+ if (!error) return true;
2829
+ if (typeof error === "object" && Object.keys(error).length === 0 && !(error instanceof Error)) {
2830
+ return true;
2831
+ }
2829
2832
  const message = error instanceof Error ? error.message : typeof error === "object" && "message" in error ? String(error.message) : String(error);
2830
2833
  return isPlaywrightAlreadyClosedMessage(message);
2831
2834
  }
@@ -3851,6 +3854,15 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
3851
3854
  await existingInit;
3852
3855
  return;
3853
3856
  }
3857
+ let resolveInit;
3858
+ let rejectInit;
3859
+ const inFlightDeferred = new Promise((res, rej) => {
3860
+ resolveInit = res;
3861
+ rejectInit = rej;
3862
+ });
3863
+ inFlightDeferred.catch(() => {
3864
+ });
3865
+ inFlightAccountInits.set(account.id, inFlightDeferred);
3854
3866
  const initPromise = (async () => {
3855
3867
  const release = await acquireAccountMutex(
3856
3868
  account.id,
@@ -4135,9 +4147,12 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
4135
4147
  release();
4136
4148
  }
4137
4149
  })();
4138
- inFlightAccountInits.set(account.id, initPromise);
4139
4150
  try {
4140
4151
  await initPromise;
4152
+ resolveInit();
4153
+ } catch (error) {
4154
+ rejectInit(error);
4155
+ throw error;
4141
4156
  } finally {
4142
4157
  inFlightAccountInits.delete(account.id);
4143
4158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
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": {
@@ -76,6 +76,12 @@ export function stripThinkingSuffix(model: string): {
76
76
  };
77
77
  }
78
78
 
79
+ // Default for non-thinking models (omni-flash, turbo)
80
+ const lower = normalizedModel.toLowerCase();
81
+ if (lower.includes("omni-flash") || lower.includes("turbo")) {
82
+ return { baseModel: normalizedModel, enableThinking: false, reasoningMode: "fast" };
83
+ }
84
+
79
85
  // Default: auto thinking (Qwen decides)
80
86
  return { baseModel: normalizedModel, enableThinking: true, reasoningMode: "auto" };
81
87
  }
@@ -27,7 +27,14 @@ export function isPlaywrightAlreadyClosedMessage(message: string): boolean {
27
27
  }
28
28
 
29
29
  export function isBenignTeardownError(error: unknown): boolean {
30
- if (!error) return false;
30
+ if (!error) return true;
31
+ if (
32
+ typeof error === "object" &&
33
+ Object.keys(error).length === 0 &&
34
+ !(error instanceof Error)
35
+ ) {
36
+ return true;
37
+ }
31
38
  const message =
32
39
  error instanceof Error
33
40
  ? error.message
@@ -17,6 +17,6 @@ export function getAppVersion(): string {
17
17
  }
18
18
  }
19
19
  } catch {}
20
- cachedVersion = "v1.6.5";
20
+ cachedVersion = "v1.6.7";
21
21
  return cachedVersion;
22
22
  }
@@ -937,7 +937,7 @@ export async function processStreamingResponse(
937
937
  let chatInProgressSameAccountRetries = 0;
938
938
  let antiBotSameAccountRetries = 0;
939
939
  const totalConfiguredAccounts = loadAccounts().length;
940
- let quotaRotationsLeft = Math.min(Math.max(1, totalConfiguredAccounts - 1), 6);
940
+ let quotaRotationsLeft = Math.max(1, totalConfiguredAccounts - 1);
941
941
  // Set when the terminal [DONE] reached the client. The `| recovered`
942
942
  // suffix on Stream done must only appear for attempts that COMPLETED after
943
943
  // a mid-stream retry, not for failed attempts that consumed retries.
@@ -2819,7 +2819,7 @@ export async function processStreamingResponse(
2819
2819
  completionId,
2820
2820
  };
2821
2821
 
2822
- if (retryable) {
2822
+ if (retryable || isNetworkLikeError(err) || isAbortError(err)) {
2823
2823
  if (activeAccountId && isNetworkLikeError(err)) {
2824
2824
  noteMidStreamNetworkFailure(activeAccountId);
2825
2825
  }
@@ -2831,28 +2831,41 @@ export async function processStreamingResponse(
2831
2831
  logger.error("[Chat] Stream callback error", errorDetails);
2832
2832
  }
2833
2833
  // The HTTP response is already committed at this point.
2834
- // 1. Emit an explicit assistant message delta so CLI/TUI clients that only
2835
- // listen for choices[].delta.content (e.g. OpenCode, Claude Code, etc.)
2836
- // render the error visibly in the chat instead of appearing blank/frozen.
2837
- // 2. Emit a terminal OpenAI-compatible SSE error event + [DONE].
2834
+ // 1. Emit an explicit assistant message delta ONLY if this was an upstream content moderation /
2835
+ // policy violation (with finish_reason: "content_filter"), so the user understands why the
2836
+ // prompt was blocked.
2837
+ // 2. For network/transient/quota errors, NEVER emit fake assistant content with finish_reason: "stop".
2838
+ // Doing so masks network failures as successful AI answers, preventing client agent retries and
2839
+ // poisoning chat history with error banners.
2840
+ // 3. Emit a terminal OpenAI-compatible SSE error event + [DONE].
2838
2841
  try {
2839
- const userFriendlyNotice = `\n\n⚠️ **[Qwen Security / Erro]** ${err.message || "A resposta foi interrompida pelo Qwen."}\n\n`;
2840
- const errorDelta = {
2841
- id: completionId,
2842
- object: "chat.completion.chunk",
2843
- created: Math.floor(Date.now() / 1000),
2844
- model: body.model,
2845
- choices: [
2846
- {
2847
- index: 0,
2848
- delta: { content: userFriendlyNotice },
2849
- logprobs: null,
2850
- finish_reason: "stop",
2851
- },
2852
- ],
2853
- };
2842
+ const isNetwork = isNetworkLikeError(err);
2843
+ const isModeration =
2844
+ normalizedErrorCode === "data_inspection_failed" ||
2845
+ normalizedErrorCode === "fail_sys_user_validate" ||
2846
+ err.message?.toLowerCase().includes("content moderation") ||
2847
+ err.message?.toLowerCase().includes("security verification");
2848
+
2849
+ if (!isNetwork && isModeration) {
2850
+ const userFriendlyNotice = `\n\n⚠️ **[Qwen Moderação / Segurança]** ${err.message || "A resposta foi interrompida pelas políticas de segurança do Qwen."}\n\n`;
2851
+ const errorDelta = {
2852
+ id: completionId,
2853
+ object: "chat.completion.chunk",
2854
+ created: Math.floor(Date.now() / 1000),
2855
+ model: body.model,
2856
+ choices: [
2857
+ {
2858
+ index: 0,
2859
+ delta: { content: userFriendlyNotice },
2860
+ logprobs: null,
2861
+ finish_reason: "content_filter",
2862
+ },
2863
+ ],
2864
+ };
2865
+
2866
+ await errorStream.write(`data: ${JSON.stringify(errorDelta)}\n\n`);
2867
+ }
2854
2868
 
2855
- await errorStream.write(`data: ${JSON.stringify(errorDelta)}\n\n`);
2856
2869
  await errorStream.write(
2857
2870
  `data: ${JSON.stringify({
2858
2871
  error: {
@@ -125,6 +125,13 @@ export async function recoverBaxiaCaptcha(
125
125
  : null;
126
126
 
127
127
  activeCaptchaRecoveries.add(accountId);
128
+ try {
129
+ const { abortActiveBrowserStreamsForAccount } = await import("./qwen.ts");
130
+ abortActiveBrowserStreamsForAccount(
131
+ accountId,
132
+ "Account stream interrupted by captcha challenge recovery",
133
+ );
134
+ } catch {}
128
135
  try {
129
136
  // recoverOnTimeout: false ensures that if the captcha solve times out,
130
137
  // withAccountPage does NOT destructively kill the browser context, which
@@ -1529,6 +1529,16 @@ export async function initPlaywrightForAccount(
1529
1529
  return;
1530
1530
  }
1531
1531
 
1532
+ let resolveInit!: () => void;
1533
+ let rejectInit!: (err: any) => void;
1534
+ const inFlightDeferred = new Promise<void>((res, rej) => {
1535
+ resolveInit = res;
1536
+ rejectInit = rej;
1537
+ });
1538
+ // Prevent UnhandledRejection if rejectInit is called when no concurrent callers are awaiting
1539
+ inFlightDeferred.catch(() => {});
1540
+ inFlightAccountInits.set(account.id, inFlightDeferred);
1541
+
1532
1542
  const initPromise = (async () => {
1533
1543
  const release = await acquireAccountMutex(
1534
1544
  account.id,
@@ -1536,10 +1546,10 @@ export async function initPlaywrightForAccount(
1536
1546
  Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, 120_000),
1537
1547
  );
1538
1548
  try {
1539
- // Double-check after acquiring lock
1540
- if (accountPages.has(account.id)) {
1541
- return;
1542
- }
1549
+ // Double-check after acquiring lock
1550
+ if (accountPages.has(account.id)) {
1551
+ return;
1552
+ }
1543
1553
 
1544
1554
  // If a context limit is configured, make room by closing idle contexts.
1545
1555
  await evictIdlePlaywrightContextsToLimit().catch(() => {});
@@ -1866,9 +1876,12 @@ export async function initPlaywrightForAccount(
1866
1876
  }
1867
1877
  })();
1868
1878
 
1869
- inFlightAccountInits.set(account.id, initPromise);
1870
1879
  try {
1871
1880
  await initPromise;
1881
+ resolveInit();
1882
+ } catch (error) {
1883
+ rejectInit(error);
1884
+ throw error;
1872
1885
  } finally {
1873
1886
  inFlightAccountInits.delete(account.id);
1874
1887
  }
@@ -117,6 +117,7 @@ interface BrowserStreamMetadata {
117
117
  }
118
118
 
119
119
  interface BrowserStreamState {
120
+ accountId?: string;
120
121
  chunks: Uint8Array[];
121
122
  done: boolean;
122
123
  error: Error | null;
@@ -128,6 +129,36 @@ const browserStreamStates = new Map<string, BrowserStreamState>();
128
129
  const browserStreamBindingPages = new WeakSet<object>();
129
130
  const browserStreamBindingContexts = new WeakSet<object>();
130
131
 
132
+ /**
133
+ * Abort all active browser streams for an account immediately.
134
+ * Called when an account page is navigated by captcha recovery, closed, or reset,
135
+ * so in-flight streams fail fast (retryable) instead of stalling for minutes.
136
+ */
137
+ export function abortActiveBrowserStreamsForAccount(
138
+ accountId: string,
139
+ reason = "Account browser stream interrupted (page context unloaded)",
140
+ ): number {
141
+ let count = 0;
142
+ for (const [requestId, state] of browserStreamStates) {
143
+ if (state.accountId === accountId && !state.done) {
144
+ count++;
145
+ handleBrowserStreamEvent(requestId, {
146
+ type: "error",
147
+ message: reason,
148
+ errorName: "AbortError",
149
+ });
150
+ }
151
+ }
152
+ if (count > 0) {
153
+ logger.debug("[Qwen] Aborted active browser streams for account", {
154
+ accountId,
155
+ count,
156
+ reason,
157
+ });
158
+ }
159
+ return count;
160
+ }
161
+
131
162
  export async function registerBrowserContextStreamBinding(
132
163
  context: BrowserContext,
133
164
  ): Promise<void> {
@@ -1263,6 +1294,7 @@ async function createQwenBrowserResponse(
1263
1294
 
1264
1295
  const requestId = uuidv4();
1265
1296
  const state: BrowserStreamState = {
1297
+ accountId,
1266
1298
  chunks: [],
1267
1299
  done: false,
1268
1300
  error: null,
@@ -1283,10 +1315,14 @@ async function createQwenBrowserResponse(
1283
1315
  let settled = false;
1284
1316
  let abortListener: (() => void) | undefined;
1285
1317
  let cancelPromise: Promise<void> | undefined;
1318
+ let pageCleanup: (() => void) | undefined;
1286
1319
  const cleanup = () => {
1287
1320
  if (settled) return;
1288
1321
  settled = true;
1289
1322
  if (abortListener) signal.removeEventListener("abort", abortListener);
1323
+ try {
1324
+ pageCleanup?.();
1325
+ } catch {}
1290
1326
  browserStreamStates.delete(requestId);
1291
1327
  };
1292
1328
  const cancel = () => {
@@ -1327,6 +1363,34 @@ async function createQwenBrowserResponse(
1327
1363
  accountId,
1328
1364
  async (page) => {
1329
1365
  await ensureBrowserStreamBinding(page);
1366
+ if (typeof (page as any).on === "function") {
1367
+ const onFrameNavigated = (frame: any) => {
1368
+ if (!settled && typeof (page as any).mainFrame === "function" && frame === (page as any).mainFrame()) {
1369
+ handleBrowserStreamEvent(requestId, {
1370
+ type: "error",
1371
+ message: "Qwen browser page navigated during stream (session interrupted)",
1372
+ errorName: "AbortError",
1373
+ });
1374
+ }
1375
+ };
1376
+ const onPageClosed = () => {
1377
+ if (!settled) {
1378
+ handleBrowserStreamEvent(requestId, {
1379
+ type: "error",
1380
+ message: "Qwen browser page closed during stream",
1381
+ errorName: "AbortError",
1382
+ });
1383
+ }
1384
+ };
1385
+ (page as any).on("framenavigated", onFrameNavigated);
1386
+ (page as any).on("close", onPageClosed);
1387
+ pageCleanup = () => {
1388
+ try {
1389
+ (page as any).off?.("framenavigated", onFrameNavigated);
1390
+ (page as any).off?.("close", onPageClosed);
1391
+ } catch {}
1392
+ };
1393
+ }
1330
1394
  if (config.captcha.enabled) {
1331
1395
  captchaWatcher = startBaxiaCaptchaWatcher(
1332
1396
  page,
@@ -3011,13 +3075,9 @@ async function createQwenStreamInternal(
3011
3075
  }
3012
3076
 
3013
3077
  // Dynamic idle timeout based on model type and payload size
3014
- // Reasoning models (thinking enabled): use REASONING_MODEL_TIMEOUT as base
3015
- // Non-reasoning models: use IDLE_STREAM_TIMEOUT as base
3016
- // Both add 30s per MB of payload. Parallel-escape streams get a tight cap
3017
- // ONLY when non-thinking (see computeDynamicIdleTimeout).
3018
- const baseTimeoutMs = enableThinking
3019
- ? config.timeouts.reasoningModelTimeout
3020
- : config.timeouts.idleStreamTimeout;
3078
+ // Base timeout for gaps between chunks is governed by IDLE_STREAM_TIMEOUT (default 60s)
3079
+ // plus dynamic payload scaling (30s per MB).
3080
+ const baseTimeoutMs = config.timeouts.idleStreamTimeout;
3021
3081
  const dynamicIdleTimeoutMs = computeDynamicIdleTimeout({
3022
3082
  enableThinking,
3023
3083
  parallelEscape: options?.parallelEscape,
@@ -3034,15 +3094,15 @@ async function createQwenStreamInternal(
3034
3094
  dynamicTimeout: dynamicIdleTimeoutMs,
3035
3095
  });
3036
3096
 
3037
- // Thinking models idle at 600s — fine for gaps AFTER data flows, but a
3038
- // stream that produced NOTHING in the first-chunk window is dead. Fail
3039
- // fast (retryable) so the account slot is not held for 10 minutes.
3097
+ // Thinking models legitimately pause before first chunk (up to reasoningModelTimeout).
3098
+ // After the first chunk, the normal dynamicIdleTimeoutMs (60s+) governs chunk gaps.
3040
3099
  const firstChunkDeadlineMs = enableThinking
3041
3100
  ? Math.max(
3101
+ config.timeouts.reasoningModelTimeout,
3042
3102
  config.timeouts.firstChunkTimeout,
3043
3103
  config.timeouts.timeToFirstByte,
3044
3104
  )
3045
- : undefined;
3105
+ : Math.max(config.timeouts.firstChunkTimeout, config.timeouts.timeToFirstByte);
3046
3106
 
3047
3107
  return addIdleTimeoutToStream(
3048
3108
  stream,