qwenproxy-cli 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1592,7 +1592,26 @@ function runMigrations(db2) {
1592
1592
  instruction_hash TEXT NOT NULL,
1593
1593
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1594
1594
  );
1595
+
1596
+ CREATE TABLE IF NOT EXISTS account_daily_usage (
1597
+ account_id TEXT NOT NULL,
1598
+ date_utc TEXT NOT NULL,
1599
+ prompt_tokens INTEGER NOT NULL DEFAULT 0,
1600
+ completion_tokens INTEGER NOT NULL DEFAULT 0,
1601
+ weighted_tokens REAL NOT NULL DEFAULT 0,
1602
+ total_tokens INTEGER NOT NULL DEFAULT 0,
1603
+ turns_count INTEGER NOT NULL DEFAULT 0,
1604
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1605
+ PRIMARY KEY (account_id, date_utc)
1606
+ );
1607
+
1608
+ CREATE INDEX IF NOT EXISTS idx_daily_usage_date ON account_daily_usage(date_utc);
1595
1609
  `);
1610
+ try {
1611
+ db2.exec(`ALTER TABLE account_daily_usage ADD COLUMN weighted_tokens REAL DEFAULT 0;`);
1612
+ } catch (err) {
1613
+ if (!isDuplicateColumnError(err)) throw err;
1614
+ }
1596
1615
  try {
1597
1616
  db2.exec(
1598
1617
  `ALTER TABLE accounts ADD COLUMN cooldown_until INTEGER DEFAULT 0;`
@@ -1693,7 +1712,11 @@ function saveAuthSession(accountId, session) {
1693
1712
  ) VALUES (
1694
1713
  ?, ?, ?, ?, ?, ?,
1695
1714
  ?, ?, ?, ?,
1696
- ?, ?, ?, ?, datetime('now')
1715
+ COALESCE(?, (SELECT user_id FROM qwen_auth_sessions WHERE account_id = ?)),
1716
+ ?,
1717
+ COALESCE(?, (SELECT refresh_token FROM qwen_auth_sessions WHERE account_id = ?)),
1718
+ ?,
1719
+ datetime('now')
1697
1720
  )
1698
1721
  `);
1699
1722
  stmt.run(
@@ -1708,8 +1731,10 @@ function saveAuthSession(accountId, session) {
1708
1731
  session.secChUaPlatform || null,
1709
1732
  session.version || null,
1710
1733
  session.userId || null,
1734
+ accountId,
1711
1735
  session.tokenExpiresAt || null,
1712
1736
  session.refreshToken || null,
1737
+ accountId,
1713
1738
  session.capturedAt ?? Date.now()
1714
1739
  );
1715
1740
  }
