qwenproxy-cli 1.6.4 → 1.6.5

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/dist/index.js CHANGED
@@ -2816,6 +2816,7 @@ var init_mutex = __esm({
2816
2816
  locked = false;
2817
2817
  lockedAt = 0;
2818
2818
  lockedByKey = "";
2819
+ epoch = 0;
2819
2820
  async acquire(timeoutMs = 3e5, key = "") {
2820
2821
  const holdLimitMs = this.maxHoldMs;
2821
2822
  if (this.locked && Date.now() - this.lockedAt > holdLimitMs) {
@@ -2826,6 +2827,7 @@ var init_mutex = __esm({
2826
2827
  this.locked = false;
2827
2828
  this.lockedAt = 0;
2828
2829
  this.lockedByKey = "";
2830
+ this.epoch++;
2829
2831
  }
2830
2832
  if (!this.locked) {
2831
2833
  this.locked = true;
@@ -2864,10 +2866,12 @@ var init_mutex = __esm({
2864
2866
  }
2865
2867
  }
2866
2868
  createRelease() {
2869
+ const epoch = this.epoch;
2867
2870
  let released = false;
2868
2871
  return () => {
2869
2872
  if (released) return;
2870
2873
  released = true;
2874
+ if (epoch !== this.epoch) return;
2871
2875
  this.release();
2872
2876
  };
2873
2877
  }
@@ -4016,6 +4020,49 @@ var init_waf_isolation = __esm({
4016
4020
  }
4017
4021
  });
4018
4022
 
4023
+ // src/utils/timezone.ts
4024
+ function formatAsciiTimezone(date = /* @__PURE__ */ new Date()) {
4025
+ const dayName = DAYS[date.getDay()];
4026
+ const monthName = MONTHS[date.getMonth()];
4027
+ const day = String(date.getDate()).padStart(2, "0");
4028
+ const year = date.getFullYear();
4029
+ const hours = String(date.getHours()).padStart(2, "0");
4030
+ const minutes = String(date.getMinutes()).padStart(2, "0");
4031
+ const seconds = String(date.getSeconds()).padStart(2, "0");
4032
+ const offsetMinutes = -date.getTimezoneOffset();
4033
+ const sign = offsetMinutes >= 0 ? "+" : "-";
4034
+ const absOffset = Math.abs(offsetMinutes);
4035
+ const offsetHours = String(Math.floor(absOffset / 60)).padStart(2, "0");
4036
+ const offsetMins = String(absOffset % 60).padStart(2, "0");
4037
+ const tzOffset = `GMT${sign}${offsetHours}${offsetMins}`;
4038
+ return `${dayName} ${monthName} ${day} ${year} ${hours}:${minutes}:${seconds} ${tzOffset}`;
4039
+ }
4040
+ function sanitizeHeaderForFetch(val) {
4041
+ if (typeof val !== "string") return "";
4042
+ return val.replace(/[^\x00-\xFF]/g, "");
4043
+ }
4044
+ var DAYS, MONTHS;
4045
+ var init_timezone = __esm({
4046
+ "src/utils/timezone.ts"() {
4047
+ "use strict";
4048
+ DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
4049
+ MONTHS = [
4050
+ "Jan",
4051
+ "Feb",
4052
+ "Mar",
4053
+ "Apr",
4054
+ "May",
4055
+ "Jun",
4056
+ "Jul",
4057
+ "Aug",
4058
+ "Sep",
4059
+ "Oct",
4060
+ "Nov",
4061
+ "Dec"
4062
+ ];
4063
+ }
4064
+ });
4065
+
4019
4066
  // src/services/qwen-headers.ts
4020
4067
  import { v4 as uuidv4 } from "uuid";
4021
4068
  function getSystemAcceptLanguage(customLocale) {
@@ -4059,8 +4106,8 @@ function buildQwenRequestHeaders(opts) {
4059
4106
  "bx-v": opts.bxV || "2.5.37",
4060
4107
  source: "web",
4061
4108
  version: opts.version || getQwenWebVersion(),
4062
- timezone: opts.extra?.timezone || opts.extra?.Timezone || (/* @__PURE__ */ new Date()).toString().split(" (")[0],
4063
- Timezone: opts.extra?.Timezone || opts.extra?.timezone || (/* @__PURE__ */ new Date()).toString().split(" (")[0],
4109
+ timezone: opts.extra?.timezone || opts.extra?.Timezone || formatAsciiTimezone(),
4110
+ Timezone: opts.extra?.Timezone || opts.extra?.timezone || formatAsciiTimezone(),
4064
4111
  // Use the real browser client-hints when captured (anti-hardcoded); fall
4065
4112
  // back to the static fingerprint otherwise.
4066
4113
  "sec-ch-ua": opts.secChUa || `"Google Chrome";v="${getChromeMajor()}", "Chromium";v="${getChromeMajor()}", "Not.A/Brand";v="99"`,
@@ -4087,6 +4134,7 @@ var init_qwen_headers = __esm({
4087
4134
  init_qwen_url();
4088
4135
  init_config();
4089
4136
  init_fingerprint();
4137
+ init_timezone();
4090
4138
  dynamicWebVersion = null;
4091
4139
  QWEN_WEB_VERSION = config.qwen.webVersion;
4092
4140
  DEFAULT_QWEN_USER_AGENT = getDefaultQwenUserAgent(151);
@@ -5723,6 +5771,22 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
5723
5771
  }
5724
5772
  }
5725
5773
  }
5774
+ const currentCache = getHeaderCache(account.id);
5775
+ const currentCookie = currentCache?.headers?.cookie || "";
5776
+ if (currentCookie && isTokenExpiringSoon(currentCookie, 5)) {
5777
+ try {
5778
+ const refreshRes = await refreshAccountToken(account.id);
5779
+ if (refreshRes.success) {
5780
+ console.log(
5781
+ `\u26A1 [Playwright] Silently refreshed expired token restored from DB for ${maskEmail(account.email)}`
5782
+ );
5783
+ }
5784
+ } catch (err) {
5785
+ console.warn(
5786
+ `\u26A0\uFE0F [Playwright] Failed to proactively refresh expired token from DB for ${maskEmail(account.email)}: ${err?.message || err}`
5787
+ );
5788
+ }
5789
+ }
5726
5790
  return;
5727
5791
  }
5728
5792
  const cookies = await acctContext.cookies();
@@ -6010,7 +6074,7 @@ async function startEmailOtpLogin(email, options = {}) {
6010
6074
  email: normalizedEmail,
6011
6075
  requestId,
6012
6076
  version: getQwenWebVersion() || "0.3.12",
6013
- timezone: (/* @__PURE__ */ new Date()).toString().split(" (")[0]
6077
+ timezone: formatAsciiTimezone()
6014
6078
  }
6015
6079
  ).catch((err) => ({ ok: false, error: err?.message || String(err) }));
6016
6080
  if (!evalRes.ok || evalRes.data?.success !== true) {
@@ -6105,7 +6169,7 @@ async function verifyEmailOtpLogin(sessionId, code) {
6105
6169
  code: cleanCode,
6106
6170
  requestId,
6107
6171
  version: getQwenWebVersion() || "0.3.12",
6108
- timezone: (/* @__PURE__ */ new Date()).toString().split(" (")[0]
6172
+ timezone: formatAsciiTimezone()
6109
6173
  }
6110
6174
  ).catch((err) => ({ ok: false, error: err?.message || String(err) }));
6111
6175
  if (!evalRes.ok || evalRes.data?.success !== true || !evalRes.data?.data) {
@@ -6386,7 +6450,7 @@ async function loginViaApi(page, email, password) {
6386
6450
  const signinUrl = "https://auth.qwen.ai/api/v2/auths/signin";
6387
6451
  const requestId = crypto5.randomUUID();
6388
6452
  const evalRes = await page.evaluate(
6389
- async ({ email: email2, password: password2, signinUrl: signinUrl2, requestId: requestId2 }) => {
6453
+ async ({ email: email2, password: password2, signinUrl: signinUrl2, requestId: requestId2, tz }) => {
6390
6454
  try {
6391
6455
  const response = await fetch(signinUrl2, {
6392
6456
  method: "POST",
@@ -6398,7 +6462,7 @@ async function loginViaApi(page, email, password) {
6398
6462
  source: "web",
6399
6463
  version: "0.3.11",
6400
6464
  "x-request-origin": "https://chat.qwen.ai",
6401
- timezone: (/* @__PURE__ */ new Date()).toString().split(" (")[0],
6465
+ timezone: tz,
6402
6466
  "x-request-id": requestId2
6403
6467
  },
6404
6468
  body: JSON.stringify({ email: email2, password: password2 })
@@ -6409,7 +6473,7 @@ async function loginViaApi(page, email, password) {
6409
6473
  return { ok: false, error: e.message };
6410
6474
  }
6411
6475
  },
6412
- { email, password: hashedPassword, signinUrl, requestId }
6476
+ { email, password: hashedPassword, signinUrl, requestId, tz: formatAsciiTimezone() }
6413
6477
  ).catch(() => null);
6414
6478
  let data = evalRes?.data;
6415
6479
  let signinSuccess = Boolean(
@@ -6418,7 +6482,7 @@ async function loginViaApi(page, email, password) {
6418
6482
  if (!signinSuccess && !data?.data?.code) {
6419
6483
  const fallbackUrl = qwenUrl("/api/v2/auths/signin");
6420
6484
  const fallbackRes = await page.evaluate(
6421
- async ({ email: email2, password: password2, fallbackUrl: fallbackUrl2, requestId: requestId2 }) => {
6485
+ async ({ email: email2, password: password2, fallbackUrl: fallbackUrl2, requestId: requestId2, tz }) => {
6422
6486
  try {
6423
6487
  const response = await fetch(fallbackUrl2, {
6424
6488
  method: "POST",
@@ -6429,7 +6493,7 @@ async function loginViaApi(page, email, password) {
6429
6493
  "content-type": "application/json",
6430
6494
  source: "web",
6431
6495
  version: "0.3.11",
6432
- timezone: (/* @__PURE__ */ new Date()).toString().split(" (")[0],
6496
+ timezone: tz,
6433
6497
  "x-request-id": requestId2
6434
6498
  },
6435
6499
  body: JSON.stringify({ email: email2, password: password2 })
@@ -6440,7 +6504,7 @@ async function loginViaApi(page, email, password) {
6440
6504
  return { ok: false, error: e.message };
6441
6505
  }
6442
6506
  },
6443
- { email, password: hashedPassword, fallbackUrl, requestId }
6507
+ { email, password: hashedPassword, fallbackUrl, requestId, tz: formatAsciiTimezone() }
6444
6508
  ).catch(() => null);
6445
6509
  if (fallbackRes?.data) {
6446
6510
  data = fallbackRes.data;
@@ -6772,7 +6836,7 @@ async function refreshAccountToken(accountId) {
6772
6836
  if (refreshToken) {
6773
6837
  try {
6774
6838
  const refreshResult = await page.evaluate(
6775
- async ({ rTok }) => {
6839
+ async ({ rTok, tz }) => {
6776
6840
  try {
6777
6841
  const response = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
6778
6842
  method: "GET",
@@ -6782,7 +6846,7 @@ async function refreshAccountToken(accountId) {
6782
6846
  source: "web",
6783
6847
  version: "0.3.11",
6784
6848
  "x-request-origin": "https://chat.qwen.ai",
6785
- timezone: (/* @__PURE__ */ new Date()).toString().split(" (")[0],
6849
+ timezone: tz,
6786
6850
  authorization: `Bearer ${rTok}`
6787
6851
  }
6788
6852
  });
@@ -6792,7 +6856,7 @@ async function refreshAccountToken(accountId) {
6792
6856
  return { ok: false, error: e.message };
6793
6857
  }
6794
6858
  },
6795
- { rTok: refreshToken }
6859
+ { rTok: refreshToken, tz: formatAsciiTimezone() }
6796
6860
  );
6797
6861
  const json = refreshResult?.data;
6798
6862
  if (json && json.success === true && json.data) {
@@ -6830,6 +6894,7 @@ async function refreshAccountToken(accountId) {
6830
6894
  const cache3 = getHeaderCache(accountId);
6831
6895
  cache3.headers.cookie = cookieStr;
6832
6896
  cache3.lastRefresh = Date.now();
6897
+ cookieCaches.set(accountId, { cookie: cookieStr, timestamp: Date.now() });
6833
6898
  markAccountHeadersReady(accountId);
6834
6899
  saveAuthSession2(accountId, {
6835
6900
  cookie: cookieStr,
@@ -6864,6 +6929,7 @@ async function refreshAccountToken(accountId) {
6864
6929
  const cache3 = getHeaderCache(accountId);
6865
6930
  cache3.headers.cookie = cookieStr;
6866
6931
  cache3.lastRefresh = Date.now();
6932
+ cookieCaches.set(accountId, { cookie: cookieStr, timestamp: Date.now() });
6867
6933
  markAccountHeadersReady(accountId);
6868
6934
  saveAuthSession2(accountId, {
6869
6935
  cookie: cookieStr,
@@ -7918,11 +7984,11 @@ async function keepAlivePlaywrightAccount(accountId) {
7918
7984
  const now = Date.now();
7919
7985
  const currentUrl = page.url();
7920
7986
  const lastRefreshAttempt = lastProactiveTokenRefresh.get(accountId) ?? 0;
7921
- const cookie = await getCookies(accountId);
7987
+ const cookie = await getCookies(accountId, true);
7922
7988
  if (cookie && isTokenExpiringSoon(cookie, 5) && now - lastRefreshAttempt > 6e4) {
7923
7989
  lastProactiveTokenRefresh.set(accountId, now);
7924
- console.log(
7925
- `\u{1F493} [SessionKeeper] Account ${accountId} access token expiring soon (<5m); silently refreshing...`
7990
+ logger.debug(
7991
+ `[SessionKeeper] Account ${accountId} access token expiring soon (<5m); silently refreshing...`
7926
7992
  );
7927
7993
  const res = await refreshAccountToken(accountId);
7928
7994
  if (res.success) {
@@ -7930,6 +7996,9 @@ async function keepAlivePlaywrightAccount(accountId) {
7930
7996
  touchAccountActivity(accountId);
7931
7997
  return true;
7932
7998
  }
7999
+ logger.warn(
8000
+ `[SessionKeeper] Failed to silently refresh expiring token for ${accountId}: ${res.error || "unknown error"}`
8001
+ );
7933
8002
  }
7934
8003
  const lastNavigation = lastKeepAliveNavigation.get(accountId) ?? 0;
7935
8004
  const shouldNavigate = !currentUrl.startsWith(qwenOrigin()) || now - lastNavigation > config.sessionKeeper.navigationIntervalMs;
@@ -8167,6 +8236,7 @@ var init_playwright = __esm({
8167
8236
  init_qwen_headers();
8168
8237
  init_paths();
8169
8238
  init_jwt();
8239
+ init_timezone();
8170
8240
  requireLocal = createRequire(import.meta.url);
8171
8241
  contextInitHooks = [];
8172
8242
  ACCOUNT_MUTEX_MAX_HOLD_MS = 18e4;
@@ -8180,7 +8250,7 @@ var init_playwright = __esm({
8180
8250
  headerCaches = /* @__PURE__ */ new Map();
8181
8251
  HEADER_CACHE_TTL = 24 * 60 * 60 * 1e3;
8182
8252
  HEADER_REFRESH_THRESHOLD = 0.8;
8183
- COOKIE_CACHE_TTL = 15 * 60 * 1e3;
8253
+ COOKIE_CACHE_TTL = 60 * 1e3;
8184
8254
  cookieCaches = /* @__PURE__ */ new Map();
8185
8255
  lastAccountActivity = /* @__PURE__ */ new Map();
8186
8256
  lastKeepAliveNavigation = /* @__PURE__ */ new Map();
@@ -9599,8 +9669,8 @@ function createUsageAccumulator(estimatedPromptTokens) {
9599
9669
  function applyUpstreamUsage(accumulator, candidate) {
9600
9670
  if (!candidate || typeof candidate !== "object") return;
9601
9671
  const usage = candidate;
9602
- const promptTokens = asFiniteNumber(usage.input_tokens);
9603
- const completionTokens = asFiniteNumber(usage.output_tokens);
9672
+ const promptTokens = asFiniteNumber(usage.input_tokens) ?? asFiniteNumber(usage.prompt_tokens);
9673
+ const completionTokens = asFiniteNumber(usage.output_tokens) ?? asFiniteNumber(usage.completion_tokens);
9604
9674
  const totalTokens = asFiniteNumber(usage.total_tokens);
9605
9675
  if (promptTokens !== null) {
9606
9676
  accumulator.promptTokens = promptTokens;
@@ -11632,6 +11702,8 @@ __export(qwen_exports, {
11632
11702
  QwenUpstreamError: () => QwenUpstreamError,
11633
11703
  QwenUpstreamUnavailableError: () => QwenUpstreamUnavailableError,
11634
11704
  RetryableQwenStreamError: () => RetryableQwenStreamError,
11705
+ STREAM_SLOT_MAX_HOLD_MS: () => STREAM_SLOT_MAX_HOLD_MS,
11706
+ acquireAccountStreamLock: () => acquireAccountStreamLock,
11635
11707
  acquireNewQwenChatSession: () => acquireNewQwenChatSession,
11636
11708
  buildCapturedQwenHeaders: () => buildCapturedQwenHeaders,
11637
11709
  buildChatNewBody: () => buildChatNewBody,
@@ -11645,10 +11717,13 @@ __export(qwen_exports, {
11645
11717
  deleteAllQwenChats: () => deleteAllQwenChats,
11646
11718
  deleteSingleQwenChat: () => deleteSingleQwenChat,
11647
11719
  disableNativeTools: () => disableNativeTools,
11720
+ ensureStaleSlotSweeperStarted: () => ensureStaleSlotSweeperStarted,
11648
11721
  fetchQwenModels: () => fetchQwenModels,
11649
11722
  fetchRemoteQwenChats: () => fetchRemoteQwenChats,
11650
11723
  flushLogicalThreadState: () => flushLogicalThreadState,
11724
+ getAccountStreamMutex: () => getAccountStreamMutex,
11651
11725
  getAnyCachedQwenModels: () => getAnyCachedQwenModels,
11726
+ getBrowserFetchHeaders: () => getBrowserFetchHeaders,
11652
11727
  getInFlightPersonalizationCount: () => getInFlightPersonalizationCount,
11653
11728
  getLogicalThreadState: () => getLogicalThreadState,
11654
11729
  getQwenErrorCode: () => getQwenErrorCode,
@@ -11664,6 +11739,8 @@ __export(qwen_exports, {
11664
11739
  requestQwenSettingsDirectFetch: () => requestQwenSettingsDirectFetch,
11665
11740
  requestQwenTextInBrowser: () => requestQwenTextInBrowser,
11666
11741
  setToolCapNotice: () => setToolCapNotice,
11742
+ stopStaleSlotSweeperForTests: () => stopStaleSlotSweeperForTests,
11743
+ sweepStaleAccountStreamSlots: () => sweepStaleAccountStreamSlots,
11667
11744
  syncQwenRequestPersonalization: () => syncQwenRequestPersonalization,
11668
11745
  updateLogicalThreadParent: () => updateLogicalThreadParent,
11669
11746
  updateLogicalThreadState: () => updateLogicalThreadState,
@@ -11950,7 +12027,7 @@ function buildCapturedQwenHeaders(headers, options = {}) {
11950
12027
  function getAccountStreamMutex(accountId) {
11951
12028
  let slots2 = accountStreamMutexes.get(accountId);
11952
12029
  if (!slots2) {
11953
- slots2 = { active: 0, queue: [] };
12030
+ slots2 = { activeSlots: /* @__PURE__ */ new Map(), queue: [] };
11954
12031
  accountStreamMutexes.set(accountId, slots2);
11955
12032
  }
11956
12033
  return slots2;
@@ -11958,27 +12035,71 @@ function getAccountStreamMutex(accountId) {
11958
12035
  function streamSlotCapacity() {
11959
12036
  return Math.max(1, config.concurrency.maxStreamsPerAccount);
11960
12037
  }
11961
- function createStreamSlotRelease(slots2) {
12038
+ function createStreamSlotRelease(slots2, slotId) {
11962
12039
  let released = false;
11963
12040
  return () => {
11964
12041
  if (released) return;
11965
12042
  released = true;
11966
- slots2.active -= 1;
12043
+ slots2.activeSlots.delete(slotId);
11967
12044
  const next = slots2.queue.shift();
11968
12045
  if (next) {
11969
- slots2.active += 1;
11970
- next();
12046
+ const nextId = nextStreamSlotId++;
12047
+ slots2.activeSlots.set(nextId, Date.now());
12048
+ next(createStreamSlotRelease(slots2, nextId));
11971
12049
  }
11972
12050
  };
11973
12051
  }
12052
+ function sweepStaleAccountStreamSlots(maxHoldMs = STREAM_SLOT_MAX_HOLD_MS) {
12053
+ const now = Date.now();
12054
+ let sweptCount = 0;
12055
+ for (const [accountId, slots2] of accountStreamMutexes.entries()) {
12056
+ for (const [slotId, startedAt] of Array.from(slots2.activeSlots.entries())) {
12057
+ if (now - startedAt >= maxHoldMs) {
12058
+ logger.warn(
12059
+ `[Qwen] Force-releasing stale stream slot | account=${accountId} | slotId=${slotId} | heldFor=${now - startedAt}ms | maxHold=${maxHoldMs}ms`
12060
+ );
12061
+ slots2.activeSlots.delete(slotId);
12062
+ sweptCount++;
12063
+ const next = slots2.queue.shift();
12064
+ if (next) {
12065
+ const nextId = nextStreamSlotId++;
12066
+ slots2.activeSlots.set(nextId, now);
12067
+ next(createStreamSlotRelease(slots2, nextId));
12068
+ }
12069
+ }
12070
+ }
12071
+ }
12072
+ return sweptCount;
12073
+ }
12074
+ function ensureStaleSlotSweeperStarted() {
12075
+ if (staleSlotSweeperTimer) return;
12076
+ staleSlotSweeperTimer = setInterval(() => {
12077
+ try {
12078
+ sweepStaleAccountStreamSlots();
12079
+ } catch (err) {
12080
+ logger.error("[Qwen] Error in stale stream slot sweeper", {
12081
+ error: err instanceof Error ? err.message : String(err)
12082
+ });
12083
+ }
12084
+ }, 6e4);
12085
+ staleSlotSweeperTimer.unref();
12086
+ }
12087
+ function stopStaleSlotSweeperForTests() {
12088
+ if (staleSlotSweeperTimer) {
12089
+ clearInterval(staleSlotSweeperTimer);
12090
+ staleSlotSweeperTimer = null;
12091
+ }
12092
+ }
11974
12093
  async function acquireAccountStreamLock(accountId) {
12094
+ ensureStaleSlotSweeperStarted();
11975
12095
  const slots2 = getAccountStreamMutex(accountId);
11976
- if (slots2.active < streamSlotCapacity()) {
11977
- slots2.active += 1;
11978
- return Promise.resolve(createStreamSlotRelease(slots2));
12096
+ if (slots2.activeSlots.size < streamSlotCapacity()) {
12097
+ const slotId = nextStreamSlotId++;
12098
+ slots2.activeSlots.set(slotId, Date.now());
12099
+ return Promise.resolve(createStreamSlotRelease(slots2, slotId));
11979
12100
  }
11980
12101
  return new Promise((resolve) => {
11981
- slots2.queue.push(() => resolve(createStreamSlotRelease(slots2)));
12102
+ slots2.queue.push((releaseFn) => resolve(releaseFn));
11982
12103
  });
11983
12104
  }
11984
12105
  function buildQwenSettingsUpdatePayload(currentSettings, instruction) {
@@ -12070,11 +12191,13 @@ function getBrowserFetchHeaders(headers) {
12070
12191
  "x-request-id",
12071
12192
  "x-accel-buffering"
12072
12193
  ]);
12073
- return Object.fromEntries(
12074
- Object.entries(headers).filter(
12075
- ([name]) => browserAllowedHeaders.has(name.toLowerCase())
12076
- )
12077
- );
12194
+ const result = {};
12195
+ for (const [name, value] of Object.entries(headers)) {
12196
+ if (browserAllowedHeaders.has(name.toLowerCase())) {
12197
+ result[name] = sanitizeHeaderForFetch(String(value ?? ""));
12198
+ }
12199
+ }
12200
+ return result;
12078
12201
  }
12079
12202
  async function requestQwenTextInBrowser(accountId, method, path21, headers, body, options = {}) {
12080
12203
  const url = qwenUrl(path21);
@@ -12095,7 +12218,7 @@ async function requestQwenTextInBrowser(accountId, method, path21, headers, body
12095
12218
  browserHeaders["Content-Type"] = "application/json";
12096
12219
  }
12097
12220
  const evaluateRequest = (page) => page.evaluate(
12098
- async ({ url: url2, method: method2, headers: headers2, body: body2, referrer, timeoutMs }) => {
12221
+ async ({ url: url2, method: method2, headers: headers2, body: body2, referrer, timeoutMs, tz }) => {
12099
12222
  const controller = new AbortController();
12100
12223
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
12101
12224
  try {
@@ -12119,7 +12242,7 @@ async function requestQwenTextInBrowser(accountId, method, path21, headers, body
12119
12242
  source: "web",
12120
12243
  version: "0.3.11",
12121
12244
  "x-request-origin": "https://chat.qwen.ai",
12122
- timezone: (/* @__PURE__ */ new Date()).toString().split(" (")[0],
12245
+ timezone: tz,
12123
12246
  authorization: `Bearer ${rTok}`
12124
12247
  },
12125
12248
  signal: AbortSignal.timeout(5e3)
@@ -12164,7 +12287,8 @@ async function requestQwenTextInBrowser(accountId, method, path21, headers, body
12164
12287
  headers: browserHeaders,
12165
12288
  body,
12166
12289
  referrer: options.referrer,
12167
- timeoutMs: options.timeoutMs ?? Math.min(config.timeouts.page, 2e4)
12290
+ timeoutMs: options.timeoutMs ?? Math.min(config.timeouts.page, 2e4),
12291
+ tz: formatAsciiTimezone()
12168
12292
  }
12169
12293
  );
12170
12294
  const recoverOnTimeout = !options.noMutexRecovery;
@@ -12842,7 +12966,7 @@ async function disableNativeTools(accountId) {
12842
12966
  async (page) => {
12843
12967
  const requestId = crypto7.randomUUID();
12844
12968
  const response = await page.evaluate(
12845
- async ({ payload: payload2, timeoutMs, requestId: requestId2 }) => {
12969
+ async ({ payload: payload2, timeoutMs, requestId: requestId2, tz }) => {
12846
12970
  const controller = new AbortController();
12847
12971
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
12848
12972
  try {
@@ -12854,7 +12978,7 @@ async function disableNativeTools(accountId) {
12854
12978
  accept: "application/json, text/plain, */*",
12855
12979
  "content-type": "application/json",
12856
12980
  "x-request-id": requestId2,
12857
- timezone: (/* @__PURE__ */ new Date()).toString().split(" (")[0],
12981
+ timezone: tz,
12858
12982
  source: "web"
12859
12983
  },
12860
12984
  body: JSON.stringify(payload2),
@@ -12866,7 +12990,7 @@ async function disableNativeTools(accountId) {
12866
12990
  clearTimeout(timeoutId);
12867
12991
  }
12868
12992
  },
12869
- { payload, timeoutMs: config.timeouts.http, requestId }
12993
+ { payload, timeoutMs: config.timeouts.http, requestId, tz: formatAsciiTimezone() }
12870
12994
  );
12871
12995
  return response;
12872
12996
  }
@@ -13083,7 +13207,7 @@ async function deleteSingleQwenChat(accountId, chatId) {
13083
13207
  const requestHeaders = {
13084
13208
  source: "web",
13085
13209
  version: "0.2.89",
13086
- timezone: (/* @__PURE__ */ new Date()).toString().split(" (")[0],
13210
+ timezone: formatAsciiTimezone(),
13087
13211
  "x-request-id": crypto7.randomUUID(),
13088
13212
  Referer: qwenUrl(`/c/${encodeURIComponent(chatId)}`)
13089
13213
  };
@@ -13127,7 +13251,7 @@ async function fetchRemoteQwenChats(accountId) {
13127
13251
  }
13128
13252
  const requestHeaders = {
13129
13253
  version: "0.2.89",
13130
- timezone: (/* @__PURE__ */ new Date()).toString().split(" (")[0],
13254
+ timezone: formatAsciiTimezone(),
13131
13255
  "x-request-id": crypto7.randomUUID(),
13132
13256
  Referer: qwenUrl("/settings/chats")
13133
13257
  };
@@ -13179,7 +13303,7 @@ async function fetchQwenModels(accountId) {
13179
13303
  async (page) => {
13180
13304
  const requestId = crypto7.randomUUID();
13181
13305
  const response2 = await page.evaluate(
13182
- async ({ timeoutMs, requestId: requestId2 }) => {
13306
+ async ({ timeoutMs, requestId: requestId2, tz }) => {
13183
13307
  const controller = new AbortController();
13184
13308
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
13185
13309
  try {
@@ -13188,7 +13312,7 @@ async function fetchQwenModels(accountId) {
13188
13312
  headers: {
13189
13313
  accept: "application/json, text/plain, */*",
13190
13314
  "x-request-id": requestId2,
13191
- timezone: (/* @__PURE__ */ new Date()).toString().split(" (")[0],
13315
+ timezone: tz,
13192
13316
  source: "web"
13193
13317
  },
13194
13318
  signal: controller.signal
@@ -13198,7 +13322,7 @@ async function fetchQwenModels(accountId) {
13198
13322
  clearTimeout(timeoutId);
13199
13323
  }
13200
13324
  },
13201
- { timeoutMs: config.timeouts.http, requestId }
13325
+ { timeoutMs: config.timeouts.http, requestId, tz: formatAsciiTimezone() }
13202
13326
  );
13203
13327
  return response2;
13204
13328
  }
@@ -13237,7 +13361,7 @@ async function fetchQwenModels(accountId) {
13237
13361
  bxUa,
13238
13362
  bxUmidtoken,
13239
13363
  extra: {
13240
- timezone: (/* @__PURE__ */ new Date()).toString()
13364
+ timezone: formatAsciiTimezone()
13241
13365
  }
13242
13366
  }),
13243
13367
  void 0,
@@ -14000,7 +14124,7 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
14000
14124
  clearTimeout(timeoutId);
14001
14125
  }
14002
14126
  }
14003
- var sleep5, BROWSER_STREAM_BINDING, BROWSER_ABORTERS_KEY, BROWSER_STREAM_FLUSH_BYTES, BROWSER_STREAM_FLUSH_MS, METADATA_TIMEOUT_PER_PAYLOAD_MB_MS, POST_CAPTCHA_METADATA_GRACE_MS, browserStreamStates, browserStreamBindingPages, browserStreamBindingContexts, MODEL_CACHE_TTL_MS, modelsCache, nativeToolsDisabled, disablingNativeToolsInProgress, lastSyncedPersonalizationHashes, directSettingsFetchConsecutiveFailures, DIRECT_SETTINGS_FETCH_BLOCK_THRESHOLD, directSettingsFetchBlocked, DIRECT_SETTINGS_FETCH_TIMEOUT_MS, activePersonalizationByAccount, accountStreamMutexes, QWEN_SAFE_SETTINGS_PATCH, QWEN_SAFE_SETTINGS_HASH, inFlightPersonalizationSyncs, DISABLE_TOOLS_MAX_RETRIES, DISABLE_TOOLS_BACKOFF_MS, UPSTREAM_RESPONSE_PREVIEW_BYTES;
14127
+ var sleep5, BROWSER_STREAM_BINDING, BROWSER_ABORTERS_KEY, BROWSER_STREAM_FLUSH_BYTES, BROWSER_STREAM_FLUSH_MS, METADATA_TIMEOUT_PER_PAYLOAD_MB_MS, POST_CAPTCHA_METADATA_GRACE_MS, browserStreamStates, browserStreamBindingPages, browserStreamBindingContexts, MODEL_CACHE_TTL_MS, modelsCache, nativeToolsDisabled, disablingNativeToolsInProgress, lastSyncedPersonalizationHashes, directSettingsFetchConsecutiveFailures, DIRECT_SETTINGS_FETCH_BLOCK_THRESHOLD, directSettingsFetchBlocked, DIRECT_SETTINGS_FETCH_TIMEOUT_MS, activePersonalizationByAccount, STREAM_SLOT_MAX_HOLD_MS, accountStreamMutexes, nextStreamSlotId, staleSlotSweeperTimer, QWEN_SAFE_SETTINGS_PATCH, QWEN_SAFE_SETTINGS_HASH, inFlightPersonalizationSyncs, DISABLE_TOOLS_MAX_RETRIES, DISABLE_TOOLS_BACKOFF_MS, UPSTREAM_RESPONSE_PREVIEW_BYTES;
14004
14128
  var init_qwen = __esm({
14005
14129
  "src/services/qwen.ts"() {
14006
14130
  "use strict";
@@ -14018,6 +14142,7 @@ var init_qwen = __esm({
14018
14142
  init_captcha_coordinator();
14019
14143
  init_captcha_solver();
14020
14144
  init_account_concurrency();
14145
+ init_timezone();
14021
14146
  init_qwen_errors();
14022
14147
  init_qwen_thread_state();
14023
14148
  init_qwen_chat_pool();
@@ -14047,7 +14172,10 @@ var init_qwen = __esm({
14047
14172
  directSettingsFetchBlocked = /* @__PURE__ */ new Set();
14048
14173
  DIRECT_SETTINGS_FETCH_TIMEOUT_MS = 1e4;
14049
14174
  activePersonalizationByAccount = /* @__PURE__ */ new Map();
14175
+ STREAM_SLOT_MAX_HOLD_MS = 30 * 60 * 1e3;
14050
14176
  accountStreamMutexes = /* @__PURE__ */ new Map();
14177
+ nextStreamSlotId = 1;
14178
+ staleSlotSweeperTimer = null;
14051
14179
  QWEN_SAFE_SETTINGS_PATCH = {
14052
14180
  ui: {
14053
14181
  autoTags: false,
@@ -16777,7 +16905,7 @@ function buildContextMeterSnapshot(input, options = getContextMeterOptions()) {
16777
16905
  function enrichUsageWithContextMeter(usage, snapshot, options = getContextMeterOptions()) {
16778
16906
  if (!snapshot || !options.enabled) return usage;
16779
16907
  const upstreamPromptTokens = Number.isFinite(usage.prompt_tokens) && usage.prompt_tokens > 0 ? usage.prompt_tokens : null;
16780
- const reportedPromptTokens = options.reportUsage ? upstreamPromptTokens ?? snapshot.estimatedContextTokens : usage.prompt_tokens;
16908
+ const reportedPromptTokens = options.reportUsage ? upstreamPromptTokens ?? usage.prompt_tokens : usage.prompt_tokens;
16781
16909
  const contextMeter = {
16782
16910
  ...snapshot,
16783
16911
  upstreamPromptTokens,
@@ -20405,6 +20533,35 @@ var init_parser = __esm({
20405
20533
  }
20406
20534
  });
20407
20535
 
20536
+ // src/tools/tool-call-heuristics.ts
20537
+ function isToolHesitationText(text) {
20538
+ if (!text) return false;
20539
+ return /\btools?\b[^.\n]{0,24}?\b(?:does\s*n[o']?t|do\s*n[o']?t|does\s*not|do\s*not)\b[^.\n]{0,12}?\bexist/i.test(
20540
+ text
20541
+ );
20542
+ }
20543
+ function decideMissingToolCallRetry(input) {
20544
+ const {
20545
+ declaredToolCount,
20546
+ successfulToolCallCount,
20547
+ malformedToolCallCount,
20548
+ emittedToolCallCount,
20549
+ text
20550
+ } = input;
20551
+ if (declaredToolCount <= 0) return { retry: false, reason: null };
20552
+ if (successfulToolCallCount > 0) return { retry: false, reason: null };
20553
+ if (malformedToolCallCount > 0) return { retry: true, reason: "malformed" };
20554
+ if (emittedToolCallCount === 0 && isToolHesitationText(text)) {
20555
+ return { retry: true, reason: "no_call" };
20556
+ }
20557
+ return { retry: false, reason: null };
20558
+ }
20559
+ var init_tool_call_heuristics = __esm({
20560
+ "src/tools/tool-call-heuristics.ts"() {
20561
+ "use strict";
20562
+ }
20563
+ });
20564
+
20408
20565
  // src/routes/chat/errors.ts
20409
20566
  function isWafChallenge(value) {
20410
20567
  const normalized = value.toLowerCase();
@@ -21104,10 +21261,17 @@ async function processNonStreamingResponse(params) {
21104
21261
  message.tool_calls = toolCallsOut;
21105
21262
  }
21106
21263
  const finishReason = toolCallsOut.length ? "tool_calls" : "stop";
21107
- const allToolsFailed = toolParser && toolParser.getMalformedToolCalls().length > 0 && toolCallsOut.length === 0;
21264
+ const retryDecision = toolParser ? decideMissingToolCallRetry({
21265
+ declaredToolCount: declaredTools.length,
21266
+ successfulToolCallCount: toolParser.getSuccessfulToolCallCount(),
21267
+ malformedToolCallCount: toolParser.getMalformedToolCalls().length,
21268
+ emittedToolCallCount: toolParser.getEmittedToolCallCount(),
21269
+ text: finalContent
21270
+ }) : { retry: false, reason: null };
21271
+ const allToolsFailed = retryDecision.retry;
21108
21272
  const malformedRetryCount = midStreamRetry?.malformedRetryCount ?? 0;
21109
21273
  if (allToolsFailed && config.retry.autoRetryMalformedTools !== false && midStreamRetry && malformedRetryCount < config.retry.autoRetryMalformedToolsMax) {
21110
- const malformedCalls = toolParser.getMalformedToolCalls();
21274
+ const malformedCalls = toolParser?.getMalformedToolCalls() ?? [];
21111
21275
  const malformedCount = malformedCalls.length;
21112
21276
  const undeclaredNames = malformedCalls.flatMap((mc) => mc.undeclaredNames || []).filter((name, index, self) => self.indexOf(name) === index);
21113
21277
  const availableToolNames = declaredTools.map((t) => t.type === "function" ? t.function?.name : t.name).filter((n) => !!n);
@@ -21115,7 +21279,9 @@ async function processNonStreamingResponse(params) {
21115
21279
 
21116
21280
  Available tools: ${availableToolNames.join(", ")}` : "";
21117
21281
  let errorMessage;
21118
- if (undeclaredNames.length > 0) {
21282
+ if (retryDecision.reason === "no_call") {
21283
+ errorMessage = `You did not emit any tool call. Respond with a single tool-call block using one of the declared tools.${toolsHint}`;
21284
+ } else if (undeclaredNames.length > 0) {
21119
21285
  errorMessage = `Your previous ${malformedCount} tool call(s) used undeclared tool names: ${undeclaredNames.join(", ")}. Only declared tools can be executed. Please retry with valid tool names.${toolsHint}`;
21120
21286
  } else {
21121
21287
  const previews = malformedCalls.slice(0, 3).map((mc) => mc.contentPreview?.substring(0, 100) || "(empty)").join("\n - ");
@@ -21191,7 +21357,7 @@ Please retry your tool call(s) with correct JSON and valid tool names from the a
21191
21357
  new Error("client aborted during malformed-tool retry")
21192
21358
  );
21193
21359
  }
21194
- console.log(`\u{1F504} [Chat] Auto-retry (${malformedRetryCount + 1}/${config.retry.autoRetryMalformedToolsMax}) | ${newStreamResult.activeAccountLabel} | ${body.model} | chat=${newStreamResult.uiSessionId.substring(0, 12)} | reason=malformed_tool_calls`);
21360
+ console.log(`\u{1F504} [Chat] Auto-retry (${malformedRetryCount + 1}/${config.retry.autoRetryMalformedToolsMax}) | ${newStreamResult.activeAccountLabel} | ${body.model} | chat=${newStreamResult.uiSessionId.substring(0, 12)} | reason=${retryDecision.reason === "no_call" ? "missing_tool_call" : "malformed_tool_calls"}`);
21195
21361
  return processNonStreamingResponse({
21196
21362
  ...params,
21197
21363
  stream: newStreamResult.stream,
@@ -22287,7 +22453,14 @@ async function processStreamingResponse(params) {
22287
22453
  let activeRetryStream = null;
22288
22454
  while (malformedRetryCount < maxMalformedRetries) {
22289
22455
  if (!toolParser) break;
22290
- const allToolsFailed = toolParser.getMalformedToolCalls().length > 0 && toolParser.getSuccessfulToolCallCount() === 0;
22456
+ const retryDecision = decideMissingToolCallRetry({
22457
+ declaredToolCount: declaredTools.length,
22458
+ successfulToolCallCount: toolParser.getSuccessfulToolCallCount(),
22459
+ malformedToolCallCount: toolParser.getMalformedToolCalls().length,
22460
+ emittedToolCallCount: toolParser.getEmittedToolCallCount(),
22461
+ text: finalContent
22462
+ });
22463
+ const allToolsFailed = retryDecision.retry;
22291
22464
  if (!allToolsFailed) break;
22292
22465
  const malformedCalls = toolParser.getMalformedToolCalls();
22293
22466
  const malformedCount = malformedCalls.length;
@@ -22297,7 +22470,9 @@ async function processStreamingResponse(params) {
22297
22470
 
22298
22471
  Available tools: ${availableToolNames.join(", ")}` : "";
22299
22472
  let errorMessage;
22300
- if (undeclaredNames.length > 0) {
22473
+ if (retryDecision.reason === "no_call") {
22474
+ errorMessage = `You did not emit any tool call. Respond with a single tool-call block using one of the declared tools.${toolsHint}`;
22475
+ } else if (undeclaredNames.length > 0) {
22301
22476
  errorMessage = `Your previous ${malformedCount} tool call(s) used undeclared tool names: ${undeclaredNames.join(", ")}. Only declared tools can be executed. Please retry with valid tool names.${toolsHint}`;
22302
22477
  } else {
22303
22478
  const previews = malformedCalls.slice(0, 3).map((mc) => mc.contentPreview?.substring(0, 100) || "(empty)").join("\n - ");
@@ -22374,7 +22549,7 @@ Please retry your tool call(s) with correct JSON and valid tool names from the a
22374
22549
  break;
22375
22550
  }
22376
22551
  console.log(
22377
- `\u{1F504} [Chat] Auto-retry (${malformedRetryCount + 1}/${maxMalformedRetries}) | ${newStreamResult.activeAccountLabel} | ${body.model} | chat=${newStreamResult.uiSessionId.substring(0, 12)} | reason=malformed_tool_calls`
22552
+ `\u{1F504} [Chat] Auto-retry (${malformedRetryCount + 1}/${maxMalformedRetries}) | ${newStreamResult.activeAccountLabel} | ${body.model} | chat=${newStreamResult.uiSessionId.substring(0, 12)} | reason=${retryDecision.reason === "no_call" ? "missing_tool_call" : "malformed_tool_calls"}`
22378
22553
  );
22379
22554
  const retryEntry = getStream(newStreamResult.completionId);
22380
22555
  removeStream(newStreamResult.completionId);
@@ -22875,6 +23050,7 @@ var init_streaming = __esm({
22875
23050
  init_retry_policy();
22876
23051
  init_account();
22877
23052
  init_parser();
23053
+ init_tool_call_heuristics();
22878
23054
  init_stream_registry();
22879
23055
  init_metrics();
22880
23056
  init_logger();
@@ -24283,7 +24459,7 @@ function chatCompletionsToResponses(chatRes, requestModel, originalRequest) {
24283
24459
  cached_tokens: chatRes.usage.prompt_tokens_details?.cached_tokens ?? 0
24284
24460
  },
24285
24461
  output_tokens_details: {
24286
- reasoning_tokens: chatRes.usage.completion_tokens_details?.reasoning_tokens ?? 0
24462
+ reasoning_tokens: chatRes.usage.completion_tokens_details?.reasoning_tokens ?? chatRes.usage.output_tokens_details?.reasoning_tokens ?? 0
24287
24463
  },
24288
24464
  ...chatRes.usage.context_meter ? { context_meter: chatRes.usage.context_meter } : {}
24289
24465
  };
@@ -25744,7 +25920,7 @@ function translateOpenAIToAnthropic(openaiResponse, requestModel) {
25744
25920
  content_filter: "end_turn"
25745
25921
  };
25746
25922
  const hasToolUse = content.some((b) => b.type === "tool_use");
25747
- const stopReason = hasToolUse ? "tool_use" : stopReasonMap[choice.finish_reason || "stop"] || "end_turn";
25923
+ const stopReason = choice.finish_reason === "length" ? "max_tokens" : choice.finish_reason === "content_filter" ? "end_turn" : hasToolUse ? "tool_use" : stopReasonMap[choice.finish_reason || "stop"] || "end_turn";
25748
25924
  return {
25749
25925
  id: generateMessageId2(),
25750
25926
  type: "message",
@@ -25882,7 +26058,7 @@ function translateStreamChunk(chunk, state) {
25882
26058
  state.contentBlockIndex++;
25883
26059
  state.currentBlockType = null;
25884
26060
  }
25885
- const stopReason = state.hasEmittedToolUse ? "tool_use" : choice.finish_reason === "length" ? "max_tokens" : "end_turn";
26061
+ const stopReason = choice.finish_reason === "length" ? "max_tokens" : choice.finish_reason === "content_filter" ? "end_turn" : state.hasEmittedToolUse ? "tool_use" : "end_turn";
25886
26062
  events.push(
25887
26063
  JSON.stringify({
25888
26064
  type: "message_delta",
@@ -26828,7 +27004,7 @@ function getAppVersion() {
26828
27004
  }
26829
27005
  } catch {
26830
27006
  }
26831
- cachedVersion = "v1.6.4";
27007
+ cachedVersion = "v1.6.5";
26832
27008
  return cachedVersion;
26833
27009
  }
26834
27010
  var cachedVersion;