@pedrofariasx/qwenproxy 1.5.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pedrofariasx/qwenproxy",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "Local OpenAI-compatible proxy API that routes requests to Qwen (chat.qwen.ai) via Playwright browser automation.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/api/models.ts CHANGED
@@ -41,9 +41,9 @@ app.get('/v1/models', async (c) => {
41
41
  'X-Request-Id': crypto.randomUUID(),
42
42
  'source': 'web',
43
43
  'bx-v': bxV,
44
- 'sec-ch-ua': '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"',
44
+ 'sec-ch-ua': '"Chromium";v="137", "Google Chrome";v="137", "Not/A)Brand";v="99"',
45
45
  'sec-ch-ua-mobile': '?0',
46
- 'sec-ch-ua-platform': '"Linux"',
46
+ 'sec-ch-ua-platform': '"Windows"',
47
47
  'Timezone': new Date().toString(),
48
48
  'Cookie': cookie,
49
49
  },
@@ -125,9 +125,9 @@ app.get('/v1/models/:model', async (c) => {
125
125
  'X-Request-Id': crypto.randomUUID(),
126
126
  'source': 'web',
127
127
  'bx-v': bxV,
128
- 'sec-ch-ua': '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"',
128
+ 'sec-ch-ua': '"Chromium";v="137", "Google Chrome";v="137", "Not/A)Brand";v="99"',
129
129
  'sec-ch-ua-mobile': '?0',
130
- 'sec-ch-ua-platform': '"Linux"',
130
+ 'sec-ch-ua-platform': '"Windows"',
131
131
  'Timezone': new Date().toString(),
132
132
  'Cookie': cookie,
133
133
  },
@@ -61,19 +61,22 @@ function getAccountHeaderCache(accountId: string): AccountHeaderCache {
61
61
  return cache;
62
62
  }
63
63
 
64
- const HEADERS_TTL = 60 * 60 * 1000;
64
+ const HEADERS_TTL = 5 * 60 * 1000;
65
65
  const COOKIE_CACHE_TTL = 5 * 60 * 1000;
66
66
  const cookieCaches = new Map<string, { cookie: string, timestamp: number }>();
67
67
  const REFRESH_THRESHOLD = 0.7;
68
68
 
69
69
  const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
70
70
 
71
+ export const CHROME_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
72
+ export const CHROME_CLIENT_HINTS = '"Chromium";v="137", "Google Chrome";v="137", "Not/A)Brand";v="99"';
73
+
71
74
  function getStealthScript(): string {
72
75
  return `
76
+ try {
77
+ delete navigator.__proto__.webdriver;
78
+ } catch(e) {}
73
79
  Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
74
- Object.defineProperty(navigator, 'plugins', {
75
- get: () => [1, 2, 3, 4, 5],
76
- });
77
80
  Object.defineProperty(navigator, 'languages', {
78
81
  get: () => ['pt-BR', 'pt', 'en-US', 'en'],
79
82
  });
@@ -82,32 +85,153 @@ function getStealthScript(): string {
82
85
  Object.defineProperty(navigator, 'platform', { get: () => 'Win32' });
83
86
  Object.defineProperty(screen, 'colorDepth', { get: () => 24 });
84
87
  Object.defineProperty(screen, 'pixelDepth', { get: () => 24 });
88
+
85
89
  window.chrome = {
86
- runtime: {},
87
- loadTimes: function() {},
88
- csi: function() {},
89
- app: {},
90
+ runtime: { onConnect: {}, onMessage: {} },
91
+ loadTimes: function() { return {}; },
92
+ csi: function() { return {}; },
93
+ app: { isInstalled: false, InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' }, RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' } },
90
94
  };
95
+
91
96
  const originalQuery = window.navigator.permissions.query;
92
97
  window.navigator.permissions.query = (parameters) =>
93
98
  parameters.name === 'notifications'
94
- ? Promise.resolve({ state: Notification.permission })
99
+ ? Promise.resolve({ state: (typeof Notification !== 'undefined' ? Notification.permission : 'default'), onchange: null })
95
100
  : originalQuery(parameters);
101
+
96
102
  const getParameter = WebGLRenderingContext.prototype.getParameter;
97
103
  WebGLRenderingContext.prototype.getParameter = function(parameter) {
98
104
  if (parameter === 37445) return 'Intel Inc.';
99
105
  if (parameter === 37446) return 'Intel Iris OpenGL Engine';
100
106
  return getParameter.apply(this, arguments);
101
107
  };
108
+ if (typeof WebGL2RenderingContext !== 'undefined') {
109
+ const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
110
+ WebGL2RenderingContext.prototype.getParameter = function(parameter) {
111
+ if (parameter === 37445) return 'Intel Inc.';
112
+ if (parameter === 37446) return 'Intel Iris OpenGL Engine';
113
+ return getParameter2.apply(this, arguments);
114
+ };
115
+ }
116
+
102
117
  Object.defineProperty(navigator, 'connection', {
103
118
  get: () => ({
104
119
  effectiveType: '4g',
105
120
  rtt: 50,
106
121
  downlink: 10,
107
122
  saveData: false,
123
+ addEventListener: () => {},
124
+ removeEventListener: () => {},
108
125
  }),
109
126
  });
110
- delete navigator.__proto__.webdriver;
127
+
128
+ (function() {
129
+ function makeMime(desc, suffixes, type) {
130
+ const m = { description: desc, suffixes: suffixes, type: type };
131
+ return m;
132
+ }
133
+ const pdfMime = makeMime('Portable Document Format', 'pdf', 'application/pdf');
134
+ const pdfxMime = makeMime('Portable Document Format', 'pdf', 'text/pdf');
135
+ const pdfPlugin = {
136
+ name: 'PDF Viewer',
137
+ description: 'Portable Document Format',
138
+ filename: 'internal-pdf-viewer',
139
+ length: 2,
140
+ 0: pdfMime,
141
+ 1: pdfxMime,
142
+ };
143
+ pdfMime.enabledPlugin = pdfPlugin;
144
+ pdfxMime.enabledPlugin = pdfPlugin;
145
+
146
+ const chromePdfMime = makeMime('Portable Document Format', 'pdf', 'application/pdf');
147
+ const chromePdfMime2 = makeMime('Portable Document Format', 'pdf', 'text/pdf');
148
+ const chromePdfPlugin = {
149
+ name: 'Chrome PDF Viewer',
150
+ description: 'Portable Document Format',
151
+ filename: 'internal-pdf-viewer',
152
+ length: 2,
153
+ 0: chromePdfMime,
154
+ 1: chromePdfMime2,
155
+ };
156
+ chromePdfMime.enabledPlugin = chromePdfPlugin;
157
+ chromePdfMime2.enabledPlugin = chromePdfPlugin;
158
+
159
+ const nativePlugin = {
160
+ name: 'Native Client',
161
+ description: '',
162
+ filename: 'internal-nacl-plugin',
163
+ length: 2,
164
+ 0: makeMime('Native Client Executable', '', 'application/x-nacl'),
165
+ 1: makeMime('Portable Native Client Executable', '', 'application/x-pnacl'),
166
+ };
167
+ nativePlugin[0].enabledPlugin = nativePlugin;
168
+ nativePlugin[1].enabledPlugin = nativePlugin;
169
+
170
+ const pluginsList = [pdfPlugin, chromePdfPlugin, nativePlugin];
171
+ const mimeList = [pdfMime, pdfxMime, chromePdfMime, chromePdfMime2, nativePlugin[0], nativePlugin[1]];
172
+
173
+ function makeNamedNodeMap(items, namedEntries) {
174
+ const arr = [...items];
175
+ for (const [k, v] of namedEntries) arr[k] = v;
176
+ arr.item = function(i) { return this[i] || null; };
177
+ arr.namedItem = function(name) { return this[name] || null; };
178
+ arr.refresh = function() {};
179
+ return arr;
180
+ }
181
+
182
+ const pluginEntries = pluginsList.map((p, i) => [p.name, p]);
183
+ const mimeEntries = mimeList.map((m) => [m.type, m]);
184
+
185
+ const pluginsArr = makeNamedNodeMap(pluginsList, pluginEntries);
186
+ const mimeArr = makeNamedNodeMap(mimeList, mimeEntries);
187
+
188
+ Object.defineProperty(navigator, 'plugins', { get: () => pluginsArr });
189
+ Object.defineProperty(navigator, 'mimeTypes', { get: () => mimeArr });
190
+ })();
191
+
192
+ (function() {
193
+ const _toDataURL = HTMLCanvasElement.prototype.toDataURL;
194
+ const _toBlob = HTMLCanvasElement.prototype.toBlob;
195
+ const _getImageData = CanvasRenderingContext2D.prototype.getImageData;
196
+
197
+ function addNoise(canvas) {
198
+ try {
199
+ const ctx = canvas.getContext('2d');
200
+ if (!ctx) return;
201
+ const style = ctx.fillStyle;
202
+ ctx.fillStyle = 'rgba(255,255,255,0.01)';
203
+ ctx.fillRect(0, 0, 1, 1);
204
+ ctx.fillStyle = style;
205
+ } catch(e) {}
206
+ }
207
+
208
+ HTMLCanvasElement.prototype.toDataURL = function(...args) {
209
+ addNoise(this);
210
+ return _toDataURL.apply(this, args);
211
+ };
212
+ HTMLCanvasElement.prototype.toBlob = function(...args) {
213
+ addNoise(this);
214
+ return _toBlob.apply(this, args);
215
+ };
216
+ })();
217
+
218
+ (function() {
219
+ if (typeof OfflineAudioContext === 'undefined') return;
220
+ const _startRendering = OfflineAudioContext.prototype.startRendering;
221
+ OfflineAudioContext.prototype.startRendering = function() {
222
+ return _startRendering.apply(this, arguments).then(buffer => {
223
+ try {
224
+ for (let ch = 0; ch < buffer.numberOfChannels; ch++) {
225
+ const data = buffer.getChannelData(ch);
226
+ for (let i = 0; i < Math.min(data.length, 100); i++) {
227
+ data[i] += (Math.random() - 0.5) * 1e-7;
228
+ }
229
+ }
230
+ } catch(e) {}
231
+ return buffer;
232
+ });
233
+ };
234
+ })();
111
235
  `;
112
236
  }
113
237
 
@@ -228,7 +352,7 @@ export async function initPlaywright(headless = true, browserType: BrowserType =
228
352
  context = await engine.launchPersistentContext(profilePath, {
229
353
  headless,
230
354
  channel,
231
- userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
355
+ userAgent: CHROME_UA,
232
356
  ignoreDefaultArgs: ['--enable-automation'],
233
357
  args: [
234
358
  '--disable-blink-features=AutomationControlled',
@@ -371,7 +495,7 @@ export async function getGuestHeaders(): Promise<Record<string, string>> {
371
495
  guestContext = await engine.launchPersistentContext(profilePath, {
372
496
  headless: config.browser.headless,
373
497
  channel,
374
- userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
498
+ userAgent: CHROME_UA,
375
499
  ignoreDefaultArgs: ['--enable-automation'],
376
500
  args: ['--disable-blink-features=AutomationControlled', '--disable-features=IsolateOrigins,site-per-process', '--disable-infobars', '--no-first-run', '--no-default-browser-check']
377
501
  });
@@ -405,7 +529,7 @@ export async function getGuestHeaders(): Promise<Record<string, string>> {
405
529
  'bx-ua': reqHeaders['bx-ua'] || '',
406
530
  'bx-umidtoken': reqHeaders['bx-umidtoken'] || '',
407
531
  'bx-v': reqHeaders['bx-v'] || '2.5.36',
408
- 'user-agent': reqHeaders['user-agent'] || 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
532
+ 'user-agent': reqHeaders['user-agent'] || CHROME_UA,
409
533
  };
410
534
 
411
535
  if (extractedHeaders['bx-ua']) {
@@ -587,7 +711,7 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
587
711
  const isOnQwen = currentUrl.includes('chat.qwen.ai');
588
712
  const isOnSpecificChat = isOnQwen && /\/c\//.test(currentUrl);
589
713
 
590
- if (!isOnQwen || forceNew || isOnSpecificChat) {
714
+ if (!isOnQwen || forceNew) {
591
715
  console.log(`[Playwright] Navigating to Qwen home for ${cacheKey}... (Current: ${currentUrl})`);
592
716
  await page.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded' });
593
717
  }
@@ -756,7 +880,7 @@ export async function initPlaywrightForAccount(account: QwenAccount, headless =
756
880
  const acctContext = await engine.launchPersistentContext(profilePath, {
757
881
  headless,
758
882
  channel,
759
- userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
883
+ userAgent: CHROME_UA,
760
884
  ignoreDefaultArgs: ['--enable-automation'],
761
885
  args: [
762
886
  '--disable-blink-features=AutomationControlled',
@@ -807,7 +931,7 @@ export async function launchManualLoginAccount(accountId: string, browserType: B
807
931
  const acctContext = await engine.launchPersistentContext(profilePath, {
808
932
  headless: false,
809
933
  channel,
810
- userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
934
+ userAgent: CHROME_UA,
811
935
  ignoreDefaultArgs: ['--enable-automation'],
812
936
  args: [
813
937
  '--disable-blink-features=AutomationControlled',
@@ -890,3 +1014,234 @@ async function loginToQwenWithContext(acctContext: BrowserContext, acctPage: Pag
890
1014
  console.error(`[Playwright] Login failed for ${email}:`, result.data || result.error);
891
1015
  return false;
892
1016
  }
1017
+
1018
+ export function getPageForAccount(accountId?: string): Page | null {
1019
+ if (accountId === 'guest') return guestPage;
1020
+ if (accountId) return accountPages.get(accountId) || null;
1021
+ return activePage;
1022
+ }
1023
+
1024
+ const streamCallbacks = new Map<string, {
1025
+ onChunk: (chunk: string) => void;
1026
+ onEnd: () => void;
1027
+ onError: (msg: string) => void;
1028
+ onMeta: (meta: { status: number; statusText: string; contentType: string; headers: Record<string, string> }) => void;
1029
+ onBody: (body: string) => void;
1030
+ }>();
1031
+
1032
+ const abortControllers = new Map<string, () => void>();
1033
+
1034
+ const pagesWithExposed = new WeakSet<Page>();
1035
+
1036
+ async function ensureStreamBridge(page: Page): Promise<void> {
1037
+ if (pagesWithExposed.has(page)) return;
1038
+ pagesWithExposed.add(page);
1039
+ await page.exposeFunction('__streamRelay', (reqId: string, type: string, data: any) => {
1040
+ const cb = streamCallbacks.get(reqId);
1041
+ if (!cb) return;
1042
+ switch (type) {
1043
+ case 'meta': cb.onMeta(data); break;
1044
+ case 'chunk': cb.onChunk(data); break;
1045
+ case 'end': cb.onEnd(); streamCallbacks.delete(reqId); abortControllers.delete(reqId); break;
1046
+ case 'error': cb.onError(data); streamCallbacks.delete(reqId); abortControllers.delete(reqId); break;
1047
+ case 'body': cb.onBody(data); streamCallbacks.delete(reqId); abortControllers.delete(reqId); break;
1048
+ }
1049
+ });
1050
+ }
1051
+
1052
+ export async function browserFetch(
1053
+ page: Page,
1054
+ url: string,
1055
+ options: {
1056
+ method?: string;
1057
+ headers?: Record<string, string>;
1058
+ body?: string;
1059
+ timeoutMs?: number;
1060
+ } = {},
1061
+ ): Promise<{ status: number; statusText: string; contentType: string; body: string; headers: Record<string, string> }> {
1062
+ await ensureStreamBridge(page);
1063
+ const reqId = crypto.randomUUID();
1064
+
1065
+ return page.evaluate(async ({ url, options, reqId }: any) => {
1066
+ const controller = new AbortController();
1067
+ const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs || 30000);
1068
+ try {
1069
+ const resp = await fetch(url, {
1070
+ method: options.method || 'POST',
1071
+ headers: options.headers || {},
1072
+ body: options.body || undefined,
1073
+ signal: controller.signal,
1074
+ });
1075
+ clearTimeout(timeoutId);
1076
+ const respHeaders: Record<string, string> = {};
1077
+ resp.headers.forEach((v: string, k: string) => { respHeaders[k] = v; });
1078
+ const body = await resp.text();
1079
+ return {
1080
+ status: resp.status,
1081
+ statusText: resp.statusText,
1082
+ contentType: resp.headers.get('content-type') || '',
1083
+ body,
1084
+ headers: respHeaders,
1085
+ };
1086
+ } catch (e: any) {
1087
+ clearTimeout(timeoutId);
1088
+ throw new Error(`browserFetch failed: ${e.message}`);
1089
+ }
1090
+ }, { url, options, reqId });
1091
+ }
1092
+
1093
+ export async function browserStreamFetch(
1094
+ page: Page,
1095
+ url: string,
1096
+ options: {
1097
+ method?: string;
1098
+ headers?: Record<string, string>;
1099
+ body?: string;
1100
+ timeoutMs?: number;
1101
+ } = {},
1102
+ ): Promise<{
1103
+ status: number;
1104
+ statusText: string;
1105
+ contentType: string;
1106
+ headers: Record<string, string>;
1107
+ stream: ReadableStream<Uint8Array>;
1108
+ body: string;
1109
+ reqId: string;
1110
+ abort: () => void;
1111
+ }> {
1112
+ await ensureStreamBridge(page);
1113
+ const reqId = crypto.randomUUID();
1114
+ const enc = new TextEncoder();
1115
+
1116
+ let metaResolve!: (value: { status: number; statusText: string; contentType: string; headers: Record<string, string> }) => void;
1117
+ const metaPromise = new Promise<{ status: number; statusText: string; contentType: string; headers: Record<string, string> }>((resolve) => {
1118
+ metaResolve = resolve;
1119
+ });
1120
+
1121
+ const metaTimeout = setTimeout(() => {
1122
+ streamCallbacks.delete(reqId);
1123
+ abortControllers.delete(reqId);
1124
+ metaResolve({ status: 0, statusText: 'Timeout', contentType: '', headers: {} });
1125
+ }, options.timeoutMs || 130000);
1126
+
1127
+ streamCallbacks.set(reqId, {
1128
+ onMeta: (meta) => {
1129
+ clearTimeout(metaTimeout);
1130
+ metaResolve(meta);
1131
+ },
1132
+ onChunk: () => {},
1133
+ onEnd: () => {},
1134
+ onError: () => {},
1135
+ onBody: () => {},
1136
+ });
1137
+
1138
+ let abortFn = () => {};
1139
+ let bodyResolve!: (value: string) => void;
1140
+ const bodyPromise = new Promise<string>((resolve) => { bodyResolve = resolve; });
1141
+
1142
+ const stream = new ReadableStream<Uint8Array>({
1143
+ start(controller) {
1144
+ const cb = streamCallbacks.get(reqId);
1145
+ if (!cb) return;
1146
+ cb.onChunk = (chunk: string) => {
1147
+ try { controller.enqueue(enc.encode(chunk)); } catch {}
1148
+ };
1149
+ cb.onEnd = () => {
1150
+ try { controller.close(); } catch {}
1151
+ streamCallbacks.delete(reqId);
1152
+ abortControllers.delete(reqId);
1153
+ };
1154
+ cb.onError = (msg: string) => {
1155
+ try { controller.error(new Error(msg)); } catch {}
1156
+ streamCallbacks.delete(reqId);
1157
+ abortControllers.delete(reqId);
1158
+ };
1159
+ cb.onBody = (text: string) => {
1160
+ bodyResolve(text);
1161
+ streamCallbacks.delete(reqId);
1162
+ abortControllers.delete(reqId);
1163
+ };
1164
+
1165
+ page.evaluate(async ({ url, options, reqId }: any) => {
1166
+ const controller = new AbortController();
1167
+ (window as any).__abortControllers = (window as any).__abortControllers || {};
1168
+ (window as any).__abortControllers[reqId] = controller;
1169
+ const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs || 130000);
1170
+ try {
1171
+ const resp = await fetch(url, {
1172
+ method: options.method || 'POST',
1173
+ headers: options.headers || {},
1174
+ body: options.body || undefined,
1175
+ signal: controller.signal,
1176
+ });
1177
+ clearTimeout(timeoutId);
1178
+ const respHeaders: Record<string, string> = {};
1179
+ resp.headers.forEach((v: string, k: string) => { respHeaders[k] = v; });
1180
+ (window as any).__streamRelay(reqId, 'meta', {
1181
+ status: resp.status,
1182
+ statusText: resp.statusText,
1183
+ contentType: resp.headers.get('content-type') || '',
1184
+ headers: respHeaders,
1185
+ });
1186
+
1187
+ if (!resp.ok || !resp.body) {
1188
+ const bodyText = await resp.text();
1189
+ (window as any).__streamRelay(reqId, 'body', bodyText);
1190
+ delete (window as any).__abortControllers[reqId];
1191
+ return;
1192
+ }
1193
+
1194
+ const reader = resp.body.getReader();
1195
+ const decoder = new TextDecoder();
1196
+ while (true) {
1197
+ const { done, value } = await reader.read();
1198
+ if (done) {
1199
+ (window as any).__streamRelay(reqId, 'end', null);
1200
+ break;
1201
+ }
1202
+ (window as any).__streamRelay(reqId, 'chunk', decoder.decode(value, { stream: true }));
1203
+ }
1204
+ delete (window as any).__abortControllers[reqId];
1205
+ } catch (e: any) {
1206
+ clearTimeout(timeoutId);
1207
+ (window as any).__streamRelay(reqId, 'error', e.message);
1208
+ delete (window as any).__abortControllers[reqId];
1209
+ }
1210
+ }, { url, options, reqId }).catch((e: any) => {
1211
+ const cb = streamCallbacks.get(reqId);
1212
+ if (cb) {
1213
+ cb.onError(e.message);
1214
+ }
1215
+ });
1216
+ },
1217
+ cancel() {
1218
+ page.evaluate((reqId: string) => {
1219
+ const c = (window as any).__abortControllers?.[reqId];
1220
+ if (c) { c.abort(); delete (window as any).__abortControllers[reqId]; }
1221
+ }, reqId).catch(() => {});
1222
+ streamCallbacks.delete(reqId);
1223
+ abortControllers.delete(reqId);
1224
+ },
1225
+ });
1226
+
1227
+ const meta = await metaPromise;
1228
+
1229
+ abortFn = () => {
1230
+ page.evaluate((reqId: string) => {
1231
+ const c = (window as any).__abortControllers?.[reqId];
1232
+ if (c) { c.abort(); delete (window as any).__abortControllers[reqId]; }
1233
+ }, reqId).catch(() => {});
1234
+ streamCallbacks.delete(reqId);
1235
+ abortControllers.delete(reqId);
1236
+ };
1237
+
1238
+ abortControllers.set(reqId, abortFn);
1239
+
1240
+ return {
1241
+ ...meta,
1242
+ stream,
1243
+ body: meta.contentType.includes('text/event-stream') ? '' : await bodyPromise,
1244
+ reqId,
1245
+ abort: abortFn,
1246
+ };
1247
+ }
@@ -1,4 +1,4 @@
1
- import { getQwenHeaders, getBasicHeaders, getGuestHeaders } from './playwright.js';
1
+ import { getQwenHeaders, getBasicHeaders, getGuestHeaders, getPageForAccount, browserFetch, browserStreamFetch, CHROME_CLIENT_HINTS, CHROME_UA } from './playwright.js';
2
2
  import { MAX_PAYLOAD_SIZE } from '../core/model-registry.js';
3
3
  import { markAccountRateLimited } from '../core/account-manager.js';
4
4
  import crypto from 'crypto';
@@ -11,7 +11,7 @@ const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
11
11
 
12
12
  function getClientHintsHeaders(): Record<string, string> {
13
13
  return {
14
- 'sec-ch-ua': '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"',
14
+ 'sec-ch-ua': CHROME_CLIENT_HINTS,
15
15
  'sec-ch-ua-mobile': '?0',
16
16
  'sec-ch-ua-platform': '"Windows"',
17
17
  };
@@ -106,7 +106,58 @@ async function getBasicQwenHeaders(accountId?: string): Promise<Record<string, s
106
106
  };
107
107
  }
108
108
 
109
- async function createRealQwenChat(header: Record<string, string>): Promise<string> {
109
+ async function createRealQwenChat(header: Record<string, string>, accountId?: string): Promise<string> {
110
+ const page = getPageForAccount(accountId);
111
+ const body = JSON.stringify({
112
+ title: 'Nova Conversa',
113
+ models: ['qwen3.7-plus'],
114
+ chat_mode: 'normal',
115
+ chat_type: 't2t',
116
+ timestamp: Date.now(),
117
+ project_id: '',
118
+ });
119
+
120
+ const pageUrl = page?.url() || '';
121
+ const isOnQwenOrigin = pageUrl.includes('chat.qwen.ai');
122
+
123
+ if (page && !page.isClosed() && isOnQwenOrigin) {
124
+ try {
125
+ const result = await browserFetch(page, 'https://chat.qwen.ai/api/v2/chats/new', {
126
+ method: 'POST',
127
+ headers: {
128
+ 'accept': 'application/json, text/plain, */*',
129
+ 'content-type': 'application/json',
130
+ 'x-request-id': crypto.randomUUID(),
131
+ 'timezone': CACHED_TIMEZONE,
132
+ },
133
+ body,
134
+ timeoutMs: 30000,
135
+ });
136
+
137
+ if (result.status === 429) {
138
+ throw new QwenUpstreamError('Qwen upstream error: RateLimited: Too many requests.', 'RateLimited', 429);
139
+ }
140
+ if (!result.status || result.status >= 400) {
141
+ throw new Error(`Failed to create chat: ${result.status} - ${result.body}`);
142
+ }
143
+ const json = JSON.parse(result.body);
144
+ if (json && json.success === false) {
145
+ const code = json.data?.code || json.code || 'UpstreamError';
146
+ const details = json.data?.details || json.message || 'Qwen returned an error';
147
+ const wait = json.data?.num !== undefined ? ` Wait about ${json.data.num} hour(s) before trying again.` : '';
148
+ let status = 502;
149
+ if (code === 'RateLimited') status = 429;
150
+ throw new QwenUpstreamError(`Qwen upstream error: ${code}: ${details}.${wait}`, code, status);
151
+ }
152
+ const chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
153
+ if (!chatId) throw new Error(`Unexpected chat response: ${JSON.stringify(json).slice(0, 200)}`);
154
+ return chatId;
155
+ } catch (err: any) {
156
+ if (err instanceof QwenUpstreamError) throw err;
157
+ console.warn('[WarmPool] browserFetch failed for chat creation, falling back to Node.js fetch:', err.message);
158
+ }
159
+ }
160
+
110
161
  const response = await fetch('https://chat.qwen.ai/api/v2/chats/new', {
111
162
  method: 'POST',
112
163
  headers: {
@@ -123,25 +174,14 @@ async function createRealQwenChat(header: Record<string, string>): Promise<strin
123
174
  'bx-umidtoken': header['bx-umidtoken'] || '',
124
175
  ...getClientHintsHeaders(),
125
176
  },
126
- body: JSON.stringify({
127
- title: 'Nova Conversa',
128
- models: ['qwen3.7-plus'],
129
- chat_mode: 'normal',
130
- chat_type: 't2t',
131
- timestamp: Date.now(),
132
- project_id: '',
133
- }),
177
+ body,
134
178
  signal: AbortSignal.timeout(30000),
135
179
  });
136
180
 
137
181
  if (!response.ok) {
138
182
  const errText = await response.text().catch(() => '');
139
183
  if (response.status === 429) {
140
- throw new QwenUpstreamError(
141
- 'Qwen upstream error: RateLimited: Too many requests.',
142
- 'RateLimited',
143
- 429
144
- );
184
+ throw new QwenUpstreamError('Qwen upstream error: RateLimited: Too many requests.', 'RateLimited', 429);
145
185
  }
146
186
  throw new Error(`Failed to create chat: ${response.status} - ${errText}`);
147
187
  }
@@ -149,16 +189,10 @@ async function createRealQwenChat(header: Record<string, string>): Promise<strin
149
189
  if (json && json.success === false) {
150
190
  const code = json.data?.code || json.code || 'UpstreamError';
151
191
  const details = json.data?.details || json.message || 'Qwen returned an error';
152
- const wait = json.data?.num !== undefined
153
- ? ` Wait about ${json.data.num} hour(s) before trying again.`
154
- : '';
192
+ const wait = json.data?.num !== undefined ? ` Wait about ${json.data.num} hour(s) before trying again.` : '';
155
193
  let status = 502;
156
194
  if (code === 'RateLimited') status = 429;
157
- throw new QwenUpstreamError(
158
- `Qwen upstream error: ${code}: ${details}.${wait}`,
159
- code,
160
- status
161
- );
195
+ throw new QwenUpstreamError(`Qwen upstream error: ${code}: ${details}.${wait}`, code, status);
162
196
  }
163
197
  const chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
164
198
  if (!chatId) throw new Error(`Unexpected chat response: ${JSON.stringify(json).slice(0, 200)}`);
@@ -181,10 +215,14 @@ async function refillPoolForAccount(accountId: string) {
181
215
  return;
182
216
  }
183
217
 
184
- const creationPromises = Array.from({ length: need }, async () => {
218
+ const acctId = accountId === 'global' ? undefined : accountId;
219
+ for (let i = 0; i < need; i++) {
220
+ if (i > 0) {
221
+ await sleep(800 + Math.floor(Math.random() * 2200));
222
+ }
185
223
  try {
186
- const chatId = await createRealQwenChat(headers);
187
- return { chatId, headers, accountId, timestamp: Date.now() };
224
+ const chatId = await createRealQwenChat(headers, acctId);
225
+ pool.push({ chatId, headers, accountId, timestamp: Date.now() });
188
226
  } catch (err: any) {
189
227
  if (err instanceof QwenUpstreamError) {
190
228
  if (err.upstreamCode === 'RateLimited' || err.upstreamStatus === 429) {
@@ -192,16 +230,11 @@ async function refillPoolForAccount(accountId: string) {
192
230
  const cooldownMs = hourHint ? parseInt(hourHint[1]) * 60 * 60 * 1000 : undefined;
193
231
  markAccountRateLimited(accountId, cooldownMs, 'RateLimited');
194
232
  console.warn(`[WarmPool] Account ${accountId} rate-limited during chat creation. Marked for cooldown.`);
233
+ break;
195
234
  }
196
235
  }
197
236
  console.error(`[WarmPool] chat creation failed for ${accountId}:`, (err as Error).message);
198
- return null;
199
237
  }
200
- });
201
-
202
- const results = await Promise.all(creationPromises);
203
- for (const entry of results) {
204
- if (entry) pool.push(entry);
205
238
  }
206
239
  }
207
240
 
@@ -304,6 +337,32 @@ export async function disableNativeTools(accountId?: string): Promise<void> {
304
337
  };
305
338
 
306
339
  console.log(`[Qwen] Disabling native tools for ${cacheKey}...`);
340
+ const page = getPageForAccount(accountId);
341
+ if (page && !page.isClosed() && page.url().includes('chat.qwen.ai')) {
342
+ try {
343
+ const result = await browserFetch(page, 'https://chat.qwen.ai/api/v2/users/user/settings/update', {
344
+ method: 'POST',
345
+ headers: {
346
+ 'accept': 'application/json, text/plain, */*',
347
+ 'content-type': 'application/json',
348
+ 'x-request-id': crypto.randomUUID(),
349
+ 'timezone': CACHED_TIMEZONE,
350
+ },
351
+ body: JSON.stringify(payload),
352
+ timeoutMs: 30000,
353
+ });
354
+ if (result.status && result.status < 400) {
355
+ console.log(`[Qwen] Native tools disabled successfully for ${cacheKey}.`);
356
+ nativeToolsDisabled.add(cacheKey);
357
+ return;
358
+ }
359
+ console.error(`[Qwen] Failed to disable native tools for ${cacheKey}: ${result.status} - ${result.body}`);
360
+ return;
361
+ } catch (err: any) {
362
+ console.warn('[Qwen] browserFetch failed for disableNativeTools, falling back:', err.message);
363
+ }
364
+ }
365
+
307
366
  const controller = new AbortController();
308
367
  const timeoutId = setTimeout(() => controller.abort(), 30000);
309
368
  const response = await fetch('https://chat.qwen.ai/api/v2/users/user/settings/update', {
@@ -346,6 +405,27 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
346
405
  return cachedModels;
347
406
  }
348
407
 
408
+ const page = getPageForAccount(accountId);
409
+ if (page && !page.isClosed() && page.url().includes('chat.qwen.ai')) {
410
+ try {
411
+ const result = await browserFetch(page, 'https://chat.qwen.ai/api/models', {
412
+ method: 'GET',
413
+ headers: {
414
+ 'accept': 'application/json, text/plain, */*',
415
+ 'x-request-id': crypto.randomUUID(),
416
+ 'timezone': CACHED_TIMEZONE,
417
+ 'source': 'web',
418
+ },
419
+ timeoutMs: 30000,
420
+ });
421
+ if (result.status && result.status < 400) {
422
+ return processModelsJson(JSON.parse(result.body));
423
+ }
424
+ } catch (err: any) {
425
+ console.warn('[Qwen] browserFetch failed for models, falling back:', err.message);
426
+ }
427
+ }
428
+
349
429
  const { cookie, userAgent, bxV, bxUa, bxUmidtoken } = await getBasicHeaders(accountId);
350
430
 
351
431
  const response = await fetch('https://chat.qwen.ai/api/models', {
@@ -370,6 +450,10 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
370
450
  }
371
451
 
372
452
  const json = await response.json();
453
+ return processModelsJson(json);
454
+ }
455
+
456
+ function processModelsJson(json: any): any[] {
373
457
  if (json.data && Array.isArray(json.data)) {
374
458
  const models = json.data.map((m: any) => ({
375
459
  id: m.id,
@@ -390,7 +474,7 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
390
474
  ];
391
475
 
392
476
  cachedModels = extendedModels;
393
- lastModelsFetch = now;
477
+ lastModelsFetch = Date.now();
394
478
  return extendedModels;
395
479
  }
396
480
 
@@ -420,36 +504,53 @@ export async function createQwenStream(
420
504
 
421
505
  if (accountId === 'guest') {
422
506
  chatHeaders = await getGuestHeaders();
423
- const response = await fetch('https://chat.qwen.ai/api/v2/chats/new', {
424
- method: 'POST',
425
- headers: {
426
- 'accept': 'application/json, text/plain, */*',
427
- 'accept-language': 'pt-BR,pt;q=0.9',
428
- 'content-type': 'application/json',
429
- cookie: chatHeaders['cookie'],
430
- origin: 'https://chat.qwen.ai',
431
- referer: 'https://chat.qwen.ai/c/guest',
432
- 'user-agent': chatHeaders['user-agent'],
433
- 'x-request-id': crypto.randomUUID(),
434
- 'bx-v': chatHeaders['bx-v'],
435
- 'bx-ua': chatHeaders['bx-ua'],
436
- 'bx-umidtoken': chatHeaders['bx-umidtoken'],
437
- ...getClientHintsHeaders(),
438
- },
439
- body: JSON.stringify({
440
- title: 'Guest Chat',
441
- models: [modelId.replace('-no-thinking', '')],
442
- chat_mode: 'guest',
443
- chat_type: 't2t',
444
- timestamp: Date.now(),
445
- project_id: '',
446
- }),
447
- signal: AbortSignal.timeout(30000),
507
+ const guestPage = getPageForAccount('guest');
508
+ const guestBody = JSON.stringify({
509
+ title: 'Guest Chat',
510
+ models: [modelId.replace('-no-thinking', '')],
511
+ chat_mode: 'guest',
512
+ chat_type: 't2t',
513
+ timestamp: Date.now(),
514
+ project_id: '',
448
515
  });
449
- if (!response.ok) throw new Error(`Failed to create guest chat: ${response.status}`);
450
- const json = await response.json();
451
- chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
452
- if (!chatId) throw new Error(`Unexpected guest chat response: ${JSON.stringify(json).slice(0, 200)}`);
516
+
517
+ if (guestPage && !guestPage.isClosed()) {
518
+ try {
519
+ const result = await browserFetch(guestPage, 'https://chat.qwen.ai/api/v2/chats/new', {
520
+ method: 'POST',
521
+ headers: { 'accept': 'application/json, text/plain, */*', 'content-type': 'application/json', 'x-request-id': crypto.randomUUID(), 'timezone': CACHED_TIMEZONE },
522
+ body: guestBody,
523
+ timeoutMs: 30000,
524
+ });
525
+ if (!result.status || result.status >= 400) throw new Error(`Failed to create guest chat: ${result.status}`);
526
+ const json = JSON.parse(result.body);
527
+ chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
528
+ if (!chatId) throw new Error(`Unexpected guest chat response: ${JSON.stringify(json).slice(0, 200)}`);
529
+ } catch (err: any) {
530
+ console.warn('[Qwen] browserFetch guest chat failed, falling back:', err.message);
531
+ const response = await fetch('https://chat.qwen.ai/api/v2/chats/new', {
532
+ method: 'POST',
533
+ headers: { 'accept': 'application/json, text/plain, */*', 'content-type': 'application/json', cookie: chatHeaders['cookie'], origin: 'https://chat.qwen.ai', referer: 'https://chat.qwen.ai/c/guest', 'user-agent': chatHeaders['user-agent'], 'x-request-id': crypto.randomUUID(), 'bx-v': chatHeaders['bx-v'], 'bx-ua': chatHeaders['bx-ua'], 'bx-umidtoken': chatHeaders['bx-umidtoken'], ...getClientHintsHeaders() },
534
+ body: guestBody,
535
+ signal: AbortSignal.timeout(30000),
536
+ });
537
+ if (!response.ok) throw new Error(`Failed to create guest chat: ${response.status}`);
538
+ const json = await response.json();
539
+ chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
540
+ if (!chatId) throw new Error(`Unexpected guest chat response: ${JSON.stringify(json).slice(0, 200)}`);
541
+ }
542
+ } else {
543
+ const response = await fetch('https://chat.qwen.ai/api/v2/chats/new', {
544
+ method: 'POST',
545
+ headers: { 'accept': 'application/json, text/plain, */*', 'content-type': 'application/json', cookie: chatHeaders['cookie'], origin: 'https://chat.qwen.ai', referer: 'https://chat.qwen.ai/c/guest', 'user-agent': chatHeaders['user-agent'], 'x-request-id': crypto.randomUUID(), 'bx-v': chatHeaders['bx-v'], 'bx-ua': chatHeaders['bx-ua'], 'bx-umidtoken': chatHeaders['bx-umidtoken'], ...getClientHintsHeaders() },
546
+ body: guestBody,
547
+ signal: AbortSignal.timeout(30000),
548
+ });
549
+ if (!response.ok) throw new Error(`Failed to create guest chat: ${response.status}`);
550
+ const json = await response.json();
551
+ chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
552
+ if (!chatId) throw new Error(`Unexpected guest chat response: ${JSON.stringify(json).slice(0, 200)}`);
553
+ }
453
554
  } else {
454
555
  let chatEntry: WarmPoolEntry;
455
556
  try {
@@ -555,6 +656,66 @@ export async function createQwenStream(
555
656
  const timeoutMs = BASE_TIMEOUT_MS + Math.ceil(payloadMB * TIMEOUT_PER_MB);
556
657
 
557
658
  const url = `https://chat.qwen.ai/api/v2/chat/completions?chat_id=${chatId}`;
659
+
660
+ const completionHeaders: Record<string, string> = {
661
+ 'accept': 'text/event-stream',
662
+ 'content-type': 'application/json',
663
+ 'x-request-id': crypto.randomUUID(),
664
+ 'timezone': CACHED_TIMEZONE,
665
+ };
666
+
667
+ const page = getPageForAccount(accountId);
668
+ if (page && !page.isClosed() && page.url().includes('chat.qwen.ai')) {
669
+ try {
670
+ const browserResult = await browserStreamFetch(page, url, {
671
+ method: 'POST',
672
+ headers: completionHeaders,
673
+ body: payloadJson,
674
+ timeoutMs,
675
+ });
676
+
677
+ if (browserResult.contentType.includes('text/event-stream') && browserResult.status < 400) {
678
+ const controller = new AbortController();
679
+ return { stream: browserResult.stream, headers: chatHeaders, uiSessionId: chatId, controller, accountId: accountId || 'guest' };
680
+ }
681
+
682
+ if (browserResult.body) {
683
+ const peekText = browserResult.body;
684
+ if (peekText.includes('FAIL_SYS_USER_VALIDATE') || peekText.includes('_____tmd_____') || peekText.includes('RGV587_ERROR')) {
685
+ console.warn('[Qwen] TMD challenge detected via browser, refreshing headers and retrying...');
686
+ try {
687
+ const { headers: freshHeaders } = await getQwenHeaders(true, accountId);
688
+ await sleep(500 + Math.floor(Math.random() * 1000));
689
+ const retryResult = await browserStreamFetch(page, url, {
690
+ method: 'POST',
691
+ headers: completionHeaders,
692
+ body: payloadJson,
693
+ timeoutMs,
694
+ });
695
+ if (retryResult.contentType.includes('text/event-stream') && retryResult.status < 400) {
696
+ const controller = new AbortController();
697
+ return { stream: retryResult.stream, headers: freshHeaders, uiSessionId: chatId, controller, accountId: accountId || 'guest' };
698
+ }
699
+ if (retryResult.body && (retryResult.body.includes('FAIL_SYS_USER_VALIDATE') || retryResult.body.includes('_____tmd_____'))) {
700
+ throw new QwenUpstreamError('Qwen TMD challenge persists after header refresh.', 'FAIL_SYS_USER_VALIDATE', 403);
701
+ }
702
+ if (retryResult.body) {
703
+ handleErrorBody(retryResult.body, retryResult.status);
704
+ }
705
+ } catch (retryErr) {
706
+ if (retryErr instanceof QwenUpstreamError) throw retryErr;
707
+ console.error('[Qwen] Browser TMD retry failed:', (retryErr as Error).message);
708
+ }
709
+ throw new QwenUpstreamError('Qwen TMD anti-bot challenge detected. Headers were refreshed but the challenge persists.', 'FAIL_SYS_USER_VALIDATE', 403);
710
+ }
711
+ handleErrorBody(peekText, browserResult.status);
712
+ }
713
+ } catch (browserErr: any) {
714
+ if (browserErr instanceof QwenUpstreamError || browserErr instanceof RetryableQwenStreamError) throw browserErr;
715
+ console.warn('[Qwen] Browser stream fetch failed, falling back to Node.js:', browserErr.message);
716
+ }
717
+ }
718
+
558
719
  const controller = new AbortController();
559
720
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
560
721
  const response = await fetch(url, {
@@ -626,34 +787,10 @@ export async function createQwenStream(
626
787
 
627
788
  const retryPeek = await retryResponse.clone().text().catch(() => '');
628
789
  if (retryPeek.includes('FAIL_SYS_USER_VALIDATE') || retryPeek.includes('_____tmd_____')) {
629
- throw new QwenUpstreamError(
630
- 'Qwen TMD challenge persists after header refresh. The account may need manual captcha resolution.',
631
- 'FAIL_SYS_USER_VALIDATE',
632
- 403,
633
- );
790
+ throw new QwenUpstreamError('Qwen TMD challenge persists after header refresh. The account may need manual captcha resolution.', 'FAIL_SYS_USER_VALIDATE', 403);
634
791
  }
635
792
 
636
793
  if (retryResponse.ok && retryResponse.body) {
637
- try {
638
- const errorJson = JSON.parse(retryPeek);
639
- if (errorJson && (errorJson.success === false || errorJson.error)) {
640
- const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
641
- const details = errorJson.data?.details || errorJson.message || errorJson.error?.message || 'Qwen returned an error';
642
- const wait = errorJson.data?.num !== undefined
643
- ? ` Wait about ${errorJson.data.num} hour(s) before trying again.`
644
- : '';
645
- let status = 502;
646
- if (code === 'RateLimited') status = 429;
647
-
648
- throw new QwenUpstreamError(
649
- `Qwen upstream error: ${code}: ${details}.${wait}`,
650
- code,
651
- status,
652
- );
653
- }
654
- } catch (e) {
655
- if (e instanceof QwenUpstreamError) throw e;
656
- }
657
794
  return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId: accountId || 'guest' };
658
795
  }
659
796
  } catch (retryErr) {
@@ -661,32 +798,9 @@ export async function createQwenStream(
661
798
  console.error('[Qwen] TMD retry failed:', (retryErr as Error).message);
662
799
  }
663
800
 
664
- throw new QwenUpstreamError(
665
- 'Qwen TMD anti-bot challenge detected. Headers were refreshed but the challenge persists.',
666
- 'FAIL_SYS_USER_VALIDATE',
667
- 403,
668
- );
801
+ throw new QwenUpstreamError('Qwen TMD anti-bot challenge detected. Headers were refreshed but the challenge persists.', 'FAIL_SYS_USER_VALIDATE', 403);
669
802
  } else {
670
- try {
671
- const errorJson = JSON.parse(peekText);
672
- if (errorJson && (errorJson.success === false || errorJson.error)) {
673
- const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
674
- const details = errorJson.data?.details || errorJson.message || errorJson.error?.message || 'Qwen returned an error';
675
- const wait = errorJson.data?.num !== undefined
676
- ? ` Wait about ${errorJson.data.num} hour(s) before trying again.`
677
- : '';
678
- let status = 502;
679
- if (code === 'RateLimited') status = 429;
680
-
681
- throw new QwenUpstreamError(
682
- `Qwen upstream error: ${code}: ${details}.${wait}`,
683
- code,
684
- status,
685
- );
686
- }
687
- } catch (e) {
688
- if (e instanceof QwenUpstreamError) throw e;
689
- }
803
+ handleErrorBody(peekText, response.status);
690
804
  }
691
805
  }
692
806
 
@@ -695,50 +809,53 @@ export async function createQwenStream(
695
809
  const contentType = response.headers.get('content-type') || '';
696
810
 
697
811
  if (contentType.includes('application/json')) {
698
- try {
699
- const errorJson = JSON.parse(errText);
700
- if (errorJson?.data?.details?.includes('chat is in progress') ||
701
- errorJson?.data?.details?.includes('The chat is in progress')) {
702
- const retryAfterMs = 2000 + Math.floor(Math.random() * 2000);
703
- throw new RetryableQwenStreamError(
704
- `Qwen: ${errorJson.data.details}`,
705
- retryAfterMs,
706
- );
707
- }
708
- if (errorJson?.success === false) {
709
- const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
710
- const details = errorJson.data?.details || errorJson.message || 'Qwen returned an error';
711
- const wait = errorJson.data?.num !== undefined
712
- ? ` Wait about ${errorJson.data.num} hour(s) before trying again.`
713
- : '';
714
- let status: number;
715
- if (code === 'RateLimited') status = 429;
716
- else if (code === 'Not_Found') status = 404;
717
- else if (code === 'UpstreamError') status = 502;
718
- else status = 502;
719
- throw new QwenUpstreamError(
720
- `Qwen upstream error: ${code}: ${details}.${wait}`,
721
- code,
722
- status,
723
- );
724
- }
725
- if (errorJson?.data?.details?.includes('is not exist') ||
726
- errorJson?.data?.details?.includes('not exist') ||
727
- errorJson.data?.details?.includes('does not exist')) {
728
- throw new RetryableQwenStreamError(
729
- `Qwen: ${errorJson.data.details}`,
730
- 0,
731
- );
732
- }
733
- } catch (parseOrRetryError) {
734
- if (parseOrRetryError instanceof RetryableQwenStreamError ||
735
- parseOrRetryError instanceof QwenUpstreamError) {
736
- throw parseOrRetryError;
737
- }
738
- }
812
+ handleJsonErrorBody(errText);
739
813
  }
740
814
  throw new Error(`Failed to fetch from Qwen: ${response.status} ${response.statusText} - ${errText}`);
741
815
  }
742
816
 
743
817
  return { stream: response.body, headers: chatHeaders, uiSessionId: chatId, controller, accountId: accountId || 'guest' };
744
818
  }
819
+
820
+ function handleErrorBody(peekText: string, status: number): never {
821
+ try {
822
+ const errorJson = JSON.parse(peekText);
823
+ if (errorJson && (errorJson.success === false || errorJson.error)) {
824
+ const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
825
+ const details = errorJson.data?.details || errorJson.message || errorJson.error?.message || 'Qwen returned an error';
826
+ const wait = errorJson.data?.num !== undefined ? ` Wait about ${errorJson.data.num} hour(s) before trying again.` : '';
827
+ let errStatus = 502;
828
+ if (code === 'RateLimited') errStatus = 429;
829
+ throw new QwenUpstreamError(`Qwen upstream error: ${code}: ${details}.${wait}`, code, errStatus);
830
+ }
831
+ } catch (e) {
832
+ if (e instanceof QwenUpstreamError) throw e;
833
+ }
834
+ throw new Error(`Qwen returned status ${status}: ${peekText.slice(0, 500)}`);
835
+ }
836
+
837
+ function handleJsonErrorBody(errText: string): never {
838
+ try {
839
+ const errorJson = JSON.parse(errText);
840
+ if (errorJson?.data?.details?.includes('chat is in progress') || errorJson?.data?.details?.includes('The chat is in progress')) {
841
+ const retryAfterMs = 2000 + Math.floor(Math.random() * 2000);
842
+ throw new RetryableQwenStreamError(`Qwen: ${errorJson.data.details}`, retryAfterMs);
843
+ }
844
+ if (errorJson?.success === false) {
845
+ const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
846
+ const details = errorJson.data?.details || errorJson.message || 'Qwen returned an error';
847
+ const wait = errorJson.data?.num !== undefined ? ` Wait about ${errorJson.data.num} hour(s) before trying again.` : '';
848
+ let status: number;
849
+ if (code === 'RateLimited') status = 429;
850
+ else if (code === 'Not_Found') status = 404;
851
+ else status = 502;
852
+ throw new QwenUpstreamError(`Qwen upstream error: ${code}: ${details}.${wait}`, code, status);
853
+ }
854
+ if (errorJson?.data?.details?.includes('is not exist') || errorJson?.data?.details?.includes('not exist') || errorJson.data?.details?.includes('does not exist')) {
855
+ throw new RetryableQwenStreamError(`Qwen: ${errorJson.data.details}`, 0);
856
+ }
857
+ } catch (e) {
858
+ if (e instanceof RetryableQwenStreamError || e instanceof QwenUpstreamError) throw e;
859
+ }
860
+ throw new Error(`Qwen JSON error: ${errText.slice(0, 500)}`);
861
+ }