@@ -5194,6 +5219,11 @@ async function getBasicHeaders(accountId) {
5194
5219
  if (!page) {
5195
5220
  throw new Error(`Playwright not initialized for account: ${accountId}`);
5196
5221
  }
5222
+ const inFlightInit = inFlightAccountInits.get(accountId);
5223
+ if (inFlightInit) {
5224
+ await inFlightInit.catch(() => {
5225
+ });
5226
+ }
5197
5227
  const release = await acquireAccountMutex(
5198
5228
  accountId,
5199
5229
  `headers:${accountId.substring(0, 12)}`
@@ -6668,6 +6698,11 @@ async function captureQwenHeaders(accountId, pageOverride, timeoutMs = config.ti
6668
6698
  const { saveAuthSession: saveAuthSession2 } = await Promise.resolve().then(() => (init_database(), database_exports));
6669
6699
  const { parseJwtExpiry: parseJwtExpiry2 } = await Promise.resolve().then(() => (init_jwt(), jwt_exports));
6670
6700
  const tokenExpiry = parseJwtExpiry2(capturedHeaders.cookie);
6701
+ let rTok;
6702
+ try {
6703
+ rTok = (await page.evaluate(() => localStorage.getItem("refresh_token") || "")).trim() || void 0;
6704
+ } catch {
6705
+ }
6671
6706
  saveAuthSession2(accountId, {
6672
6707
  cookie: capturedHeaders.cookie,
6673
6708
  userAgent: capturedHeaders["user-agent"],
@@ -6679,6 +6714,7 @@ async function captureQwenHeaders(accountId, pageOverride, timeoutMs = config.ti
6679
6714
  secChUaPlatform: capturedHeaders["sec-ch-ua-platform"],
6680
6715
  version: capturedHeaders["version"],
6681
6716
  tokenExpiresAt: tokenExpiry || void 0,
6717
+ refreshToken: rTok,
6682
6718
  capturedAt: Date.now()
6683
6719
  });
6684
6720
  } catch {
@@ -6710,38 +6746,57 @@ async function captureQwenHeaders(accountId, pageOverride, timeoutMs = config.ti
6710
6746
  const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
6711
6747
  const isSuspectedGuest = isAuthUrl || attempt >= 2 && !await isPageLoggedIn(page, 5e3);
6712
6748
  if (isSuspectedGuest) {
6713
- const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
6714
- const creds = getAccountCredentials2(accountId);
6715
- if (creds && creds.email && creds.password) {
6716
- console.warn(
6717
- `\u26A0\uFE0F [Playwright] Session expired or guest state detected during header capture for ${accountId}; re-authenticating...`
6749
+ const silentRefresh = await refreshAccountToken(accountId);
6750
+ if (silentRefresh.success) {
6751
+ console.log(
6752
+ `\u26A1 [Playwright] Token refreshed silently during header capture for ${accountId}`
6718
6753
  );
6719
- armOverallDeadline();
6720
- const ok = await loginToQwen(accountId, creds.email, creds.password);
6721
- if (ok) {
6754
+ if (isAuthUrl) {
6722
6755
  await page.goto(qwenUrl("/"), {
6723
6756
  waitUntil: "domcontentloaded",
6724
6757
  timeout: Math.min(config.timeouts.navigation, timeoutMs)
6725
6758
  });
6726
- await sleep2(2e3);
6759
+ await sleep2(1500);
6727
6760
  }
6728
- if (!ok || !await isPageLoggedIn(page, 6e3)) {
6761
+ if (await isPageLoggedIn(page, 5e3)) {
6762
+ armOverallDeadline();
6763
+ }
6764
+ }
6765
+ const stillSuspected = !silentRefresh.success || !await isPageLoggedIn(page, 2e3);
6766
+ if (stillSuspected) {
6767
+ const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
6768
+ const creds = getAccountCredentials2(accountId);
6769
+ if (creds && creds.email && creds.password) {
6770
+ console.warn(
6771
+ `\u26A0\uFE0F [Playwright] Session expired or guest state detected during header capture for ${accountId}; re-authenticating...`
6772
+ );
6773
+ armOverallDeadline();
6774
+ const ok = await loginToQwen(accountId, creds.email, creds.password);
6775
+ if (ok) {
6776
+ await page.goto(qwenUrl("/"), {
6777
+ waitUntil: "domcontentloaded",
6778
+ timeout: Math.min(config.timeouts.navigation, timeoutMs)
6779
+ });
6780
+ await sleep2(2e3);
6781
+ }
6782
+ if (!ok || !await isPageLoggedIn(page, 6e3)) {
6783
+ settle(
6784
+ new Error(
6785
+ `Header capture failed for ${accountId}: re-login after session expiry did not succeed`
6786
+ )
6787
+ );
6788
+ return;
6789
+ }
6790
+ armOverallDeadline();
6791
+ if (settled || page.isClosed()) return;
6792
+ } else {
6729
6793
  settle(
6730
6794
  new Error(
6731
- `Header capture failed for ${accountId}: re-login after session expiry did not succeed`
6795
+ `Header capture failed for ${accountId}: session expired and no credentials available for re-login (run 'qpx login')`
6732
6796
  )
6733
6797
  );
6734
6798
  return;
6735
6799
  }
6736
- armOverallDeadline();
6737
- if (settled || page.isClosed()) return;
6738
- } else {
6739
- settle(
6740
- new Error(
6741
- `Header capture failed for ${accountId}: session expired and no credentials available for re-login (run 'qpx login')`
6742
- )
6743
- );
6744
- return;
6745
6800
  }
6746
6801
  }
6747
6802
  if (settled || page.isClosed()) return;
@@ -7057,7 +7112,13 @@ async function refreshHeadersInternal(accountId, timeoutMs = config.timeouts.hea
7057
7112
  `[Playwright] Navigation check failed during refresh for ${accountId}:`,
7058
7113
  navErr.message
7059
7114
  );
7060
- await executeReauth();
7115
+ if (await isPageLoggedIn(page, 3e3)) {
7116
+ } else {
7117
+ const refreshed = await refreshAccountToken(accountId);
7118
+ if (!refreshed.success) {
7119
+ await executeReauth();
7120
+ }
7121
+ }
7061
7122
  }
7062
7123
  }
7063
7124
  if (hasRequiredQwenHeaders(cache3.headers)) {
@@ -7071,6 +7132,11 @@ async function refreshHeadersInternal(accountId, timeoutMs = config.timeouts.hea
7071
7132
  const { saveAuthSession: saveAuthSession2 } = await Promise.resolve().then(() => (init_database(), database_exports));
7072
7133
  const tokenCookie = liveCookies.find((c) => c.name === "token");
7073
7134
  const exp = tokenCookie ? parseJwtExpiry(tokenCookie.value) : void 0;
7135
+ let rTok;
7136
+ try {
7137
+ rTok = (await page.evaluate(() => localStorage.getItem("refresh_token") || "")).trim() || void 0;
7138
+ } catch {
7139
+ }
7074
7140
  saveAuthSession2(accountId, {
7075
7141
  cookie: cookieStr,
7076
7142
  userAgent: cache3.headers["user-agent"] || "",
@@ -7082,6 +7148,7 @@ async function refreshHeadersInternal(accountId, timeoutMs = config.timeouts.hea
7082
7148
  secChUaPlatform: cache3.headers["sec-ch-ua-platform"] || void 0,
7083
7149
  version: cache3.headers["version"] || void 0,
7084
7150
  tokenExpiresAt: exp || void 0,
7151
+ refreshToken: rTok,
7085
7152
  capturedAt: Date.now()
7086
7153
  });
7087
7154
  } catch {
@@ -7468,8 +7535,10 @@ async function keepAlivePlaywrightAccount(accountId) {
7468
7535
  if (!page || page.isClosed()) return false;
7469
7536
  const now = Date.now();
7470
7537
  const currentUrl = page.url();
7538
+ const lastRefreshAttempt = lastProactiveTokenRefresh.get(accountId) ?? 0;
7471
7539
  const cookie = await getCookies(accountId);
7472
- if (cookie && isTokenExpiringSoon(cookie, 5)) {
7540
+ if (cookie && isTokenExpiringSoon(cookie, 5) && now - lastRefreshAttempt > 6e4) {
7541
+ lastProactiveTokenRefresh.set(accountId, now);
7473
7542
  console.log(
7474
7543
  `\u{1F493} [SessionKeeper] Account ${accountId} access token expiring soon (<5m); silently refreshing...`
7475
7544
  );
@@ -7513,6 +7582,7 @@ function cleanupPlaywrightAccountState(accountId) {
7513
7582
  cookieCaches.delete(accountId);
7514
7583
  lastAccountActivity.delete(accountId);
7515
7584
  lastKeepAliveNavigation.delete(accountId);
7585
+ lastProactiveTokenRefresh.delete(accountId);
7516
7586
  clearFingerprintCache(accountId);
7517
7587
  unmarkAccountHeadersReady(accountId);
7518
7588
  }
@@ -7696,7 +7766,7 @@ async function getTokenDiagnostics(accountId) {
7696
7766
  }
7697
7767
  };
7698
7768
  }
7699
- var requireLocal, contextInitHooks, ACCOUNT_MUTEX_MAX_HOLD_MS, accountMutexes, accountContexts, accountPages, cachedUserAgents, inFlightAccountInits, sharedBrowser, sharedBrowserPromise, headerCaches, HEADER_CACHE_TTL, HEADER_REFRESH_THRESHOLD, COOKIE_CACHE_TTL, cookieCaches, lastAccountActivity, lastKeepAliveNavigation, profileResetQueue, profileResetChain, closingAllPlaywright, sleep2, HEADER_CAPTURE_SETTLE_MS, PLAYWRIGHT_MUTEX_WAIT_MS, ACCOUNT_PAGE_OPERATION_TIMEOUT_MS, SESSION_PROBE_NAVIGATION_TIMEOUT_MS, HEADER_CAPTURE_TRIGGER_GRACE_MS, FIRST_TRIGGER_GRACE_MS, HEADER_CAPTURE_TRIGGER_ATTEMPTS, CHAT_INPUT_APPEAR_TIMEOUT_MS, CHAT_INPUT_ACTION_TIMEOUT_MS, PROFILE_RESET_TIMEOUT_MS;
7769
+ var requireLocal, contextInitHooks, ACCOUNT_MUTEX_MAX_HOLD_MS, accountMutexes, accountContexts, accountPages, cachedUserAgents, inFlightAccountInits, sharedBrowser, sharedBrowserPromise, headerCaches, HEADER_CACHE_TTL, HEADER_REFRESH_THRESHOLD, COOKIE_CACHE_TTL, cookieCaches, lastAccountActivity, lastKeepAliveNavigation, lastProactiveTokenRefresh, profileResetQueue, profileResetChain, closingAllPlaywright, sleep2, HEADER_CAPTURE_SETTLE_MS, PLAYWRIGHT_MUTEX_WAIT_MS, ACCOUNT_PAGE_OPERATION_TIMEOUT_MS, SESSION_PROBE_NAVIGATION_TIMEOUT_MS, HEADER_CAPTURE_TRIGGER_GRACE_MS, FIRST_TRIGGER_GRACE_MS, HEADER_CAPTURE_TRIGGER_ATTEMPTS, CHAT_INPUT_APPEAR_TIMEOUT_MS, CHAT_INPUT_ACTION_TIMEOUT_MS, PROFILE_RESET_TIMEOUT_MS;
7700
7770
  var init_playwright = __esm({
7701
7771
  "src/services/playwright.ts"() {
7702
7772
  "use strict";
@@ -7732,6 +7802,7 @@ var init_playwright = __esm({
7732
7802
  cookieCaches = /* @__PURE__ */ new Map();
7733
7803
  lastAccountActivity = /* @__PURE__ */ new Map();
7734
7804
  lastKeepAliveNavigation = /* @__PURE__ */ new Map();
7805
+ lastProactiveTokenRefresh = /* @__PURE__ */ new Map();
7735
7806
  profileResetQueue = /* @__PURE__ */ new Map();
7736
7807
  profileResetChain = Promise.resolve();
7737
7808
  closingAllPlaywright = false;
@@ -8547,9 +8618,13 @@ var init_model_registry = __esm({
8547
8618
  // src/services/captcha-coordinator.ts
8548
8619
  var captcha_coordinator_exports = {};
8549
8620
  __export(captcha_coordinator_exports, {
8621
+ isCaptchaRecoveryActive: () => isCaptchaRecoveryActive,
8550
8622
  recoverBaxiaCaptcha: () => recoverBaxiaCaptcha,
8551
8623
  solveChallengeOnPage: () => solveChallengeOnPage
8552
8624
  });
8625
+ function isCaptchaRecoveryActive(accountId) {
8626
+ return activeCaptchaRecoveries.has(accountId);
8627
+ }
8553
8628
  async function gotoBestEffort(page, url) {
8554
8629
  await page.goto(url, {
8555
8630
  waitUntil: "domcontentloaded",
@@ -8592,11 +8667,12 @@ async function recoverBaxiaCaptcha(accountId, label, options = {}) {
8592
8667
  return false;
8593
8668
  }
8594
8669
  metrics.increment("captcha.challenges.detected", 1, { solver });
8595
- const challengeUrl = options.challengeBody ? extractBaxiaChallengeUrl(options.challengeBody, config.qwen.baseUrl) : null;
8596
8670
  const solverOperationTimeoutMs = Math.max(
8597
8671
  15e3,
8598
8672
  config.captcha.timeoutMs + config.captcha.maxAttempts * (3e3 + config.captcha.retryDelayMs + config.captcha.settleMs) + 2 * CHALLENGE_NAVIGATION_TIMEOUT_MS
8599
8673
  );
8674
+ const challengeUrl = options.challengeBody ? extractBaxiaChallengeUrl(options.challengeBody, config.qwen.baseUrl) : null;
8675
+ activeCaptchaRecoveries.add(accountId);
8600
8676
  try {
8601
8677
  const solved = await withAccountPage(
8602
8678
  accountId,
@@ -8629,9 +8705,11 @@ async function recoverBaxiaCaptcha(accountId, label, options = {}) {
8629
8705
  detail
8630
8706
  });
8631
8707
  return false;
8708
+ } finally {
8709
+ activeCaptchaRecoveries.delete(accountId);
8632
8710
  }
8633
8711
  }
8634
- var CHALLENGE_NAVIGATION_TIMEOUT_MS, CHALLENGE_PATH_MARKER, FAILED_RECOVERY_BACKOFF_MS, lastFailedRecoveryAt;
8712
+ var CHALLENGE_NAVIGATION_TIMEOUT_MS, CHALLENGE_PATH_MARKER, FAILED_RECOVERY_BACKOFF_MS, lastFailedRecoveryAt, activeCaptchaRecoveries;
8635
8713
  var init_captcha_coordinator = __esm({
8636
8714
  "src/services/captcha-coordinator.ts"() {
8637
8715
  "use strict";
@@ -8644,6 +8722,7 @@ var init_captcha_coordinator = __esm({
8644
8722
  CHALLENGE_PATH_MARKER = "_____tmd_____";
8645
8723
  FAILED_RECOVERY_BACKOFF_MS = 3e4;
8646
8724
  lastFailedRecoveryAt = /* @__PURE__ */ new Map();
8725
+ activeCaptchaRecoveries = /* @__PURE__ */ new Set();
8647
8726
  }
8648
8727
  });
8649
8728
 
@@ -11110,6 +11189,7 @@ __export(qwen_exports, {
11110
11189
  fetchRemoteQwenChats: () => fetchRemoteQwenChats,
11111
11190
  flushLogicalThreadState: () => flushLogicalThreadState,
11112
11191
  getAnyCachedQwenModels: () => getAnyCachedQwenModels,
11192
+ getInFlightPersonalizationCount: () => getInFlightPersonalizationCount,
11113
11193
  getLogicalThreadState: () => getLogicalThreadState,
11114
11194
  getQwenErrorCode: () => getQwenErrorCode,
11115
11195
  getSessionParent: () => getSessionParent,
@@ -12042,12 +12122,51 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
12042
12122
  captchaWatcher?.stop();
12043
12123
  }
12044
12124
  }
12125
+ function getInFlightPersonalizationCount() {
12126
+ return inFlightPersonalizationSyncs.size;
12127
+ }
12045
12128
  async function syncQwenRequestPersonalization(instruction, accountId, metadata = {}) {
12046
- if (isAuthMockEnabled()) {
12047
- if (process.env.TEST_PERSONALIZATION_SYNC_FAIL === "true") return false;
12048
- return true;
12049
- }
12050
12129
  const cacheKey = accountId || "global";
12130
+ const sent = textSize(instruction);
12131
+ const syncHash = sent.hash ? `${sent.hash}:${QWEN_SAFE_SETTINGS_HASH}` : null;
12132
+ const inFlightKey = `${cacheKey}:${syncHash || "empty"}`;
12133
+ const existingInFlight = inFlightPersonalizationSyncs.get(inFlightKey);
12134
+ if (existingInFlight) {
12135
+ logger.debug("[Qwen] Personalization sync already in flight, sharing promise", {
12136
+ accountId: cacheKey
12137
+ });
12138
+ return existingInFlight;
12139
+ }
12140
+ let syncPromise;
12141
+ syncPromise = (async () => {
12142
+ await Promise.resolve();
12143
+ try {
12144
+ if (isAuthMockEnabled()) {
12145
+ const delay = parseInt(process.env.TEST_PERSONALIZATION_DELAY_MS || "0", 10);
12146
+ if (delay > 0) {
12147
+ await sleep5(delay);
12148
+ }
12149
+ if (process.env.TEST_PERSONALIZATION_SYNC_FAIL === "true") return false;
12150
+ return true;
12151
+ }
12152
+ return await syncQwenRequestPersonalizationInternal(
12153
+ instruction,
12154
+ accountId,
12155
+ metadata,
12156
+ cacheKey,
12157
+ sent,
12158
+ syncHash
12159
+ );
12160
+ } finally {
12161
+ if (inFlightPersonalizationSyncs.get(inFlightKey) === syncPromise) {
12162
+ inFlightPersonalizationSyncs.delete(inFlightKey);
12163
+ }
12164
+ }
12165
+ })();
12166
+ inFlightPersonalizationSyncs.set(inFlightKey, syncPromise);
12167
+ return syncPromise;
12168
+ }
12169
+ async function syncQwenRequestPersonalizationInternal(instruction, accountId, metadata, cacheKey, sent, syncHash) {
12051
12170
  let forceRefresh = false;
12052
12171
  try {
12053
12172
  const basic = await getBasicHeaders2(accountId);
@@ -12065,8 +12184,6 @@ async function syncQwenRequestPersonalization(instruction, accountId, metadata =
12065
12184
  });
12066
12185
  let currentSettings = null;
12067
12186
  let payload = buildQwenSettingsUpdatePayload(currentSettings, instruction);
12068
- const sent = textSize(instruction);
12069
- const syncHash = sent.hash ? `${sent.hash}:${QWEN_SAFE_SETTINGS_HASH}` : null;
12070
12187
  const bypassCache = metadata.forceSync === true;
12071
12188
  const cachedHash = lastSyncedPersonalizationHashes.get(cacheKey);
12072
12189
  if (!bypassCache && syncHash && cachedHash === syncHash) {
@@ -12736,9 +12853,13 @@ function parseQwenJsonError(raw, status, accountId) {
12736
12853
  }
12737
12854
  if (errorJson?.success === false) {
12738
12855
  const code = errorJson.data?.code || errorJson.code || "UpstreamError";
12739
- if (status === 401 || code === "Unauthorized" || typeof details === "string" && (details.includes("login") || details.includes("session"))) {
12856
+ const detailsStr = typeof details === "string" ? details : "";
12857
+ const codeStr = String(code).toLowerCase();
12858
+ const detailsLower = detailsStr.toLowerCase();
12859
+ const isAuthExpired = status === 401 || codeStr.includes("unauthorized") || detailsLower.includes("token has expired") || detailsLower.includes("log in") || detailsLower.includes("login") || detailsLower.includes("session") || detailsLower.includes("n\xE3o autorizado") || detailsLower.includes("unauthorized");
12860
+ if (isAuthExpired) {
12740
12861
  return new QwenSessionExpiredError(
12741
- `Session expired: ${details}`,
12862
+ `Session expired: ${detailsStr || codeStr}`,
12742
12863
  accountId || "global"
12743
12864
  );
12744
12865
  }
@@ -13277,8 +13398,24 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
13277
13398
  )
13278
13399
  );
13279
13400
  }
13401
+ const parsedJsonErr = parseQwenJsonError(errText, response.status, accountId);
13402
+ if (parsedJsonErr instanceof QwenSessionExpiredError && accountId && !retriedNonSseResponse) {
13403
+ retriedNonSseResponse = true;
13404
+ logger.warn(
13405
+ "[Qwen] Token expired on completion fetch (200 JSON); refreshing session and retrying",
13406
+ { accountId, chatId: chatSessionId ?? "new" }
13407
+ );
13408
+ const { refreshAccountToken: refreshAccountToken2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
13409
+ const refreshed = await refreshAccountToken2(accountId);
13410
+ if (refreshed.success) {
13411
+ const fresh = await getQwenHeaders(false, accountId);
13412
+ activeHeaders = fresh.headers;
13413
+ response = await fetchCompletion(activeHeaders);
13414
+ continue;
13415
+ }
13416
+ }
13280
13417
  throw withCreatedChatMetadata(
13281
- parseQwenJsonError(errText, response.status, accountId) ?? new QwenUpstreamError(
13418
+ parsedJsonErr ?? new QwenUpstreamError(
13282
13419
  `Qwen returned non-stream JSON response: ${errText.substring(0, 300)}`,
13283
13420
  "NonStreamJsonResponse",
13284
13421
  502
@@ -13329,6 +13466,31 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
13329
13466
  response.status,
13330
13467
  accountId
13331
13468
  );
13469
+ if (parsedError instanceof QwenSessionExpiredError && accountId && !retriedNonSseResponse) {
13470
+ retriedNonSseResponse = true;
13471
+ logger.warn(
13472
+ "[Qwen] Token expired on completion fetch (!response.ok); refreshing session and retrying",
13473
+ { accountId, chatId: chatSessionId ?? "new", status: response.status }
13474
+ );
13475
+ const { refreshAccountToken: refreshAccountToken2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
13476
+ const refreshed = await refreshAccountToken2(accountId);
13477
+ if (refreshed.success) {
13478
+ const fresh = await getQwenHeaders(false, accountId);
13479
+ activeHeaders = fresh.headers;
13480
+ response = await fetchCompletion(activeHeaders);
13481
+ if (response.ok && response.body) {
13482
+ return {
13483
+ stream: wrapUpstreamStream(response.body, controller),
13484
+ headers: activeHeaders,
13485
+ uiSessionId: chatSessionId || "",
13486
+ controller,
13487
+ accountId: accountId ?? "global",
13488
+ createdNewChat,
13489
+ tokenEstimationContext
13490
+ };
13491
+ }
13492
+ }
13493
+ }
13332
13494
  if (parsedError) {
13333
13495
  throw withCreatedChatMetadata(parsedError);
13334
13496
  }
@@ -13366,7 +13528,7 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
13366
13528
  clearTimeout(timeoutId);
13367
13529
  }
13368
13530
  }
13369
- 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, DISABLE_TOOLS_MAX_RETRIES, DISABLE_TOOLS_BACKOFF_MS, UPSTREAM_RESPONSE_PREVIEW_BYTES;
13531
+ 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;
13370
13532
  var init_qwen = __esm({
13371
13533
  "src/services/qwen.ts"() {
13372
13534
  "use strict";
@@ -13453,6 +13615,7 @@ var init_qwen = __esm({
13453
13615
  }
13454
13616
  };
13455
13617
  QWEN_SAFE_SETTINGS_HASH = crypto7.createHash("sha256").update(JSON.stringify(QWEN_SAFE_SETTINGS_PATCH)).digest("hex").slice(0, 12);
13618
+ inFlightPersonalizationSyncs = /* @__PURE__ */ new Map();
13456
13619
  DISABLE_TOOLS_MAX_RETRIES = 3;
13457
13620
  DISABLE_TOOLS_BACKOFF_MS = 2e3;
13458
13621
  UPSTREAM_RESPONSE_PREVIEW_BYTES = 8 * 1024;
@@ -16385,8 +16548,9 @@ function resolveInitialAccount(preferredAccountId, excludeAccountIds) {
16385
16548
  syncCooldownsFromDb(configuredAccounts);
16386
16549
  const excluded = new Set(excludeAccountIds ?? []);
16387
16550
  if (typeof preferredAccountId === "string" && preferredAccountId) {
16551
+ const needle = preferredAccountId.toLowerCase();
16388
16552
  const preferred = configuredAccounts.find(
16389
- (candidate) => candidate.id === preferredAccountId
16553
+ (candidate) => candidate.id === preferredAccountId || candidate.id.toLowerCase().startsWith(needle) || candidate.email.toLowerCase() === needle || candidate.email.toLowerCase().includes(needle)
16390
16554
  );
16391
16555
  if (preferred && !getAccountCooldownInfo(preferred.id)) {
16392
16556
  return { account: preferred, configuredAccounts };
@@ -16741,7 +16905,12 @@ function shouldWaitQueueForever(isThreadOwnerWaiting, heldByOtherSession, hasFre
16741
16905
  }
16742
16906
  async function tryCreateStreamWithRetry(params, accountId, accountEmail) {
16743
16907
  const maxAttempts = Math.max(1, config.retry.maxAttempts);
16744
- const maxAccountSwitches = Math.max(0, config.retry.maxAccountSwitches);
16908
+ const accounts = loadAccounts();
16909
+ const configuredAccountCount = accounts.length;
16910
+ const maxAccountSwitches = Math.max(
16911
+ config.retry.maxAccountSwitches,
16912
+ Math.min(configuredAccountCount - 1, 6)
16913
+ );
16745
16914
  let attemptsLeft = maxAttempts;
16746
16915
  let retryDelay = config.retry.baseDelayMs;
16747
16916
  let attempt = 0;
@@ -16753,7 +16922,6 @@ async function tryCreateStreamWithRetry(params, accountId, accountEmail) {
16753
16922
  let chatInProgressOriginAccountEmail = null;
16754
16923
  let lastAttemptError = null;
16755
16924
  let invalidInputSameAccountRetried = false;
16756
- const accounts = loadAccounts();
16757
16925
  const isSingleAccount = accounts.length <= 1;
16758
16926
  let currentAccountId = accountId;
16759
16927
  let currentAccountEmail = accountEmail;
@@ -16966,14 +17134,24 @@ ${loopReminder}` : truncation.prompt;
16966
17134
  const acquireDeadlineMs = config.concurrency.acquireDeadlineMs;
16967
17135
  let acquireDeadlineTimer;
16968
17136
  const acquireDeadline = new Promise((_, reject) => {
16969
- acquireDeadlineTimer = setTimeout(() => {
17137
+ const checkDeadline = async () => {
17138
+ try {
17139
+ const { isCaptchaRecoveryActive: isCaptchaRecoveryActive2 } = await Promise.resolve().then(() => (init_captcha_coordinator(), captcha_coordinator_exports));
17140
+ if (currentAccountId && isCaptchaRecoveryActive2(currentAccountId)) {
17141
+ acquireDeadlineTimer = setTimeout(checkDeadline, 3e4);
17142
+ acquireDeadlineTimer.unref?.();
17143
+ return;
17144
+ }
17145
+ } catch {
17146
+ }
16970
17147
  acquireAbort.abort();
16971
17148
  const err2 = new Error(
16972
17149
  `Acquire deadline (${acquireDeadlineMs}ms) exceeded creating stream on ${currentAccountEmail}`
16973
17150
  );
16974
17151
  err2.code = "acquire_deadline";
16975
17152
  reject(err2);
16976
- }, acquireDeadlineMs);
17153
+ };
17154
+ acquireDeadlineTimer = setTimeout(checkDeadline, acquireDeadlineMs);
16977
17155
  acquireDeadlineTimer.unref?.();
16978
17156
  });
16979
17157
  result = await Promise.race([
@@ -17106,16 +17284,16 @@ ${loopReminder}` : truncation.prompt;
17106
17284
  error: new Error("Failed to create Qwen stream")
17107
17285
  };
17108
17286
  }
17109
- if (err instanceof QwenSessionExpiredError || err.name === "QwenSessionExpiredError") {
17287
+ const isSessionExpired = err instanceof QwenSessionExpiredError || err?.name === "QwenSessionExpiredError" || err instanceof Error && (err.message.toLowerCase().includes("token has expired") || err.message.toLowerCase().includes("session expired") || err.message.toLowerCase().includes("401 n\xE3o autorizado") || err.message.toLowerCase().includes("unauthorized"));
17288
+ if (isSessionExpired) {
17110
17289
  console.warn(
17111
- `\u{1F504} [Chat] Session expired for ${currentAccountEmail} (${currentAccountId}). Attempting re-login...`
17290
+ `\u{1F504} [Chat] Session expired for ${currentAccountEmail} (${currentAccountId}). Attempting silent token refresh/re-login...`
17112
17291
  );
17113
17292
  const reLoginOk = await attemptRelogin(
17114
17293
  currentAccountId,
17115
17294
  currentAccountEmail
17116
17295
  );
17117
17296
  if (reLoginOk) continue;
17118
- return { success: false, error: err };
17119
17297
  }
17120
17298
  if (isAccountUnavailableError(err)) {
17121
17299
  const quotaMsg = err.message || "Unknown quota error";
@@ -17255,6 +17433,10 @@ ${loopReminder}` : truncation.prompt;
17255
17433
  currentAccountId = nextAccount.id;
17256
17434
  currentAccountEmail = maskEmail(nextAccount.email);
17257
17435
  accountSwitches++;
17436
+ const isRealQuotaError = policy.reason === "quota_or_rate_limit" && (policy.accountCooldownReason === "RateLimited" || policy.accountCooldownReason === "QuotaExceeded");
17437
+ if (isRealQuotaError) {
17438
+ attemptsLeft = Math.max(attemptsLeft, 1);
17439
+ }
17258
17440
  if (params.useThreadNative) {
17259
17441
  const abandonedChatId = params.existingThread?.chatSessionId;
17260
17442
  if (abandonedChatId) {
@@ -19875,6 +20057,199 @@ var init_token_estimation_metrics = __esm({
19875
20057
  }
19876
20058
  });
19877
20059
 
20060
+ // src/core/daily-usage.ts
20061
+ var daily_usage_exports = {};
20062
+ __export(daily_usage_exports, {
20063
+ DAILY_ACCOUNT_TOKEN_BASELINE: () => DAILY_ACCOUNT_TOKEN_BASELINE,
20064
+ MODEL_DAILY_OUTPUT_BASELINES: () => MODEL_DAILY_OUTPUT_BASELINES,
20065
+ STANDARD_DAILY_TOKEN_BASELINE: () => STANDARD_DAILY_TOKEN_BASELINE,
20066
+ getAccountDailyUsage: () => getAccountDailyUsage,
20067
+ getAllAccountsDailyUsage: () => getAllAccountsDailyUsage,
20068
+ getModelQuotaWeight: () => getModelQuotaWeight,
20069
+ getPoolDailyUsageSummary: () => getPoolDailyUsageSummary,
20070
+ getUtcDateString: () => getUtcDateString,
20071
+ recordTurnUsage: () => recordTurnUsage
20072
+ });
20073
+ function getModelQuotaWeight(model) {
20074
+ if (!model) return 1;
20075
+ const resolved = mapClientModelToQwen(model).toLowerCase();
20076
+ if (resolved.includes("max")) {
20077
+ return STANDARD_DAILY_TOKEN_BASELINE / MODEL_DAILY_OUTPUT_BASELINES.max;
20078
+ }
20079
+ if (resolved.includes("omni") || resolved.includes("flash")) {
20080
+ return STANDARD_DAILY_TOKEN_BASELINE / MODEL_DAILY_OUTPUT_BASELINES.omni;
20081
+ }
20082
+ return 1;
20083
+ }
20084
+ function getUtcDateString(timestampMs = Date.now()) {
20085
+ return new Date(timestampMs).toISOString().slice(0, 10);
20086
+ }
20087
+ function recordTurnUsage(accountId, usage, modelOrDate, maybeDateUtc) {
20088
+ if (!accountId || accountId === "global") return;
20089
+ let model;
20090
+ let dateUtc;
20091
+ if (modelOrDate && /^\d{4}-\d{2}-\d{2}$/.test(modelOrDate)) {
20092
+ dateUtc = modelOrDate;
20093
+ model = void 0;
20094
+ } else {
20095
+ model = modelOrDate;
20096
+ dateUtc = maybeDateUtc || getUtcDateString();
20097
+ }
20098
+ const prompt = Math.max(0, Number(usage.prompt_tokens) || 0);
20099
+ const completion = Math.max(0, Number(usage.completion_tokens) || 0);
20100
+ const total = Math.max(
20101
+ prompt + completion,
20102
+ Number(usage.total_tokens) || prompt + completion
20103
+ );
20104
+ const weight = getModelQuotaWeight(model);
20105
+ const weighted = completion * weight;
20106
+ try {
20107
+ const db2 = getDatabase();
20108
+ db2.prepare(
20109
+ `
20110
+ INSERT INTO account_daily_usage (
20111
+ account_id, date_utc, prompt_tokens, completion_tokens, weighted_tokens, total_tokens, turns_count, updated_at
20112
+ ) VALUES (
20113
+ ?, ?, ?, ?, ?, ?, 1, datetime('now')
20114
+ )
20115
+ ON CONFLICT(account_id, date_utc) DO UPDATE SET
20116
+ prompt_tokens = prompt_tokens + excluded.prompt_tokens,
20117
+ completion_tokens = completion_tokens + excluded.completion_tokens,
20118
+ weighted_tokens = weighted_tokens + excluded.weighted_tokens,
20119
+ total_tokens = total_tokens + excluded.total_tokens,
20120
+ turns_count = turns_count + 1,
20121
+ updated_at = datetime('now')
20122
+ `
20123
+ ).run(accountId, dateUtc, prompt, completion, weighted, total);
20124
+ } catch (err) {
20125
+ }
20126
+ }
20127
+ function getAccountDailyUsage(accountId, dateUtc = getUtcDateString()) {
20128
+ try {
20129
+ const db2 = getDatabase();
20130
+ const row = db2.prepare(
20131
+ `SELECT prompt_tokens, completion_tokens, weighted_tokens, total_tokens, turns_count
20132
+ FROM account_daily_usage
20133
+ WHERE account_id = ? AND date_utc = ?`
20134
+ ).get(accountId, dateUtc);
20135
+ const promptTokens = Number(row?.prompt_tokens) || 0;
20136
+ const completionTokens = Number(row?.completion_tokens) || 0;
20137
+ const weightedTokens = Number(row?.weighted_tokens) || completionTokens;
20138
+ const totalTokens = Number(row?.total_tokens) || 0;
20139
+ const turnsCount = Number(row?.turns_count) || 0;
20140
+ const usagePercent = Math.min(
20141
+ 100,
20142
+ Math.round(weightedTokens / STANDARD_DAILY_TOKEN_BASELINE * 100)
20143
+ );
20144
+ return {
20145
+ accountId,
20146
+ dateUtc,
20147
+ promptTokens,
20148
+ completionTokens,
20149
+ weightedTokens,
20150
+ totalTokens,
20151
+ turnsCount,
20152
+ usagePercent
20153
+ };
20154
+ } catch {
20155
+ return {
20156
+ accountId,
20157
+ dateUtc,
20158
+ promptTokens: 0,
20159
+ completionTokens: 0,
20160
+ weightedTokens: 0,
20161
+ totalTokens: 0,
20162
+ turnsCount: 0,
20163
+ usagePercent: 0
20164
+ };
20165
+ }
20166
+ }
20167
+ function getAllAccountsDailyUsage(dateUtc = getUtcDateString()) {
20168
+ const result = /* @__PURE__ */ new Map();
20169
+ try {
20170
+ const db2 = getDatabase();
20171
+ const rows = db2.prepare(
20172
+ `SELECT account_id, prompt_tokens, completion_tokens, weighted_tokens, total_tokens, turns_count
20173
+ FROM account_daily_usage
20174
+ WHERE date_utc = ?`
20175
+ ).all(dateUtc);
20176
+ for (const row of rows) {
20177
+ const promptTokens = Number(row.prompt_tokens) || 0;
20178
+ const completionTokens = Number(row.completion_tokens) || 0;
20179
+ const weightedTokens = Number(row.weighted_tokens) || completionTokens;
20180
+ const totalTokens = Number(row.total_tokens) || 0;
20181
+ const turnsCount = Number(row.turns_count) || 0;
20182
+ const usagePercent = Math.min(
20183
+ 100,
20184
+ Math.round(weightedTokens / STANDARD_DAILY_TOKEN_BASELINE * 100)
20185
+ );
20186
+ result.set(row.account_id, {
20187
+ accountId: row.account_id,
20188
+ dateUtc,
20189
+ promptTokens,
20190
+ completionTokens,
20191
+ weightedTokens,
20192
+ totalTokens,
20193
+ turnsCount,
20194
+ usagePercent
20195
+ });
20196
+ }
20197
+ } catch {
20198
+ }
20199
+ return result;
20200
+ }
20201
+ function getPoolDailyUsageSummary(targetAccountIds, dateUtc = getUtcDateString()) {
20202
+ const accounts = targetAccountIds && targetAccountIds.length > 0 ? targetAccountIds : loadAccounts().map((a) => a.id);
20203
+ const accountCount = Math.max(1, accounts.length);
20204
+ const poolCapacityTokens = accountCount * STANDARD_DAILY_TOKEN_BASELINE;
20205
+ const usageMap = getAllAccountsDailyUsage(dateUtc);
20206
+ let totalPromptTokens = 0;
20207
+ let totalCompletionTokens = 0;
20208
+ let totalWeightedTokens = 0;
20209
+ let totalTokens = 0;
20210
+ let totalTurns = 0;
20211
+ for (const id of accounts) {
20212
+ const usage = usageMap.get(id);
20213
+ if (usage) {
20214
+ totalPromptTokens += usage.promptTokens;
20215
+ totalCompletionTokens += usage.completionTokens;
20216
+ totalWeightedTokens += usage.weightedTokens;
20217
+ totalTokens += usage.totalTokens;
20218
+ totalTurns += usage.turnsCount;
20219
+ }
20220
+ }
20221
+ const poolUsagePercent = Math.min(
20222
+ 100,
20223
+ Math.round(totalWeightedTokens / poolCapacityTokens * 100)
20224
+ );
20225
+ return {
20226
+ dateUtc,
20227
+ totalPromptTokens,
20228
+ totalCompletionTokens,
20229
+ totalWeightedTokens,
20230
+ totalTokens,
20231
+ totalTurns,
20232
+ poolCapacityTokens,
20233
+ poolUsagePercent
20234
+ };
20235
+ }
20236
+ var STANDARD_DAILY_TOKEN_BASELINE, DAILY_ACCOUNT_TOKEN_BASELINE, MODEL_DAILY_OUTPUT_BASELINES;
20237
+ var init_daily_usage = __esm({
20238
+ "src/core/daily-usage.ts"() {
20239
+ "use strict";
20240
+ init_database();
20241
+ init_accounts();
20242
+ init_model_alias();
20243
+ STANDARD_DAILY_TOKEN_BASELINE = 5e5;
20244
+ DAILY_ACCOUNT_TOKEN_BASELINE = STANDARD_DAILY_TOKEN_BASELINE;
20245
+ MODEL_DAILY_OUTPUT_BASELINES = {
20246
+ max: 28e4,
20247
+ plus: 5e5,
20248
+ omni: 62e4
20249
+ };
20250
+ }
20251
+ });
20252
+
19878
20253
  // src/routes/chat/streaming.ts
19879
20254
  import { stream as honoStream } from "hono/streaming";
19880
20255
  function firstString(...values) {
@@ -20380,6 +20755,9 @@ Please retry your tool call(s) with correct JSON and valid tool names from the a
20380
20755
  mode: "non-stream",
20381
20756
  context: currentTokenEstimationContext
20382
20757
  });
20758
+ if (activeAccountId && usage) {
20759
+ recordTurnUsage(activeAccountId, usage, body.model);
20760
+ }
20383
20761
  if (pendingParentId) {
20384
20762
  rememberParent(pendingParentId);
20385
20763
  }
@@ -20475,6 +20853,8 @@ async function processStreamingResponse(params) {
20475
20853
  let activeReader = null;
20476
20854
  let invalidInputSameAccountRetries = 0;
20477
20855
  let chatInProgressSameAccountRetries = 0;
20856
+ const totalConfiguredAccounts = loadAccounts().length;
20857
+ let quotaRotationsLeft = Math.min(Math.max(1, totalConfiguredAccounts - 1), 6);
20478
20858
  let streamCompletedOk = false;
20479
20859
  let stoppedByToolCap = false;
20480
20860
  const runDisconnectTeardown = () => {
@@ -20832,13 +21212,18 @@ async function processStreamingResponse(params) {
20832
21212
  reason: "network"
20833
21213
  }
20834
21214
  ) : null;
20835
- if (!normalizedError || clientDisconnected && !gracePending || c.req.raw.signal.aborted || retryContext.retriesLeft <= 0 || !midStreamRetry || emittedModelOutput) {
21215
+ if (!normalizedError || clientDisconnected && !gracePending || c.req.raw.signal.aborted || !midStreamRetry || emittedModelOutput) {
20836
21216
  return false;
20837
21217
  }
20838
21218
  const policy = classifyRetryAction(normalizedError, {
20839
21219
  requestAborted: c.req.raw.signal.aborted
20840
21220
  });
20841
21221
  if (!policy.retryable) return false;
21222
+ const isRealQuotaError = policy.reason === "quota_or_rate_limit" && (policy.accountCooldownReason === "RateLimited" || policy.accountCooldownReason === "QuotaExceeded");
21223
+ const hasBudget = isRealQuotaError ? quotaRotationsLeft > 0 : retryContext.retriesLeft > 0;
21224
+ if (!hasBudget) {
21225
+ return false;
21226
+ }
20842
21227
  if (logger.isLevelEnabled("info")) {
20843
21228
  console.log(
20844
21229
  `\u{1F9ED} [Chat] Stream recovery policy | account=${currentAccountId} | reason=${policy.reason} | retryable=${policy.retryable} | switch=${policy.switchAccount} | newChat=${policy.forceNewChat} | fullPrompt=${policy.retryWithFullPrompt} | retryAfter=${policy.retryAfterMs}ms`
@@ -20847,9 +21232,13 @@ async function processStreamingResponse(params) {
20847
21232
  if (policy.reason === "corrupted_chat_history") {
20848
21233
  invalidateLogicalThreadParent(midStreamRetry.sessionId);
20849
21234
  }
20850
- retryContext.retriesLeft--;
21235
+ if (isRealQuotaError) {
21236
+ quotaRotationsLeft--;
21237
+ } else {
21238
+ retryContext.retriesLeft--;
21239
+ }
20851
21240
  console.warn(
20852
- `\u{1F504} [Chat] Stream recovery | account=${currentAccountId} | reason=${policy.reason} | error=${normalizedError.message.substring(0, 150)} | retries_left=${retryContext.retriesLeft}`
21241
+ `\u{1F504} [Chat] Stream recovery | account=${currentAccountId} | reason=${policy.reason} | error=${normalizedError.message.substring(0, 150)} | retries_left=${isRealQuotaError ? quotaRotationsLeft : retryContext.retriesLeft}`
20853
21242
  );
20854
21243
  const retryInvalidInputOnSameAccount = shouldRetryInvalidInputOnSameAccount(
20855
21244
  policy.reason,
@@ -21730,6 +22119,9 @@ Please retry your tool call(s) with correct JSON and valid tool names from the a
21730
22119
  mode: "stream",
21731
22120
  context: currentTokenEstimationContext
21732
22121
  });
22122
+ if (activeAccountId && usage) {
22123
+ recordTurnUsage(activeAccountId, usage, body.model);
22124
+ }
21733
22125
  } else {
21734
22126
  if (isToolcallDebugEnabled()) {
21735
22127
  logger.debug(
@@ -21935,6 +22327,7 @@ var init_streaming = __esm({
21935
22327
  init_qwen_url();
21936
22328
  init_qwen();
21937
22329
  init_account();
22330
+ init_accounts();
21938
22331
  init_account_manager();
21939
22332
  init_account_concurrency();
21940
22333
  init_retry_policy();
@@ -21950,6 +22343,7 @@ var init_streaming = __esm({
21950
22343
  init_errors2();
21951
22344
  init_retry_policy();
21952
22345
  init_token_estimation_metrics();
22346
+ init_daily_usage();
21953
22347
  init_context_meter();
21954
22348
  init_helpers();
21955
22349
  MAX_INITIAL_PROTOCOL_BYTES = 64 * 1024;
@@ -22389,7 +22783,7 @@ async function chatCompletions(c) {
22389
22783
  updateLogicalThread: parallelEscape ? false : ctx.updateLogicalThread,
22390
22784
  allowThreadReuse: ctx.allowThreadReuse,
22391
22785
  forceNewChat: parallelEscape,
22392
- preferredAccountId: void 0,
22786
+ preferredAccountId: c.req.header("x-qwenproxy-account") || void 0,
22393
22787
  messageCount: msgCount,
22394
22788
  fullMessageCount: parsed.messageCount,
22395
22789
  toolsCount: declaredTools.length || void 0,
@@ -25891,7 +26285,7 @@ function getAppVersion() {
25891
26285
  }
25892
26286
  } catch {
25893
26287
  }
25894
- cachedVersion = "v1.5.2";
26288
+ cachedVersion = "v1.6.0";
25895
26289
  return cachedVersion;
25896
26290
  }
25897
26291
  var cachedVersion;
@@ -26600,7 +26994,8 @@ var init_server = __esm({
26600
26994
  captchasDetected: Number(metrics.get("captcha.challenges.detected")?.value ?? 0),
26601
26995
  captchasSolved: Number(metrics.get("captcha.solves.succeeded")?.value ?? 0),
26602
26996
  chatsCleaned: Number(metrics.get("chats.cleaned")?.value ?? 0)
26603
- }
26997
+ },
26998
+ dailyUsage: (await Promise.resolve().then(() => (init_daily_usage(), daily_usage_exports))).getPoolDailyUsageSummary()
26604
26999
  });
26605
27000
  });
26606
27001
  app5.get("/diagnostics/tokens", async (c) => {
@@ -27362,12 +27757,17 @@ async function fetchProxyStatus() {
27362
27757
  }
27363
27758
  } catch {
27364
27759
  }
27760
+ const dailyUsageMap = getAllAccountsDailyUsage();
27365
27761
  const accounts = cachedAccounts.map((acc) => {
27366
27762
  const concurrency = concurrencyMap.get(acc.id);
27763
+ const daily = dailyUsageMap.get(acc.id);
27367
27764
  return {
27368
27765
  ...acc,
27369
27766
  activeStreams: concurrency?.active ?? 0,
27370
- streamLimit: concurrency?.limit ?? config.concurrency.maxStreamsPerAccount
27767
+ streamLimit: concurrency?.limit ?? config.concurrency.maxStreamsPerAccount,
27768
+ dailyTokens: daily?.completionTokens ?? 0,
27769
+ dailyUsagePercent: daily?.usagePercent ?? 0,
27770
+ dailyTurns: daily?.turnsCount ?? 0
27371
27771
  };
27372
27772
  });
27373
27773
  let rssMb = 0;
@@ -27426,6 +27826,7 @@ async function fetchProxyStatus() {
27426
27826
  cacheHitRatio,
27427
27827
  cacheBytesSaved
27428
27828
  },
27829
+ dailyUsage: getPoolDailyUsageSummary(cachedAccounts.map((a) => a.id)),
27429
27830
  accounts
27430
27831
  };
27431
27832
  }
@@ -27566,6 +27967,7 @@ var init_proxy_client = __esm({
27566
27967
  init_account_concurrency();
27567
27968
  init_memory_usage();
27568
27969
  init_metrics();
27970
+ init_daily_usage();
27569
27971
  init_model_alias();
27570
27972
  init_model_alias();
27571
27973
  cachedAccounts = [];
@@ -28296,12 +28698,24 @@ var init_status_view = __esm({
28296
28698
  const poolPct = accounts.length > 0 ? Math.round(availableCount / accounts.length * 100) : 0;
28297
28699
  const poolColor = poolPct >= 70 ? theme.green : poolPct >= 40 ? theme.yellow : theme.red;
28298
28700
  const poolBar = renderProgressBar(poolPct, 8, poolColor);
28701
+ const poolDailyUsage = data?.dailyUsage;
28702
+ const poolUsagePct = poolDailyUsage?.poolUsagePercent ?? 0;
28703
+ const poolUsageColor = poolUsagePct >= 80 ? theme.red : poolUsagePct >= 40 ? theme.yellow : theme.cyan;
28704
+ const poolUsageBar = renderProgressBar(poolUsagePct, 8, poolUsageColor);
28299
28705
  const innerRightW = Math.max(30, rightW - 2);
28300
- const emailWidth = Math.max(16, Math.min(20, innerRightW - 27));
28706
+ const showUsageCol = innerRightW >= 52;
28707
+ const emailWidth = showUsageCol ? Math.max(14, Math.min(18, innerRightW - 33)) : Math.max(16, Math.min(20, innerRightW - 27));
28301
28708
  const rightContent = [
28302
28709
  ` ${theme.bold("Disponibilidade:")} [${poolBar}] ${poolColor(`${availableCount}/${accounts.length} (${poolPct}%)`)}`,
28710
+ ...poolDailyUsage ? [
28711
+ ` ${theme.bold("Cota Pool Hoje:")} [${poolUsageBar}] ${poolUsageColor(
28712
+ `${poolUsagePct}%`
28713
+ )} ${theme.dim(
28714
+ `(~${Math.round(poolDailyUsage.totalTokens / 1e3)}k tok)`
28715
+ )}`
28716
+ ] : [],
28303
28717
  ` ${theme.dim("\u2500".repeat(Math.max(32, innerRightW - 2)))}`,
28304
- ` ${theme.dim(`# ${pad("Conta", emailWidth)} Carga Status`)}`,
28718
+ ` ${theme.dim(`# ${pad("Conta", emailWidth)} Carga ${showUsageCol ? "Uso% " : ""}Status`)}`,
28305
28719
  ` ${theme.dim("\u2500".repeat(Math.max(32, innerRightW - 2)))}`
28306
28720
  ];
28307
28721
  if (accounts.length === 0) {
@@ -28313,7 +28727,17 @@ var init_status_view = __esm({
28313
28727
  const name = pad(truncate(acc.emailOrName, emailWidth - 1), emailWidth);
28314
28728
  const active = acc.activeStreams || 0;
28315
28729
  const limit = acc.streamLimit || 1;
28316
- const loadBadge = active > 0 ? theme.yellow(`[${active}/${limit}]`) : theme.dim(`[0/${limit}]`);
28730
+ const usagePct = acc.dailyUsagePercent ?? 0;
28731
+ let loadBadge;
28732
+ if (active > 0) {
28733
+ loadBadge = theme.yellow(`[${active}/${limit}]`);
28734
+ } else if (!showUsageCol && usagePct > 0) {
28735
+ const pctColor = usagePct >= 80 ? theme.red : usagePct >= 40 ? theme.yellow : theme.cyan;
28736
+ loadBadge = pctColor(pad(`[${usagePct}%]`, 5));
28737
+ } else {
28738
+ loadBadge = theme.dim(`[0/${limit}]`);
28739
+ }
28740
+ const usageCol = showUsageCol ? (usagePct > 0 ? usagePct >= 80 ? theme.red(pad(`${usagePct}%`, 5)) : usagePct >= 40 ? theme.yellow(pad(`${usagePct}%`, 5)) : theme.cyan(pad(`${usagePct}%`, 5)) : theme.dim(pad("0%", 5))) + " " : "";
28317
28741
  let status = theme.green(`${glyphs.bullet} Pronto`);
28318
28742
  if (active > 0 && !acc.onCooldown && acc.headersReady) {
28319
28743
  status = theme.yellow(`\u25CF Gerando `);
@@ -28330,7 +28754,7 @@ var init_status_view = __esm({
28330
28754
  } else if (!acc.headersReady) {
28331
28755
  status = acc.isInitialized ? theme.yellow(`\u25D0 Aquecendo...`) : theme.muted(`\u25CB Standby`);
28332
28756
  }
28333
- rightContent.push(` ${num}${name} ${loadBadge} ${status}`);
28757
+ rightContent.push(` ${num}${name} ${loadBadge} ${usageCol}${status}`);
28334
28758
  });
28335
28759
  }
28336
28760
  const summaryParts = [];
@@ -32712,6 +33136,7 @@ var init_accounts_view = __esm({
32712
33136
  init_accounts();
32713
33137
  init_server_manager();
32714
33138
  init_config();
33139
+ init_status_view();
32715
33140
  AccountsView = class {
32716
33141
  id = "accounts";
32717
33142
  title = "Contas";
@@ -33477,9 +33902,11 @@ var init_accounts_view = __esm({
33477
33902
  if (this.selectedIndex >= accounts.length && accounts.length > 0) {
33478
33903
  this.selectedIndex = accounts.length - 1;
33479
33904
  }
33905
+ const showUsageCol = leftW >= 48;
33906
+ const headerUsage = showUsageCol ? "Uso% " : "";
33480
33907
  const leftContent = [
33481
33908
  "",
33482
- ` ${theme.dim("# Conta Status")}`,
33909
+ ` ${theme.dim(`# Conta ${headerUsage}Status`)}`,
33483
33910
  ` ${theme.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}`
33484
33911
  ];
33485
33912
  if (accounts.length === 0) {
@@ -33524,7 +33951,9 @@ var init_accounts_view = __esm({
33524
33951
  } else if (!acc.headersReady) {
33525
33952
  status = acc.isInitialized ? theme.yellow(`\u25D0 Aquecendo...`) : theme.muted(`\u25CB Standby `);
33526
33953
  }
33527
- const line = `${pointer}${num}${name}${status}`;
33954
+ const usagePct = acc.dailyUsagePercent ?? 0;
33955
+ const usagePctStr = showUsageCol ? (usagePct > 0 ? usagePct >= 80 ? theme.red(pad(`${usagePct}%`, 5)) : usagePct >= 40 ? theme.yellow(pad(`${usagePct}%`, 5)) : theme.cyan(pad(`${usagePct}%`, 5)) : theme.dim(pad("0%", 5))) + " " : "";
33956
+ const line = `${pointer}${num}${name}${usagePctStr}${status}`;
33528
33957
  if (isHovered) {
33529
33958
  leftContent.push(theme.bgHover(line));
33530
33959
  } else if (isFocused) {
@@ -33565,6 +33994,12 @@ var init_accounts_view = __esm({
33565
33994
  rightContent.push(` ${theme.bold("Conta:")} ${theme.cyan(email)}`);
33566
33995
  rightContent.push(` ${theme.bold("Sistema ID:")} ${theme.muted(selected.id.slice(0, 14))}`);
33567
33996
  rightContent.push(` ${theme.bold("N\xEDvel:")} ${selected.priority}`);
33997
+ const usagePct = selected.dailyUsagePercent ?? 0;
33998
+ const usageTokens = selected.dailyTokens ?? 0;
33999
+ const usageTurns = selected.dailyTurns ?? 0;
34000
+ const usageColor = usagePct >= 80 ? theme.red : usagePct >= 40 ? theme.yellow : theme.cyan;
34001
+ const usageBar = renderProgressBar(usagePct, 6, usageColor);
34002
+ rightContent.push(` ${theme.bold("Cota Hoje:")} [${usageBar}] ${usageColor(`${usagePct}%`)} ${theme.dim(`(~${Math.round(usageTokens / 1e3)}k / ${usageTurns} reqs)`)}`);
33568
34003
  const cdStatus = selected.onCooldown ? theme.yellow(`[!] Cooldown ${Math.round(selected.remainingCooldownMs / 6e4)}m`) : theme.green(`${glyphs.check} Dispon\xEDvel`);
33569
34004
  rightContent.push(` ${theme.bold("Estado:")} ${cdStatus}`);
33570
34005
  const hStatus = selected.headersReady ? theme.green(`${glyphs.check} Capturados`) : selected.isInitialized ? theme.yellow(`\u25D0 Aquecendo...`) : theme.muted(`${glyphs.circle} Standby (Sob Demanda)`);