@pedrofariasx/qwenproxy 1.1.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/LICENSE +13 -0
- package/README.md +292 -0
- package/bin/qwenproxy.mjs +11 -0
- package/package.json +56 -0
- package/src/api/models.ts +183 -0
- package/src/api/server.ts +126 -0
- package/src/cache/memory-cache.ts +186 -0
- package/src/core/account-manager.ts +132 -0
- package/src/core/accounts.ts +78 -0
- package/src/core/config.ts +91 -0
- package/src/core/database.ts +92 -0
- package/src/core/logger.ts +96 -0
- package/src/core/metrics.ts +169 -0
- package/src/core/model-registry.ts +30 -0
- package/src/core/stream-registry.ts +40 -0
- package/src/core/watchdog.ts +130 -0
- package/src/index.ts +7 -0
- package/src/linter/extraction-engine.ts +165 -0
- package/src/linter/index.ts +258 -0
- package/src/linter/repair-normalize.ts +245 -0
- package/src/linter/safety-gate.ts +219 -0
- package/src/linter/streaming-state-machine.ts +252 -0
- package/src/linter/structural-parser.ts +352 -0
- package/src/linter/types.ts +74 -0
- package/src/login.ts +228 -0
- package/src/routes/chat.ts +801 -0
- package/src/routes/upload.ts +700 -0
- package/src/services/playwright.ts +778 -0
- package/src/services/qwen.ts +500 -0
- package/src/tests/advanced.test.ts +227 -0
- package/src/tests/agenticStress.test.ts +360 -0
- package/src/tests/concurrency.test.ts +103 -0
- package/src/tests/concurrentChat.test.ts +71 -0
- package/src/tests/delta.test.ts +63 -0
- package/src/tests/index.test.ts +356 -0
- package/src/tests/jsonFix.test.ts +98 -0
- package/src/tests/linter.test.ts +151 -0
- package/src/tests/parallel.test.ts +42 -0
- package/src/tests/parser.test.ts +89 -0
- package/src/tests/rotation.test.ts +45 -0
- package/src/tests/streamingOptimizations.test.ts +328 -0
- package/src/tests/structureVerification.test.ts +176 -0
- package/src/tools/ast.ts +15 -0
- package/src/tools/coercion.ts +67 -0
- package/src/tools/confidence.ts +48 -0
- package/src/tools/detector.ts +40 -0
- package/src/tools/executor.ts +236 -0
- package/src/tools/parser.ts +446 -0
- package/src/tools/pipeline.ts +122 -0
- package/src/tools/registry-runtime.ts +34 -0
- package/src/tools/registry.ts +142 -0
- package/src/tools/repair.ts +42 -0
- package/src/tools/schema.ts +285 -0
- package/src/tools/types.ts +104 -0
- package/src/tools/validator.ts +33 -0
- package/src/utils/context-truncation.ts +61 -0
- package/src/utils/json.ts +114 -0
- package/src/utils/qwen-stream-parser.ts +286 -0
- package/src/utils/types.ts +101 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { getQwenHeaders, getBasicHeaders } from './playwright.ts';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
|
|
4
|
+
export class RetryableQwenStreamError extends Error {
|
|
5
|
+
readonly retryAfterMs: number;
|
|
6
|
+
constructor(message: string, retryAfterMs: number) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'RetryableQwenStreamError';
|
|
9
|
+
this.retryAfterMs = retryAfterMs;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class QwenUpstreamError extends Error {
|
|
14
|
+
readonly upstreamCode: string;
|
|
15
|
+
readonly upstreamStatus: number;
|
|
16
|
+
constructor(message: string, upstreamCode: string, upstreamStatus: number) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'QwenUpstreamError';
|
|
19
|
+
this.upstreamCode = upstreamCode;
|
|
20
|
+
this.upstreamStatus = upstreamStatus;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SessionEntry {
|
|
25
|
+
parentId: string | null;
|
|
26
|
+
timestamp: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const sessionStates: Map<string, SessionEntry> = (globalThis as any)._sessionStates || new Map();
|
|
30
|
+
(globalThis as any)._sessionStates = sessionStates;
|
|
31
|
+
const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
function cleanupStaleSessions() {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
for (const [key, entry] of sessionStates.entries()) {
|
|
36
|
+
if (now - entry.timestamp > SESSION_TTL_MS) {
|
|
37
|
+
sessionStates.delete(key);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function updateSessionParent(sessionId: string, parentId: string | null) {
|
|
43
|
+
if (sessionId) {
|
|
44
|
+
if (sessionStates.size > 10000) cleanupStaleSessions();
|
|
45
|
+
sessionStates.set(sessionId, { parentId, timestamp: Date.now() });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getSessionParent(sessionId: string): string | null | undefined {
|
|
50
|
+
const entry = sessionStates.get(sessionId);
|
|
51
|
+
if (!entry) return undefined;
|
|
52
|
+
if (Date.now() - entry.timestamp > SESSION_TTL_MS) {
|
|
53
|
+
sessionStates.delete(sessionId);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return entry.parentId;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface WarmPoolEntry {
|
|
60
|
+
chatId: string;
|
|
61
|
+
headers: Record<string, string>;
|
|
62
|
+
accountId: string;
|
|
63
|
+
timestamp: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const warmPool: Map<string, WarmPoolEntry[]> = (globalThis as any)._warmPool || new Map();
|
|
67
|
+
(globalThis as any)._warmPool = warmPool;
|
|
68
|
+
|
|
69
|
+
const WARM_POOL_SIZE = 5;
|
|
70
|
+
const WARM_POOL_TTL_MS = 10 * 60 * 1000;
|
|
71
|
+
|
|
72
|
+
function cleanupStalePool(accountId: string) {
|
|
73
|
+
const pool = warmPool.get(accountId);
|
|
74
|
+
if (!pool) return;
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
for (let i = pool.length - 1; i >= 0; i--) {
|
|
77
|
+
if (now - pool[i].timestamp > WARM_POOL_TTL_MS) pool.splice(i, 1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function getBasicQwenHeaders(accountId?: string): Promise<Record<string, string>> {
|
|
82
|
+
const { getBasicHeaders } = await import('./playwright.ts');
|
|
83
|
+
const { cookie, userAgent, bxV } = await getBasicHeaders(accountId);
|
|
84
|
+
return {
|
|
85
|
+
cookie,
|
|
86
|
+
'user-agent': userAgent,
|
|
87
|
+
'bx-v': bxV,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function createRealQwenChat(header: Record<string, string>): Promise<string> {
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
94
|
+
const response = await fetch('https://chat.qwen.ai/api/v2/chats/new', {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
'accept': 'application/json, text/plain, */*',
|
|
98
|
+
'accept-language': 'pt-BR,pt;q=0.9',
|
|
99
|
+
'content-type': 'application/json',
|
|
100
|
+
cookie: header['cookie'],
|
|
101
|
+
origin: 'https://chat.qwen.ai',
|
|
102
|
+
referer: 'https://chat.qwen.ai/c/new-chat',
|
|
103
|
+
'user-agent': header['user-agent'],
|
|
104
|
+
'x-request-id': uuidv4(),
|
|
105
|
+
'bx-v': header['bx-v'],
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
title: 'Nova Conversa',
|
|
109
|
+
models: ['qwen3.7-plus'],
|
|
110
|
+
chat_mode: 'normal',
|
|
111
|
+
chat_type: 't2t',
|
|
112
|
+
timestamp: Date.now(),
|
|
113
|
+
project_id: '',
|
|
114
|
+
}),
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
});
|
|
117
|
+
clearTimeout(timeoutId);
|
|
118
|
+
|
|
119
|
+
if (!response.ok) throw new Error(`Failed to create chat: ${response.status}`);
|
|
120
|
+
const json = await response.json();
|
|
121
|
+
const chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
|
|
122
|
+
if (!chatId) throw new Error(`Unexpected chat response: ${JSON.stringify(json).slice(0, 200)}`);
|
|
123
|
+
return chatId;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function refillPoolForAccount(accountId: string) {
|
|
127
|
+
let pool = warmPool.get(accountId);
|
|
128
|
+
if (!pool) { pool = []; warmPool.set(accountId, pool); }
|
|
129
|
+
cleanupStalePool(accountId);
|
|
130
|
+
const need = Math.max(0, WARM_POOL_SIZE - pool.length);
|
|
131
|
+
for (let i = 0; i < need; i++) {
|
|
132
|
+
try {
|
|
133
|
+
const headers = await getBasicQwenHeaders(accountId === 'global' ? undefined : accountId);
|
|
134
|
+
const chatId = await createRealQwenChat(headers);
|
|
135
|
+
pool.push({ chatId, headers, accountId, timestamp: Date.now() });
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error(`[WarmPool] refill failed for ${accountId}:`, (err as Error).message);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function getWarmedChat(accountId?: string) {
|
|
144
|
+
const key = accountId || 'global';
|
|
145
|
+
let pool = warmPool.get(key);
|
|
146
|
+
if (!pool) { pool = []; warmPool.set(key, pool); }
|
|
147
|
+
cleanupStalePool(key);
|
|
148
|
+
if (pool.length === 0) {
|
|
149
|
+
await refillPoolForAccount(key);
|
|
150
|
+
}
|
|
151
|
+
if (pool.length === 0) throw new Error(`Warm pool empty for ${key}`);
|
|
152
|
+
return pool.shift()!;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function warmAllPools(accountIds: string[]) {
|
|
156
|
+
for (const id of accountIds) refillPoolForAccount(id).catch(() => {});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface QwenMessage {
|
|
160
|
+
fid: string;
|
|
161
|
+
parentId: string | null;
|
|
162
|
+
childrenIds: string[];
|
|
163
|
+
role: 'user' | 'assistant';
|
|
164
|
+
content: string;
|
|
165
|
+
user_action: string;
|
|
166
|
+
files: any[];
|
|
167
|
+
timestamp: number;
|
|
168
|
+
models: string[];
|
|
169
|
+
chat_type: string;
|
|
170
|
+
feature_config: {
|
|
171
|
+
thinking_enabled: boolean;
|
|
172
|
+
output_schema: string;
|
|
173
|
+
research_mode: string;
|
|
174
|
+
auto_thinking: boolean;
|
|
175
|
+
thinking_mode: string;
|
|
176
|
+
thinking_format: string;
|
|
177
|
+
auto_search: boolean;
|
|
178
|
+
};
|
|
179
|
+
extra: {
|
|
180
|
+
meta: {
|
|
181
|
+
subChatType: string;
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
sub_chat_type: string;
|
|
185
|
+
parent_id: string | null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface QwenPayload {
|
|
189
|
+
stream: boolean;
|
|
190
|
+
version: string;
|
|
191
|
+
incremental_output: boolean;
|
|
192
|
+
chat_id: string;
|
|
193
|
+
chat_mode: string;
|
|
194
|
+
model: string;
|
|
195
|
+
parent_id: string | null;
|
|
196
|
+
messages: QwenMessage[];
|
|
197
|
+
timestamp: number;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let cachedModels: any[] | null = null;
|
|
201
|
+
let lastModelsFetch = 0;
|
|
202
|
+
|
|
203
|
+
const nativeToolsDisabled = new Set<string>();
|
|
204
|
+
const disablingNativeToolsInProgress = new Set<string>();
|
|
205
|
+
|
|
206
|
+
export async function disableNativeTools(accountId?: string): Promise<void> {
|
|
207
|
+
const cacheKey = accountId || 'global';
|
|
208
|
+
if (nativeToolsDisabled.has(cacheKey) || disablingNativeToolsInProgress.has(cacheKey)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
disablingNativeToolsInProgress.add(cacheKey);
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const { headers } = await getQwenHeaders(false, accountId);
|
|
215
|
+
|
|
216
|
+
const payload = {
|
|
217
|
+
tools_enabled: {
|
|
218
|
+
web_extractor: false,
|
|
219
|
+
web_search_image: false,
|
|
220
|
+
web_search: false,
|
|
221
|
+
image_gen_tool: false,
|
|
222
|
+
code_interpreter: false,
|
|
223
|
+
history_retriever: false,
|
|
224
|
+
image_edit_tool: false,
|
|
225
|
+
bio: false,
|
|
226
|
+
image_zoom_in_tool: false
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
console.log(`[Qwen] Disabling native tools for ${cacheKey}...`);
|
|
231
|
+
const controller = new AbortController();
|
|
232
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
233
|
+
const response = await fetch('https://chat.qwen.ai/api/v2/users/user/settings/update', {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
headers: {
|
|
236
|
+
'accept': 'application/json, text/plain, */*',
|
|
237
|
+
'accept-language': 'pt-BR,pt;q=0.9',
|
|
238
|
+
'content-type': 'application/json',
|
|
239
|
+
'cookie': headers['cookie'],
|
|
240
|
+
'origin': 'https://chat.qwen.ai',
|
|
241
|
+
'referer': 'https://chat.qwen.ai/',
|
|
242
|
+
'user-agent': headers['user-agent'],
|
|
243
|
+
'x-request-id': uuidv4(),
|
|
244
|
+
'bx-ua': headers['bx-ua'],
|
|
245
|
+
'bx-umidtoken': headers['bx-umidtoken'],
|
|
246
|
+
'bx-v': headers['bx-v']
|
|
247
|
+
},
|
|
248
|
+
body: JSON.stringify(payload),
|
|
249
|
+
signal: controller.signal
|
|
250
|
+
});
|
|
251
|
+
clearTimeout(timeoutId);
|
|
252
|
+
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
const text = await response.text();
|
|
255
|
+
console.error(`[Qwen] Failed to disable native tools for ${cacheKey}: ${response.status} - ${text}`);
|
|
256
|
+
} else {
|
|
257
|
+
console.log(`[Qwen] Native tools disabled successfully for ${cacheKey}.`);
|
|
258
|
+
nativeToolsDisabled.add(cacheKey);
|
|
259
|
+
}
|
|
260
|
+
} catch (err: any) {
|
|
261
|
+
console.error(`[Qwen] Error disabling native tools for ${cacheKey}: ${err.message}`);
|
|
262
|
+
} finally {
|
|
263
|
+
disablingNativeToolsInProgress.delete(cacheKey);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function fetchQwenModels(accountId?: string): Promise<any[]> {
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
if (cachedModels && (now - lastModelsFetch < 3600000)) {
|
|
270
|
+
return cachedModels;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const { cookie, userAgent, bxV } = await getBasicHeaders(accountId);
|
|
274
|
+
|
|
275
|
+
const response = await fetch('https://chat.qwen.ai/api/models', {
|
|
276
|
+
headers: {
|
|
277
|
+
'accept': 'application/json, text/plain, */*',
|
|
278
|
+
'accept-language': 'pt-BR,pt;q=0.9',
|
|
279
|
+
'cookie': cookie,
|
|
280
|
+
'referer': 'https://chat.qwen.ai/',
|
|
281
|
+
'user-agent': userAgent,
|
|
282
|
+
'x-request-id': uuidv4(),
|
|
283
|
+
'bx-v': bxV,
|
|
284
|
+
'timezone': new Date().toString(),
|
|
285
|
+
'source': 'web'
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (!response.ok) {
|
|
290
|
+
throw new Error(`Failed to fetch models from Qwen: ${response.status} ${response.statusText}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const json = await response.json();
|
|
294
|
+
if (json.data && Array.isArray(json.data)) {
|
|
295
|
+
const models = json.data.map((m: any) => ({
|
|
296
|
+
id: m.id,
|
|
297
|
+
object: 'model',
|
|
298
|
+
created: m.info?.created_at || Math.floor(Date.now() / 1000),
|
|
299
|
+
owned_by: m.owned_by || 'qwen'
|
|
300
|
+
}));
|
|
301
|
+
|
|
302
|
+
const hasPlus = models.some((m: any) => m.id === 'qwen3.7-plus');
|
|
303
|
+
const base = [
|
|
304
|
+
...models,
|
|
305
|
+
...(hasPlus ? [] : [{ id: 'qwen3.7-plus', object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'qwen' }])
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
const extendedModels = [
|
|
309
|
+
...base,
|
|
310
|
+
...base.map((m: any) => ({ ...m, id: `${m.id}-no-thinking` }))
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
cachedModels = extendedModels;
|
|
314
|
+
lastModelsFetch = now;
|
|
315
|
+
return extendedModels;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export interface QwenFileEntry {
|
|
322
|
+
type: string;
|
|
323
|
+
file: any;
|
|
324
|
+
id: string;
|
|
325
|
+
url: string;
|
|
326
|
+
name: string;
|
|
327
|
+
[key: string]: any;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function createQwenStream(
|
|
331
|
+
prompt: string,
|
|
332
|
+
enableThinking: boolean,
|
|
333
|
+
modelId: string,
|
|
334
|
+
forcedParentId?: string | null,
|
|
335
|
+
accountId?: string,
|
|
336
|
+
files?: QwenFileEntry[],
|
|
337
|
+
pendingMultimodal?: Array<Array<{ type: string; text?: string; image_url?: { url: string }; video_url?: { url: string }; audio_url?: { url: string }; file_url?: { url: string } }>>
|
|
338
|
+
): Promise<{ stream: ReadableStream, headers: Record<string, string>, uiSessionId: string, controller: AbortController, accountId: string }> {
|
|
339
|
+
let chatEntry: WarmPoolEntry;
|
|
340
|
+
try {
|
|
341
|
+
chatEntry = await getWarmedChat(accountId);
|
|
342
|
+
} catch (err: any) {
|
|
343
|
+
if (err.message?.includes('chat is in progress') || err.message?.includes('The chat is in progress')) {
|
|
344
|
+
const retryAfterMs = 2000 + Math.floor(Math.random() * 2000);
|
|
345
|
+
throw new RetryableQwenStreamError(`Qwen: ${err.message}`, retryAfterMs);
|
|
346
|
+
}
|
|
347
|
+
throw err;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const chatId = chatEntry.chatId;
|
|
351
|
+
const chatHeaders = chatEntry.headers;
|
|
352
|
+
const actualParentId: string | null = null;
|
|
353
|
+
|
|
354
|
+
// Process pending multimodal uploads using warm pool headers (no extra Playwright roundtrip)
|
|
355
|
+
let resolvedFiles = files || [];
|
|
356
|
+
if (pendingMultimodal && pendingMultimodal.length > 0 && resolvedFiles.length === 0) {
|
|
357
|
+
try {
|
|
358
|
+
const { processImagesForQwen } = await import('../routes/upload.ts');
|
|
359
|
+
const uploadHeaders: Record<string, string> = {
|
|
360
|
+
cookie: chatHeaders['cookie'] || '',
|
|
361
|
+
'user-agent': chatHeaders['user-agent'] || '',
|
|
362
|
+
'bx-ua': chatHeaders['bx-ua'] || '',
|
|
363
|
+
'bx-umidtoken': chatHeaders['bx-umidtoken'] || '',
|
|
364
|
+
'bx-v': chatHeaders['bx-v'] || '',
|
|
365
|
+
};
|
|
366
|
+
// Process all multimodal parts in parallel
|
|
367
|
+
const results = await Promise.all(
|
|
368
|
+
pendingMultimodal.map(parts => processImagesForQwen(parts, uploadHeaders))
|
|
369
|
+
);
|
|
370
|
+
for (const r of results) {
|
|
371
|
+
resolvedFiles.push(...r.files);
|
|
372
|
+
}
|
|
373
|
+
} catch (err: any) {
|
|
374
|
+
console.error('[Qwen] Failed to process multimodal uploads:', err.message);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
379
|
+
const fid = uuidv4();
|
|
380
|
+
const model = modelId.replace('-no-thinking', '');
|
|
381
|
+
|
|
382
|
+
const payload: QwenPayload = {
|
|
383
|
+
stream: true,
|
|
384
|
+
version: '2.1',
|
|
385
|
+
incremental_output: true,
|
|
386
|
+
chat_id: chatId,
|
|
387
|
+
chat_mode: 'normal',
|
|
388
|
+
model: model,
|
|
389
|
+
parent_id: actualParentId,
|
|
390
|
+
messages: [
|
|
391
|
+
{
|
|
392
|
+
fid: fid,
|
|
393
|
+
parentId: actualParentId,
|
|
394
|
+
childrenIds: [],
|
|
395
|
+
role: 'user',
|
|
396
|
+
content: prompt,
|
|
397
|
+
user_action: 'chat',
|
|
398
|
+
files: resolvedFiles,
|
|
399
|
+
timestamp: timestamp,
|
|
400
|
+
models: [model],
|
|
401
|
+
chat_type: 't2t',
|
|
402
|
+
feature_config: {
|
|
403
|
+
thinking_enabled: enableThinking,
|
|
404
|
+
output_schema: 'phase',
|
|
405
|
+
research_mode: 'normal',
|
|
406
|
+
auto_thinking: false,
|
|
407
|
+
thinking_mode: 'Thinking',
|
|
408
|
+
thinking_format: 'summary',
|
|
409
|
+
auto_search: false
|
|
410
|
+
},
|
|
411
|
+
extra: {
|
|
412
|
+
meta: {
|
|
413
|
+
subChatType: 't2t'
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
sub_chat_type: 't2t',
|
|
417
|
+
parent_id: actualParentId
|
|
418
|
+
}
|
|
419
|
+
],
|
|
420
|
+
timestamp: timestamp + 1
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const url = `https://chat.qwen.ai/api/v2/chat/completions?chat_id=${chatId}`;
|
|
424
|
+
const controller = new AbortController();
|
|
425
|
+
const timeoutId = setTimeout(() => controller.abort(), 120000);
|
|
426
|
+
const response = await fetch(url, {
|
|
427
|
+
method: 'POST',
|
|
428
|
+
headers: {
|
|
429
|
+
'accept': 'application/json',
|
|
430
|
+
'accept-language': 'pt-BR,pt;q=0.9',
|
|
431
|
+
'content-type': 'application/json',
|
|
432
|
+
'cookie': chatHeaders['cookie'],
|
|
433
|
+
'origin': 'https://chat.qwen.ai',
|
|
434
|
+
'referer': `https://chat.qwen.ai/c/${chatId}`,
|
|
435
|
+
'sec-fetch-dest': 'empty',
|
|
436
|
+
'sec-fetch-mode': 'cors',
|
|
437
|
+
'sec-fetch-site': 'same-origin',
|
|
438
|
+
'timezone': new Date().toString().split(' (')[0],
|
|
439
|
+
'user-agent': chatHeaders['user-agent'],
|
|
440
|
+
'x-accel-buffering': 'no',
|
|
441
|
+
'x-request-id': uuidv4(),
|
|
442
|
+
'bx-v': chatHeaders['bx-v'],
|
|
443
|
+
},
|
|
444
|
+
body: JSON.stringify(payload),
|
|
445
|
+
signal: controller.signal
|
|
446
|
+
});
|
|
447
|
+
clearTimeout(timeoutId);
|
|
448
|
+
|
|
449
|
+
if (!response.ok || !response.body) {
|
|
450
|
+
const errText = await response.text().catch(() => '');
|
|
451
|
+
const contentType = response.headers.get('content-type') || '';
|
|
452
|
+
|
|
453
|
+
if (contentType.includes('application/json')) {
|
|
454
|
+
try {
|
|
455
|
+
const errorJson = JSON.parse(errText);
|
|
456
|
+
if (errorJson?.data?.details?.includes('chat is in progress') ||
|
|
457
|
+
errorJson?.data?.details?.includes('The chat is in progress')) {
|
|
458
|
+
const retryAfterMs = 2000 + Math.floor(Math.random() * 2000);
|
|
459
|
+
throw new RetryableQwenStreamError(
|
|
460
|
+
`Qwen: ${errorJson.data.details}`,
|
|
461
|
+
retryAfterMs,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (errorJson?.success === false) {
|
|
465
|
+
const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
|
|
466
|
+
const details = errorJson.data?.details || errorJson.message || 'Qwen returned an error';
|
|
467
|
+
const wait = errorJson.data?.num !== undefined
|
|
468
|
+
? ` Wait about ${errorJson.data.num} hour(s) before trying again.`
|
|
469
|
+
: '';
|
|
470
|
+
let status: number;
|
|
471
|
+
if (code === 'RateLimited') status = 429;
|
|
472
|
+
else if (code === 'Not_Found') status = 404;
|
|
473
|
+
else if (code === 'UpstreamError') status = 502;
|
|
474
|
+
else status = 502;
|
|
475
|
+
throw new QwenUpstreamError(
|
|
476
|
+
`Qwen upstream error: ${code}: ${details}.${wait}`,
|
|
477
|
+
code,
|
|
478
|
+
status,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
if (errorJson?.data?.details?.includes('is not exist') ||
|
|
482
|
+
errorJson?.data?.details?.includes('not exist') ||
|
|
483
|
+
errorJson.data?.details?.includes('does not exist')) {
|
|
484
|
+
throw new RetryableQwenStreamError(
|
|
485
|
+
`Qwen: ${errorJson.data.details}`,
|
|
486
|
+
0,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
} catch (parseOrRetryError) {
|
|
490
|
+
if (parseOrRetryError instanceof RetryableQwenStreamError ||
|
|
491
|
+
parseOrRetryError instanceof QwenUpstreamError) {
|
|
492
|
+
throw parseOrRetryError;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
throw new Error(`Failed to fetch from Qwen: ${response.status} ${response.statusText} - ${errText}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return { stream: response.body, headers: chatHeaders, uiSessionId: chatId, controller, accountId: chatEntry.accountId };
|
|
500
|
+
}
|